From e6f6803a15d4b182dae157f4c23312f17dff2893 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Tue, 7 Jul 2026 16:50:29 -0400 Subject: [PATCH] docs(gfx): implementation plan for the gfx command-buffer subsystem Co-Authored-By: Claude Opus 4.8 --- .../plans/2026-07-07-gfx-command-buffer.md | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-07-gfx-command-buffer.md diff --git a/docs/superpowers/plans/2026-07-07-gfx-command-buffer.md b/docs/superpowers/plans/2026-07-07-gfx-command-buffer.md new file mode 100644 index 0000000..f36d032 --- /dev/null +++ b/docs/superpowers/plans/2026-07-07-gfx-command-buffer.md @@ -0,0 +1,153 @@ +# GFX Command-Buffer Subsystem Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix the background/sprite render drift (`Screenshot 2026-07-06 211353.png`) by modelling the AGE engine's native graphics command-buffer host-side and *executing* the gfx ops instead of stubbing them, so per-object slot selection and geometry are correct. + +**Architecture:** The drift is settled as **(b) a native command-buffer op** (`docs/engine-re.md`, op `0x215`): the gfx ops maintain a native **object-record array** (`ctx+0x53d64`, stride `0x78`) plus a **handle→object registry** (a hash map) that `0x215` queries to pick each object's slot; stubbing collapses every draw onto slot 0. Because the VM is ours, we reproduce this as a clean **host-side model** behind `IHost` (the same pattern as `GetTextureSize`): the gfx ops call into a shared `GfxState`, and `label_12649`'s bytecode geometry math — already correct — then produces correct `dst`/`w`/`h`. The registry inserts are bytecode-driven, so the state rebuilds itself from the same scripts; **no Frida, no state-seeding**. + +**Tech Stack:** C# / .NET 8 (`engine/`, solution `AgeEngine.sln`, xUnit); Godot 4.7 .NET (`godot/Himegari.csproj`); Python 3.11 for the opcode build (`tools/opcodes_build.py`); Ghidra 12.1.2 + ghidra-mcp for the RE phase (engine dump `build/engine-dump/range_00400000.bin`, base `0x400000`). + +## Global Constraints + +- **This is a discovery-gated plan.** Phase 1 (RE) is fully specified and executable now; it produces the **op-contract table** that is the spec for Phases 3–4. **Phase 2 is a re-plan checkpoint:** after Phase 1, expand Phases 3–4 into bite-sized TDD tasks with exact code (a second `writing-plans` pass) — do not write handler code before its contract is reversed. +- **Run Python as** `py -3.11 -X utf8 tools/.py …` (utf8 mode mandatory on Windows for cp932). +- **Never hand-edit generated files.** After editing `vm-map/opcodes.toml`, run `py -3.11 -X utf8 tools/opcodes_build.py --build` (regenerates `tools/age_opcodes_himegari.py`, `build/opcodes.json`, `docs/opcode-reference.md`, `build/opcode-coverage.md`); `--lint` must report `0 errors`. +- **Ghidra VA-drift rule:** never analyse a native op by its Kelebek `u00XXXXXX` VA. Resolve the real handler through the dispatch table **`ctx[0x26c93 + op]` = the store `[ESI + (0x26c93+op)*4] = ` in `FUN_00413860`** (`docs/engine-re.md` master key). +- **Engine seam rule:** `Age.Engine/Vm` references only `Model` + `Hosting` + `Diagnostics` (never `Sys4`). Gfx state lives **host-side**, reached through `IHost`. `IHost` lives in `Age.Engine/Hosting`. +- **Parity guardrail:** non-Godot hosts (`CaptureHost`, test/CLI recording hosts) must keep any new `IHost` method a pure value-returning no-op that leaves trace/step-count unchanged, so existing base-ISA tests stay byte-identical. Verify with `dotnet test engine/AgeEngine.sln`. +- **Testing principle (user-directed):** synthesize test data; never disable a feature to keep a real scene matching a frozen number. Use `Age.Engine/Sys4/ScriptAssembler` to build synthetic gfx scenes for unit tests. +- **Opcode `category` vocabulary** is fixed: `marker/structural/control/adv/draw/audio/input/compute/unknown`. Gfx ops = `draw`. **Confidence:** `high` only once the handler is Ghidra-read; `med` for inference. +- **Canonical doc homes** (do not duplicate): native-op decode → `docs/engine-re.md`; opcode semantics → `vm-map/opcodes.toml`; slice status/results → `docs/phase-a-slice-plan.md`; host-side design → this plan's Phase-2 spec. + +--- + +## File structure + +| File | Responsibility | Phase | +|---|---|---| +| `docs/engine-re.md` | Canonical **op-contract table** for the gfx command-buffer family (per-op: handler VA, cmd-type, record/registry effects, operand roles) | 1 | +| `vm-map/opcodes.toml` | Per-op `semantics` for each reversed gfx op (summary/evidence/source/confidence) | 1 | +| `docs/superpowers/specs/2026-07-07-gfx-command-buffer-design.md` | Host-side model design (record set, registry, `IHost` surface, compositor) — written from the Phase-1 contract | 2 | +| `engine/Age.Engine/Hosting/GfxState.cs` | **New.** Host-side command-buffer model: object-record set + handle→object registry; the ops mutate/query it | 3 | +| `engine/Age.Engine/Hosting/IHost.cs` | New gfx-op methods (register/query/geometry), mirroring the `GetTextureSize` pattern | 3 | +| `engine/Age.Engine/Vm/VirtualMachine.cs` | Dispatch the reversed gfx ops to the new `IHost` methods (replace `OnStub` fall-through) | 3 | +| `engine/Age.Engine/Hosting/CaptureHost.cs` + test/CLI hosts | Parity no-op impls of the new `IHost` methods | 3 | +| `engine/Age.Cli/Program.cs` (`GfxTraceHost`) | Drive the real `GfxState` so `Age.Cli gfx` reports true per-object geometry | 3 | +| `godot/GodotAdvHost.cs`, `godot/Main.cs` | Composite from the `GfxState` object set (per-object slots) instead of collapsing to slot 0 | 3 | +| `engine/Age.Engine.Tests/GfxCommandBufferTests.cs` | **New.** Synthetic-scene tests: register→query returns the object; two different-sized bgs don't drift | 3 | + +--- + +## Phase 0 — Branch + +- [ ] **Step 1: Create the working branch** + +Run: `cd age-reimpl && git checkout -b feat/gfx-command-buffer` +Expected: `Switched to a new branch 'feat/gfx-command-buffer'` + +--- + +## Phase 1 — Reverse the gfx command-buffer op contract (executable now) + +**Deliverable:** a complete **op-contract table** in `docs/engine-re.md` covering every still-stubbed gfx-family op, plus updated `vm-map/opcodes.toml` semantics and Ghidra annotations. This is the spec for Phases 3–4. No C# is written in this phase. + +**Target ops** (the `draw`-category + adjacent handlers that build/query the command buffer; `0x215` is already done as the worked example): +`0x1a2` (gfx-cmd-register, cmd-type 3), `0x1f7` (ui-elem?), `0x1fa` (ui-clear?), `0x1ff` (draw?), `0x202` (draw-blit?), `0x203` (draw?), `0x212` (`u00421090`), `0x213` (`u004210D0`), `0x216` (`u004211A0`), `0x217`/`0x218`/`0x21a` (gfx-geom?), `0x219` (`u004212E0`). +Already implemented (read only if a contract dependency surfaces): `0x1f8` create-texture, `0x1f9` set-texture, `0x1fb` draw-texture, `0x208` get-texture-size. + +**Per-op procedure** (repeat for each target op; `0x215` in `engine-re.md` is the template): + +1. **Resolve the real handler.** Compute the table byte-offset `off = (0x26c93 + op) * 4`. In Ghidra (mcp) run `search_instructions` with `operand_pattern` = that offset in hex (e.g. op `0x217` → `(0x26c93+0x217)*4 = 0x9baac`) — the single match in `FUN_00413860` is `MOV dword ptr [ESI + 0x…], `. `` is the real handler. +2. **Decompile** the handler (`decompile_function `) and, if it shares code, the operand helpers (`FUN_0041b940` operand-fetch, `FUN_00425fb0` operand-write, `FUN_0047f280` registry-find, `FUN_0042cf70` registry-insert are already identified). +3. **Extract the contract:** argc + operand roles; which **record-array** fields it writes (`ctx+0x53d64 + curidx*0x78`; cmd-type at `+0x24`; note any geometry offsets); whether it **inserts/queries** the registry; and any output written back to a VM global. +4. **Annotate Ghidra:** `rename_function_by_address gfx_op_0x_` + `set_plate_comment` with the decode (as done for `0x42a0b0`/`0x47f280`/`0x42cf70`). `save_program` at the end of the phase. +5. **Record the contract:** add a row to the `engine-re.md` gfx-op-contract table and set the op's `vm-map/opcodes.toml` `semantics` (`source = "investigation"`, `confidence = "high"`, evidence = handler VA + effects). + +- [ ] **Step 1: Add the contract-table skeleton to `engine-re.md`** + +Under the op-`0x215` section in `docs/engine-re.md`, add a new subsection `#### gfx command-buffer — op contract table` with a Markdown table header: `| op | handler | argc | cmd-type | record fields | registry | operand roles | summary |`. Seed the `0x215`, `0x1a2` rows from what's already known. + +- [ ] **Step 2: Reverse each target op (one checkbox per op)** + +Work the per-op procedure above. Check off as each contract row lands: + - [ ] `0x1a2` gfx-cmd-register (confirm cmd-type 3 + registry insert key format) + - [ ] `0x212` `u00421090` + - [ ] `0x213` `u004210D0` + - [ ] `0x216` `u004211A0` + - [ ] `0x217` gfx-geom? + - [ ] `0x218` gfx-geom? + - [ ] `0x219` `u004212E0` + - [ ] `0x21a` gfx-geom? + - [ ] `0x1f7` ui-elem? + - [ ] `0x1fa` ui-clear? + - [ ] `0x1ff` draw? + - [ ] `0x202` draw-blit? + - [ ] `0x203` draw? + +- [ ] **Step 3: Cross-check against `label_12649`** + +Re-read `build/disasm/SC0000.asm` around `label_12649` (and `label_123ef`, `label_125bd @0x0050f`) and confirm the reversed contracts explain the slot-select branch and the per-object slot assignment (slots 4..13 via record table `0x3239`). Note in the contract table exactly which op(s) the branch keys on and what a correct return is (found ≥0 → existing slot; -1 → new). + +- [ ] **Step 4: Rebuild generated opcode files + lint** + +Run: `py -3.11 -X utf8 tools/opcodes_build.py --build` +Then: `py -3.11 -X utf8 tools/opcodes_build.py --lint` +Expected: build writes 4 files; lint = `0 errors, 0 warnings`. + +- [ ] **Step 5: Save Ghidra + commit the RE** + +`save_program` (mcp). Then: +```bash +git add docs/engine-re.md vm-map/opcodes.toml tools/age_opcodes_himegari.py docs/opcode-reference.md +git commit -m "docs(gfx): reverse the gfx command-buffer op contract (0x1a2/0x212-0x21a/…) + +Co-Authored-By: Claude Opus 4.8 " +``` + +**Phase 1 exit criteria:** every target op has (a) a real handler VA, (b) a Ghidra rename + plate comment, (c) a contract-table row, (d) `opcodes.toml` semantics at `confidence high`; and the `label_12649` slot-select branch is explained end-to-end. **← Re-plan checkpoint: Phase 3/4 tasks are authored now, from this table.** + +--- + +## Phase 2 — Host-side model design (spec) — gated on Phase 1 + +**Deliverable:** `docs/superpowers/specs/2026-07-07-gfx-command-buffer-design.md`, approved before implementation. Written from the Phase-1 contract table. Must specify, concretely: + +- [ ] **The `GfxState` model** — the object-record set (fields the reversed ops actually touch: handle, slot, cmd-type, geometry) and the handle→object **registry** (a `Dictionary`), with the insert/query semantics the contract table dictates. Only the fields the ops use — YAGNI. +- [ ] **The `IHost` surface** — one method per op-group (register / query / geometry-write), each mirroring the `GetTextureSize` pattern: the VM handler calls it and writes any result into the op's output operand(s). Exact C# signatures. +- [ ] **State ownership** — `GfxState` lives in the host layer and is shared by `GodotAdvHost` (real compositing) and the CLI `GfxTraceHost` (numeric oracle); the VM stays seam-clean (`Vm` → `Model`+`Hosting`+`Diagnostics`). +- [ ] **Compositor change** — how `godot/Main.cs` composites the per-object slot set (replacing the collapse-to-slot-0 blit), and how it stays compatible with the existing `BlitSlot`/`0x208` geometry path. +- [ ] **Parity + test strategy** — non-Godot no-op contracts; synthetic-scene unit tests (register→query→correct slot; two different-sized bgs → no drift); the `Age.Cli gfx` and screenshot oracles. +- [ ] **Explicit deferrals** — alpha/blend (`AE*`, `0x202/0x203` blend semantics), green chromakey, true multi-surface. In scope: **static positioned geometry only** (kills the drift). + +**Phase 2 exit criteria:** design doc self-reviewed (no placeholders/contradictions) and user-approved. Then run `writing-plans` again to expand Phase 3 into TDD tasks. + +--- + +## Phase 3 — Implement (task-level; expanded to TDD steps after Phase 2) + +Each task is test-first (synthetic scenes via `ScriptAssembler`), ends with `dotnet test engine/AgeEngine.sln` green + a commit, and preserves parity on non-Godot hosts. + +- [ ] **Task 3a — `GfxState` model + parity `IHost` surface.** Create `GfxState.cs`; add the Phase-2 `IHost` methods; give every non-Godot host a value-returning no-op. Unit test: register an object then query its handle → returns the object's slot; unknown handle → -1. **Gate:** all existing engine tests byte-identical. +- [ ] **Task 3b — Dispatch the reversed ops in the VM.** Replace the `OnStub` fall-through for each contract'd op in `VirtualMachine.Step`, calling the new `IHost` methods and writing outputs to operands per the contract. Unit test: a synthetic scene that registers two different-sized backgrounds and draws them lands each at its own correct `dst` (no cumulative drift). **Gate:** `--selftest` + engine suite green. +- [ ] **Task 3c — CLI oracle uses the real model.** Wire `GfxTraceHost` to a real `GfxState` so `Age.Cli gfx ` reports true per-object slots/geometry. Verify on the drift scene: background resolves to `(0,0)` full-frame, not `(300,500)`. +- [ ] **Task 3d — Godot compositor.** `GodotAdvHost`/`Main.cs` composite the per-object slot set. Build (`godot --import` → `dotnet build godot/Himegari.csproj`), selftest parity. + +--- + +## Phase 4 — Validate against the drift scene + +- [ ] **Task 4a — Numeric oracle.** `Age.Cli gfx ` shows every `BG*`/sprite at plausible on-screen geometry (no `0×0`, no marching `dst`). Record the before/after numbers in `docs/phase-a-slice-plan.md` (A2b section). +- [ ] **Task 4b — Screenshot acceptance.** `godot --path godot -- --scene --shot [--shot-page N]`; the background fills the frame correctly (the bottom-right castle from `Screenshot 2026-07-06 211353.png` is snapped into place). This is the human oracle — no machine oracle for pixels. +- [ ] **Task 4c — Regression + docs.** Full `dotnet test engine/AgeEngine.sln` green; Godot `--selftest` OK; update `docs/phase-a-slice-plan.md` (A2b: drift RESOLVED, implemented) and the status memory. Then `finishing-a-development-branch` for merge. + +**Phase 4 exit criteria:** the drift scene renders correctly (screenshot-verified), engine + selftest green, docs/memory updated to "drift resolved (implemented)". + +--- + +## Self-review + +- **Spec coverage:** the (b) verdict → Phase 1 reverses the whole family; the fix ("model + execute the ops") → Phases 2–3; acceptance (the drift screenshot) → Phase 4. ✓ +- **Discovery gate honesty:** Phase 3/4 are deliberately task-level, not fake TDD code, because the handler contracts are Phase 1's output; the plan states the re-plan checkpoint explicitly. ✓ +- **Placeholder scan:** Phase 1 & 2 are fully actionable now; Phase 3/4 carry concrete files, gates, and acceptance criteria, with the expansion gate named. No "TBD/handle edge cases" hand-waving. ✓ +- **Seam/parity consistency:** every phase re-states the `Vm`→`Model+Hosting+Diagnostics` seam and the non-Godot no-op parity rule; `IHost`-method pattern matches the established `GetTextureSize`. ✓