feat(a2b): BmpHeader.ReadDims + gfx diagnostic (headless geometry oracle)
BmpHeader.ReadDims reads texture dims from the pre-converted BMP header (VM- thread-safe, no pixel decode) — the data source for GetTextureSize. `Age.Cli gfx <SCENE>` runs a scene and dumps set-texture/get-texture-size/draw-texture in order with resolved file + computed geometry, mirroring the `audio` diagnostic. Confirms 0x208 now yields real dims (800x600/800x800/800x227) and sane UI geometry; surfaces the first-load anchor edge (unseeded state, Phase B). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,28 @@ if (args[0] == "audio")
|
||||
return 0;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (args[0] == "trace")
|
||||
{
|
||||
var scene = new Regex(@"^S[CP]\d{4}\.BIN$");
|
||||
@@ -86,3 +108,44 @@ sealed class AudioTraceHost : IHost
|
||||
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);
|
||||
}
|
||||
|
||||
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) { }
|
||||
}
|
||||
|
||||
33
engine/Age.Engine.Tests/BmpHeaderTests.cs
Normal file
33
engine/Age.Engine.Tests/BmpHeaderTests.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
25
engine/Age.Engine/Sys4/BmpHeader.cs
Normal file
25
engine/Age.Engine/Sys4/BmpHeader.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
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); }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user