docs(gfx): RE the full render model (surfaces+objects+handle-order composite); redo spec+Phase1
Reversed create/set/draw-texture handlers + gfx_render_frame: surfaces at ctx+0x52bd4[slot] (set-texture loads a file with a colorkey); objects in the ctx+0x408 registry reference a surface by slot (live) + rect + position (V24) + visible bit; render iterates the registry in ASCENDING HANDLE ORDER (= z-order) and composites visible objects. Answers both unknowns (z-order = handle; slot 0 not special). Design spec + Phase 1 plan rebuilt on this model, replacing the flawed flat-layer version. Ghidra annotated (gfx_op_0x1f8/9/b, gfx_object_bind_draw, gfx_render_frame, gfx_object_composite, gfx_op_0x20c_present_frame). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -273,6 +273,39 @@ Consequence: reproducing the fades needs a **retained per-frame animated composi
|
||||
immediate-mode permanent canvas can neither fade nor clear. Design: `docs/superpowers/specs/2026-07-07-
|
||||
animated-compositor-design.md`.
|
||||
|
||||
### The full gfx render model — surfaces + objects + composite (2026-07-07)
|
||||
|
||||
Reversed the create/set/draw-texture handlers + the render loop (all annotated in Ghidra). **This is the
|
||||
canonical model** (an earlier flat "draw layers to one screen" attempt was WRONG — it had no surface concept
|
||||
and snapshotted textures at draw time; symptoms: alternating grey, glow over backgrounds, vanishing sprites).
|
||||
|
||||
**Two distinct stores:**
|
||||
- **Surfaces** — image buffers at `ctx+0x52bd4[slot]`, indexed by slot. `gfx_op_0x1f8_create_surface`
|
||||
(`0x4222d0`) allocates a blank one (releasing any old); `gfx_op_0x1f9_load_surface` (`0x422360`, op `0x1f9`
|
||||
set-texture) resolves `resId` via the SYS4INI resolver (`FUN_0044f390`) and loads the file into the slot's
|
||||
surface **with a colorkey/chromakey** (op arg 3 — never modelled before), also releasing the old surface.
|
||||
A surface persists at its slot until the next set-texture overwrites it.
|
||||
- **Objects** — the `ctx+0x408` registry, keyed by handle (a `std::map`). `gfx_op_0x1fb_draw_bind` (`0x422510`,
|
||||
op `0x1fb` draw-texture) → `gfx_object_bind_draw` (`0x47e870`): sets the object's **source slot** (`obj+4`),
|
||||
**source rect** (`obj+8..0x14` = left,top,right,bottom), **position** (`obj+0x24/28/2c` = V24), and the
|
||||
**visible** flag (bit 0). The object references its surface **by slot index, live** (re-resolved each frame),
|
||||
NOT a snapshot. Objects also carry anchor V18 (`obj+0x18`), animation (flag bit 2 + progress `obj+0x214` /
|
||||
duration `obj+0x228` / target `obj+0x244..`), and color/alpha (`0x202/0x203`).
|
||||
|
||||
**Render frame** — `gfx_render_frame` (`0x4820b0`), driven by op `0x20c` present (`gfx_op_0x20c_present_frame`
|
||||
`0x4174a0`, which also updates the frame timer `ctx+0x51b64/68`): iterate the object registry **in ascending
|
||||
handle order — that IS the z-order** (lower handle behind, higher on top; `std::map` key order). For each
|
||||
object with visible bit 0, `gfx_object_composite` (`0x47f650`) computes its transform from geometry, **applies
|
||||
the animation interpolation if bit 2 is set**, and blits `surface[obj.slot]` with alpha/colorkey. Then swap
|
||||
buffers (present). **Slot 0 is NOT special** — a normal slot; several objects may share one surface.
|
||||
|
||||
**⇒ Faithful port:** a `SurfaceStore` (`slot → {image, colorkey}`, from create/set-texture) + an `ObjectStore`
|
||||
(`handle → {slot, srcRect, position, anchor, scale, anim, alpha, visible}`, from draw-texture + the gfx ops) +
|
||||
a host per-frame compositor that draws visible objects **in ascending-handle order** from their live surface,
|
||||
interpolating animations by elapsed time. No VM/host lockstep (op `0x238` clock is non-blocking; animations
|
||||
play during the wait-for-input park). Open detail for implementation: the exact scale/transform math in
|
||||
`gfx_object_composite` (`FUN_00472f00`/`FUN_00473ed0`) and the colorkey format.
|
||||
|
||||
---
|
||||
|
||||
## Native walls backlog (targets for this loop)
|
||||
|
||||
@@ -1,275 +1,172 @@
|
||||
# Animated Compositor — Phase 1: Retained Compositor Implementation Plan (TDD)
|
||||
# Animated Compositor — Phase 1 (Surfaces + Objects + Composite) — TDD Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax.
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans. Steps use `- [ ]` checkboxes.
|
||||
|
||||
**Goal:** Replace the immediate-mode permanent canvas with a **retained-object compositor** — `draw-texture` records/updates a persistent layer keyed by its object handle; the Godot host clears + re-composites all active layers each frame; `0x1f7` erase removes them. This is the foundation for alpha (Phase 2) and time-animation (Phase 3).
|
||||
**Goal:** Implement the RE-confirmed render model — a **SurfaceStore** + per-object **source slot / rect /
|
||||
position / visible** in `GfxState`, and a Godot host that **clears and composites the visible objects in
|
||||
ascending-handle order** from their live surface each frame. No alpha/colorkey/animation yet (opaque). This
|
||||
replaces the reverted flat-layer attempt and must render the booted CGs correctly (the guardrail).
|
||||
|
||||
**Architecture:** The retained layer set lives in **`GfxState`** (VM-owned, version-neutral, thread-safe): the VM's `draw-texture` updates it (additive — engine parity preserved), `EraseRange` removes from it. The Godot host stops immediate-blitting; `Main._Process` snapshots the layers each frame and composites them (in execution order = current correct paint order) using the host's slot→image map. Design: `docs/superpowers/specs/2026-07-07-animated-compositor-design.md`.
|
||||
**Model (see `docs/engine-re.md` "The full gfx render model" + the design spec):** surfaces are image buffers
|
||||
per slot (create/set-texture); objects reference a surface by slot (live) + a rect + a position (V24) + a
|
||||
visible flag (draw-texture); the render loop iterates objects by ascending handle (= z-order) and blits each
|
||||
visible object's surface-rect at its position.
|
||||
|
||||
**Tech Stack:** C# / .NET 8 (`engine/`, xUnit); Godot 4.7 .NET (`godot/`).
|
||||
**Tech:** C#/.NET 8 (`engine/`, xUnit), Godot 4.7 .NET.
|
||||
|
||||
## Global Constraints
|
||||
## Global constraints
|
||||
|
||||
- **Composite order = execution order, handle-keyed update-in-place** (matches the current, correct immediate-mode render — the guardrail is "booted SC0000 CGs must not regress"). Erase removes by handle. This ordering policy is the Phase-1 design choice; validate by screenshot no-regression.
|
||||
- **Engine parity:** `draw-texture` gains a `GfxState` update but stays one step / `pc+1`; non-Godot hosts are unaffected (they read nothing new). `dotnet test engine/AgeEngine.sln` must stay green; Godot `--selftest` (synthetic, no gfx) must stay green.
|
||||
- **Thread-safety:** `GfxState` layers are mutated on the VM thread and read on the Godot main thread → all layer access is `lock`-guarded; the host reads via an immutable `SnapshotLayers()`.
|
||||
- **No auto-screenshot:** per the user, do NOT screenshot-and-quit for validation; the user drives the live window. Provide the numeric `Age.Cli gfx` oracle for headless checks.
|
||||
- **Seam:** `GfxState` is `Age.Engine/Model`; the VM references `Model`. `DrawLayer` lives with `GfxState`.
|
||||
- **Model-first, no guessing.** If an assumption isn't in the RE, RE it or pin it via the `gfx` oracle before coding on it.
|
||||
- **Guardrail:** `godot -- --boot` opening CGs must render correctly at Phase 1 (the reverted attempt failed this).
|
||||
- **Parity:** VM ops stay one step / `pc+1`; non-Godot hosts unaffected; `dotnet test` + Godot `--selftest` stay green.
|
||||
- **Threading:** `GfxState` mutated on the VM thread, read on the Godot main thread → `lock`-guarded; host reads one immutable snapshot per frame.
|
||||
- **Seam:** `GfxState` stores `ResId`/`ColorKey`/geometry (version-neutral); the host resolves `ResId → BMP` via `ResourceMap`.
|
||||
- **No auto-screenshot** for validation — user drives the live window.
|
||||
|
||||
## File structure
|
||||
|
||||
| File | Responsibility | Task |
|
||||
|---|---|---|
|
||||
| `engine/Age.Engine/Model/GfxState.cs` | `DrawLayer` + retained layer list (add/update, remove, snapshot; lock-guarded); `EraseRange` also drops layers | 1.1 |
|
||||
| `engine/Age.Engine.Tests/GfxStateTests.cs` | Layer add/update-in-place/order/erase unit tests | 1.1 |
|
||||
| `engine/Age.Engine/Vm/VirtualMachine.cs` | `draw-texture` records a `DrawLayer` in `GfxState` | 1.2 |
|
||||
| `engine/Age.Engine.Tests/GfxCommandBufferTests.cs` | Synthetic scene: draws → layer list; erase → removed | 1.2 |
|
||||
| `engine/Age.Cli/Program.cs` (`gfx` cmd) | Dump the retained layer list (headless oracle) | 1.3 |
|
||||
| `godot/GodotAdvHost.cs` | `DrawTexture` no longer blits (retained loop composites); keep slot→BMP map | 1.4 |
|
||||
| `godot/Main.cs` | `_Process` snapshots `GfxState` layers → clear + composite each frame | 1.4 |
|
||||
| `engine/Age.Engine/Model/GfxState.cs` | `SurfaceStore` (slot→{resId,colorkey}); object `SourceSlot`/`SrcRect`/`Visible`; `RenderObject` snapshot (visible, ascending-handle) | 1.1 |
|
||||
| `engine/Age.Engine.Tests/GfxStateTests.cs` | surface set/get; object bind; ascending-handle visible snapshot | 1.1 |
|
||||
| `engine/Age.Engine/Vm/VirtualMachine.cs` | `set/create-texture` → surface; `draw-texture` → object bind (slot/rect/pos/visible) | 1.2 |
|
||||
| `engine/Age.Engine.Tests/GfxCommandBufferTests.cs` | synthetic scene: set-texture + draw-texture → a visible render object | 1.2 |
|
||||
| `engine/Age.Cli/Program.cs` (`gfx`) | dump visible render objects (handle order) with resolved surface | 1.3 |
|
||||
| `godot/GodotAdvHost.cs`, `godot/Main.cs` | per-frame clear + composite from `SnapshotVisibleObjects()` | 1.4 |
|
||||
|
||||
---
|
||||
|
||||
### Task 1.1 — `DrawLayer` + retained layer list in `GfxState` (pure data)
|
||||
### Task 1.1 — `SurfaceStore` + object fields + `RenderObject` snapshot (pure data)
|
||||
|
||||
**Files:** Modify `engine/Age.Engine/Model/GfxState.cs`; Test `engine/Age.Engine.Tests/GfxStateTests.cs`.
|
||||
**Interfaces produced:** on `GfxState`: `void SetSurface(int slot, long resId, long colorKey)`;
|
||||
`void ClearSurface(int slot)` (create-texture blank); object mutators `void BindDraw(long handle, int slot,
|
||||
int sx, int sy, int w, int h, int dstX, int dstY)` (sets SourceSlot/SrcRect/Position(V24)/Visible=true);
|
||||
`IReadOnlyList<RenderObject> SnapshotVisibleObjects()` (visible objects, ascending handle, with resolved
|
||||
surface resId+colorkey + rect + position). `readonly record struct RenderObject(long Handle, long SurfaceResId,
|
||||
long ColorKey, int SrcX, int SrcY, int W, int H, int DstX, int DstY)`. `GfxObject` gains `int SourceSlot=-1`,
|
||||
`(int X,int Y,int W,int H) SrcRect`, `bool Visible`.
|
||||
|
||||
**Interfaces produced:** `readonly record struct DrawLayer(long Handle, int Slot, int SrcX, int SrcY, int W, int H, int DstX, int DstY)`; on `GfxState`: `void AddOrUpdateLayer(DrawLayer l)` (update-in-place by `Handle`, else append — preserving order), `void RemoveLayers(long handle)`, `IReadOnlyList<DrawLayer> SnapshotLayers()`. `EraseRange` also calls `RemoveLayers` per erased handle.
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
- [ ] **Step 1: failing tests** (append to `GfxStateTests.cs`)
|
||||
```csharp
|
||||
[Fact]
|
||||
public void LayersAppendInOrderAndUpdateInPlace()
|
||||
public void BindDrawMakesAVisibleRenderObjectFromItsSurface()
|
||||
{
|
||||
var g = new GfxState();
|
||||
g.AddOrUpdateLayer(new DrawLayer(0xA, 4, 0, 0, 800, 600, 0, 0));
|
||||
g.AddOrUpdateLayer(new DrawLayer(0xB, 5, 0, 0, 200, 200, 100, 100));
|
||||
g.AddOrUpdateLayer(new DrawLayer(0xA, 4, 0, 0, 800, 600, 0, 50)); // re-draw A -> update in place
|
||||
var s = g.SnapshotLayers();
|
||||
Assert.Equal(2, s.Count);
|
||||
Assert.Equal(0xA, s[0].Handle); // order preserved (A still first)
|
||||
Assert.Equal(50, s[0].DstY); // updated
|
||||
Assert.Equal(0xB, s[1].Handle);
|
||||
g.SetSurface(4, 0x25, 0); // load resId 0x25 into surface slot 4
|
||||
g.BindDraw(0xcb2a, 4, 0, 0, 800, 600, 0, 0); // object 0xcb2a draws surface 4 at (0,0)
|
||||
var vis = g.SnapshotVisibleObjects();
|
||||
Assert.Single(vis);
|
||||
Assert.Equal(0xcb2a, vis[0].Handle);
|
||||
Assert.Equal(0x25, vis[0].SurfaceResId); // resolved from the object's source slot
|
||||
Assert.Equal((800, 600, 0, 0), (vis[0].W, vis[0].H, vis[0].DstX, vis[0].DstY));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EraseRangeAlsoDropsLayers()
|
||||
public void VisibleObjectsComeInAscendingHandleOrder() // ascending handle == z-order
|
||||
{
|
||||
var g = new GfxState();
|
||||
g.AddOrUpdateLayer(new DrawLayer(0x10, 4, 0, 0, 10, 10, 0, 0));
|
||||
g.AddOrUpdateLayer(new DrawLayer(0x20, 5, 0, 0, 10, 10, 0, 0));
|
||||
g.EraseRange(0x10, 1);
|
||||
var s = g.SnapshotLayers();
|
||||
Assert.Single(s);
|
||||
Assert.Equal(0x20, s[0].Handle);
|
||||
g.SetSurface(4, 0x1, 0); g.SetSurface(5, 0x2, 0);
|
||||
g.BindDraw(0xcf08, 5, 0, 0, 10, 10, 0, 0); // higher handle
|
||||
g.BindDraw(0xcb20, 4, 0, 0, 10, 10, 0, 0); // lower handle
|
||||
var vis = g.SnapshotVisibleObjects();
|
||||
Assert.Equal(new long[] { 0xcb20, 0xcf08 }, vis.Select(v => v.Handle).ToArray());
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run to verify failure**
|
||||
- [ ] **Step 2:** run → FAIL (missing members). `dotnet test engine/AgeEngine.sln --filter FullyQualifiedName~GfxStateTests`
|
||||
|
||||
Run: `dotnet test engine/AgeEngine.sln --filter FullyQualifiedName~GfxStateTests`
|
||||
Expected: FAIL — `DrawLayer`/`AddOrUpdateLayer` don't exist (compile error).
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
In `engine/Age.Engine/Model/GfxState.cs`, add the record (top of namespace) and the layer members:
|
||||
- [ ] **Step 3: implement** in `GfxState.cs`
|
||||
```csharp
|
||||
public readonly record struct DrawLayer(long Handle, int Slot, int SrcX, int SrcY, int W, int H, int DstX, int DstY);
|
||||
public readonly record struct RenderObject(long Handle, long SurfaceResId, long ColorKey,
|
||||
int SrcX, int SrcY, int W, int H, int DstX, int DstY);
|
||||
```
|
||||
Inside `GfxState`:
|
||||
On `GfxObject` add: `public int SourceSlot = -1; public (int X, int Y, int W, int H) SrcRect; public bool Visible;`
|
||||
On `GfxState` (all `_surfaces`/object access under the existing `_lock`):
|
||||
```csharp
|
||||
private readonly List<DrawLayer> _layers = new();
|
||||
private readonly object _lock = new();
|
||||
private readonly Dictionary<int, (long ResId, long ColorKey)> _surfaces = new();
|
||||
public void SetSurface(int slot, long resId, long colorKey) { lock (_lock) { _surfaces[slot] = (resId, colorKey); } }
|
||||
public void ClearSurface(int slot) { lock (_lock) { _surfaces[slot] = (0, 0); } }
|
||||
|
||||
public void AddOrUpdateLayer(DrawLayer l)
|
||||
public void BindDraw(long handle, int slot, int sx, int sy, int w, int h, int dstX, int dstY)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
for (int i = 0; i < _layers.Count; i++)
|
||||
if (_layers[i].Handle == l.Handle) { _layers[i] = l; return; }
|
||||
_layers.Add(l);
|
||||
var o = GetOrCreate(handle);
|
||||
o.SourceSlot = slot; o.SrcRect = (sx, sy, w, h); o.V24 = (dstX, dstY, 0); o.Visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveLayers(long handle)
|
||||
public IReadOnlyList<RenderObject> SnapshotVisibleObjects()
|
||||
{
|
||||
lock (_lock) { _layers.RemoveAll(l => l.Handle == handle); }
|
||||
}
|
||||
|
||||
public IReadOnlyList<DrawLayer> SnapshotLayers()
|
||||
{
|
||||
lock (_lock) { return _layers.ToArray(); }
|
||||
}
|
||||
```
|
||||
Extend `EraseRange` to also drop layers — change its body to:
|
||||
```csharp
|
||||
public void EraseRange(long handle, long count)
|
||||
{
|
||||
if (count > 1) for (long i = handle; i < handle + count; i++) { Release(i); RemoveLayers(i); }
|
||||
else { Release(handle); RemoveLayers(handle); }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run to verify pass**
|
||||
|
||||
Run: `dotnet test engine/AgeEngine.sln --filter FullyQualifiedName~GfxStateTests`
|
||||
Expected: PASS (all facts, including the pre-existing ones).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
```bash
|
||||
git add engine/Age.Engine/Model/GfxState.cs engine/Age.Engine.Tests/GfxStateTests.cs
|
||||
git commit -m "feat(gfx): retained DrawLayer list in GfxState (thread-safe)
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 1.2 — `draw-texture` records a retained layer
|
||||
|
||||
**Files:** Modify `engine/Age.Engine/Vm/VirtualMachine.cs`; Test `engine/Age.Engine.Tests/GfxCommandBufferTests.cs`.
|
||||
|
||||
**Interfaces:** Consumes `GfxState.AddOrUpdateLayer`. The `draw-texture` case builds a `DrawLayer` from `a[0]`(handle),`a[1]`(slot),`a[2..7]` and records it; it **keeps** the existing `_host.DrawTexture(...)` call (the CLI oracle logs it; the Godot host will no-op-blit in Task 1.4).
|
||||
|
||||
- [ ] **Step 1: Write the failing test** (in `GfxCommandBufferTests.cs`)
|
||||
```csharp
|
||||
private static (int, Operand[]) DrawTex(int handle, int slot, int w, int h, int dx, int dy)
|
||||
=> (0x1fb, new[] { G(handle), G(slot), I(0), I(0), G(w), G(h), G(dx), G(dy) });
|
||||
|
||||
[Fact]
|
||||
public void DrawTextureRecordsARetainedLayer()
|
||||
{
|
||||
var t = T();
|
||||
var scene = ScriptAssembler.Assemble(t, "GFX", new List<(int, Operand[])>
|
||||
lock (_lock)
|
||||
{
|
||||
MovGI(1, 0xA), MovGI(2, 4), MovGI(3, 800), MovGI(4, 600), MovGI(5, 0), MovGI(6, 0),
|
||||
DrawTex(1, 2, 3, 4, 5, 6), Exit(),
|
||||
}, System.Array.Empty<string>());
|
||||
var vm = new VirtualMachine(scene, t, new RecordingHost());
|
||||
vm.Run();
|
||||
var layers = vm.Gfx.SnapshotLayers();
|
||||
Assert.Single(layers);
|
||||
Assert.Equal(0xA, layers[0].Handle);
|
||||
Assert.Equal((800, 600, 0, 0), (layers[0].W, layers[0].H, layers[0].DstX, layers[0].DstY));
|
||||
var list = new List<RenderObject>();
|
||||
foreach (var kv in _objects.OrderBy(k => k.Key))
|
||||
{
|
||||
var o = kv.Value;
|
||||
if (!o.Visible) continue;
|
||||
var (resId, ck) = _surfaces.TryGetValue(o.SourceSlot, out var s) ? s : (0L, 0L);
|
||||
list.Add(new RenderObject(kv.Key, resId, ck, o.SrcRect.X, o.SrcRect.Y, o.SrcRect.W, o.SrcRect.H, (int)o.V24.X, (int)o.V24.Y));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
```
|
||||
(Note: `draw-texture` reads slot from `a[1]`, dst from `a[6]/a[7]`, w/h from `a[4]/a[5]` — mirror the existing case's operand indices exactly.)
|
||||
(Add `using System.Linq;` if needed. `GetOrCreate` already exists; `_lock` already exists from the prior layer work — reuse it. Remove the old `DrawLayer`/`_layers`/`AddOrUpdateLayer`/`SnapshotLayers`/`RemoveLayers` from the reverted flat model if still present, and the `EraseRange`→`RemoveLayers` call.)
|
||||
|
||||
- [ ] **Step 2: Run to verify failure**
|
||||
|
||||
Run: `dotnet test engine/AgeEngine.sln --filter FullyQualifiedName~DrawTextureRecords`
|
||||
Expected: FAIL — no layer recorded (`Empty` snapshot).
|
||||
|
||||
- [ ] **Step 3: Implement** — in `VirtualMachine.Step`, extend the `draw-texture` case:
|
||||
```csharp
|
||||
case "draw-texture": // (handle, slot, srcX, srcY, w, h, dstX, dstY)
|
||||
Gfx.AddOrUpdateLayer(new DrawLayer(Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), (int)Read(a[3]),
|
||||
(int)Read(a[4]), (int)Read(a[5]), (int)Read(a[6]), (int)Read(a[7])));
|
||||
_host.DrawTexture((int)Read(a[1]), (int)Read(a[2]), (int)Read(a[3]), (int)Read(a[4]),
|
||||
(int)Read(a[5]), (int)Read(a[6]), (int)Read(a[7])); return pc + 1;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run to verify pass + full suite (parity)**
|
||||
|
||||
Run: `dotnet test engine/AgeEngine.sln`
|
||||
Expected: all green — `draw-texture` still one step; non-Godot hosts unaffected; new test passes.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
```bash
|
||||
git add engine/Age.Engine/Vm/VirtualMachine.cs engine/Age.Engine.Tests/GfxCommandBufferTests.cs
|
||||
git commit -m "feat(gfx): draw-texture records a retained layer in GfxState
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
||||
```
|
||||
- [ ] **Step 4:** run → PASS. **Step 5:** commit (`feat(gfx): SurfaceStore + object bind + ascending-handle render snapshot`).
|
||||
|
||||
---
|
||||
|
||||
### Task 1.3 — CLI `gfx` oracle dumps the retained layer list
|
||||
### Task 1.2 — VM: `set/create-texture` → surface; `draw-texture` → object bind
|
||||
|
||||
**Files:** Modify `engine/Age.Cli/Program.cs` (the `gfx` command, after the run).
|
||||
- [ ] **Step 1: failing test** (`GfxCommandBufferTests.cs`): a synthetic scene that `set-texture(0x25, slot 4)` then `draw-texture(handle 0xcb2a, slot 4, 800x600 @ 0,0)` yields one visible `RenderObject` with `SurfaceResId==0x25`. (Use op `0x1f9` set-texture args `(resId, slot, colorkey)`, op `0x1fb` draw-texture args `(handle, slot, sx, sy, w, h, dx, dy)`.)
|
||||
|
||||
- [ ] **Step 1: Add the layer dump** — after the existing `gfx objects` print:
|
||||
```csharp
|
||||
var layers = vm.Gfx.SnapshotLayers();
|
||||
Console.WriteLine($" layers ({layers.Count}, composite order):");
|
||||
foreach (var l in layers)
|
||||
Console.WriteLine($" h=0x{l.Handle:x} slot={l.Slot} src=({l.SrcX},{l.SrcY} {l.W}x{l.H}) dst=({l.DstX},{l.DstY})");
|
||||
```
|
||||
- [ ] **Step 2:** run → FAIL.
|
||||
|
||||
- [ ] **Step 2: Build + eyeball on the drift scene**
|
||||
- [ ] **Step 3: implement** — in `VirtualMachine.Step`:
|
||||
- `set-texture` (`0x1f9`): `Gfx.SetSurface((int)Read(a[1]), Read(a[0]), a.Count > 2 ? Read(a[2]) : 0);` then keep `_host.SetTexture(...)` (host still loads dims for `get-texture-size`).
|
||||
- `create-texture` (`0x1f8`): `Gfx.ClearSurface((int)Read(a[0]));` then keep `_host.CreateTexture(...)`.
|
||||
- `draw-texture` (`0x1fb`): `Gfx.BindDraw(Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), (int)Read(a[3]), (int)Read(a[4]), (int)Read(a[5]), (int)Read(a[6]), (int)Read(a[7]));` — and **drop the `_host.DrawTexture` call** (the retained compositor renders now; the Godot host's `DrawTexture` becomes a no-op in 1.4).
|
||||
|
||||
Run: `dotnet run --project engine/Age.Cli -- gfx --boot SC0000.BIN`
|
||||
Expected: prints a retained layer list. Sanity: the full-screen CGs appear as layers at `(0,0) 800x600`; erased handles are absent (fewer layers than raw draw-texture calls). This is the headless proof the retained set is built correctly.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
```bash
|
||||
git add engine/Age.Cli/Program.cs
|
||||
git commit -m "feat(gfx): gfx oracle dumps the retained layer list
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
||||
```
|
||||
- [ ] **Step 4:** run full suite → green (parity). **Step 5:** commit.
|
||||
|
||||
---
|
||||
|
||||
### Task 1.4 — Godot: per-frame clear + recomposite from the retained layers
|
||||
### Task 1.3 — CLI `gfx` oracle: dump visible render objects (handle order)
|
||||
|
||||
**Files:** Modify `godot/GodotAdvHost.cs`, `godot/Main.cs`.
|
||||
|
||||
**Interfaces:** Consumes `_vm.Gfx.SnapshotLayers()` + the host's slot→BMP map. Produces a per-frame composite in `Main._Process`.
|
||||
|
||||
- [ ] **Step 1: Godot host — stop immediate-blitting; keep slot→BMP**
|
||||
|
||||
In `godot/GodotAdvHost.cs`, make `DrawTexture` a no-op for compositing (the retained loop handles it) but keep the slot→BMP map current (still populated by `SetTexture`). Expose the slot→BMP map (e.g. `public string? SlotBmp(int slot)`), since `Main._Process` needs it to composite a layer's slot.
|
||||
|
||||
- [ ] **Step 2: Main — per-frame recomposite in `_Process`**
|
||||
|
||||
In `godot/Main.cs`, give `Main` access to the VM (`_vm` already a field) and the host. In `_Process`, each frame:
|
||||
- [ ] **Step 1:** after the run, replace/add the layer dump with:
|
||||
```csharp
|
||||
// Retained recomposite: clear, then blit each layer (execution order) from its slot's BMP.
|
||||
var layers = _vm.Gfx.SnapshotLayers();
|
||||
_screen.Fill(new Color(0, 0, 0, 0)); // clear (transparent; the frame is rebuilt)
|
||||
foreach (var l in layers)
|
||||
var vis = vm.Gfx.SnapshotVisibleObjects();
|
||||
Console.WriteLine($" visible objects ({vis.Count}, ascending-handle = z-order):");
|
||||
foreach (var v in vis)
|
||||
Console.WriteLine($" h=0x{v.Handle:x} surf=0x{v.SurfaceResId:x} ({res.Resolve(sceneKey, v.SurfaceResId)?.Name ?? "?"}) src=({v.SrcX},{v.SrcY} {v.W}x{v.H}) dst=({v.DstX},{v.DstY})");
|
||||
```
|
||||
- [ ] **Step 2:** `dotnet run --project engine/Age.Cli -- gfx --boot SC0000.BIN` — sanity: the CGs appear as visible objects with real surfaces + plausible positions, in ascending-handle order. **Step 3:** commit.
|
||||
|
||||
---
|
||||
|
||||
### Task 1.4 — Godot: per-frame clear + composite from visible objects
|
||||
|
||||
- [ ] **Step 1:** `GodotAdvHost.cs`: make `DrawTexture` a no-op (retained compositor renders); keep `SetTexture` populating dims. Add `public string? ResolveResIdTexture(long resId)` (`_res.Resolve(_scene, resId) → TexturePath`).
|
||||
- [ ] **Step 2:** `Main.cs`: add `Recomposite()` called from `_Process` (guard `!_selftest && _vm != null`); replace `BlitSlot` with a cached `BlitLayer`. Recomposite:
|
||||
```csharp
|
||||
_screen.Fill(new Color(0, 0, 0, 0));
|
||||
foreach (var v in _vm.Gfx.SnapshotVisibleObjects()) // already ascending-handle = z-order
|
||||
{
|
||||
var bmp = _host.SlotBmp(l.Slot);
|
||||
if (bmp == null) continue;
|
||||
BlitLayer(bmp, l.SrcX, l.SrcY, l.W, l.H, l.DstX, l.DstY); // same clamp logic as the old BlitSlot
|
||||
if (v.SurfaceResId == 0) continue;
|
||||
var bmp = _host.ResolveResIdTexture(v.SurfaceResId);
|
||||
if (bmp != null) BlitLayer(bmp, v.SrcX, v.SrcY, v.W, v.H, v.DstX, v.DstY);
|
||||
}
|
||||
_screenTex.Update(_screen);
|
||||
```
|
||||
`BlitLayer` is the old `BlitSlot` body (load BMP, clamp src rect, `BlitRect`) minus the per-call `_screenTex.Update` (do one update after the loop). Cache loaded `Image`s by path to avoid re-reading every frame (a `Dictionary<string, Image>`), since `_Process` runs every frame.
|
||||
|
||||
**Guardrail:** the booted opening CGs must render identically to before (same execution-order composite, now rebuilt each frame). If a CG flickers/disappears, the layer set or order is wrong — check the `gfx --boot` oracle layer dump against the expected draws.
|
||||
|
||||
- [ ] **Step 3: Build**
|
||||
|
||||
Run: `godot --headless --path godot --import && dotnet build godot/Himegari.csproj`
|
||||
Expected: builds clean.
|
||||
|
||||
- [ ] **Step 4: Selftest parity**
|
||||
|
||||
Run: `godot --headless --path godot -- --selftest`
|
||||
Expected: `SELFTEST OK …` (synthetic scene, no gfx layers → unaffected).
|
||||
|
||||
- [ ] **Step 5: Live check (user-driven, NOT auto-screenshot)**
|
||||
|
||||
Launch for the user in the background: `godot --path godot -- --boot`. Ask them to confirm: (a) the opening CGs still render correctly (no regression), and (b) the stuck bottom overlay no longer persists indefinitely across pages (it's now rebuilt per frame from the active layer set; it clears when its handle is erased or no longer drawn). *A still-opaque, non-fading glow while its handle is active is EXPECTED at Phase 1 — alpha is Phase 2, fading is Phase 3.*
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
```bash
|
||||
git add godot/GodotAdvHost.cs godot/Main.cs
|
||||
git commit -m "feat(gfx): retained per-frame compositor in Godot (clear + recomposite)
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
`BlitLayer` = the old `BlitSlot` body, caching `Image`s by path (`Dictionary<string, Image?>`), one `_screenTex.Update` after the loop. (Removed: the immediate `CallDeferred("BlitSlot")` in `DrawTexture`.)
|
||||
- [ ] **Step 3:** build Godot (`--import` → `dotnet build godot/Himegari.csproj`); **Step 4:** `--selftest` green.
|
||||
- [ ] **Step 5: live, user-driven:** launch `godot -- --boot` in the background; ask the user to confirm the CGs render correctly across pages (no alternating grey, no vanished content). Expected caveats (Phases 2/3): glow opaque, no fade, possible green boxes on sprites, occasional off-position CG (cold-anchor residual). **Step 6:** commit.
|
||||
|
||||
## Self-review
|
||||
|
||||
- **Spec coverage (Phase 1):** retained layer model in `GfxState` → 1.1; `draw-texture` retained → 1.2; per-frame clear + recomposite → 1.4; headless oracle → 1.3. Alpha/animation explicitly NOT here (Phases 2/3). ✓
|
||||
- **Parity:** `draw-texture` stays one step + keeps `_host.DrawTexture`; non-Godot hosts read nothing new; engine suite + selftest re-asserted (1.2 Step 4, 1.4 Step 4). ✓
|
||||
- **Threading:** all `_layers` access lock-guarded; host reads via immutable `SnapshotLayers()`; the Image cache prevents per-frame disk reads. ✓
|
||||
- **Placeholder scan:** 1.1–1.3 have complete code; 1.4 is host glue (concrete approach + key code, validated live) — the honest boundary (no unit oracle for pixels). ✓
|
||||
- **Guardrail explicit:** "booted CGs must not regress" stated at 1.4; the Phase-1 layer-order/reset policy (execution-order, handle-keyed, erase-removes) is called out for screenshot validation. ✓
|
||||
|
||||
**Next:** Phase 2 (alpha/blend) then Phase 3 (time-animation), per the design spec.
|
||||
- Model-first: every field comes from the RE (`obj+4` slot, `obj+8..0x14` rect, `V24` position, visible bit 0, ascending-handle z-order). No flat-layer/snapshot logic. ✓
|
||||
- Parity + threading + seam constraints restated per task. ✓
|
||||
- Guardrail (booted CGs render) is the Phase-1 live check. ✓
|
||||
- Open items (scale math, colorkey bits, visible-flag clear) are explicitly deferred to later phases / to be RE'd, not guessed. ✓
|
||||
|
||||
@@ -1,119 +1,79 @@
|
||||
# Design: Retained Animated Compositor (the AE* fades / gfx effects)
|
||||
# Design: Surface + Object Compositor (the AE* fades / gfx effects)
|
||||
|
||||
Status: **approved (design)** · Date: 2026-07-07
|
||||
RE source: `docs/engine-re.md` (op `0x215` + "The gfx animation/effects subsystem"). Builds on the gfx
|
||||
command-buffer work (`docs/phase-a-slice-plan.md` A2b; `docs/superpowers/specs/2026-07-07-gfx-command-buffer-design.md`).
|
||||
Touches: `engine/Age.Engine/Model/GfxState.cs`, `engine/Age.Engine/Vm/VirtualMachine.cs`,
|
||||
`engine/Age.Cli/Program.cs` (`GfxTraceHost`), `godot/{Main,GodotAdvHost}.cs`.
|
||||
Status: **approved model, redesigned 2026-07-07** (supersedes the earlier flat-layer version, which was
|
||||
wrong — see below). RE source: `docs/engine-re.md` "The full gfx render model". Touches:
|
||||
`engine/Age.Engine/Model/GfxState.cs`, `engine/Age.Engine/Vm/VirtualMachine.cs`, `engine/Age.Cli/Program.cs`,
|
||||
`godot/{Main,GodotAdvHost}.cs`.
|
||||
|
||||
## Problem / goal
|
||||
## Why the first attempt failed (do not repeat)
|
||||
|
||||
SC0000's opening event CGs now render (gfx command-buffer + system boot), but the `AE*` flash/glow effects
|
||||
draw **opaque and never clear** — a white "explosion" glow stays at the bottom of the screen. Root cause
|
||||
(RE, `engine-re.md`): those effects are a **native time-animated retained render loop** — objects carry
|
||||
animation state (progress/duration/target), op `0x238` sets a non-blocking global animation clock, and the
|
||||
engine's per-frame loop interpolates + composites. Our **immediate-mode permanent canvas** can neither
|
||||
animate (fade) nor clear (remove the overlay). **Goal:** replace it with a **retained, host-driven,
|
||||
per-frame animated compositor** so effects fade and clear correctly — completing SC0000's rendering, which
|
||||
proves the ADV effectful-op layer for all ADV scenes.
|
||||
The first pass modelled rendering as a flat list of "draw layers" blitted to one screen, and resolved a
|
||||
layer's texture from its *slot* at composite time. That has no **surface** concept and snapshots textures
|
||||
wrong. Symptoms: alternating grey CGs, the glow drawn over backgrounds, vanishing sprites. Reverted. The RE
|
||||
(`engine-re.md`) shows the real model is **surfaces + objects + a per-frame composite in handle order** — this
|
||||
spec is built on that, model-first.
|
||||
|
||||
## Key architectural shift: immediate → retained
|
||||
## The model (RE-confirmed — build exactly this)
|
||||
|
||||
Today `draw-texture` (and `BlitSlot`) blit **immediately and permanently** onto one canvas. The real engine
|
||||
is **retained**: the VM configures object/layer state; a per-frame render loop clears, advances animations,
|
||||
and re-composites the active set. We adopt that:
|
||||
**Surfaces** — image buffers indexed by slot (`ctx+0x52bd4[slot]`). `create-texture` makes a blank one;
|
||||
`set-texture(resId, slot, colorkey)` loads a file into the slot's surface with a chromakey; it *replaces* the
|
||||
old surface. Surfaces persist until overwritten.
|
||||
|
||||
- **The VM's gfx ops update retained `GfxState`** (they already do for geometry; extend to alpha + animation
|
||||
+ a draw/layer list). They do **not** paint pixels.
|
||||
- **The host owns a per-frame render loop** (`godot/_Process`): advance time, interpolate, clear, composite.
|
||||
- **No VM/host frame-lockstep** — the VM parks at `wait-for-input` and the host animates during the park;
|
||||
the bytecode's `configure → wait` structure gives the timing (RE-confirmed: `0x238` is non-blocking).
|
||||
**Objects** — a registry keyed by handle. Each object carries: a **source slot** + **source rect** + a
|
||||
**position** (V24) + **anchor** (V18) + scale + **animation** (progress/duration/target) + **alpha/color** +
|
||||
a **visible** flag. `draw-texture(handle, slot, srcRect, dstXY)` binds slot/rect/position and sets visible;
|
||||
the geometry/anim/color ops set the rest. **An object references its surface by slot index, live** (resolved
|
||||
each frame) — never a snapshot.
|
||||
|
||||
## The model — `GfxState` extensions
|
||||
**Render frame** — iterate objects **in ascending handle order (= z-order)**; for each *visible* object,
|
||||
compute its transform from geometry, interpolate if animating, and blit `surface[slot]`'s rect at its position
|
||||
with alpha + colorkey. Then present. Slot 0 is not special.
|
||||
|
||||
`GfxState` (VM-owned execution state) gains, per object, the reversed animation fields and a compositor-
|
||||
facing draw list:
|
||||
## Architecture
|
||||
|
||||
```
|
||||
GfxObject (additions)
|
||||
int Alpha // 0..255, from 0x202/0x203 (default 255)
|
||||
bool Animating // native flag bit 4
|
||||
long AnimDuration // obj+0x228 (from gfx_anim_start / 0x234)
|
||||
long AnimProgress // obj+0x214 (advanced by the host clock)
|
||||
(long X,Y,Z) AnimTarget // obj+0x244/248/24c
|
||||
// (existing: Slot, V18/V24/V16c, Field64/68/6c, Color)
|
||||
- **`GfxState` (VM-owned) gains two coherent stores:**
|
||||
- **`SurfaceStore`**: `slot → Surface{ long ResId, long ColorKey }` (blank if create-texture). Set by the
|
||||
`set-texture`/`create-texture` VM cases.
|
||||
- **Object fields** (extend the existing `GfxObject`): `SourceSlot`, `(int X,Y,W,H) SrcRect`, `bool Visible`
|
||||
(plus the existing `Slot`, `V18`, `V24`, animation, `Color`). Set by `draw-texture` + the gfx ops.
|
||||
- **Host compositor** (`godot/Main._Process`): each frame, iterate `GfxState` objects **ordered by ascending
|
||||
handle**; for each `Visible` object, resolve `SurfaceStore[obj.SourceSlot].ResId → BMP` (host), and blit its
|
||||
`SrcRect` at `obj.V24` position. Clear+recomposite each frame. (Alpha, colorkey, animation added in later
|
||||
phases.)
|
||||
- **VM↔host:** no lockstep. Objects/surfaces are retained VM state; the host composites what's current each
|
||||
frame while the VM is parked at wait-for-input. The `0x238` clock is non-blocking (confirmed).
|
||||
- **Seam:** `GfxState` stays version-neutral — it stores `ResId`/`ColorKey`/geometry (no BMP paths). The host
|
||||
resolves `ResId → BMP` (it already does via `ResourceMap`).
|
||||
|
||||
GfxState (additions)
|
||||
long AnimClockElapsed, AnimClockDuration // op 0x238 global clock (ctx+0x51b78/7c)
|
||||
IReadOnlyList<DrawLayer> Layers // retained composite list (order = paint order)
|
||||
```
|
||||
## Phasing (each runnable + user-eyeballed; RE-first, no guessing)
|
||||
|
||||
`DrawLayer` = a resolved draw command (slot/source-rect/dst/alpha) captured when the VM executes
|
||||
`draw-texture`/`0x202`/`0x203`, **replacing** the immediate blit. The host composites `Layers` each frame.
|
||||
| Phase | Deliverable | Live check |
|
||||
|---|---|---|
|
||||
| **1. Surfaces + objects + composite** | `SurfaceStore`; object gets `SourceSlot`/`SrcRect`/`Visible`; host composites visible objects in ascending-handle order from their live surface. No alpha/anim (opaque). | CGs render correctly across pages (fixes the alternating grey); no vanished content |
|
||||
| **2. Colorkey + alpha** | chromakey transparency from `set-texture`'s colorkey; per-object alpha (`0x202/0x203`) in the blit | glow translucent; sprites' green boxes gone |
|
||||
| **3. Time-animation** | host advances the anim clock + per-object interpolation (`0x234`/`0x1fd`/`0x238`) by elapsed time | the explosion/glow fades over its duration |
|
||||
| **4. Transform + remaining ops** | scale/rotate from `gfx_object_composite` math; residual effect/render ops | opening correct end-to-end |
|
||||
|
||||
- **Layer lifecycle:** a full-screen opaque draw resets the layer set (it covers everything, matching the
|
||||
engine's frame); `0x1f7` erase / animation-complete removes layers; a new frame's draws append. Exact
|
||||
reset policy is a Phase-1 decision, validated by the oracle/screenshot (see Phase 1 plan).
|
||||
- **Native workers still NOT modelled** — only the retained data the render loop needs.
|
||||
## Testing
|
||||
|
||||
## Data flow (per frame, host)
|
||||
- **Engine (host-agnostic):** `SurfaceStore` + object fields are pure data → unit-tested (set-texture records a
|
||||
surface; draw-texture sets the object's source slot/rect/position/visible; ascending-handle iteration).
|
||||
- **`Age.Cli gfx` oracle:** dump the object list (handle-ordered) with resolved surface + rect + position — the
|
||||
headless numeric check.
|
||||
- **Godot `--selftest`:** stays green (synthetic scene, no gfx).
|
||||
- **Live, user-driven:** the user runs `godot -- --boot` and eyeballs per phase. **No auto-screenshot-and-quit.**
|
||||
- **Guardrail:** booted CGs must render correctly at Phase 1 (the flat-layer version regressed this — the
|
||||
faithful model must not).
|
||||
|
||||
```
|
||||
VM thread: gfx ops → mutate GfxState (layers, alpha, anim target/duration/clock) → park at wait-for-input
|
||||
Host _Process(delta):
|
||||
1. advance AnimClockElapsed += delta-scaled; per-object AnimProgress += ... (Phase 3)
|
||||
2. interpolate each Animating object's current props toward AnimTarget by progress/dur (Phase 3)
|
||||
3. clear screen; composite Layers in order, each blended by its Alpha (Phase 1–2)
|
||||
4. present (update the TextureRect)
|
||||
```
|
||||
## Risks / open (RE during implementation, don't guess)
|
||||
|
||||
## Phasing (each a runnable, screenshot-checkable checkpoint)
|
||||
1. **Scale/transform math** in `gfx_object_composite` (`FUN_00472f00`/`FUN_00473ed0`) — Phase 4; Phase 1 uses
|
||||
position + rect only.
|
||||
2. **Colorkey format** (the `>>0x10 | 0xff00` packing in set-texture) — Phase 2; RE the exact bits then.
|
||||
3. **Visible-flag lifecycle** — bit 0 is set by draw-texture; confirm what clears it (erase? a hide op?) so
|
||||
objects stop compositing when they should. Pin empirically via the oracle before relying on it.
|
||||
|
||||
- **Phase 1 — Retained compositor + clear.** `draw-texture` registers a `DrawLayer` instead of blitting;
|
||||
Godot `_Process` clears + composites the layer set each frame. No alpha/anim yet (layers opaque). Fixes
|
||||
the *never-clears* half — stuck overlays disappear when the frame resets. **Check:** opening CGs still
|
||||
render; the stuck bottom glow no longer persists across pages.
|
||||
- **Phase 2 — Alpha/blend.** `0x202/0x203` set per-object/layer `Alpha`; the composite blends by alpha.
|
||||
**Check:** the glow renders translucent, not solid white.
|
||||
- **Phase 3 — Time-animation.** Host clock + per-object interpolation (`0x234`/`0x1fd`/`0x238`) advance in
|
||||
`_Process`. **Check:** the explosion/glow **fades** over its duration.
|
||||
- **Phase 4 — Remaining effect ops + polish.** RE + fold in the render/present family
|
||||
(`0x243/0x20c/0x21c/0x224`), green chromakey for sprites, residual effect ops. **Check:** opening correct
|
||||
end-to-end by-eye.
|
||||
## Success criteria (design)
|
||||
|
||||
## Parity & testing
|
||||
|
||||
- **Engine tests stay host-agnostic.** `GfxState` additions are pure data → unit-testable (layer list built
|
||||
by the ops; alpha/anim fields set; clock advance is a pure function). No Godot needed.
|
||||
- **`Age.Cli gfx` oracle** dumps the resolved layer list (slot/dst/alpha) + anim state — the headless
|
||||
numeric check that the VM builds the right retained state.
|
||||
- **Godot `--selftest`** stays green (synthetic scene, no gfx) — the compositor rewrite must not perturb it.
|
||||
- **Screenshot / by-eye** on `SC0000 --boot` is the human oracle per phase (the user drives it live — do
|
||||
**not** auto-screenshot-and-quit).
|
||||
- **Guardrail:** the existing booted CG render (event CGs at `(0,0)`) must not regress at any phase.
|
||||
|
||||
## Deferrals (explicit)
|
||||
|
||||
- **SRPG rendering ops** (map/sprite `RENDERMAP`/`DRAWOBJ` family) — a separate surface SC0000 doesn't
|
||||
exercise; out of scope.
|
||||
- **Exact easing curve** of the interpolation (linear vs the engine's curve) — start linear; refine if
|
||||
by-eye shows it matters.
|
||||
- **The render/present family internals** (`0x243/0x20c/0x21c/0x224`) — RE in Phase 4 as needed; Phases 1–3
|
||||
don't require them (the host loop is our present).
|
||||
|
||||
## Risks / open questions
|
||||
|
||||
1. **Layer-reset policy** (when does the frame clear / which layers persist): the main Phase-1 design risk.
|
||||
Fallback: reset on each full-screen opaque draw + on `0x1f7` erase; validated by the oracle + screenshot,
|
||||
iterate if wrong.
|
||||
2. **VM-runs-ahead** vs animation timing: mitigated by the bytecode's `setup → wait-for-input` structure
|
||||
(the VM parks after each animation setup). If a sequence sets up an animation without a following wait,
|
||||
the retained state still animates from its configured values — acceptable.
|
||||
3. **Time base** for the clock (`delta` seconds vs the engine's unit): calibrate the elapsed→progress scale
|
||||
against a known-duration effect (e.g. the `9000` explosion) by-eye in Phase 3.
|
||||
|
||||
## Success criteria (design done)
|
||||
|
||||
- This spec approved; the retained/host-loop architecture + 4-phase split committed.
|
||||
- Phase 1 expanded into a TDD plan (`docs/superpowers/plans/2026-07-07-animated-compositor-phase1.md`).
|
||||
- RE findings recorded in `engine-re.md` (done).
|
||||
- Model recorded in `engine-re.md` (done); this spec rebuilt on it; Phase 1 replanned (surfaces+objects, not
|
||||
flat layers). Then implement Phase 1 TDD.
|
||||
|
||||
Reference in New Issue
Block a user