tishlang_runtime_gba 3.10.9

no_std runtime facade for Tish compiled output on the Game Boy Advance (agb)
//! GBA runtime entry points, called by the generated `#[agb::entry] agb_main`.
//!
//! Contract (see tish-gba/CONTRACT.md §4):
//! ```ignore
//! #[agb::entry]
//! fn agb_main(gba: agb::Gba) -> ! {
//!     tishlang_runtime::gba::init(gba);
//!     let _ = run();              // P5: block_on(run())
//!     tishlang_runtime::gba::halt()
//! }
//! ```

use alloc::vec::Vec;
use core::cell::RefCell;
use tishlang_core::SingleCore;

// The `agb::Gba` peripheral bundle, stashed at boot so the low-level binding
// crate (`tish-agb`) can take it and drive graphics/input/etc. Single-core, so a
// plain static behind the SingleCore `.with()` API is sound.
static GBA: SingleCore<RefCell<Option<agb::Gba>>> = SingleCore::new(RefCell::new(None));

/// Stash the `agb::Gba` peripheral bundle for the binding crate to claim via
/// [`take_gba`]. Called by the generated `agb_main` before the program body runs.
pub fn init(gba: agb::Gba) {
    GBA.with(|c| {
        *c.borrow_mut() = Some(gba);
    });
}

/// Claim the `agb::Gba` peripheral bundle (once). `tish-agb` calls this on first
/// use to set up its graphics/input context. Returns `None` if already taken.
pub fn take_gba() -> Option<agb::Gba> {
    GBA.with(|c| c.borrow_mut().take())
}

/// Divergent halt for the end of `agb_main` (after `run()` returns).
pub fn halt() -> ! {
    loop {
        agb::halt();
    }
}

// ── Per-frame hooks ──────────────────────────────────────────────────────────
// `tish-agb`'s frame driver and (later) the engine pipeline register here; the
// frame loop runs them once per frame before `commit()`. Single-core, so a plain
// static with the SingleCore `.with()` API.
static PRE_COMMIT: SingleCore<RefCell<Vec<fn()>>> = SingleCore::new(RefCell::new(Vec::new()));

/// Register a callback to run once per frame, before the display commit.
pub fn register_pre_commit(f: fn()) {
    PRE_COMMIT.with(|c| c.borrow_mut().push(f));
}

/// Run all registered pre-commit hooks (called by the frame driver / executor).
pub fn run_pre_commit() {
    PRE_COMMIT.with(|c| {
        // Clone out the fn pointers so a hook can register another without a
        // nested borrow of the same RefCell.
        let hooks: Vec<fn()> = c.borrow().clone();
        for f in hooks {
            f();
        }
    });
}

// ── Asset registry ───────────────────────────────────────────────────────────
// Sprite sheets from `asset:` imports. The generated `agb_main` calls
// `__asset_register_sheet` once per imported asset (in import order) BEFORE
// `run()`, handing over the `&'static [Sprite]` slice that agb's
// `include_aseprite_inner!` produced in the generated crate. The returned i32 is
// the handle a tish program passes to `tish-agb`'s sprite APIs; `tish-agb` reads
// the slice back with [`asset_sheet`]. The registry lives here (not in tish-agb)
// so the generated crate — which always depends on this facade as
// `tishlang_runtime` — can register without needing the `cargo:tish_agb` dep.

/// One registered sprite sheet: the frames of an `asset:` import.
static ASSET_SHEETS: SingleCore<RefCell<Vec<&'static [agb::display::object::Sprite]>>> =
    SingleCore::new(RefCell::new(Vec::new()));

/// Register a sprite sheet, returning its i32 handle (= its registration order).
/// Called by the generated `agb_main`, in import order, before the program body.
pub fn __asset_register_sheet(sheet: &'static [agb::display::object::Sprite]) -> i32 {
    ASSET_SHEETS.with(|c| {
        let mut v = c.borrow_mut();
        let idx = v.len() as i32;
        v.push(sheet);
        idx
    })
}

/// Look up a registered sprite sheet by handle. `None` if out of range.
pub fn asset_sheet(handle: i32) -> Option<&'static [agb::display::object::Sprite]> {
    ASSET_SHEETS.with(|c| c.borrow().get(handle as usize).copied())
}

/// One registered background: its palettes + full-screen tile data (agb
/// `include_background_gfx!` output). Registered by the generated `agb_main` for
/// each `background:` import; `tish-agb`'s `bg_new` builds a `RegularBackground`.
type BgAsset = (
    &'static [agb::display::Palette16],
    &'static agb::display::tile_data::TileData,
);

/// ⚠️ A FIXED TABLE, not a `Vec`, because every entry is `&'static` cartridge data and the registry
/// never needed to own one.
///
/// As a `Vec` it did: registering N backgrounds ran N pushes with log2(N) reallocs, all during
/// module init before the program body, and the freed buffers left the heap too fragmented for the
/// one big contiguous block a GBA UI canvas wants. A game with a handful of backgrounds booted; a
/// game with 162 died in the allocator with plenty of total heap free.
///
/// This costs `MAX_BGS * size_of::<Option<BgAsset>>()` of STATIC memory and allocates nothing.
/// ⚠️ Not a `Vec` with `reserve` — that charges every small game for the largest one.
const MAX_BGS: usize = 256;
static ASSET_BGS: SingleCore<RefCell<[Option<BgAsset>; MAX_BGS]>> =
    SingleCore::new(RefCell::new([None; MAX_BGS]));
