// Cross-platform sprite codegen runtime: jlSpriteCompile runs the shared
// staging pass (spriteStage.c) to emit every (shift, op) routine into a
// scratch image, allocates a slot in the codegen arena, copies the emitted
// bytes in, and populates sp->slot + sp->routineOffsets. The staging + the
// per-CPU emit dispatch live in spriteStage.c so the offline bake tool can
// reuse them without this file's arena/heap dependencies.

#include <stdlib.h>
#include <string.h>

#include "joey/sprite.h"
#include "codegenArenaInternal.h"
#include "port.h"
#include "spriteInternal.h"
#include "spriteStage.h"
#include "surfaceInternal.h"   // coreSetError




/* #19: the ORIGINAL IIgs sprite codegen (a self-modifying call stub + inline
 * asm + hand-rolled per-call bank/offset patching) had a layout-dependent
 * memory corruption that proved impossible to instrument (the GS/OS
 * multi-segment loader relocates BSS / the arena / the back buffer to
 * nondeterministic banks+offsets and there is no fixed C->emulator diagnostic
 * channel). It was REWRITTEN rather than patched: the emitted routines are now
 * clean C-ABI functions invoked through ordinary C function pointers, so the
 * compiler emits the call and there is no self-modifying code or manual address
 * math -- see spriteCompiledDraw + the prologue/epilogue in spriteEmitIigs.c.
 * Codegen is ON by default; set JOEY_IIGS_SPRITE_CODEGEN to 0 to force the
 * interpreter. */
/* PERF-AUDIT.md #77 -- ROOT-CAUSED 2026-07-04, FIXED toolchain-side
 * 2026-07-06 (LLVM816-ASKS.md item 5, JOEYLIB-REPLY.md): the GS/OS
 * loader used to Memory-Manager-claim only each segment's IMAGE bytes,
 * so the BSS+heap span above the image was MM-free space and the codegen
 * arena's NewHandle could land INSIDE the app's own entry-bank BSS
 * ($08:4200 empirically), where emitting routines clobbered live app
 * state (the emitted code itself was always byte-valid -- the bug was
 * arena PLACEMENT). link816/omfEmit now embed each segment's BSS in the
 * image so the Loader reserves it in place, and crt0Gsos moves the C
 * heap into a locked MM handle -- so NewHandle/malloc no longer collide
 * with live state. Codegen re-enabled on IIgs 2026-07-06; the emitted-
 * code path needed no JoeyLib-side change (as predicted). Set to 0 to
 * fall back to the interpreter. */
#ifndef JOEY_IIGS_SPRITE_CODEGEN
#define JOEY_IIGS_SPRITE_CODEGEN 1
#endif


bool jlSpriteCompile(jlSpriteT *sp) {
    uint8_t     *staging;
    ArenaSlotT  *slot;
    uint32_t     total;

    if (sp == NULL) {
        return false;
    }
    if (sp->slot != NULL) {
        return true;
    }
    if (sp->tileData == NULL) {
        return false;
    }
#if defined(JOEYLIB_PLATFORM_IIGS) && !JOEY_IIGS_SPRITE_CODEGEN
    /* #19: leave sp->slot NULL -> interpreter path. See the note above. */
    return false;
#endif

    staging = (uint8_t *)jlpBigAlloc(SPRITE_STAGING_BYTES);
    if (staging == NULL) {
        return false;
    }

    // One greedy pass: emit every included variant straight into `staging`
    // and record its slot-relative offset. No separate measure pass -- the
    // emitter runs once per variant instead of twice.
    total = spriteStageAndPlan(staging, sp, false);
    if (total == 0) {
        /* Platforms whose emitter returns 0 for every (shift, op) have
         * no compiled bytes -- spriteCompiledDraw / SaveUnder /
         * RestoreUnder would dereference a degenerate slot or chunky
         * shadow. Bail so sp->slot stays NULL and the dispatcher
         * routes through the interpreted jlpSprite*Planes ops path. */
        jlpBigFree(staging);
        return false;
    }

    slot = codegenArenaAlloc(total);
    if (slot == NULL) {
        // Fragmented arena: total free space may suffice even though no
        // single hole does (sprite create/destroy churn across levels).
        // Honor the arena's compact-and-retry contract
        // (codegenArenaInternal.h) once before giving up -- failure-path
        // only, zero hot-path cost (#5).
        codegenArenaCompact();
        slot = codegenArenaAlloc(total);
    }
    if (slot == NULL) {
        // Degrade tier: re-stage with only the cheap byte-aligned DRAW
        // phases (SPRITE_DEGRADE_DRAW_MASK) + the save/restore pairs, so an
        // arena too small for the full variant set still compiles what
        // pre-Phase-1 builds compiled instead of losing the whole sprite to
        // the interpreter. Deterministic (depends only on the sprite and
        // the mask); the second emit only happens on this arena-full path.
        total = spriteStageAndPlan(staging, sp, true);
        if (total != 0) {
            slot = codegenArenaAlloc(total);
            if (slot == NULL) {
                codegenArenaCompact();
                slot = codegenArenaAlloc(total);
            }
        }
    }
    if (slot == NULL) {
        // Specific message so a swallowed jlSpritePrewarm/jlSpriteCompile
        // failure is diagnosable via jlLastError. Clear the staged offsets
        // (sp->slot stays NULL, so the dispatcher interprets regardless).
        memset(sp->routineOffsets, 0xFF, sizeof(sp->routineOffsets));
        coreSetError("jlSpriteCompile: codegen arena full even after compaction; raise jlConfigT.codegenBytes or destroy unused sprites");
        jlpBigFree(staging);
        return false;
    }

    // Copy the staged image into the slot in one shot. routineOffsets are
    // already slot-relative (offset within `staging` == offset within the
    // slot after this copy), so nothing else needs fixing up.
    memcpy(codegenArenaBase() + slot->offset, staging, (size_t)total);
    sp->slot = slot;
    // Registry (#34): every creation path (including jlSpriteBankLoad,
    // #32) already registered sp via spriteInitFields; this idempotent
    // re-add is a belt-and-braces guard, since compile success is the
    // moment sp first holds arena state that jlShutdown must reset.
    spriteRegistryAdd(sp);
    jlpBigFree(staging);
    return true;
}


// The restore shift used to be re-derived here from the backup
// (spriteChunkyRestoreShift). The jlSpriteRestoreUnder gate in
// src/core/sprite.c already derives it to pick the routine, so it now
// passes shift (and routeOffset) straight into the callees and the
// re-derivation is gone (#29).


// The spriteCompiled* runtime dispatchers moved to
// src/core/spriteDispatch.h as always-inline functions consumed by
// sprite.c (NATIVE-PERF Phase 2 R5). This TU keeps only the
// compile-time machinery.
