Skip to content

Extract RollerRuntime replay stepping - #284

Draft
deevus wants to merge 9 commits into
masterfrom
worktree/green-forest-0a1b
Draft

Extract RollerRuntime replay stepping#284
deevus wants to merge 9 commits into
masterfrom
worktree/green-forest-0a1b

Conversation

@deevus

@deevus deevus commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add an SDL-free RollerRuntime C API for fixed-step simulation ownership
  • route replay snapshot ticking through RollerRuntime behind --runtime-snapshot
  • add zig build test-runtime-snapshots as a parallel replay snapshot gate
  • support stack-owned runtimes for embedders such as gdzig/Godot, with heap New/Delete convenience APIs

Validation

  • zig build test-roller-runtime-api test-roller-runtime-step
  • python3 tools/check_roller_core_manifest.py
  • runtime replay scratch snapshots byte-match legacy replay scratch snapshots on this host

Notes

This PR intentionally keeps normal interactive gameplay on the existing loop. The runtime is currently wired into the replay snapshot harness only.

Non-scratch snapshot gates still need canonical reference-host verification. On this Linux host, checked-in PNG baselines produce host-dependent diffs; those PNG changes were not committed.

Design/spec and implementation-plan artifacts are attached as PR comments rather than kept in the branch.

@deevus

deevus commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

Design spec (planning artifact)

Moved out of the branch and attached here for review context.

RollerRuntime extraction design

RollerRuntime extraction design

  • Date: 2026-08-02
  • Status: Design approved in chat; pending written-spec review
  • Diagram: docs/superpowers/diagrams/roller-runtime-architecture.mmd

Goal

Extract the game loop toward a reusable RollerRuntime module so the game simulation can be driven outside the current executable loop. The long-term consumers are tests, tooling, editor integration, and external engines.

The first useful milestone is not arbitrary high-level input. It is a replay-driven runtime path that can run the existing snapshot harness. Replay loading/parsing stays outside RollerRuntime; the runtime consumes an input/timeline source and must reproduce the existing VCS-managed snapshot PNG baselines.

Non-goals for the first milestone

  • Do not rewrite physics, control, replay, or race rules.
  • Do not expose SDL in the public runtime interface.
  • Do not expose a RollerRuntime_RenderFrame function.
  • Do not design the final external-engine rendering interface yet.
  • Do not replace the existing snapshot harness immediately.

Design principles

Final interface first, minimal implementation first

Create the RollerRuntime seam now, but initially implement it as a thin strangler around the existing exact tick path. The interface should be stable and SDL-free; the implementation may still drive legacy globals and existing functions while parity is established.

Runtime and rendering remain separate modules

RollerRuntime owns and advances authoritative simulation/game state. It does not render.

Framebuffer production belongs to renderer consumers. Existing game_render and scene_render should become consumers of runtime state, and a future public renderer module can wrap those consumers. RollerRuntime_RenderFrame would be too tightly coupled and is explicitly rejected.

Existing snapshot PNGs are the baseline

The current VCS-managed snapshot PNGs remain authoritative. A runtime-driven snapshot path should write the same baseline files and use the existing git-diff workflow to detect pixel drift.

Target architecture

flowchart LR
  subgraph Inputs[Input sources]
    Replay[Replay files]
    Scripted[Future high-level actions]
    Host[Future external host input]
  end

  ReplaySource[Replay input source]
  Runtime[RollerRuntime SDL-free interface]
  Adapter[Runtime adapter legacy-compatible implementation]
  StateView[Renderer-facing runtime state view]

  subgraph Legacy[Existing engine internals]
    Setup[Direct and replay setup]
    Tick[Existing tick path]
    State[Authoritative simulation state]
  end

  subgraph Renderers[Renderer consumers]
    GameRender[game_render]
    SceneRender[scene_render]
    Snapshot[Snapshot adapter]
  end

  Baselines[VCS-managed snapshot PNG baselines]
  Telemetry[Future stable telemetry]

  Replay --> ReplaySource
  ReplaySource --> Runtime
  Scripted -. milestone 2 .-> Runtime
  Host -. later .-> Runtime
  Runtime --> Adapter
  Adapter --> Setup
  Adapter --> Tick
  Tick --> State
  State --> StateView
  StateView --> GameRender
  StateView --> SceneRender
  GameRender --> Snapshot
  SceneRender --> Snapshot
  Snapshot --> Baselines
  Runtime -. later .-> Telemetry
Loading

Public RollerRuntime surface

The public runtime interface should use plain C types and should not require including SDL headers. The first version should support only replay-driven stepping:

  • create/destroy runtime
  • configure deterministic/headless runtime settings
  • attach or select a caller-owned input source
  • step a fixed number of logical ticks
  • query status/error information

A future milestone can add high-level player action input. That input is still valuable for tests, bots, and external engines, but replay-sourced stepping comes first because it has lower parity risk and plugs directly into the existing snapshot baseline. The replay file format and loader remain a separate module/adapter concern, not a RollerRuntime responsibility.

Runtime implementation strategy

The initial implementation should preserve behavior by driving the existing code path:

  • consume replay playback through a separate replay/input-source adapter
  • reuse current RNG behavior
  • call tick_clock_step, game_tick_step, and control_one_tick as the primary stepping mechanism; any replacement must prove parity first
  • bypass frontend/menu flow for runtime-driven tests
  • disable or stub audio, network, window, and SDL input devices
  • keep any SDL use internal and temporary

The runtime adapter translates public runtime calls into the legacy state transitions the current engine already understands. The public interface must not expose Car[], SDL events, scancodes, or mutable globals.

Renderer-facing runtime state

The renderer-facing state view is the key seam between runtime and rendering.

Initially, this view may be the existing legacy global state. That is acceptable because it maximizes parity and minimizes extraction risk. Over time, game_render and scene_render should read through a more explicit state view owned by RollerRuntime rather than freely reaching into mutable legacy globals.

The intended direction is:

  1. RollerRuntime drives legacy state.
  2. Existing snapshot rendering reads that runtime-driven state and reproduces current PNG baselines.
  3. game_render and scene_render gradually consume an explicit runtime-state view.
  4. A future RollerRenderer module can provide public rendering/presentation functionality while staying separate from RollerRuntime.

Snapshot migration strategy

Add a parallel runtime snapshot build step before replacing the current harness:

  • keep zig build test-snapshots unchanged
  • add zig build test-runtime-snapshots
  • drive the same replay/frame list through RollerRuntime
  • capture the same PNG filenames under tests/snapshots/baselines/
  • compare by the existing VCS-managed baseline workflow

Because the PNGs are version controlled, a mismatch naturally appears as a working-tree diff. An optional scratch mode may write to zig-out/runtime-snapshot-scratch/ for manual debugging, but the main acceptance path should use the checked-in baselines.

Migration stages:

  1. Parallel path: existing snapshots remain authoritative; runtime snapshots prove parity.
  2. Divergence debugging: if runtime snapshots differ, compare legacy-harness and runtime-harness output to isolate extraction regressions.
  3. Default switch: once stable, make test-snapshots use the runtime path.
  4. Old driver removal: after a confidence period, remove the legacy snapshot driver path.

Milestones

Milestone 1: replay-driven runtime snapshot parity

  • Introduce SDL-free RollerRuntime public interface.
  • Implement replay/input-source attachment and fixed stepping through the runtime adapter.
  • Wire a parallel test-runtime-snapshots build step.
  • Use existing VCS-managed snapshot PNG baselines as the authoritative comparison.
  • Acceptance: both zig build test-snapshots and zig build test-runtime-snapshots pass against the same baselines.

Milestone 2: stable runtime observability

  • Add small telemetry/status structs for tests and tools.
  • Keep telemetry copied out of runtime state; do not expose mutable legacy state.
  • Use telemetry for debugging and targeted tests after snapshot parity is established.

Milestone 3: high-level input source

  • Extend the input-source abstraction behind RollerRuntime.
  • Support high-level player actions such as steering, throttle, brake, gear up/down, and action/cheat.
  • Convert those actions into the same legacy input data path as current gameplay.
  • Use this for controllable simulations, bots, fuzz tests, and external host integration.

Milestone 4: explicit renderer-facing state view

  • Move game_render and scene_render toward consuming a runtime-owned state view.
  • Keep rendering separate from runtime.
  • Prepare a future RollerRenderer module without adding RollerRuntime_RenderFrame.

Acceptance criteria

The design is successful when:

  • RollerRuntime has a small SDL-free public interface.
  • The first runtime implementation preserves current replay behavior.
  • Runtime-driven snapshots match the existing checked-in PNG baselines.
  • The old and new snapshot paths can run side by side during migration.
  • Rendering remains a separate consumer of runtime state rather than a method on runtime.
  • Later high-level input can be added without changing the runtime/rendering seam.

Open decisions for implementation planning

  • Exact public header names and symbol prefixes.
  • Which existing startup/init functions can be reused safely in headless runtime mode.
  • Whether the separate replay/input-source adapter should accept file paths, caller-provided bytes, or both.
  • How to structure test-only snapshot adapters versus future public renderer modules.
  • When to switch test-snapshots from the legacy path to the runtime path.

@deevus

deevus commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

Architecture diagram (planning artifact)

Moved out of the branch and attached here for review context.

flowchart LR
  subgraph Inputs[Input sources]
    Replay[Replay files]
    Scripted[Future high-level actions]
    Host[Future external host input]
  end

  ReplaySource[Replay input source]
  Runtime[RollerRuntime SDL-free interface]
  Adapter[Runtime adapter legacy-compatible implementation]
  StateView[Renderer-facing runtime state view]

  subgraph Legacy[Existing engine internals]
    Setup[Direct and replay setup]
    Tick[Existing tick path]
    State[Authoritative simulation state]
  end

  subgraph Renderers[Renderer consumers]
    GameRender[game_render]
    SceneRender[scene_render]
    Snapshot[Snapshot adapter]
  end

  Baselines[VCS-managed snapshot PNG baselines]
  Telemetry[Future stable telemetry]

  Replay --> ReplaySource
  ReplaySource --> Runtime
  Scripted -. milestone 2 .-> Runtime
  Host -. later .-> Runtime
  Runtime --> Adapter
  Adapter --> Setup
  Adapter --> Tick
  Tick --> State
  State --> StateView
  StateView --> GameRender
  StateView --> SceneRender
  GameRender --> Snapshot
  SceneRender --> Snapshot
  Snapshot --> Baselines
  Runtime -. later .-> Telemetry

Loading

@deevus

deevus commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

Implementation plan (planning artifact)

Moved out of the branch and attached here for review context.

Full implementation plan

RollerRuntime Replay Snapshot Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use subagent-driven-development to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add the first replay-driven RollerRuntime seam and wire a parallel runtime snapshot test path that must match the existing VCS-managed PNG baselines.

Architecture: Introduce a small SDL-free public runtime interface that owns stepping, not replay file loading. Replay loading/parsing remains outside runtime; the first snapshot integration composes existing replay setup with RollerRuntime_Step. The current snapshot render/capture code remains the framebuffer producer, and test-snapshots stays unchanged while test-runtime-snapshots runs beside it.

Tech Stack: C11, Zig build system, existing SDL-linked legacy engine internals, existing snapshot PNG baseline workflow.

Global Constraints

  • The public RollerRuntime interface must not include SDL headers.
  • RollerRuntime must not load replay files or know .gss file-format details.
  • Do not add RollerRuntime_RenderFrame; rendering remains a separate consumer of runtime state.
  • Preserve exact current replay behavior by calling the existing tick path.
  • Keep zig build test-snapshots unchanged until runtime parity is proven.
  • Add zig build test-runtime-snapshots as a parallel path against the same VCS-managed PNG baselines.
  • Use Conventional Commit messages.

File Structure

  • PROJECTS/ROLLER/roller_runtime.h: new SDL-free public runtime interface. Owns lifecycle, input-source attachment, stepping, status, and errors. Does not mention replay files.
  • PROJECTS/ROLLER/roller_runtime.c: new runtime adapter implementation. Initially delegates stepping to existing tick functions.
  • tests/roller_runtime_api_test.c: public API/lifecycle test that includes only roller_runtime.h from the runtime surface.
  • tests/roller_runtime_step_test.c: fixed-step seam test using test hook macros so it can verify the runtime calls the input-source callback and legacy tick sequence without booting assets.
  • PROJECTS/ROLLER/3d.c: parse --runtime-snapshot, create/destroy a runtime for replay snapshots, attach a no-op input source because existing snapshot setup already loaded the replay state, and route snapshot tick advancement through the runtime when requested.
  • build.zig: compile the new runtime source, add unit tests, and add test-runtime-snapshots.
  • CMakeLists.txt: add roller_runtime.c to the CMake source list for source-set parity.
  • roller-core.srclist: classify roller_runtime.c as a kept public runtime source.
  • tests/snapshots/README.md: document test-runtime-snapshots and the relationship to test-snapshots.

Task 1: Add SDL-free RollerRuntime public API and lifecycle test

