From 5af56226018c9738e22fce8a339386b2ee187f9f Mon Sep 17 00:00:00 2001 From: gamer147 Date: Mon, 6 Jul 2026 15:37:05 -0400 Subject: [PATCH] docs(a2b): spec for the engine-driven background layer (SC0000) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../specs/2026-07-06-a2b-background-design.md | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-06-a2b-background-design.md diff --git a/docs/superpowers/specs/2026-07-06-a2b-background-design.md b/docs/superpowers/specs/2026-07-06-a2b-background-design.md new file mode 100644 index 0000000..6fc686a --- /dev/null +++ b/docs/superpowers/specs/2026-07-06-a2b-background-design.md @@ -0,0 +1,115 @@ +# Design: A2b-Background — Engine-Driven Background Layer (SC0000) + +Status: **approved (design)** · Date: 2026-07-06 +Related: `docs/phase-a-slice-plan.md` (A2), the A2a Godot project (`godot/`), `engine/Age.Engine`, +`build/data/CGINIT.json`, `extracted/DATA1/AGF2BMP2AGF.exe`, `extracted/DATA2|DATA5/*.AGF`. + +## Problem / goal + +A2a plays SC0000's dialogue on a black screen. A2b-Background renders the scene's **background layer**, +**driven by the bytecode** — the VM's `create/set/draw-texture` ops execute for real, the Godot backend +renders the slots whose resource it can resolve through a **data map**, and the background appears +*because* those ops ran (remove them → nothing draws). This is **not** a pinned/hardcoded image; it is +the first real graphics op-path promoted from stub to backend, with resolution as growable profile data. + +## Scope + +**In:** the single **background layer** for SC0000 — promote `create-texture`/`set-texture`/ +`draw-texture` to typed `IHost` methods; a `resourceId → AGF` resolution map (seeded for SC0000's bg via +investigation); offline AGF→image conversion; Godot composites the resolved full-screen background behind +the dialogue. **Out:** the other ~239 draws (full sprite/UI/effects compositor), general resolution for +*all* resources, per-op fade/blend semantics, voice, choices, non-SC0000 scenes. These follow later; the +resolution map and rendered slots grow over time. + +## The engine-driven principle (why this isn't a pin) + +The bytecode drives every texture op. The backend keeps a `slot → texture` table; `SetTexture(resId,slot)` +resolves `resId` via the map and loads the (pre-converted) image into that slot; `DrawTexture(slot,…)` +renders a resolved full-screen slot into the background node. Unresolved slots (the other 239 draws) render +nothing **yet**. The background shows only because `set-texture`/`draw-texture` executed and `resId` +resolved. The map is per-game profile *data*, not a code shortcut; coverage grows as we map more resources. + +## Architecture + +### Engine — promote texture ops (`Age.Engine`) +Add to `IHost` (mirrors the `WaitForInput` addition; `CaptureHost` no-ops them so **A1 trace-diff + A2a +selftest stay green**): +```csharp +void CreateTexture(int slot, int width, int height); +void SetTexture(long resourceId, int slot); +void DrawTexture(int slot, int x, int y, int width, int height); +``` +In `VirtualMachine.Step`, replace the `OnStub` fall-through for these opcodes with handlers that read the +operands and call the host (operand roles are **inferred** from disassembly — Phase 1 confirms/adjusts): +- `create-texture` args `(slot, w, h, ?)` → `CreateTexture(Read(a0), (int)Read(a1), (int)Read(a2))` +- `set-texture` args `(resId, slot, ?)` → `SetTexture(Read(a0), (int)Read(a1))` +- `draw-texture` args `(fade, slot, x, y, w, h, ?, ?)` → `DrawTexture((int)Read(a1), (int)Read(a2), (int)Read(a3), (int)Read(a4), (int)Read(a5))` + +Opcode numbers (from `vm-map/opcodes.toml`): `create-texture`, `set-texture`, `draw-texture` are named +Kelebek ops; the VM dispatches on those labels exactly as it does `show-text`. + +### Resolution map (profile data) +`build/resources.json` (or a profile dir): `{ "": "" }`, seeded by Phase 1 +for SC0000's background. **Format is tentative** — Phase 1 may show ids are scene-scoped (then keyed +`{scene: {id: agf}}`) rather than global. Lives on the backend/profile side, never in the VM core. + +### AGF → image pipeline +Offline: run `AGF2BMP2AGF.exe .AGF` (verified: produces an 800×600 24-bit BMP) on the mapped AGFs, +into a Godot-loadable assets dir (e.g. `build/textures/`). Godot loads with `Image.LoadFromFile` + +`ImageTexture.CreateFromImage`. (A tiny `tools/convert_agf.py` wrapper converts the set named in +`resources.json`.) + +### Godot backend + compositing (`godot/`) +`Main` adds a background `TextureRect` behind the dialogue Label (full-rect, `StretchMode` = keep-aspect- +covered), created before the dialogue label so it's z-behind. `GodotAdvHost` implements the texture ops: +- `SetTexture(resId, slot)` → resolve `resId` in the map; if found, `CallDeferred` load the pre-converted + image into a `slot → ImageTexture` dict (main thread). +- `DrawTexture(slot, x, y, w, h)` → if the slot has a texture and the draw is full-screen (≈800×600), + `CallDeferred` set the background `TextureRect`'s texture. Unresolved/non-fullscreen slots: ignore. +- `CreateTexture` → record slot dimensions (used to distinguish full-screen bg from small textures). + +## Phase 1 — Investigation (gated; the real unknown) + +Goal: resolve **SC0000's background AGF** and document the `resourceId → AGF` mechanism. +1. **Static:** confirm the operand roles of `create/set/draw-texture`; identify which full-screen draw is + the *visible* background (SC0000 draws slot 3 cross-context and slot 0xe from local `set-texture 0x21`); + probe the resolution — `SYS4INI` (ALF filename index), `*INIT` tables, any id→name path. (`CGINIT` is a + per-CG numeric record table, **not** a direct filename map, so resolution is likely a runtime path.) +2. **Frida (if static is inconclusive):** attach to the running translated game on SC0000; hook the + graphic-load call; capture the actual `resourceId → AGF filename` for the background draw. This yields + both the mechanism and a **ground-truth oracle**. +3. **Confirm:** convert the resolved AGF, eyeball that it is SC0000's intro background; seed + `resources.json`. + +**Gate:** Phase 1's result confirms the operand mapping + resolution shape before the engine handlers and +map format are finalized. Expect Frida to be needed (first Frida use in the project). + +## Validation + +- **Headless resolve-oracle:** a test runs SC0000 with a recording host and asserts the background draw + path fires `SetTexture` with the expected resource id, and the map resolves it to the expected AGF + (Phase 1 ground truth) — validates engine-driven resolution *without pixels*. +- **Regression:** `dotnet test engine/AgeEngine.sln` stays green and the A2a `--selftest` still prints + `SELFTEST OK` (new `IHost` methods no-op in `CaptureHost`). +- **Manual visual (human):** the converted background renders behind SC0000's dialogue in the Godot window + and looks like the intro art; dialogue still advances on click/Enter. + +## Risks + +- **Resolution is the crux and uncertain-sized.** `CGINIT` isn't a filename map and SC0000's primary bg + slot is loaded cross-context; static RE may quickly stall → Frida. Mitigation: the machinery (op wiring, + pipeline, compositing, map-lookup) is built independently of *how* the map is populated; Phase 1 only has + to yield SC0000's one bg entry to unblock the visual. +- **Draw-op operand roles are inferred** — Phase 1 confirms before the handlers are trusted; the headless + oracle (which resId the bg path resolves) catches a wrong mapping. +- **No pixel oracle** — background correctness is human-eyeballed; the resolve-oracle is the automatable + proxy. Frida capture is the strongest available ground truth. +- **First Frida use** — attaching to the packed AGE.EXE at runtime (packer is bypassed once running); + budget setup time. If Frida is impractical, static-plus-eyeball is the fallback for this one bg. +- **Trace parity** — adding `IHost` texture methods must not change `Steps`/`Emitted`; guaranteed by + `CaptureHost` no-ops (verified by the A1 trace-diff regression). + +## Out of scope (later slices) + +Full draw compositor (all 239 draws, layers, blends, fades), general/automated resolution for all +resources, sprite/CG/UI rendering, voice + BGM, choices, `call-script`/state seeding, non-SC0000 scenes.