Skip to content

T3 · TracePlayer + sealed LumosSource — replay traces behind the existing render path #73

Description

@wow-miley

Suggested model tier: Sonnet 4.6 medium effort (small API surface, but the seam placement matters)

Context

The playback half. The architectural seam already exists: consumers render from a Flow<VoxelFrame> (or equivalent — confirm in recon) that feeds ComposeLattice → LumosCanvas. TracePlayer produces that same stream from a VoxelTrace instead of a live simulation. Everything downstream — projection, rotation, glyph overlay, theming — stays live and unchanged, which is exactly why Option A was chosen: event-driven glyphs still fire over baked traces, because glyph overlay happens after the recording point.

LumosSource is the consumer-facing seal: callers declare where frames come from and never touch player internals.

Objective

A consumer can swap a live simulation for a trace with a one-line source change and see pixel-identical output for the same seed.

Expected outcomes

  • TracePlayer(trace: VoxelTrace) in :phosphor-trace: frames(timeSource: TimeSource = Monotonic): Flow<VoxelFrame> — maps elapsed wall time → recorded frame index (hold-last-frame semantics between recorded frames; display refresh runs faster than the recorded fps and that's fine), honors loop on the active segment, supports play/pause/seek(frameIndex)/setSegment(name).
  • Sealed LumosSource { data class Live(...existing runtime inputs...); data class Trace(trace: VoxelTrace, initialSegment: String) } with a single factory producing the frame stream either way — recon determines whether this lives in :phosphor-trace or :phosphor-lumos to keep dependency direction clean.
  • Reconstruction: dynamic channels + static lattice → full VoxelFrame instances, allocation-conscious (reuse buffers; no 1,500-object churn per frame).

Technical constraints

commonMain; kotlinx-coroutines Flow allowed here; zero changes to ComposeLattice/LumosCanvas signatures; Result-typed trace-load errors.

Tasks

A — Recon (comment before code): Identify the exact live frame-stream type consumers collect today (#54 shows lumosFrames.collectAsState() — find its producer and type). Confirm where LumosSource should live. STOP for approval.

B — TracePlayer + LumosSource: Implement per expected outcomes.

C — Golden equivalence test: Capture a trace with seed S (T2), run the live sim with seed S at the same fixed dt, assert frame-by-frame VoxelFrame equality. This test is the epic's keystone — it proves replay ≡ live.

D — Playback semantics tests: loop wrap has no index gap; seek lands exactly; pause holds frame; time-mapping holds correct frame between recorded timestamps.

Validation / DoD

Golden equivalence green on JVM; playback semantics tests green on JVM + iOS sim; demo app runs a trace through the existing LumosCanvas with glyph overlay visibly functioning on top.

Out of scope

State-transition logic between segments (T5 — this ticket only plays one segment at a time); crossfading; Socket integration; any change to projection or canvas code.

Human review gate

Miley reviews the recon seam decision before B; visually confirms the demo (trace + live glyph) before close.

Agent handoff prompt

You are implementing PHO-37 in socket-link/phosphor. Depends on PHO-36 (merged).
1. RECON FIRST: find the live frame-stream producer consumers collect
   (the thing behind lumosFrames in PHO-27). Comment its type, location, and
   your proposed LumosSource placement. STOP for approval.
2. Implement TracePlayer (time→frame mapping, hold-last semantics, loop,
   play/pause/seek/setSegment) and sealed LumosSource(Live | Trace).
3. Zero signature changes to ComposeLattice / LumosCanvas. Reuse frame buffers —
   no per-frame 1,500-object allocation.
4. Tests: golden equivalence (trace seed S == live seed S, frame by frame);
   loop/seek/pause/time-mapping semantics.
5. Wire the demo to play a .vxt through LumosCanvas with a glyph firing on top.
6. DoD: all green JVM + iosSimulatorArm64; demo screenshot in PR.
If recon contradicts any assumption in this ticket, STOP and comment.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions