feat(a2b): implement 0x208 get-texture-size (geometry keystone)

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>
This commit is contained in:
gamer147
2026-07-06 22:48:10 -04:00
parent e96b1a6fef
commit ea0f1ddc7a
10 changed files with 75 additions and 14 deletions

View File

@@ -81,6 +81,11 @@
- **grounding:** source=inference, confidence=med
- **evidence:** confirm via frida
### 0x208 `get-texture-size` (get-texture-size, argc 3)
- **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)
- **grounding:** source=inference, confidence=med
- **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
### 0x217 `gfx-geom?` (u004211E0, argc 4)
- **summary:** 4 global-ints; part of a 0x217/0x218/0x21a geometry chain
- **grounding:** source=inference, confidence=low
@@ -840,10 +845,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x208 `u00420BF0` (u00420BF0, argc 3)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x20a `u00420CE0` (u00420CE0, argc 1)
- **summary:** —
- **grounding:** source=kelebek, confidence=low

View File

@@ -84,4 +84,5 @@ sealed class AudioTraceHost : IHost
public void CreateTexture(int slot, int width, int height) { }
public void SetTexture(long resourceId, int slot) { }
public void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY) { }
public (int Width, int Height) GetTextureSize(int slot) => (0, 0);
}

View File

@@ -0,0 +1,46 @@
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]);
}
}

View File

@@ -18,6 +18,7 @@ public class TextureOpsTests
public void CreateTexture(int slot, int w, int h) => Creates++;
public void SetTexture(long resId, int slot) => Sets.Add((resId, slot));
public void DrawTexture(int slot, int srcX, int srcY, int w, int h, int dstX, int dstY) => Draws.Add((slot, w, h));
public (int Width, int Height) GetTextureSize(int slot) => (0, 0);
public void PlayBgm(long id) { }
public void PlayVoice(long id) { }
}

View File

@@ -17,6 +17,7 @@ public class WaitForInputTests
public void CreateTexture(int slot, int w, int h) { }
public void SetTexture(long resId, int slot) { }
public void DrawTexture(int slot, int srcX, int srcY, int w, int h, int dstX, int dstY) { }
public (int Width, int Height) GetTextureSize(int slot) => (0, 0);
public void PlayBgm(long id) { }
public void PlayVoice(long id) { }
}

View File

@@ -11,6 +11,7 @@ public sealed class CaptureHost : IHost
public void CreateTexture(int slot, int width, int height) { }
public void SetTexture(long resourceId, int slot) { }
public void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY) { }
public (int Width, int Height) GetTextureSize(int slot) => (0, 0);
public void PlayBgm(long id) { }
public void PlayVoice(long id) { }
}

View File

@@ -8,6 +8,7 @@ public interface IHost
void CreateTexture(int slot, int width, int height);
void SetTexture(long resourceId, int slot);
void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY);
(int Width, int Height) GetTextureSize(int slot);
void PlayBgm(long id);
void PlayVoice(long id);
}

View File

@@ -175,6 +175,12 @@ public sealed class VirtualMachine
case "draw-texture": // (handle, slot, srcX, srcY, w, h, dstX, dstY)
_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;
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;
}
case "play-bgm": _host.PlayBgm(Read(a[0])); return pc + 1;
case "play-voice": _host.PlayVoice(Read(a[0])); return pc + 1;
default:

View File

@@ -54,6 +54,9 @@ public sealed class GodotAdvHost : IHost
_main.CallDeferred("DrawSlot", slot, bmp, dstX, dstY, width, height);
}
// Temporary stub — replaced by the real BMP-header-backed impl in Task 3 (blit compositor).
public (int Width, int Height) GetTextureSize(int slot) => (0, 0);
// ---- audio ops (OGG plays natively in Godot) ----
// BGM: addressed by direct name (BGM{id:D3}.OGG), NOT the manifest. Voice: via the per-scene manifest.
public void PlayBgm(long id)

View File

@@ -4978,33 +4978,33 @@ observed_types = ["imm", "l-int"]
[[opcode]]
op = 0x208
label = "u00420BF0"
label = "get-texture-size"
argc = 3
abi_source = "kelebek+decode-validated"
[opcode.semantics]
name = "u00420BF0"
category = "unknown"
summary = ""
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 = "kelebek"
confidence = "low"
source = "inference"
confidence = "med"
depends_on = []
evidence = ""
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 = ""
role = "slot"
observed_types = ["imm", "g-int", "l-int"]
[[opcode.semantics.args]]
i = 2
role = ""
role = "out_width"
observed_types = ["g-int", "l-int"]
[[opcode.semantics.args]]
i = 3
role = ""
role = "out_height"
observed_types = ["g-int", "l-int"]
[[opcode]]