The retained-mode "2nd CG renders off-screen" bug: GfxState conflated two
distinct native structures. It assigned a fabricated AcquireSlot() slot on
every GetOrCreate (called by all geometry/draw ops) and returned it from
QuerySlot (op 0x215). But Ghidra (gfx_op_0x215_register_query @0x42a0b0 /
gfx_op_0x1a2_registry_insert @0x42d360) shows 0x215 does map.find(handle) over
a registry populated ONLY by op 0x1a2 -- it never allocates a slot.
So a CG handle (never 0x1a2-registered) read back as "existing", took the
existing branch of label_12649, ran get-texture-size on the wrong slot (0),
got size 0, and computed dst = pos(0,0) - (w/2,h) = (-400,-600) -> off-screen.
The real engine returns -1 -> the fresh branch -> anchor from the INIT2 arrays
-> dst=(0,0).
Fix: GfxState keeps a separate _registry (HashSet) populated only by
Register() (op 0x1a2); QuerySlot returns the handle if registered else -1, and
no longer consults the geometry store or invents slots. Drop AcquireSlot /
GfxObject.Slot / the free-list.
Verified: Age.Cli gfx --boot SC0000.BIN -> all event CGs dst=(0,0), zero
(-400,-600) draws; Godot --boot pages 1/2/4 render opening CGs full-screen;
engine 44/44; sweep parity 284 exit / 13 STEP-LIMIT unchanged.
Docs: engine-re.md (query-registry-vs-geometry-store section), opcodes.toml
0x1a2/0x215 rebuilt; Ghidra helpers gfx_registry_map_find/hash_insert
annotated + saved. Tests rewritten to the native contract.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GfxState: SurfaceStore (slot->{resId,colorkey} from create/set-texture) + object
SourceSlot/SrcRect/Visible (from draw-texture bind); SnapshotVisibleObjects returns
visible objects in ascending-handle order (=z-order) with their live surface resolved.
VM set/create/draw-texture wired to it. Oracle dumps visible objects. Erase removes the
object from the registry (faithful). Engine 44 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
RE correction: op 0x1f7's worker gfx_registry_erase_range (@0x47d8b0) loops
gfx_registry_erase over [handle, handle+count) — a teardown, not a create.
Objects are created lazily by the geometry SET ops (gfx_object_get_or_create).
GfxState.EraseRange + VM case + tests; opcodes.toml renamed gfx-elem-create ->
gfx-elem-erase; Ghidra handler + workers annotated. Booted SC0000 CG geometry
unchanged for the working CGs (no regression). Engine 40 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
0x202/0x203 pack (alpha,color) via GfxState.PackColor onto the object; the actual
alpha/additive blend is deferred and surfaced once through the trace sink (observe-
only, parity held). Full suite 37 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gfx property on VirtualMachine + dispatch cases for the query ops (0x215/0x216/
0x218/0x21a), set ops (0x217/0x219/0x1ff/0x212/0x213) and lifecycle (0x1a2/0x1f7/
0x1fa). Query ops write results to operands like get-texture-size; set ops mutate
GfxState. Round-trip + distinct-slot tests pass; full suite 36 green (parity held).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Also gate Stub events under TracingSteps: stubbed ops are per-instruction
frequency (0x258/0x259 stmt markers en masse), so --trace stays a clean
high-level flow view; --trace-steps shows step+stub detail.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Integration: ADDILL executes ADDILLSUB + CALCREVISE and returns to its own exit.
Sweep (execution on): 284/297 exit clean, 13 STEP-LIMIT (input/state-gated ADV
scenes that now spin headless once subroutine global-writes drive their loops —
state divergence, not a call-script bug; 0 depth-cap, 0 unresolved, 0 crashes).
Removed the dead _halted field (halt propagates via FrameOutcome).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Promote 0x208 from stub to a real VM op that writes the loaded texture's
width/height into its two output globals, via new IHost.GetTextureSize. This is
the single native primitive the CG-load subroutine (SC0000 label_12649) needs;
all downstream centering/anchor geometry is already computed in bytecode.
Non-Godot hosts return (0,0) so trace/selftest parity holds (engine 9/9 incl.
TraceDiffTests). Godot host gets a temporary (0,0) stub; real impl in the
compositor task.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire audio end-to-end (OGG plays natively in Godot; no Frida, no decode):
- IHost.PlayBgm/PlayVoice + VM dispatch for play-bgm(0xbf)/play-voice(0xc4).
Non-Godot hosts no-op them so --selftest + engine 8/8 stay byte-identical.
- GodotAdvHost resolves id->OGG; Main plays via two AudioStreamPlayer nodes
(BGM looping; voice interrupt-on-new).
Key finding (by-ear, systematic-debugging): the two audio ops use DIFFERENT
addressing — the earlier "unified manifest" assumption was wrong for BGM.
- play-voice -> per-scene manifest files[base+id] (offset 0, same as textures).
Confirmed by ear; upgraded med->HIGH.
- play-bgm -> DIRECT LITERAL NAME BGM{id:03d}.OGG, NOT the manifest.
Real game: play-bgm 5->BGM005, 8->BGM008 (manifest gave +1). Proven by
play-bgm 0x23->BGM035 (real standalone track; BGM set skips 030-034) that the
manifest mis-resolved to a graphics entry. Fix is BGM-only:
ResourceMap.BgmPathById; voices/textures unchanged.
Also: Lily's silence root-caused as correct form-gating (G[0xa57/0xa58/0xa59]),
left unseeded by choice (no dummy state). Added diagnostic
`Age.Cli audio <SCENE.BIN> [0xADDR=VAL ...]`. Corrected opcodes.toml (play-bgm
direct-name, play-voice HIGH) + docs/memory (dropped the bogus unified-manifest
/ Frida-BGM006 claims).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Runtime finding: SC0000's bg is a slot-0 full-screen slideshow (res 0x23 first), not the
static-path res 0x21. Target corrected to 0x23. CaptureHost/CountHost no-op the new methods.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>