OpenSNES
Modern Open-Source SNES Development SDK
Loading...
Searching...
No Matches
scene.h File Reference

Opt-in scene/state stack — push/pop game scenes without rolling your own state machine. More...

#include <snes/types.h>

Go to the source code of this file.

Classes

struct  Scene
 A single scene's callbacks. More...

Macros

#define SCENE_STACK_MAX   8
 Maximum scene stack depth.

Functions

u8 scenePop (void)
 Pop the top scene. Resumes the scene below.
u8 scenePush (const Scene *next)
 Push a scene onto the stack. Suspends the current top.
void sceneReplace (const Scene *next)
 Replace the top scene with another, at any depth — the bottom one included.
void sceneRun (const Scene *initial)
 Run the scene stack with initial at the bottom. Never returns.

Detailed Description

Opt-in scene/state stack — push/pop game scenes without rolling your own state machine.

Third "framework opt-in" from PHILOSOPHY.md (alongside gameloop D.1 and the asset bundle convention D.2). This module gives you a tiny stack of Scene callbacks: the topmost scene's update runs every VBlank; pushing a new scene suspends the current one and runs the new scene's init + update; popping restores the suspended scene without re-running its init.

Before
enum State { TITLE, PLAY, PAUSE };
enum State current = TITLE;
enum State suspend_stack[4]; int suspend_top = 0;
while (1) {
switch (current) {
case TITLE: title_update(); break;
case PLAY: play_update(); break;
case PAUSE: pause_update(); break;
}
}
static void title_update(void)
Definition main.c:98
static void pause_update(void)
Definition main.c:146
void WaitForVBlank(void)
Wait for next VBlank period.
After
static const Scene title = { .init = title_init, .update = title_update };
static const Scene play = { .init = play_init, .update = play_update };
static const Scene pause = { .init = pause_init, .update = pause_update };
// somewhere in title_update: scenePush(&play);
// somewhere in play_update: scenePush(&pause);
// somewhere in pause_update: scenePop();
int main(void) {
sceneRun(&title);
return 0; // never reached
}
int main(void)
Definition main.c:66
static void play(u8 sample, u8 vol, u8 pan, u16 pitch, u16 tint)
Play + tint the backdrop + update the probe oracles.
Definition main.c:71
static void pause_init(void)
Definition main.c:142
static void title_init(void)
Definition main.c:91
void sceneRun(const Scene *initial)
Run the scene stack with initial at the bottom. Never returns.
A single scene's callbacks.
Definition scene.h:148
Behaviour
  • init runs once per PUSH — every time the scene is pushed, not only the first — and it is deferred: it runs at the top of the next frame, after WaitForVBlank, not inside scenePush(). It does NOT re-run on resume after scenePop. If a scene needs "did I just resume?" logic, it owns that bookkeeping in its own state.
  • update runs every VBlank while the scene is on top of the stack. Suspended scenes (below the top) get no callbacks.
  • The stack has a fixed capacity (SCENE_STACK_MAX = 8). Push beyond that is a silent no-op — the new scene is dropped, the active scene continues. Pop on a stack of size 1 is also a silent no-op: the bottom scene stays active (the stack is never empty after sceneRun is called).
  • scenePush does not run the new scene's update synchronously. The currently-executing update finishes its frame; the next VBlank dispatches to the new top.
init timing — eager for the initial scene, deferred for pushes
  • The scene passed to sceneRun has its init called eagerly, before the first WaitForVBlank. This matches gameLoopRun's pattern and is required because the canonical init body (consoleInit → setMode → palettes → setScreenOn) must finish before the first NMI runs its DMAs.
  • Scenes pushed via scenePush have their init deferred to the next VBlank dispatch — scenePush only records the new top, the dispatcher calls init right after WaitForVBlank() returns. This guarantees a fresh ~33 K-cycle VBlank budget for init's DMAs (palette load, tile upload, tilemap copy) instead of sharing whatever the caller's update already consumed.
  • Edge cases:
    • Calling scenePop from inside init pops the scene before its update ever runs; the scene below resumes on the same frame. The popped scene effectively "ran init then bailed".
    • Calling scenePush from inside init cascades: the newly pushed scene's init also runs in the same VBlank window, chained right after the current init returns. If both inits do heavy DMAs and the combined cost exceeds the VBlank budget, move part of the work into update (which has a full new VBlank window of its own).
What this module does NOT do
  • No transitions (fade-in/out, slide). The caller renders those.
  • No resume/cleanup callbacks. Most scenes don't need them; if yours does, the first 1-2 frames of update post-pop can do the work.
  • No serialization, save-state, or scope substacks. A scene is just a pair of function pointers; you keep the data.
  • No inter-scene argument passing. Use a file-scope global for shared state — examples/basics/scene_stack does this with static u16 counter_value, which survives the counter→pause→ counter cycle because counter's init runs once on first push.
  • To replace the top scene without keeping it on the stack, call sceneReplace(&next). (Until 2026-09-20 this list recommended scenePop(); scenePush(&next);, which cannot replace the BOTTOM scene: the pop is refused at depth 1, so the push stacks — a title → game → title cycle leaked one slot per lap until pushes were dropped.)
  • No NMI integration. Pushing/popping from inside an NMI callback is undefined; do it from update only.
Performance
sceneRun adds one indirect call per frame (the top scene's update) and one direct call (WaitForVBlank) — same shape as gameLoopRun. scenePush does an array bounds check and an index update (the init call is deferred to the next frame). scenePop is a single index decrement. RAM footprint: 8 pointers × 4 bytes (the post-A6 pointer size on this target, see compiler/ABI.md) = 32 bytes of BSS, plus a single byte for the stack depth.
Modules required
scene (and gameloop's transitive WaitForVBlank from the runtime). No other lib module is pulled in by sceneRun itself.
Relationship with gameloop (D.1)
GameLoopConfig from <snes/gameloop.h> is an alias for Scene. The two types are interchangeable; gameLoopRun(&cfg) is semantically equivalent to running a single-scene stack with sceneRun(&cfg). Pick whichever name fits the use site: GameLoopConfig for "this is just a main loop", Scene for "this is one of several swappable screens".
See also
gameloop.h, system.h (WaitForVBlank, frame_count)

Macro Definition Documentation

◆ SCENE_STACK_MAX

#define SCENE_STACK_MAX   8

Maximum scene stack depth.

scenePush beyond this depth is a silent no-op. 8 is enough for realistic SNES game shapes (title → menu → play → pause → over leaves 3 spare slots).

Function Documentation

◆ scenePop()

u8 scenePop ( void )

Pop the top scene. Resumes the scene below.

The popped scene gets no callbacks; the resumed scene's update runs on the next VBlank. The resumed scene's init is NOT re-invoked — it ran once at first push.

Returns
1 if a scene was popped; 0 at depth 1 — the stack is never empty after sceneRun — where nothing happens. Note that scenePop(); scenePush(&next); therefore cannot replace the BOTTOM scene: the pop fails and the push stacks on top. Use sceneReplace().

◆ scenePush()

u8 scenePush ( const Scene * next)

Push a scene onto the stack. Suspends the current top.

Does NOT call next->init itself: it records the scene, and sceneRun() calls init (if non-NULL) at the top of the next frame, then update. The caller's currently-executing update finishes its frame first.

Parameters
nextScene to push. Same lifetime contract as sceneRun's initial.
Returns
1 if pushed; 0 if the stack is already at SCENE_STACK_MAX and nothing happened. (It returned nothing, and failed silently, until 2026-09-20.)

◆ sceneReplace()

void sceneReplace ( const Scene * next)

Replace the top scene with another, at any depth — the bottom one included.

The scene-swap primitive scenePop(); scenePush(&next); only approximates: that idiom leaks a stack slot per swap when the scene being replaced is the bottom one, until the 8-deep stack silently drops pushes. next->init runs at the top of the next frame, like after a push.

Parameters
nextScene that takes the top slot. Same lifetime contract as sceneRun's initial.

◆ sceneRun()

void sceneRun ( const Scene * initial)

Run the scene stack with initial at the bottom. Never returns.

Pushes initial, calls its init (if non-NULL), and enters the dispatch loop:

while (1) { WaitForVBlank(); top->update(); }
Parameters
initialFirst scene. Must be non-NULL and have a non-NULL update. The pointer is held for the lifetime of the scene's stack residency, so the caller MUST keep the Scene struct alive for at least that long (typically: place it in static const).