static ASSET_BGS_N: SingleCore<RefCell<usize>> = SingleCore::new(RefCell::new(0));

/// Register a background (palettes + tile data), returning its i32 handle.
pub fn __asset_register_bg(bg: BgAsset) -> i32 {
    ASSET_BGS_N.with(|n| {
        let mut n = n.borrow_mut();
        if *n >= MAX_BGS {
            return -1;
        }
        let idx = *n as i32;
        ASSET_BGS.with(|c| c.borrow_mut()[*n] = Some(bg));
        *n += 1;
        idx
    })
}

/// Look up a registered background by handle. `None` if out of range.
pub fn asset_bg(handle: i32) -> Option<BgAsset> {
    if handle < 0 || handle as usize >= MAX_BGS {
        return None;
    }
    ASSET_BGS.with(|c| c.borrow()[handle as usize])
}

/// Registered fonts (`font<N>:` imports) — a compile-time-baked `Font`. All `font<N>:` schemes share
/// this one arena via an IDENTICAL registration call (no baked-in size — the renderer sizes sprites
/// from the font's own `line_height`), so their handles number correctly across sizes. tish-agb's
/// `text_draw` looks them up by handle.
static ASSET_FONTS: SingleCore<RefCell<Vec<&'static agb::display::font::Font>>> =
    SingleCore::new(RefCell::new(Vec::new()));

/// Register a baked font, returning its i32 handle.
pub fn __asset_register_font(font: &'static agb::display::font::Font) -> i32 {
    ASSET_FONTS.with(|c| {
        let mut v = c.borrow_mut();
        let idx = v.len() as i32;
        v.push(font);
        idx
    })
}

/// Look up a registered font by handle. `None` if out of range.
pub fn asset_font(handle: i32) -> Option<&'static agb::display::font::Font> {
    ASSET_FONTS.with(|c| c.borrow().get(handle as usize).copied())
}

/// The single global emoji fallback atlas (`emoji:` import): the SerenityOS colour sprite frames plus
/// a codepoint→frame table sorted by codepoint. There is at most one — it is a fallback shared by
/// every font, so any font lacking a glyph for an emoji codepoint renders the matching colour sprite
/// instead of a tofu box. tish-agb's text renderer reads it via [`emoji_sprite`].
type EmojiAtlas = (
    &'static [agb::display::object::Sprite],
    &'static [(u32, u16)],
);

static ASSET_EMOJI: SingleCore<RefCell<Option<EmojiAtlas>>> = SingleCore::new(RefCell::new(None));

/// Register the emoji atlas (frames + codepoint→frame table). Called once by the generated `agb_main`
/// for the `emoji:` import; the handle is unused (the atlas is a global fallback, not addressed by id).
pub fn __asset_register_emoji(
    sprites: &'static [agb::display::object::Sprite],
    table: &'static [(u32, u16)],
) -> i32 {
    ASSET_EMOJI.with(|c| *c.borrow_mut() = Some((sprites, table)));
    0
}

/// The colour sprite frame for an emoji codepoint, if an emoji atlas is registered and contains it.
/// `None` ⇒ no emoji import, or this codepoint wasn't baked ⇒ the caller falls back to the font glyph.
pub fn emoji_sprite(cp: u32) -> Option<&'static agb::display::object::Sprite> {
    ASSET_EMOJI.with(|c| {
        let (sprites, table) = (*c.borrow())?;
        let i = table.binary_search_by_key(&cp, |&(c, _)| c).ok()?;
        sprites.get(table[i].1 as usize)
    })
}

/// Registered maps (`map:` imports) — the raw ROM bytes of a baked map binary. Kept in ROM
/// (`include_bytes!`), never copied to the tish heap, so large maps don't blow EWRAM.
static ASSET_MAPS: SingleCore<RefCell<Vec<&'static [u8]>>> =
    SingleCore::new(RefCell::new(Vec::new()));

/// Register a baked map's ROM bytes, returning its i32 handle.
pub fn __asset_register_map(data: &'static [u8]) -> i32 {
    ASSET_MAPS.with(|c| {
        let mut v = c.borrow_mut();
        let idx = v.len() as i32;
        v.push(data);
        idx
    })
}

/// Look up a registered map's ROM bytes by handle. `None` if out of range.
pub fn asset_map(handle: i32) -> Option<&'static [u8]> {
    ASSET_MAPS.with(|c| c.borrow().get(handle as usize).copied())
}

/// Registered sounds (`wav:` imports). `SoundData` is `Copy` (a `&'static [u8]` handle).
static ASSET_WAVS: SingleCore<RefCell<Vec<agb::sound::mixer::SoundData>>> =
    SingleCore::new(RefCell::new(Vec::new()));

/// Register a sound, returning its i32 handle.
pub fn __asset_register_wav(data: agb::sound::mixer::SoundData) -> i32 {
    ASSET_WAVS.with(|c| {
        let mut v = c.borrow_mut();
        let idx = v.len() as i32;
        v.push(data);
        idx
    })
}

/// Look up a registered sound by handle. `None` if out of range.
pub fn asset_wav(handle: i32) -> Option<agb::sound::mixer::SoundData> {
    ASSET_WAVS.with(|c| c.borrow().get(handle as usize).copied())
}