27 KiB
A2b Graphics Geometry 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: Implement opcode 0x208 (get-texture-size) so the bytecode's own geometry math produces correct sprite/background positions, and replace the Godot host's TextureRect-per-slot approximation with a faithful 800×600 immediate-mode blit compositor.
Architecture: The CG-load subroutine (SC0000.asm label_12649) computes all geometry in bytecode from anchor globals + the texture's width/height; the only missing native primitive is 0x208 = get-texture-size(slot) → (out_w, out_h). We add IHost.GetTextureSize, make 0x208 a real VM op that writes the two output globals, and give the Godot host a screen-backbuffer blit compositor whose draw-texture blits a source slot's image (src rect → dst) into one shared canvas in execution order. Texture dims are read from the pre-converted BMP header on the VM thread (synchronous); pixel blits happen deferred on the Godot main thread.
Tech Stack: C# / .NET 8 (engine/, solution AgeEngine.sln), Godot 4.7 .NET (godot/Himegari.csproj), Python 3.11 for the opcode build (tools/opcodes_build.py), xUnit for engine tests.
Global Constraints
- Run Python as
py -3.11 -X utf8 tools/<name>.py …(utf8 mode mandatory on Windows). - Never hand-edit generated files.
tools/age_opcodes_himegari.py,build/opcodes.json,docs/opcode-reference.md,build/opcode-coverage.mdare generated fromvm-map/opcodes.tomlviapy -3.11 -X utf8 tools/opcodes_build.py --build. - Trace/selftest parity is a guardrail. After any VM change, the C# trace must stay byte-identical to
build/vm0-trace.json(offsets + halt + step count). Non-Godot hosts (CaptureHost, test/CLIRecHosts) must no-op the new op (return(0,0)). - Seam rule:
Age.Engine/Vmreferences onlyModel+Hosting(neverSys4).IHostlives inAge.Engine/Hosting. - Opcode
categoryvocabulary is fixed:marker/structural/control/adv/draw/audio/input/compute/unknown. Usedrawfor0x208. - Valid opcode
confidence: usemedfor0x208(inference from disassembly, not Frida-confirmed). - Build engine + tests:
dotnet test engine/AgeEngine.sln. Build Godot:godot --headless --path godot --importthendotnet build godot/Himegari.csproj. Godot selftest:godot --headless --path godot -- --selftest.
File structure
| File | Responsibility | Task |
|---|---|---|
vm-map/opcodes.toml |
Rename 0x208 → get-texture-size (source edit) |
1 |
engine/Age.Engine/Hosting/IHost.cs |
Add GetTextureSize to the host contract |
1 |
engine/Age.Engine/Hosting/CaptureHost.cs |
No-op impl → (0,0) (parity) |
1 |
engine/Age.Engine/Vm/VirtualMachine.cs |
case "get-texture-size" handler (writes 2 globals) |
1 |
engine/Age.Cli/Program.cs |
Add impl to AudioTraceHost; add gfx command + GfxTraceHost |
1, 2 |
engine/Age.Engine.Tests/TextureOpsTests.cs |
Add GetTextureSize to test RecHost |
1 |
engine/Age.Engine.Tests/TextureGeometryTests.cs |
New: 0x208 writes host dims into output globals |
1 |
engine/Age.Engine/Sys4/BmpHeader.cs |
New: ReadDims(path) → (w,h) from BMP header |
2 |
engine/Age.Engine.Tests/BmpHeaderTests.cs |
New: ReadDims on a known BMP |
2 |
godot/GodotAdvHost.cs |
Per-slot BMP path + dims; real GetTextureSize; DrawTexture → blit |
1 (stub), 3 |
godot/Main.cs |
Screen Image backbuffer + one TextureRect; BlitSlot |
3 |
Task 1: 0x208 get-texture-size — real VM op + host contract
Make 0x208 a VM op that writes the loaded texture's width/height into its two output globals, backed by IHost.GetTextureSize. All non-Godot hosts return (0,0) so trace/selftest parity holds. The Godot host gets a temporary (0,0) stub here (real impl in Task 3) so Himegari.csproj keeps compiling.
Files:
- Modify:
vm-map/opcodes.toml(op0x208block, ~line 4980) - Modify:
engine/Age.Engine/Hosting/IHost.cs:10(add method) - Modify:
engine/Age.Engine/Hosting/CaptureHost.cs - Modify:
engine/Age.Engine/Vm/VirtualMachine.cs(add case near the other texture ops, ~line 175) - Modify:
engine/Age.Cli/Program.cs(AudioTraceHost) - Modify:
engine/Age.Engine.Tests/TextureOpsTests.cs(RecHost) - Modify:
godot/GodotAdvHost.cs(temporary stub) - Test:
engine/Age.Engine.Tests/TextureGeometryTests.cs(new)
Interfaces:
-
Produces:
IHost.GetTextureSize(int slot) → (int Width, int Height). VM handler for label"get-texture-size": reads operand 0 as the slot, writesWidthto operand 1 andHeightto operand 2 (both viaWrite). -
Consumes: existing
Read/Writeoperand helpers;OpcodeTable.Label(op)dispatch (dispatches on thelabelfield, confirmed:set-texture/draw-texturecarry friendly labels inopcodes.toml). -
Step 1: Rename the opcode in the source-of-truth and rebuild
Edit vm-map/opcodes.toml, the op = 0x208 block. Change the label line and the [opcode.semantics] fields (leave argc = 3 and the three [[opcode.semantics.args]] blocks; optionally set their roles):
[[opcode]]
op = 0x208
label = "get-texture-size"
argc = 3
abi_source = "kelebek+decode-validated"
[opcode.semantics]
name = "get-texture-size"
category = "draw"
summary = "0x208 (slot)(out_w)(out_h) — writes the loaded texture's width/height into two output globals; keystone for bytecode-computed sprite/bg geometry (SC0000 label_12649)"
noop_headless = false
source = "inference"
confidence = "med"
depends_on = []
evidence = "SC0000 label_12649: set-texture(resId,slot) then 0x208(slot)->w,h feeds w/2 horizontal-center + foot-anchor subtraction into draw-texture dst; stubbing yields 0x0 sizes / off-center draws"
[[opcode.semantics.args]]
i = 1
role = "slot"
observed_types = ["imm", "g-int", "l-int"]
[[opcode.semantics.args]]
i = 2
role = "out_width"
observed_types = ["g-int", "l-int"]
[[opcode.semantics.args]]
i = 3
role = "out_height"
observed_types = ["g-int", "l-int"]
Then rebuild the generated files:
Run: py -3.11 -X utf8 tools/opcodes_build.py --build
Expected: succeeds, writes the 4 generated files.
Verify the label propagated:
Run: grep -n '"label": "get-texture-size"' build/opcodes.json
Expected: one match.
- Step 2: Add
GetTextureSizetoIHostand all existing implementors (compile green)
In engine/Age.Engine/Hosting/IHost.cs, add after DrawTexture (line 10):
(int Width, int Height) GetTextureSize(int slot);
In engine/Age.Engine/Hosting/CaptureHost.cs, add:
public (int Width, int Height) GetTextureSize(int slot) => (0, 0);
In engine/Age.Cli/Program.cs, add to AudioTraceHost (with the other no-op IHost members):
public (int Width, int Height) GetTextureSize(int slot) => (0, 0);
In engine/Age.Engine.Tests/TextureOpsTests.cs, add to RecHost:
public (int Width, int Height) GetTextureSize(int slot) => (0, 0);
In godot/GodotAdvHost.cs, add a temporary stub (real impl in Task 3), next to the texture ops:
public (int Width, int Height) GetTextureSize(int slot) => (0, 0);
Run: dotnet build engine/AgeEngine.sln
Expected: build succeeds (no IHost member missing).
- Step 3: Write the failing test
Create engine/Age.Engine.Tests/TextureGeometryTests.cs:
using System.Collections.Generic;
using Age.Engine.Hosting;
using Age.Engine.Model;
using Age.Engine.Sys4;
using Age.Engine.Vm;
using Xunit;
public class TextureGeometryTests
{
private sealed class FakeSizeHost : IHost
{
public void ShowText(int o, string t) { }
public void CallScript(long id) { }
public void OnStub(int op) { }
public void WaitForInput() { }
public void CreateTexture(int slot, int w, int h) { }
public void SetTexture(long resId, int slot) { }
public void DrawTexture(int slot, int sx, int sy, int w, int h, int dx, int dy) { }
public void PlayBgm(long id) { }
public void PlayVoice(long id) { }
public (int Width, int Height) GetTextureSize(int slot) => (0x140, 0xC8);
}
[Fact]
public void GetTextureSizeWritesHostDimsIntoOutputGlobals()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
// 0x208 (global-int 50)(global-int 60)(global-int 61): slot=50, out_w=G[60], out_h=G[61]
const int T_GINT = 3;
var ins = new Instruction(0, 0x208, new[]
{
new Operand(T_GINT, 50), new Operand(T_GINT, 60), new Operand(T_GINT, 61),
});
var script = new Script
{
Header = new ScriptHeader(0, 0, 0, 0, 0, 0),
Instructions = new[] { ins },
IndexByOffset = new Dictionary<int, int> { { 0, 0 } },
Strings = new Dictionary<int, string>(),
};
var vm = new VirtualMachine(script, table, new FakeSizeHost());
vm.Run();
Assert.Equal(0x140, vm.Globals[60]);
Assert.Equal(0xC8, vm.Globals[61]);
}
}
- Step 4: Run the test to verify it fails
Run: dotnet test engine/AgeEngine.sln --filter FullyQualifiedName~TextureGeometryTests
Expected: FAIL — 0x208 currently dispatches to OnStub, so G[60]/G[61] are never written and vm.Globals[60] throws KeyNotFoundException (or the asserts fail).
- Step 5: Add the VM handler
In engine/Age.Engine/Vm/VirtualMachine.cs, add a case alongside the other texture ops (after the draw-texture case, ~line 177):
case "get-texture-size": // 0x208 (slot) (out_w) (out_h)
{
var (gw, gh) = _host.GetTextureSize((int)Read(a[0]));
Write(a[1], gw); Write(a[2], gh);
return pc + 1;
}
- Step 6: Run the new test to verify it passes
Run: dotnet test engine/AgeEngine.sln --filter FullyQualifiedName~TextureGeometryTests
Expected: PASS.
- Step 7: Run the full engine suite (parity guardrail)
Run: dotnet test engine/AgeEngine.sln
Expected: all tests PASS — including TraceDiffTests (C# trace byte-identical to build/vm0-trace.json). Rationale: with CaptureHost.GetTextureSize returning (0,0), the handler writes 0/0, which equals the prior default; the op is still one step, so offsets/halt/steps are unchanged.
- Step 8: Confirm the Godot project still compiles
Run: dotnet build godot/Himegari.csproj
Expected: build succeeds (the temporary GetTextureSize stub satisfies IHost).
- Step 9: Commit
git add vm-map/opcodes.toml tools/age_opcodes_himegari.py build/opcodes.json docs/opcode-reference.md build/opcode-coverage.md \
engine/Age.Engine/Hosting/IHost.cs engine/Age.Engine/Hosting/CaptureHost.cs \
engine/Age.Engine/Vm/VirtualMachine.cs engine/Age.Cli/Program.cs \
engine/Age.Engine.Tests/TextureOpsTests.cs engine/Age.Engine.Tests/TextureGeometryTests.cs \
godot/GodotAdvHost.cs
git commit -m "feat(a2b): implement 0x208 get-texture-size (geometry keystone)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
Task 2: BmpHeader.ReadDims + gfx diagnostic (headless numeric oracle)
Add a reusable BMP-dimension reader and an Age.Cli gfx <SCENE> command that runs a scene and dumps each set-texture / get-texture-size / draw-texture with resolved file + computed geometry — so we can validate geometry numerically without Godot (mirrors the audio command).
Files:
- Create:
engine/Age.Engine/Sys4/BmpHeader.cs - Test:
engine/Age.Engine.Tests/BmpHeaderTests.cs(new) - Modify:
engine/Age.Cli/Program.cs(addgfxcommand +GfxTraceHost)
Interfaces:
-
Produces:
BmpHeader.ReadDims(string path) → (int Width, int Height)(0,0 on failure/missing);Age.Cli gfx <SCENE.BIN> [0xADDR=VAL ...]. -
Consumes:
ResourceMap.Resolve/ResourceMap.TexturePath(Task-independent, existing); the0x208handler from Task 1. -
Step 1: Write the failing test for
ReadDims
Create engine/Age.Engine.Tests/BmpHeaderTests.cs:
using System.IO;
using Age.Engine.Sys4;
using Xunit;
public class BmpHeaderTests
{
[Fact]
public void ReadDimsReadsWidthAndHeightFromBmpHeader()
{
// Minimal 54-byte BMP header (BITMAPFILEHEADER 14 + BITMAPINFOHEADER 40); width=4, height=3.
var b = new byte[54];
b[0] = (byte)'B'; b[1] = (byte)'M';
System.BitConverter.GetBytes(40).CopyTo(b, 14); // header size
System.BitConverter.GetBytes(4).CopyTo(b, 18); // width
System.BitConverter.GetBytes(3).CopyTo(b, 22); // height
var tmp = Path.Combine(Path.GetTempPath(), "agehdr_test.bmp");
File.WriteAllBytes(tmp, b);
try
{
var (w, h) = BmpHeader.ReadDims(tmp);
Assert.Equal(4, w);
Assert.Equal(3, h);
}
finally { File.Delete(tmp); }
}
[Fact]
public void ReadDimsReturnsZeroForMissingFile()
{
var (w, h) = BmpHeader.ReadDims(Path.Combine(Path.GetTempPath(), "does_not_exist_agehdr.bmp"));
Assert.Equal((0, 0), (w, h));
}
}
- Step 2: Run the test to verify it fails
Run: dotnet test engine/AgeEngine.sln --filter FullyQualifiedName~BmpHeaderTests
Expected: FAIL — BmpHeader does not exist (compile error).
- Step 3: Implement
BmpHeader
Create engine/Age.Engine/Sys4/BmpHeader.cs:
namespace Age.Engine.Sys4;
/// <summary>
/// Reads pixel dimensions from a BMP file header (BITMAPINFOHEADER: width at byte 18, height at byte 22,
/// both little-endian int32; height may be negative for top-down bitmaps). Used to give the VM the
/// texture size that opcode 0x208 (get-texture-size) needs, without decoding pixels. Our textures are
/// pre-converted BMPs (tools/convert_agf.py).
/// </summary>
public static class BmpHeader
{
public static (int Width, int Height) ReadDims(string? path)
{
if (string.IsNullOrEmpty(path) || !File.Exists(path)) return (0, 0);
try
{
var b = new byte[26];
using var fs = File.OpenRead(path);
if (fs.Read(b, 0, 26) < 26 || b[0] != (byte)'B' || b[1] != (byte)'M') return (0, 0);
int w = System.BitConverter.ToInt32(b, 18);
int h = System.BitConverter.ToInt32(b, 22);
return (System.Math.Abs(w), System.Math.Abs(h));
}
catch { return (0, 0); }
}
}
- Step 4: Run the test to verify it passes
Run: dotnet test engine/AgeEngine.sln --filter FullyQualifiedName~BmpHeaderTests
Expected: PASS (both facts).
- Step 5: Add the
gfxcommand andGfxTraceHostto the CLI
In engine/Age.Cli/Program.cs, add a new command block after the audio block (before the trace block):
if (args[0] == "gfx")
{
// gfx <SCENE.BIN> [0xADDR=VAL ...] — run the scene and dump executed texture ops in order with
// resolved file + computed geometry (set-texture / get-texture-size / draw-texture). Diagnostic only.
var sceneName = args[1];
var sceneKey = Path.GetFileNameWithoutExtension(sceneName).ToUpperInvariant();
var res = ResourceMap.Load();
var host = new GfxTraceHost(res, sceneKey);
var vm = new VirtualMachine(Sys4Loader.Load(Paths.Scripts()[sceneName.ToUpperInvariant()], table), table, host);
foreach (var s in args.Skip(2))
{
var kv = s.Split('=');
int k = kv[0].StartsWith("0x") ? Convert.ToInt32(kv[0], 16) : int.Parse(kv[0]);
long v = kv[1].StartsWith("0x") ? Convert.ToInt64(kv[1], 16) : long.Parse(kv[1]);
vm.Globals[k] = v;
}
vm.Run();
Console.WriteLine($"{sceneName}: {host.Events.Count} texture ops (halt: {vm.HaltReason})");
foreach (var line in host.Events) Console.WriteLine(" " + line);
return 0;
}
Add the GfxTraceHost class next to AudioTraceHost at the bottom of Program.cs:
sealed class GfxTraceHost : IHost
{
private readonly ResourceMap _res;
private readonly string _scene;
private readonly Dictionary<int, string?> _slotBmp = new(); // slot -> resolved BMP path (or null)
public List<string> Events { get; } = new();
public GfxTraceHost(ResourceMap res, string scene) { _res = res; _scene = scene; }
public (int Width, int Height) GetTextureSize(int slot)
{
_slotBmp.TryGetValue(slot, out var bmp);
var (w, h) = BmpHeader.ReadDims(bmp);
Events.Add($"get-tex-size slot={slot} -> {w}x{h}");
return (w, h);
}
public void SetTexture(long resId, int slot)
{
var e = _res.Resolve(_scene, resId);
var bmp = e != null ? ResourceMap.TexturePath(e) : null;
_slotBmp[slot] = bmp;
Events.Add($"set-texture slot={slot} res=0x{resId:x} -> {(e?.Name ?? "<unresolved>")}"
+ (bmp == null ? " [NO BMP]" : ""));
}
public void DrawTexture(int slot, int sx, int sy, int w, int h, int dx, int dy)
{
_slotBmp.TryGetValue(slot, out var bmp);
Events.Add($"draw-texture slot={slot} src=({sx},{sy} {w}x{h}) dst=({dx},{dy}) "
+ $"file={(bmp != null ? System.IO.Path.GetFileName(bmp) : "<none>")}");
}
public void CreateTexture(int slot, int width, int height) => Events.Add($"create-texture slot={slot} {width}x{height}");
public void ShowText(int offset, string text) { }
public void CallScript(long id) { }
public void OnStub(int opcode) { }
public void WaitForInput() { }
public void PlayBgm(long id) { }
public void PlayVoice(long id) { }
}
- Step 6: Run the diagnostic and inspect the geometry
Run: dotnet run --project engine/Age.Cli -- gfx SC0000.BIN
Expected: prints an ordered list of texture ops. Sanity checks:
- The full-screen background/event-CG draws show
src=(0,0 800x600)(0x320×0x258)dst=(0,0)— i.e.800x600/800x500(bg is natively 800×500) at the origin. - Any sprite
draw-textureshows non-zerow×hand a plausible on-screendst(not0x0, not wildly off-canvas). Before Task 1 these were0×0; theget-tex-sizelines now report real dims from the BMP header.
(No unit test for the command itself — it's a diagnostic, like audio. The ReadDims unit test covers the load-bearing logic.)
- Step 7: Commit
git add engine/Age.Engine/Sys4/BmpHeader.cs engine/Age.Engine.Tests/BmpHeaderTests.cs engine/Age.Cli/Program.cs
git commit -m "feat(a2b): BmpHeader.ReadDims + gfx diagnostic (headless geometry oracle)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
Task 3: Godot screen-backbuffer blit compositor
Replace the TextureRect-per-slot approximation with one 800×600 screen Image that draw-texture blits into (src rect → dst) in execution order, displayed by a single TextureRect. The Godot host reports real texture dims (from the BMP header, on the VM thread) via GetTextureSize, replacing the Task-1 stub. This is the pass that makes sprites and backgrounds actually appear at correct positions/sizes on screen.
Files:
- Modify:
godot/GodotAdvHost.cs(per-slot path + dims; realGetTextureSize;DrawTexture→ deferred blit) - Modify:
godot/Main.cs(screenImagebackbuffer + oneTextureRect;BlitSlot; remove per-slotTextureRects)
Interfaces:
-
Consumes:
BmpHeader.ReadDims(Task 2);ResourceMap.Resolve/TexturePath(existing);IHost.GetTextureSize(Task 1). -
Produces:
Main.BlitSlot(string bmpPath, int srcX, int srcY, int w, int h, int dstX, int dstY)(main-thread,CallDeferred-invoked); a single displayed screen texture. -
Step 1: Rework
GodotAdvHost— cache path + dims per slot, realGetTextureSize, blit on draw
In godot/GodotAdvHost.cs, replace the texture-ops region (the _slotBmp field stays; add a dims cache) so it reads:
private readonly Dictionary<int, string?> _slotBmp = new(); // slot -> pre-converted BMP path
private readonly Dictionary<int, (int W, int H)> _slotDims = new(); // slot -> texture dims (BMP header)
// ---- texture ops (run on the VM thread; marshal Godot node work to the main thread) ----
public void CreateTexture(int slot, int width, int height) { _slotBmp[slot] = null; _slotDims[slot] = (0, 0); }
public void SetTexture(long resourceId, int slot)
{
var asset = _res.Resolve(_scene, resourceId);
var bmp = asset != null ? ResourceMap.TexturePath(asset) : null;
_slotBmp[slot] = bmp;
_slotDims[slot] = BmpHeader.ReadDims(bmp); // synchronous: dims from the header, no Godot Image
}
// Dims are read from the BMP header on the VM thread so the bytecode's geometry math (which calls this
// synchronously right after set-texture) sees the real size. Pixels are blitted later on the main thread.
public (int Width, int Height) GetTextureSize(int slot)
=> _slotDims.TryGetValue(slot, out var d) ? (d.W, d.H) : (0, 0);
public void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY)
{
if (_slotBmp.TryGetValue(slot, out var bmp) && bmp != null)
_main.CallDeferred("BlitSlot", bmp, srcX, srcY, width, height, dstX, dstY);
}
Add the BmpHeader namespace use — GodotAdvHost.cs already has using Age.Engine.Sys4;, so BmpHeader.ReadDims resolves. Remove the temporary GetTextureSize stub added in Task 1 (it is replaced by the real one above).
- Step 2: Rework
Main— one screen backbuffer, blit into it, drop per-slot TextureRects
In godot/Main.cs, replace the slot fields and _Ready stage setup so the stage holds one TextureRect showing a shared screen Image. Replace:
private Control _stage = null!; // texture layer (behind the text)
private readonly Dictionary<int, TextureRect> _slots = new();
with:
private TextureRect _screenView = null!; // shows the composited screen backbuffer
private Image _screen = null!; // 800x600 immediate-mode canvas
private ImageTexture _screenTex = null!;
In _Ready, replace the _stage block with:
// Screen backbuffer: one 800x600 canvas that draw-texture blits into, shown behind the dialogue.
_screen = Image.CreateEmpty(800, 600, false, Image.Format.Rgba8);
_screenTex = ImageTexture.CreateFromImage(_screen);
_screenView = new TextureRect
{
Texture = _screenTex,
ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize,
StretchMode = TextureRect.StretchModeEnum.Scale,
MouseFilter = MouseFilterEnum.Ignore,
};
_screenView.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
AddChild(_screenView); // added first -> draws behind the text/status labels
Replace the DrawSlot method with BlitSlot:
// Blit a source BMP (src rect) onto the screen backbuffer at (dstX,dstY), then refresh the display
// texture. Execution order == paint order, so later draws (sprites) land over earlier ones (bg).
public void BlitSlot(string bmpPath, int srcX, int srcY, int w, int h, int dstX, int dstY)
{
var src = new Image();
if (src.LoadBmpFromBuffer(System.IO.File.ReadAllBytes(bmpPath)) != Error.Ok)
{ GD.Print($"BMP load failed {bmpPath}"); return; }
if (src.GetFormat() != Image.Format.Rgba8) src.Convert(Image.Format.Rgba8);
// Clamp the source rect to the image; a zero/negative size falls back to the full image.
int sw = w > 0 ? w : src.GetWidth();
int sh = h > 0 ? h : src.GetHeight();
sw = System.Math.Min(sw, src.GetWidth() - srcX);
sh = System.Math.Min(sh, src.GetHeight() - srcY);
if (sw <= 0 || sh <= 0) return;
_screen.BlitRect(src, new Rect2I(srcX, srcY, sw, sh), new Vector2I(dstX, dstY));
_screenTex.Update(_screen);
}
- Step 3: Build the Godot project
Run: godot --headless --path godot --import
Then: dotnet build godot/Himegari.csproj
Expected: both succeed (no references to the removed _stage/_slots/DrawSlot).
- Step 4: Run the headless selftest (VM-behavior guardrail)
Run: godot --headless --path godot -- --selftest
Expected: SELFTEST OK: 186 lines match vm0 trace, exit 0. Dialogue flow does not branch on pixel geometry, so returning real dims from GetTextureSize must not change the emitted show-text offsets. If it FAILS, do not disable the check — investigate which geometry global leaked into control flow (a genuine finding); use Age.Cli gfx SC0000.BIN to see the computed values and trace the divergence.
- Step 5: Eyeball the opening (human oracle)
Run: godot --path godot
Expected: SC0000 opening renders with the background placed correctly (filling the frame / at the origin, not off-center) and any sprites at plausible positions and non-zero sizes. Known-and-expected caveats (out of scope this pass, do not treat as regressions): sprites may show an opaque background box (no chromakey yet) and AE* fades draw opaque/instant (no alpha yet).
- Step 6: Commit
git add godot/GodotAdvHost.cs godot/Main.cs
git commit -m "feat(a2b): screen-backbuffer blit compositor + real GetTextureSize
Correct sprite/background geometry: get-texture-size feeds the bytecode's own
centering/anchor math; draw-texture blits src rect -> dst into one 800x600
canvas in execution order. Selftest byte-parity preserved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
Self-review
Spec coverage:
0x208real op +IHost.GetTextureSize→ Task 1. ✓- Screen backbuffer blit compositor, source images per slot, dest-handle collapse → Task 3. ✓
Age.Cli gfxdiagnostic → Task 2 (Step 5–6). ✓- Trace/selftest parity preserved → Task 1 Step 7 (engine
TraceDiffTests) + Task 3 Step 4 (Godot selftest). ✓ - Opcode rename in
opcodes.toml+ rebuild → Task 1 Step 1. ✓ - Empty-slot / missing-BMP / off-canvas / dims-mismatch edge cases →
BmpHeader.ReadDimsreturns(0,0);BlitSlotclamps src rect and returns on empty;GetTextureSizereturns(0,0)for empty slots. ✓ - Deferred (out of scope): fades/alpha, chromakey, true multi-surface, animation ops → not implemented; noted in Task 3 Step 5 caveats. ✓
Placeholder scan: No TBD/TODO; every code step shows complete code; commands have expected output. ✓
Type consistency: GetTextureSize(int slot) → (int Width, int Height) identical across IHost, CaptureHost, AudioTraceHost, test RecHost, FakeSizeHost, GfxTraceHost, GodotAdvHost. BmpHeader.ReadDims(string?) → (int Width, int Height) consistent in Tasks 2 and 3. Main.BlitSlot(string,int,int,int,int,int,int) matches the CallDeferred("BlitSlot", bmp, srcX, srcY, width, height, dstX, dstY) argument order in GodotAdvHost.DrawTexture. VM dispatch label "get-texture-size" matches the label set in opcodes.toml. ✓