Edit Timeline — Slab History for Editing, Undo, and Time Travel
← Editor Overview | Debugging | GG-UI | Rollback Netcode | Home
Status: Foundation (M0) complete 2026-09-30 — prerequisite for GG-UI. Production and Differentiator steps pending.
In edit mode the World stops ticking. The editor submits edits, Brain applies them and commits each as a slab frame. Undo is a transaction history of handle-based deltas. The Temporal ring powers time travel: pause, step backward and forward, scrub, and resume from the past in PIE, with the renderer and world UI showing whichever frame is selected.
One History Mechanism
The Temporal ring already serves rollback: "rewinding the slab to frame N means switching the read index — no copy needed" (Rollback Netcode). This design gives the same ring more consumers instead of building parallel systems — the engine's one data model, many consumers pattern:
| Consumer | Uses the ring for |
|---|---|
| Rollback netcode | Rewind to a corrected frame, resimulate (exists) |
| PIE time travel | Pause, step, scrub, resume from the past |
| Debugging suite | Deep Temporal Scrubbing, Play-From-Here Resimulation, slab heatmap history |
| World UI | Stays correct automatically: it is expanded from whichever frame is published (GG-UI) |
This document is the implementation path for the timeline tools already designed in Debugging — Deep Temporal Scrubbing, Play-From-Here Resimulation, and (for long ranges) Deterministic Input Replay.
How Edit Mode Works Today
| Behavior | Where | Problem |
|---|---|---|
| Editor starts the World paused | TrinyxEngine.cpp:290 | — |
LogicThread::TickPause | Sleeps until woken; commits a frame only when something changed | |
FieldEdit → WorldBase:: → Registry:: | Details, gizmo, and undo/redo submit edits; the Brain is the only slab writer | |
Archetype* / Chunk* / slot pointers | FieldEditCommand | Entity by handle; a destroyed or recycled entity is rejected by generation |
EntityTransformCommand snapshots every entity field as JSON | FieldEditCommand | Stores before/after only for the changed fields (8 bytes each, inline) |
UndoHistory | Named transactions, 1000 deep, one per interaction | |
EditorCamera on WorldViewport | Edit viewports fly on the render thread; PIE viewports show the world camera |
Decisions
| Topic | Decision |
|---|---|
| History in editor builds | Always on — already true: editor builds force TNX_ENABLE_ROLLBACK on. |
| Build flags | Rollback and determinism are independent flags, so editor history doesn't force Fixed32 math on non-deterministic projects. Exact resimulation across machines (input replay, cross-platform) remains a determinism-build guarantee; same-machine resimulation is reproducible either way. |
| Undo mechanism | Transactions of handle-based deltas (UndoHistory) — one mechanism for every tier and system. Decided 2026-09-30: ring-backed undo was dropped. Deltas are a few dozen bytes per field and apply in milliseconds even for bulk edits, while ring undo would have meant a second undo path, a Temporal/Volatile/Cold split per command, coverage of only the last 128 commits, and reverting non-transacted state. |
| Role of the ring | Time travel: PIE pause/step/scrub, Play-From-Here, debugging timelines. Edit commits still land in it, so a read-only preview of the edit session stays possible later. |
| Undo depth | Bounded by the history, not the ring (1000 transactions today; persistence is an open question). |
| Defrag in edit mode | Off by default. Must be toggleable so developers can reproduce real relocation bugs in the editor instead of release builds, even at a slower editing experience. |
| Blending after discontinuities | Explicit snap support — global per frame and per entity (see below). |
| Editor camera | Owned by the viewport; it is editor-view state, not simulation state. |
Edit Mode Model
Single Writer, Event-Driven Brain
- In edit mode Brain sleeps on an atomic wait instead of spinning in
TickPause. - The editor never writes the slab. It submits edit commands to Brain (World queue); Brain applies them to the write frame and commits:
PublishCompletedFrame→PropagateFrame. Brain remains the only slab writer — the same contract as the spawn handshake. - Continuous edits (gizmo drags, slider scrubs) commit a frame for each step of the interaction, so the viewport follows the drag; the undo history groups the whole interaction into one transaction, coalescing its commands.
- The render thread keeps drawing the last published frame; nothing ticks while nothing changes.
Undo History
Undo is a stack of transactions (UndoHistory). Entries reference entities by handle, never by Archetype*/Chunk* pointers, and store only the changed fields of changed entities (FieldEditCommand), or the lifetime change (EntityLifetimeCommand: retire/revive). Undo and redo resubmit the entries through the World queue, so Brain stays the only slab writer and each undo/redo commits as a frame.
Timeline Cursor (Time Travel)
The ring is a window of committed frames. A read-frame override lets the renderer and world UI show any frame in that window while Brain holds still: the published frame stays where the simulation is; the override chooses what is displayed. Paused PIE steps the override back and forward; resuming from an earlier frame reuses rollback (below).
Transactions
Undo operates on transactions, not individual frames or entries.
- A transaction is a named, atomic group of changes: "Move 50 Entities", "Paste", "Add Component". Undo and redo apply all of it or none of it.
- Transactions cover every editor-owned system through one history: ECS data, level and asset edits, and editor settings that are meant to be undoable. Individual systems contribute entries; the transaction owns ordering.
- Selection changes are recorded so undo restores what the user was looking at.
- Undo and redo must stay interactive for bulk transactions: edits are submitted to Brain in batches, not one job per field.
- An Undo History panel shows the transaction list and lets the user jump to any point.
- The entry format is a serializable, handle-based description of operations. That keeps the door open to collaborative multi-user editing later (transactions exchanged between editors), and to persisting history.
History Gates Recycling
If a destroyed entity's slot or handle is recycled while history still references it, undo or a time-travel step shows stale data under a handle that now means something else. Slots and handles are only reclaimed once nothing in the history references them — the same safety-window idea as the networked despawn CommittedFrameHorizon and ConfirmLocalRecycles() (Entity Lifecycle).
- Edit world: deleting retires the entity (
Registry::); it is destroyed only when the undo history discards the delete.Retire - PIE time travel: gameplay destruction must not reclaim a slot until the destroy frame has left the ring.
Defrag
- Default: disabled in edit mode (determinism builds already disable defrag of live authoritative entities).
- Toggle: an editor setting enables defrag so relocation bugs surface during development.
- Undo history names entities by handle, so it survives relocation. Ring history is per cache index, so a scrubbed frame from before a move shows the entity at its old index — expected for time travel, which displays the past.
Jolt in Edit Mode
✅ Resolved: the edit world has no live Jolt representation, so there is nothing to keep in sync. PIE worlds are built from a serialization of the edit registry taken when PIE starts (EntityBuilder::), so bodies are created at the edited transforms, unsaved edits included.
Snapping: No Interpolation Across Discontinuities
The scatter pass (scatter.slang) interpolates every field between the previous and current frame at the same EntityCacheIndex, using one global Alpha per frame. There is currently no way to disable blending, globally or per entity. Consequences today:
- Defrag moves an entity to a new index → one frame blended between two different entities.
- Slot reuse after tombstoning → a newly spawned entity likely blends from the dead entity's last position.
- Teleports and corrections blend through the discontinuity.
Two mechanisms are needed:
| Mechanism | Scope | Used for | Implementation sketch |
|---|---|---|---|
| Frame snap | Whole frame | Edit commits, undo/redo, timeline scrubbing, time-travel steps | Force Alpha = 1 in GpuFrameData for that frame |
| Entity discontinuity bit | Per entity | Spawn (esp. into a reused slot), defrag relocation, teleports | New TemporalFlagBits::Discontinuity (bit 26 is the only unused engine bit); scatter uses alpha = 1 for that entity |
The per-entity bit must be cumulative until the render thread acknowledges it, like Dirty — not per logic frame like DirtiedFrame. At 512Hz, several logic frames pass between renders, so a per-frame bit could be cleared before the render thread ever sees it (Dirty-Bit Upload).
Time Travel in PIE
Built on the same timeline once edit mode works.
- Pause and step backward/forward through the ring. The render thread needs a read-frame override to display any historical frame, with frame snap on each step. This is Deep Temporal Scrubbing.
- Resume from the past reuses rollback: restore the nearest Jolt snapshot at or before the frame, resimulate forward, discard the old future (
RollbackImplalready rewinds and resims). Editing a value before resuming is Play-From-Here Resimulation. - Presentation during fast-forward: the
PresentationReconcilersuppressesTemporalEventswhile resimulating, so audio and VFX don't spam; Anti-Events cover anything already playing. - Networked PIE: only the authority (or standalone) can rewind; clients cannot rewind the server.
- Range: at 512Hz, 128 frames is ~0.25s. In edit mode that's 128 edits, which is plenty; for gameplay it's short. Longer gameplay rewind uses periodic keyframes + recorded inputs and resimulates — exact in determinism builds — building on Deterministic Input Replay rather than a deeper ring.
Staging
Foundation — correctness and the contracts everything else builds on
Single writer and event-driven Brain; edits through the World queue; viewport-owned camera; frame snap and the per-entity discontinuity bit; transactions of handle-based deltas; history gates recycling; defrag off by default; the read-frame override with PIE pause and single-step, so timeline UI has one mechanism to attach to. None of these can be retrofitted cheaply, so all land first.
Production — parity with professional editor workflows
One transaction history across ECS, level, asset, and settings edits; Undo History panel; defrag toggle; PIE scrubbing across the whole ring.
Differentiators — where Trinyx goes further
World UI and meshes always consistent with the scrubbed frame; Play-From-Here resimulation; long-range gameplay rewind from keyframes + inputs, exact in determinism builds; serializable transactions as the basis for multi-user editing.
Work Breakdown
| Step | Stage | Outcome |
|---|---|---|
| 1 | Foundation | ✅ Editor builds always allocate a history ring (editor forces TNX_ENABLE_ROLLBACK; independent of TNX_DETERMINISM) |
| 2 | Foundation | ✅ Event-driven Brain in edit mode: sleeps on a semaphore, wakes on world-queue jobs, pause changes, stop, and RequestEditCommit (100 ms heartbeat as a safety net). Edits commit as one frame each, with visual transforms snapped. Editor builds now actually use EditorFrame — before, pause was compiled out and the edit world simulated |
| 3 | Foundation | ✅ Editor edit commands via the World queue: FieldEdit (handle + component + field index + inline value) submitted with WorldBase::, applied by Registry::. Details panel, gizmo, and undo/redo all go through it (FieldEditCommand replaces the pointer/JSON commands); the gizmo works from its own matrix mid-drag since edits land a frame later. Consecutive edits of the same fields merge into one undo step — per-interaction transaction boundaries arrive with step 7 |
| 4 | Foundation | ✅ Viewport-owned editor camera (off Brain): EditorCamera, ResolveViewCamera shared by GPU data, gizmo, and grid; RMB fly with WASD/QE, wheel speed, Shift boost. Orbit/pan/focus arrive with the GG-UI viewport, where its input model is decided |
| 5 | Foundation | ✅ Frame snap (Alpha = 1 while paused) and per-entity TemporalFlagBits::Discontinuity (bit 26): scatter snaps flagged entities. Set on create (covers reused slots), defrag moves, and Registry:: (teleports); cleared with Dirty once render acks. Logic-side smoothing honors it too: marked entities have VisPos snapped to Pos after PostPhysics (Registry::), so players (VisBlend 0.6–0.8) don't glide on respawn or teleport |
| 6 | Foundation | ✅ Read-frame override: WorldViewport::DisplayFrame shows a ring frame while Brain holds still (positions from that frame's CTransform; Volatile fields stay current; a frame that left the ring falls back to live; no RenderAck while showing the past). PIE pause/resume fixed (the toolbar paused the edit world); single-step via LogicThreadBase:: (one fixed step, no duplicate commit). Step back walks the ring; step forward returns toward live, then simulates every PIE world together. Toolbar |< >| with frame label, Play menu, , / . | | 7 | Foundation | ✅ Transactions: UndoHistory of named, nestable Transactions of handle-based deltas (1000 deep); widget and gizmo interactions are one transaction each (open while an item is active or the gizmo is in use); coalescing only within a transaction; selection at transaction start restored on undo/redo; Edit menu shows the transaction name. Edits are submitted in batches (WorldBase::): a bulk undo or multi-field gizmo step is one job | | 8 | Foundation | ✅ (edit world) Recycling gated on history: delete retires (Registry:: — not Active/Alive, record/slot/handle kept) and undo revives the same entity; EntityLifetimeCommand covers delete and prefab placement; retired entities are destroyed only when the history discards the command (cap or truncated redo). Defrag off for the edit world. Gating gameplay recycling on the ring for PIE time travel is part of step 11 | | 9 | Production | Defrag toggle for the edit world | | 10 | Production | Undo History panel (early GG-UI consumer) | | 11 | Production | PIE scrubbing across the whole ring (timeline UI on the step 6 override); gameplay recycling gated on the ring | | 12 | Differentiator | Resume from the past and Play-From-Here via rollback; keyframe + input long-range rewind |
Open Questions
- Whether undo history persists across editor sessions, and its serialized form.
- Transaction scope for asset edits made outside the level (e.g. prefab and material editors).
- Serialization format for transactions, suitable for persistence and future multi-user editing.
- Keyframe interval and storage for long-range PIE rewind.
- How the history ring is sized in editor builds relative to the game's
TemporalFrameCount.