Files:

  • Create: PROJECTS/ROLLER/roller_runtime.h
  • Create: PROJECTS/ROLLER/roller_runtime.c
  • Create: tests/roller_runtime_api_test.c
  • Modify: build.zig
  • Modify: CMakeLists.txt
  • Modify: roller-core.srclist

Interfaces:

  • Consumes: no new project interfaces.

  • Produces:

    • typedef struct RollerRuntime RollerRuntime;
    • typedef eRollerRuntimeResult (ROLLER_RUNTIME_CALL *RollerRuntimeInputAdvanceFn)(void *pUserData, uint32_t uiTickIndex);
    • eRollerRuntimeResult RollerRuntime_Create(const tRollerRuntimeConfig *pConfig, RollerRuntime **ppRuntime);
    • void RollerRuntime_Destroy(RollerRuntime *pRuntime);
    • eRollerRuntimeResult RollerRuntime_SetInputSource(RollerRuntime *pRuntime, const tRollerRuntimeInputSource *pSource);
    • eRollerRuntimeResult RollerRuntime_ClearInputSource(RollerRuntime *pRuntime);
    • eRollerRuntimeResult RollerRuntime_Step(RollerRuntime *pRuntime, uint32_t uiTicks);
    • eRollerRuntimeStatus RollerRuntime_GetStatus(const RollerRuntime *pRuntime);
    • const char *RollerRuntime_GetLastError(const RollerRuntime *pRuntime);
  • Step 1: Write the failing API lifecycle test

Create tests/roller_runtime_api_test.c:

#include "roller_runtime.h"

#include <stdio.h>
#include <string.h>

#define CHECK(condition) \
  do { \
    if (!(condition)) { \
      fprintf(stderr, "roller_runtime_api_test failed at line %d: %s\n", \
              __LINE__, #condition); \
      return 1; \
    } \
  } while (0)

static eRollerRuntimeResult ROLLER_RUNTIME_CALL test_advance(void *pUserData, uint32_t uiTickIndex)
{
  int *piAdvanceCount = (int *)pUserData;
  if (!piAdvanceCount)
    return ROLLER_RUNTIME_RESULT_INVALID_ARGUMENT;
  *piAdvanceCount += (int)uiTickIndex + 1;
  return ROLLER_RUNTIME_RESULT_OK;
}

int main(void)
{
  RollerRuntime *pRuntime = (RollerRuntime *)0x1;
  int iAdvanceCount = 0;
  tRollerRuntimeConfig config = {
    .uiStructSize = sizeof(config),
    .uiVersion = ROLLER_RUNTIME_API_VERSION,
    .uiFlags = ROLLER_RUNTIME_FLAG_HEADLESS | ROLLER_RUNTIME_FLAG_DETERMINISTIC,
  };
  tRollerRuntimeInputSource source = {
    .uiStructSize = sizeof(source),
    .uiVersion = ROLLER_RUNTIME_API_VERSION,
    .pUserData = &iAdvanceCount,
    .pfnAdvance = test_advance,
  };

  CHECK(RollerRuntime_Create(NULL, &pRuntime) == ROLLER_RUNTIME_RESULT_INVALID_ARGUMENT);
  CHECK(RollerRuntime_Create(&config, NULL) == ROLLER_RUNTIME_RESULT_INVALID_ARGUMENT);

  config.uiVersion++;
  pRuntime = NULL;
  CHECK(RollerRuntime_Create(&config, &pRuntime) == ROLLER_RUNTIME_RESULT_INVALID_VERSION);
  CHECK(pRuntime == NULL);

  config.uiVersion = ROLLER_RUNTIME_API_VERSION;
  CHECK(RollerRuntime_Create(&config, &pRuntime) == ROLLER_RUNTIME_RESULT_OK);
  CHECK(pRuntime != NULL);
  CHECK(RollerRuntime_GetStatus(pRuntime) == ROLLER_RUNTIME_STATUS_CREATED);
  CHECK(strcmp(RollerRuntime_GetLastError(pRuntime), "") == 0);

  CHECK(RollerRuntime_SetInputSource(NULL, &source) == ROLLER_RUNTIME_RESULT_INVALID_ARGUMENT);
  CHECK(RollerRuntime_SetInputSource(pRuntime, NULL) == ROLLER_RUNTIME_RESULT_INVALID_ARGUMENT);

  source.uiVersion++;
  CHECK(RollerRuntime_SetInputSource(pRuntime, &source) == ROLLER_RUNTIME_RESULT_INVALID_VERSION);

  source.uiVersion = ROLLER_RUNTIME_API_VERSION;
  source.pfnAdvance = NULL;
  CHECK(RollerRuntime_SetInputSource(pRuntime, &source) == ROLLER_RUNTIME_RESULT_INVALID_ARGUMENT);

  source.pfnAdvance = test_advance;
  CHECK(RollerRuntime_SetInputSource(pRuntime, &source) == ROLLER_RUNTIME_RESULT_OK);
  CHECK(RollerRuntime_GetStatus(pRuntime) == ROLLER_RUNTIME_STATUS_READY);
  CHECK(RollerRuntime_ClearInputSource(pRuntime) == ROLLER_RUNTIME_RESULT_OK);
  CHECK(RollerRuntime_GetStatus(pRuntime) == ROLLER_RUNTIME_STATUS_CREATED);

  RollerRuntime_Destroy(pRuntime);
  RollerRuntime_Destroy(NULL);
  return 0;
}
  • Step 2: Run the test to verify it fails

Run: zig build test-roller-runtime-api

Expected: FAIL because test-roller-runtime-api and PROJECTS/ROLLER/roller_runtime.h do not exist.

  • Step 3: Add the public API header

Create PROJECTS/ROLLER/roller_runtime.h:

#ifndef ROLLER_RUNTIME_H
#define ROLLER_RUNTIME_H

#include <stddef.h>
#include <stdint.h>

#if defined(_WIN32)
#  if defined(ROLLER_RUNTIME_BUILD_SHARED)
#    if defined(ROLLER_RUNTIME_EXPORTS)
#      define ROLLER_RUNTIME_API __declspec(dllexport)
#    else
#      define ROLLER_RUNTIME_API __declspec(dllimport)
#    endif
#  else
#    define ROLLER_RUNTIME_API
#  endif
#  define ROLLER_RUNTIME_CALL __cdecl
#elif defined(__GNUC__) || defined(__clang__)
#  define ROLLER_RUNTIME_API __attribute__((visibility("default")))
#  define ROLLER_RUNTIME_CALL
#else
#  define ROLLER_RUNTIME_API
#  define ROLLER_RUNTIME_CALL
#endif

#if defined(__cplusplus)
extern "C" {
#endif

#define ROLLER_RUNTIME_API_VERSION 1u

typedef struct RollerRuntime RollerRuntime;

typedef uint32_t eRollerRuntimeResult;
enum
{
  ROLLER_RUNTIME_RESULT_OK = 0u,
  ROLLER_RUNTIME_RESULT_INVALID_ARGUMENT = 1u,
  ROLLER_RUNTIME_RESULT_INVALID_VERSION = 2u,
  ROLLER_RUNTIME_RESULT_OUT_OF_MEMORY = 3u,
  ROLLER_RUNTIME_RESULT_INVALID_STATE = 4u,
  ROLLER_RUNTIME_RESULT_STEP_FAILED = 5u,
};

typedef uint32_t eRollerRuntimeStatus;
enum
{
  ROLLER_RUNTIME_STATUS_EMPTY = 0u,
  ROLLER_RUNTIME_STATUS_CREATED = 1u,
  ROLLER_RUNTIME_STATUS_READY = 2u,
  ROLLER_RUNTIME_STATUS_RUNNING = 3u,
  ROLLER_RUNTIME_STATUS_FINISHED = 4u,
  ROLLER_RUNTIME_STATUS_FAILED = 5u,
};

enum
{
  ROLLER_RUNTIME_FLAG_HEADLESS = 1u << 0,
  ROLLER_RUNTIME_FLAG_DETERMINISTIC = 1u << 1,
};

typedef eRollerRuntimeResult (ROLLER_RUNTIME_CALL *RollerRuntimeInputAdvanceFn)(
    void *pUserData, uint32_t uiTickIndex);

typedef struct
{
  uint32_t uiStructSize;
  uint32_t uiVersion;
  uint32_t uiFlags;
} tRollerRuntimeConfig;

typedef struct
{
  uint32_t uiStructSize;
  uint32_t uiVersion;
  void *pUserData;
  RollerRuntimeInputAdvanceFn pfnAdvance;
} tRollerRuntimeInputSource;

ROLLER_RUNTIME_API eRollerRuntimeResult ROLLER_RUNTIME_CALL
RollerRuntime_Create(const tRollerRuntimeConfig *pConfig,
                     RollerRuntime **ppRuntime);

ROLLER_RUNTIME_API void ROLLER_RUNTIME_CALL
RollerRuntime_Destroy(RollerRuntime *pRuntime);

ROLLER_RUNTIME_API eRollerRuntimeResult ROLLER_RUNTIME_CALL
RollerRuntime_SetInputSource(RollerRuntime *pRuntime,
                             const tRollerRuntimeInputSource *pSource);

ROLLER_RUNTIME_API eRollerRuntimeResult ROLLER_RUNTIME_CALL
RollerRuntime_ClearInputSource(RollerRuntime *pRuntime);

ROLLER_RUNTIME_API eRollerRuntimeResult ROLLER_RUNTIME_CALL
RollerRuntime_Step(RollerRuntime *pRuntime, uint32_t uiTicks);

ROLLER_RUNTIME_API eRollerRuntimeStatus ROLLER_RUNTIME_CALL
RollerRuntime_GetStatus(const RollerRuntime *pRuntime);

ROLLER_RUNTIME_API const char *ROLLER_RUNTIME_CALL
RollerRuntime_GetLastError(const RollerRuntime *pRuntime);

#if defined(__cplusplus)
}
#endif

#endif
  • Step 4: Add minimal lifecycle implementation

Create PROJECTS/ROLLER/roller_runtime.c:

#include "roller_runtime.h"

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

struct RollerRuntime
{
  uint32_t uiFlags;
  eRollerRuntimeStatus eStatus;
  tRollerRuntimeInputSource InputSource;
  int iHasInputSource;
  char szLastError[512];
};

static void runtime_clear_error(RollerRuntime *pRuntime)
{
  if (pRuntime)
    pRuntime->szLastError[0] = '\0';
}

static void runtime_set_error(RollerRuntime *pRuntime, const char *szFormat, ...)
{
  if (!pRuntime)
    return;

  va_list args;
  va_start(args, szFormat);
  vsnprintf(pRuntime->szLastError, sizeof(pRuntime->szLastError), szFormat, args);
  va_end(args);
  pRuntime->szLastError[sizeof(pRuntime->szLastError) - 1u] = '\0';
}

static eRollerRuntimeResult runtime_validate_struct(uint32_t uiStructSize,
                                                    uint32_t uiVersion,
                                                    uint32_t uiRequiredSize)
{
  if (uiVersion != ROLLER_RUNTIME_API_VERSION)
    return ROLLER_RUNTIME_RESULT_INVALID_VERSION;
  if (uiStructSize < uiRequiredSize)
    return ROLLER_RUNTIME_RESULT_INVALID_ARGUMENT;
  return ROLLER_RUNTIME_RESULT_OK;
}

eRollerRuntimeResult ROLLER_RUNTIME_CALL
RollerRuntime_Create(const tRollerRuntimeConfig *pConfig,
                     RollerRuntime **ppRuntime)
{
  eRollerRuntimeResult eResult;
  RollerRuntime *pRuntime;

  if (!ppRuntime)
    return ROLLER_RUNTIME_RESULT_INVALID_ARGUMENT;
  *ppRuntime = NULL;
  if (!pConfig)
    return ROLLER_RUNTIME_RESULT_INVALID_ARGUMENT;

  eResult = runtime_validate_struct(pConfig->uiStructSize, pConfig->uiVersion,
                                    sizeof(*pConfig));
  if (eResult != ROLLER_RUNTIME_RESULT_OK)
    return eResult;

  pRuntime = (RollerRuntime *)calloc(1, sizeof(*pRuntime));
  if (!pRuntime)
    return ROLLER_RUNTIME_RESULT_OUT_OF_MEMORY;

  pRuntime->uiFlags = pConfig->uiFlags;
  pRuntime->eStatus = ROLLER_RUNTIME_STATUS_CREATED;
  *ppRuntime = pRuntime;
  return ROLLER_RUNTIME_RESULT_OK;
}

void ROLLER_RUNTIME_CALL RollerRuntime_Destroy(RollerRuntime *pRuntime)
{
  free(pRuntime);
}

eRollerRuntimeResult ROLLER_RUNTIME_CALL
RollerRuntime_SetInputSource(RollerRuntime *pRuntime,
                             const tRollerRuntimeInputSource *pSource)
{
  eRollerRuntimeResult eResult;

  if (!pRuntime || !pSource)
    return ROLLER_RUNTIME_RESULT_INVALID_ARGUMENT;
  runtime_clear_error(pRuntime);

  eResult = runtime_validate_struct(pSource->uiStructSize, pSource->uiVersion,
                                    sizeof(*pSource));
  if (eResult != ROLLER_RUNTIME_RESULT_OK)
    return eResult;
  if (!pSource->pfnAdvance) {
    runtime_set_error(pRuntime, "input source requires pfnAdvance");
    return ROLLER_RUNTIME_RESULT_INVALID_ARGUMENT;
  }

  pRuntime->InputSource = *pSource;
  pRuntime->iHasInputSource = 1;
  pRuntime->eStatus = ROLLER_RUNTIME_STATUS_READY;
  return ROLLER_RUNTIME_RESULT_OK;
}

eRollerRuntimeResult ROLLER_RUNTIME_CALL
RollerRuntime_ClearInputSource(RollerRuntime *pRuntime)
{
  if (!pRuntime)
    return ROLLER_RUNTIME_RESULT_INVALID_ARGUMENT;
  runtime_clear_error(pRuntime);
  memset(&pRuntime->InputSource, 0, sizeof(pRuntime->InputSource));
  pRuntime->iHasInputSource = 0;
  pRuntime->eStatus = ROLLER_RUNTIME_STATUS_CREATED;
  return ROLLER_RUNTIME_RESULT_OK;
}

eRollerRuntimeResult ROLLER_RUNTIME_CALL
RollerRuntime_Step(RollerRuntime *pRuntime, uint32_t uiTicks)
{
  (void)uiTicks;
  if (!pRuntime)
    return ROLLER_RUNTIME_RESULT_INVALID_ARGUMENT;
  runtime_set_error(pRuntime, "runtime stepping is not connected yet");
  pRuntime->eStatus = ROLLER_RUNTIME_STATUS_FAILED;
  return ROLLER_RUNTIME_RESULT_STEP_FAILED;
}

eRollerRuntimeStatus ROLLER_RUNTIME_CALL
RollerRuntime_GetStatus(const RollerRuntime *pRuntime)
{
  return pRuntime ? pRuntime->eStatus : ROLLER_RUNTIME_STATUS_EMPTY;
}

const char *ROLLER_RUNTIME_CALL
RollerRuntime_GetLastError(const RollerRuntime *pRuntime)
{
  return pRuntime ? pRuntime->szLastError : "";
}
  • Step 5: Add build entries for the runtime API test

Modify build.zig by adding PROJECTS/ROLLER/roller_runtime.c to the main executable source list beside PROJECTS/ROLLER/roller_core_error.c.

Add a test configuration function near the existing test configuration helpers:

fn configureRollerRuntimeApiTests(
    b: *Build,
    target: ResolvedTarget,
    optimize: OptimizeMode,
    c_flags: []const []const u8,
) void {
    const runtime_mod = b.createModule(.{
        .target = target,
        .optimize = optimize,
        .link_libc = true,
    });
    runtime_mod.addIncludePath(b.path("PROJECTS/ROLLER"));
    runtime_mod.addCSourceFiles(.{
        .flags = c_flags,
        .files = &.{
            "PROJECTS/ROLLER/roller_runtime.c",
            "tests/roller_runtime_api_test.c",
        },
    });

    const runtime_test = b.addExecutable(.{
        .name = "roller_runtime_api_test",
        .root_module = runtime_mod,
    });
    const run_runtime_test = b.addRunArtifact(runtime_test);
    const runtime_tests = b.step(
        "test-roller-runtime-api",
        "Run RollerRuntime public API lifecycle tests",
    );
    runtime_tests.dependOn(&run_runtime_test.step);

    const test_step = b.step("test", "Run unit tests");
    test_step.dependOn(runtime_tests);
}

Call it from build() near the other test configuration calls:

    configureRollerRuntimeApiTests(b, target, optimize, c_flags);
  • Step 6: Update alternate source manifests

Modify CMakeLists.txt ROLLER_GAME_BASE_SOURCES to include:

    PROJECTS/ROLLER/roller_runtime.c

Modify roller-core.srclist in the KEEP section to include:

KEEP|PROJECTS/ROLLER/roller_runtime.c
  • Step 7: Run the API test and source-set checks

Run: zig build test-roller-runtime-api

Expected: PASS.

Run: python3 tools/check_roller_core_manifest.py

Expected: output begins with roller-core manifest check passed:.

Run: zig build -Dpython-checks=true

Expected: build succeeds or fails only for missing local asset prerequisites unrelated to compilation. If it fails for source-set drift, update CMakeLists.txt, build.zig, and roller-core.srclist consistently before continuing.

  • Step 8: Commit

Run: git status --short

Expected: only the runtime API, test, and build/source-list files are modified.

Run: git add PROJECTS/ROLLER/roller_runtime.h PROJECTS/ROLLER/roller_runtime.c tests/roller_runtime_api_test.c build.zig CMakeLists.txt roller-core.srclist && git commit -m "feat: add RollerRuntime public API shell"

Expected: commit succeeds.


Task 2: Connect RollerRuntime_Step to input-source advancement and the existing tick path

Files:

  • Modify: PROJECTS/ROLLER/roller_runtime.c
  • Create: tests/roller_runtime_step_test.c
  • Modify: build.zig

Interfaces:

  • Consumes: Task 1 RollerRuntime API.

  • Produces: RollerRuntime_Step calls the attached input-source advance callback and then the legacy tick sequence once per requested tick.

  • Step 1: Write the failing step test

Create tests/roller_runtime_step_test.c:

#include "roller_runtime.h"

#include <stdio.h>

int g_runtime_test_frontend_on = 0;
int g_runtime_test_tick_clock_calls = 0;
int g_runtime_test_game_tick_calls = 0;
int g_runtime_test_clear_pending_calls = 0;
int g_runtime_test_input_advance_calls = 0;
uint32_t g_runtime_test_last_tick_index = 999u;

void runtime_test_tick_clock_step(void)
{
  g_runtime_test_tick_clock_calls++;
}

void runtime_test_game_tick_step(void)
{
  g_runtime_test_game_tick_calls++;
}

void runtime_test_clear_pending_ticks(void)
{
  g_runtime_test_clear_pending_calls++;
}

static eRollerRuntimeResult ROLLER_RUNTIME_CALL input_advance(void *pUserData, uint32_t uiTickIndex)
{
  int *piAccumulator = (int *)pUserData;
  if (!piAccumulator)
    return ROLLER_RUNTIME_RESULT_INVALID_ARGUMENT;
  g_runtime_test_input_advance_calls++;
  g_runtime_test_last_tick_index = uiTickIndex;
  *piAccumulator += 1;
  return ROLLER_RUNTIME_RESULT_OK;
}

#define CHECK(condition) \
  do { \
    if (!(condition)) { \
      fprintf(stderr, "roller_runtime_step_test failed at line %d: %s\n", \
              __LINE__, #condition); \
      return 1; \
    } \
  } while (0)

int main(void)
{
  RollerRuntime *pRuntime = NULL;
  int iInputAccumulator = 0;
  tRollerRuntimeConfig config = {
    .uiStructSize = sizeof(config),
    .uiVersion = ROLLER_RUNTIME_API_VERSION,
    .uiFlags = ROLLER_RUNTIME_FLAG_HEADLESS | ROLLER_RUNTIME_FLAG_DETERMINISTIC,
  };
  tRollerRuntimeInputSource source = {
    .uiStructSize = sizeof(source),
    .uiVersion = ROLLER_RUNTIME_API_VERSION,
    .pUserData = &iInputAccumulator,
    .pfnAdvance = input_advance,
  };

  CHECK(RollerRuntime_Step(NULL, 1) == ROLLER_RUNTIME_RESULT_INVALID_ARGUMENT);
  CHECK(RollerRuntime_Create(&config, &pRuntime) == ROLLER_RUNTIME_RESULT_OK);
  CHECK(RollerRuntime_Step(pRuntime, 1) == ROLLER_RUNTIME_RESULT_INVALID_STATE);

  CHECK(RollerRuntime_SetInputSource(pRuntime, &source) == ROLLER_RUNTIME_RESULT_OK);
  CHECK(RollerRuntime_Step(pRuntime, 0) == ROLLER_RUNTIME_RESULT_INVALID_ARGUMENT);
  CHECK(RollerRuntime_Step(pRuntime, 3) == ROLLER_RUNTIME_RESULT_OK);
  CHECK(g_runtime_test_input_advance_calls == 3);
  CHECK(g_runtime_test_last_tick_index == 2u);
  CHECK(iInputAccumulator == 3);
  CHECK(g_runtime_test_tick_clock_calls == 3);
  CHECK(g_runtime_test_clear_pending_calls == 3);
  CHECK(g_runtime_test_game_tick_calls == 3);
  CHECK(RollerRuntime_GetStatus(pRuntime) == ROLLER_RUNTIME_STATUS_RUNNING);

  g_runtime_test_frontend_on = 1;
  CHECK(RollerRuntime_Step(pRuntime, 2) == ROLLER_RUNTIME_RESULT_OK);
  CHECK(g_runtime_test_input_advance_calls == 5);
  CHECK(g_runtime_test_tick_clock_calls == 5);
  CHECK(g_runtime_test_clear_pending_calls == 5);
  CHECK(g_runtime_test_game_tick_calls == 3);

  RollerRuntime_Destroy(pRuntime);
  return 0;
}
  • Step 2: Run the test to verify it fails

Run: zig build test-roller-runtime-step

Expected: FAIL because the build step does not exist.

  • Step 3: Add test seam hooks to roller_runtime.c

At the top of PROJECTS/ROLLER/roller_runtime.c, after standard includes, add:

#if defined(ROLLER_RUNTIME_TEST_SEAMS)
extern int g_runtime_test_frontend_on;
void runtime_test_tick_clock_step(void);
void runtime_test_game_tick_step(void);
void runtime_test_clear_pending_ticks(void);
#define RUNTIME_FRONTEND_ON() (g_runtime_test_frontend_on)
#define RUNTIME_TICK_CLOCK_STEP() runtime_test_tick_clock_step()
#define RUNTIME_GAME_TICK_STEP() runtime_test_game_tick_step()
#define RUNTIME_CLEAR_PENDING_TICKS() runtime_test_clear_pending_ticks()
#else
#include "frontend.h"
#include "roller.h"
#include "sound.h"
#include <SDL3/SDL_atomic.h>
#define RUNTIME_FRONTEND_ON() (frontend_on)
#define RUNTIME_TICK_CLOCK_STEP() tick_clock_step()
#define RUNTIME_GAME_TICK_STEP() game_tick_step()
#define RUNTIME_CLEAR_PENDING_TICKS() SDL_SetAtomicInt(&iTicksPending, 0)
#endif

Replace RollerRuntime_Step with:

eRollerRuntimeResult ROLLER_RUNTIME_CALL
RollerRuntime_Step(RollerRuntime *pRuntime, uint32_t uiTicks)
{
  if (!pRuntime)
    return ROLLER_RUNTIME_RESULT_INVALID_ARGUMENT;
  runtime_clear_error(pRuntime);
  if (uiTicks == 0u) {
    runtime_set_error(pRuntime, "step tick count must be greater than zero");
    return ROLLER_RUNTIME_RESULT_INVALID_ARGUMENT;
  }
  if (!pRuntime->iHasInputSource) {
    runtime_set_error(pRuntime, "runtime requires an input source before stepping");
    return ROLLER_RUNTIME_RESULT_INVALID_STATE;
  }

  for (uint32_t i = 0; i < uiTicks; ++i) {
    eRollerRuntimeResult eAdvance = pRuntime->InputSource.pfnAdvance(
        pRuntime->InputSource.pUserData, i);
    if (eAdvance != ROLLER_RUNTIME_RESULT_OK) {
      runtime_set_error(pRuntime, "input source advance failed with result %u",
                        (unsigned)eAdvance);
      pRuntime->eStatus = ROLLER_RUNTIME_STATUS_FAILED;
      return ROLLER_RUNTIME_RESULT_STEP_FAILED;
    }
    RUNTIME_TICK_CLOCK_STEP();
    RUNTIME_CLEAR_PENDING_TICKS();
    if (!RUNTIME_FRONTEND_ON())
      RUNTIME_GAME_TICK_STEP();
  }

  pRuntime->eStatus = ROLLER_RUNTIME_STATUS_RUNNING;
  return ROLLER_RUNTIME_RESULT_OK;
}
  • Step 4: Add the step test build entry

Add this helper in build.zig near configureRollerRuntimeApiTests:

fn configureRollerRuntimeStepTests(
    b: *Build,
    target: ResolvedTarget,
    optimize: OptimizeMode,
    c_flags: []const []const u8,
) void {
    const runtime_step_mod = b.createModule(.{
        .target = target,
        .optimize = optimize,
        .link_libc = true,
    });
    runtime_step_mod.addIncludePath(b.path("PROJECTS/ROLLER"));
    runtime_step_mod.addCSourceFiles(.{
        .flags = c_flags,
        .files = &.{
            "PROJECTS/ROLLER/roller_runtime.c",
            "tests/roller_runtime_step_test.c",
        },
    });
    runtime_step_mod.addCMacro("ROLLER_RUNTIME_TEST_SEAMS", "1");

    const runtime_step_test = b.addExecutable(.{
        .name = "roller_runtime_step_test",
        .root_module = runtime_step_mod,
    });
    const run_runtime_step_test = b.addRunArtifact(runtime_step_test);
    const runtime_step_tests = b.step(
        "test-roller-runtime-step",
        "Run RollerRuntime fixed-step adapter tests",
    );
    runtime_step_tests.dependOn(&run_runtime_step_test.step);

    const test_step = b.step("test", "Run unit tests");
    test_step.dependOn(runtime_step_tests);
}

Call it from build():

    configureRollerRuntimeStepTests(b, target, optimize, c_flags);
  • Step 5: Run tests

Run: zig build test-roller-runtime-api test-roller-runtime-step

Expected: PASS.

  • Step 6: Commit

Run: git status --short

Expected: changes are limited to PROJECTS/ROLLER/roller_runtime.c, tests/roller_runtime_step_test.c, and build.zig.

Run: git add PROJECTS/ROLLER/roller_runtime.c tests/roller_runtime_step_test.c build.zig && git commit -m "feat: connect RollerRuntime fixed stepping"

Expected: commit succeeds.


Task 3: Route replay snapshot ticking through RollerRuntime behind a CLI flag

Files:

  • Modify: PROJECTS/ROLLER/3d.c

Interfaces:

  • Consumes: Task 2 RollerRuntime_Step and RollerRuntime_SetInputSource.

  • Produces: --runtime-snapshot CLI flag for replay snapshots only.

  • Step 1: Add a failing CLI validation test using the binary

Run: zig build run -- --runtime-snapshot --help

Expected before implementation: FAIL with ERROR: Unknown argument '--runtime-snapshot'.

  • Step 2: Wire 3d.c to create a runtime for runtime snapshots

Add #include "roller_runtime.h" near the existing snapshot includes at the top of PROJECTS/ROLLER/3d.c.

Add file-scope state near other static CLI/runtime state in 3d.c:

static int g_bRuntimeSnapshotMode = 0;
static RollerRuntime *g_pSnapshotRuntime = NULL;

Add this no-op input source callback near main_loop_iteration helpers:

static eRollerRuntimeResult ROLLER_RUNTIME_CALL RuntimeSnapshotAdvanceInput(
    void *pUserData, uint32_t uiTickIndex)
{
  (void)pUserData;
  (void)uiTickIndex;
  return ROLLER_RUNTIME_RESULT_OK;
}

Update print_usage after the --snapshot line:

  cli_fprintf(f, " --runtime-snapshot    drive replay snapshot ticks through RollerRuntime\n");

In the argument parser, add a branch before the final unknown-argument check:

    } else if (strcmp(argv[i], "--runtime-snapshot") == 0) {
      g_bRuntimeSnapshotMode = 1;
      consumed = 1;

After the snapshot validation block that checks g_SnapshotConfig, add:

  if (g_bRuntimeSnapshotMode) {
    if (!g_bSnapshotMode || g_SnapshotConfig.eKind != SNAPSHOT_KIND_REPLAY) {
      cli_fprintf(stderr, "ERROR: '--runtime-snapshot' requires replay '--snapshot' mode\n");
      return 1;
    }
  }

Before frontend_run_game_loop(...), after race_set_track(TrackLoad);, create the runtime and attach the no-op source. The actual replay file has already been selected by existing snapshot/replay setup; this keeps replay loading outside RollerRuntime.

  if (g_bRuntimeSnapshotMode) {
    tRollerRuntimeConfig runtimeConfig = {
      .uiStructSize = sizeof(runtimeConfig),
      .uiVersion = ROLLER_RUNTIME_API_VERSION,
      .uiFlags = ROLLER_RUNTIME_FLAG_HEADLESS | ROLLER_RUNTIME_FLAG_DETERMINISTIC,
    };
    tRollerRuntimeInputSource inputSource = {
      .uiStructSize = sizeof(inputSource),
      .uiVersion = ROLLER_RUNTIME_API_VERSION,
      .pUserData = NULL,
      .pfnAdvance = RuntimeSnapshotAdvanceInput,
    };
    eRollerRuntimeResult runtimeResult = RollerRuntime_Create(
      &runtimeConfig, &g_pSnapshotRuntime);
    if (runtimeResult == ROLLER_RUNTIME_RESULT_OK)
      runtimeResult = RollerRuntime_SetInputSource(g_pSnapshotRuntime, &inputSource);
    if (runtimeResult != ROLLER_RUNTIME_RESULT_OK) {
      cli_fprintf(stderr, "ERROR: failed to initialize RollerRuntime snapshot driver: %s\n",
                  RollerRuntime_GetLastError(g_pSnapshotRuntime));
      RollerRuntime_Destroy(g_pSnapshotRuntime);
      g_pSnapshotRuntime = NULL;
      return 1;
    }
  }

After frontend_run_game_loop(...) and before the final shutdown check, destroy it:

  RollerRuntime_Destroy(g_pSnapshotRuntime);
  g_pSnapshotRuntime = NULL;

In the gameplay snapshot block around SnapshotAdvanceTick(), replace:

    SnapshotAdvanceTick();

with:

    if (g_pSnapshotRuntime) {
      eRollerRuntimeResult runtimeResult = RollerRuntime_Step(g_pSnapshotRuntime, 1u);
      if (runtimeResult != ROLLER_RUNTIME_RESULT_OK) {
        SDL_Log("RollerRuntime snapshot step failed: %s",
                RollerRuntime_GetLastError(g_pSnapshotRuntime));
        quit_game = 1;
        racing = 0;
      }
    } else {
      SnapshotAdvanceTick();
    }
  • Step 3: Run CLI validation commands

Run: zig build run -- --runtime-snapshot --help

Expected: PASS, help output includes --runtime-snapshot.

Run: zig build run -- --runtime-snapshot

Expected: FAIL with ERROR: '--runtime-snapshot' requires replay '--snapshot' mode.

  • Step 4: Run unit tests

Run: zig build test-roller-runtime-api test-roller-runtime-step

Expected: PASS.

  • Step 5: Commit

Run: git status --short

Expected: changes are limited to PROJECTS/ROLLER/3d.c.

Run: git add PROJECTS/ROLLER/3d.c && git commit -m "feat: add runtime-driven replay snapshot mode"

Expected: commit succeeds.


Task 4: Add zig build test-runtime-snapshots

Files:

  • Modify: build.zig
  • Modify: tests/snapshots/README.md

Interfaces:

  • Consumes: Task 3 --runtime-snapshot CLI flag.

  • Produces: zig build test-runtime-snapshots, a parallel snapshot build step that overwrites replay PNG baselines through RollerRuntime and runs the same git-diff check.

  • Step 1: Run the missing build step to verify failure

Run: zig build test-runtime-snapshots

Expected: FAIL with an unknown step error.

  • Step 2: Refactor replay snapshot run creation in build.zig

Add this file-scope helper near configureSnapshotTests:

fn addSnapshotReplayRuns(
    b: *Build,
    roller_exe: *Compile,
    assets_path: LazyPath,
    out_abs: []const u8,
    runtime_snapshot: bool,
    prev_run: *?*Step,
) void {
    for (snapshot_replays) |replay| {
        const run_capture = b.addRunArtifact(roller_exe);
        run_capture.addArg("--no-crash-handler");
        run_capture.addArg("--whiplash-root");
        run_capture.addDirectoryArg(assets_path);
        run_capture.addArg("--snapshot");
        run_capture.addArg(b.fmt("{s}.gss", .{replay.name}));
        if (runtime_snapshot)
            run_capture.addArg("--runtime-snapshot");
        run_capture.addArg("--frames");
        run_capture.addArg(replay.frames);
        run_capture.addArg("--out");
        run_capture.addArg(out_abs);
        run_capture.has_side_effects = true;
        if (prev_run.*) |p| run_capture.step.dependOn(p);
        prev_run.* = &run_capture.step;
    }
}

Replace the existing replay loop in configureSnapshotTests with:

    addSnapshotReplayRuns(b, roller_exe, assets_path, out_abs, false, &prev_run);

Leave the existing scene loop unchanged.

  • Step 3: Add the runtime snapshot step

Still inside configureSnapshotTests, after the test_snapshots step is created, add:

    const test_runtime_snapshots = b.step(
        "test-runtime-snapshots",
        "Run runtime-driven replay snapshot regression tests",
    );

If assets are missing, make both snapshot steps depend on the same fail step before returning:

        test_snapshots.dependOn(&missing_assets.step);
        test_runtime_snapshots.dependOn(&missing_assets.step);
        return;

After the existing test_snapshots diff-check setup, add the runtime replay runs and diff check:

    var runtime_prev_run: ?*Step = null;
    addSnapshotReplayRuns(b, roller_exe, assets_path, out_abs, true, &runtime_prev_run);

    if (scratch) {
        if (runtime_prev_run) |p| test_runtime_snapshots.dependOn(p);
    } else {
        const runtime_diff_check = b.addSystemCommand(&.{
            "git",
            "diff",
            "--exit-code",
            "--stat",
            "--",
            baselines_dir,
        });
        runtime_diff_check.has_side_effects = true;
        if (runtime_prev_run) |p| runtime_diff_check.step.dependOn(p);
        test_runtime_snapshots.dependOn(&runtime_diff_check.step);
    }
  • Step 4: Update snapshot documentation

Modify tests/snapshots/README.md to add this section after the existing command examples:

### Runtime-driven replay snapshots

```bash
zig build test-runtime-snapshots

This runs the replay snapshot list through RollerRuntime using the same
VCS-managed PNG baselines as zig build test-snapshots. Replay file loading
remains part of the existing snapshot/replay setup; runtime owns fixed stepping
only. This is a parallel migration gate: the legacy snapshot harness remains
authoritative while the runtime-driven replay path proves it can reproduce the
same pixels.

test-runtime-snapshots currently covers replay entries from
build.zig's snapshot_replays table. Named scene snapshots remain covered by
test-snapshots until scene rendering is wired to an explicit runtime-state
view.


Also add a row to the options table:

```markdown
| `test-runtime-snapshots` | Build step that drives replay snapshots through `RollerRuntime` and compares against the same checked-in baselines. |
  • Step 5: Run the runtime snapshot step in scratch mode first

Run: zig build test-runtime-snapshots -Dscratch

Expected: PASS if assets are available. Captures land in zig-out/snapshot-scratch/ and the working tree remains clean.

  • Step 6: Run both authoritative snapshot steps

Run: zig build test-snapshots

Expected: PASS with no baseline diff.

Run: zig build test-runtime-snapshots

Expected: PASS with no baseline diff.

If the runtime snapshot command produces PNG diffs, do not bless the files. Compare the changed replay PNGs to determine whether the runtime path failed to preserve the legacy tick sequence.

  • Step 7: Commit

Run: git status --short

Expected: only build.zig and tests/snapshots/README.md are modified. No PNG baselines should be modified.

Run: git add build.zig tests/snapshots/README.md && git commit -m "test: add runtime-driven replay snapshots"

Expected: commit succeeds.


Task 5: Final verification and cleanup

Files:

  • Modify only if verification finds a defect in files changed by Tasks 1-4.

Interfaces:

  • Consumes: all previous tasks.

  • Produces: verified branch ready for review.

  • Step 1: Run formatting/whitespace checks

Run: git diff --check HEAD~4..HEAD

Expected: no output.

  • Step 2: Run unit tests

Run: zig build test-roller-runtime-api test-roller-runtime-step

Expected: PASS.

  • Step 3: Run source manifest check

Run: python3 tools/check_roller_core_manifest.py

Expected: output begins with roller-core manifest check passed:.

  • Step 4: Run legacy snapshot gate

Run: zig build test-snapshots

Expected: PASS and no modified files under tests/snapshots/baselines/.

  • Step 5: Run runtime snapshot gate

Run: zig build test-runtime-snapshots

Expected: PASS and no modified files under tests/snapshots/baselines/.

  • Step 6: Confirm clean tree

Run: git status --short

Expected: no output.

  • Step 7: Commit verification fixes if any were needed

If Step 1-6 required fixes, commit them:

Run: git add <fixed-files> && git commit -m "fix: stabilize runtime snapshot parity"

Expected: commit succeeds. If no fixes were needed, skip this step.


Self-Review

Spec coverage:

  • SDL-free public runtime surface: Task 1.
  • Runtime does not load replay files: Task 1 exposes input-source attachment, not replay loading; Task 3 composes existing snapshot/replay setup externally.
  • Replay-driven first milestone: Tasks 2-4.
  • Existing tick path preservation: Task 2 and Task 3.
  • Parallel runtime snapshot path: Task 4.
  • Existing snapshot path unchanged: Task 4 explicitly preserves test-snapshots and adds test-runtime-snapshots beside it.
  • Renderer separation: Task 3 routes only ticking through runtime; snapshot rendering remains an external consumer.

Red-flag scan: This plan contains no incomplete markers and no unspecified code steps.

Type consistency: The runtime types and function names introduced in Task 1 are the same names consumed by Tasks 2-4.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

ROLLER browser preview: https://worktree-green-forest-0a1b.roller-web.pages.dev

Updated for commit 0acf76d.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant