Decode AGF textures through the asset store

This commit is contained in:
gamer147
2026-07-11 09:55:58 -04:00
parent b6c3b1660d
commit a1f6d0ab51
15 changed files with 468 additions and 158 deletions

View File

@@ -9,7 +9,8 @@ public sealed class GodotAdvHost : IHost
private readonly Main _main;
private readonly ResourceMap _res;
private readonly string _scene; // e.g. "SC0000" — for section_base
private readonly Dictionary<int, string?> _slotBmp = new(); // slot -> pre-converted BMP path
private readonly object _imageLock = new();
private readonly Dictionary<int, RgbaImage?> _images = new(); // raw catalog id -> decoded pixels
private readonly string?[] _sfxPaths = new string?[10]; // SC0000 native channel subset
// slot -> dims. Slot 0 is the primary/screen surface (800x600), normally created at engine boot which
// the single-scene harness skips; seed it so the first CG's anchor math stays correct (not 0x0).
@@ -230,22 +231,20 @@ public sealed class GodotAdvHost : IHost
public void CreateTexture(int slot, int width, int height)
{
lock (_textLock) _surfaceText.Remove(slot);
_slotBmp[slot] = null; _slotDims[slot] = (width, height);
_slotDims[slot] = (width, height);
if (TraceOps) Godot.GD.Print($"[op] create-texture slot={slot} {width}x{height}");
}
public void SetTexture(long resourceId, int slot)
{
lock (_textLock) _surfaceText.Remove(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
if (TraceOps) Godot.GD.Print($"[op] set-texture slot={slot} resId=0x{resourceId:x} -> {(bmp != null ? System.IO.Path.GetFileName(bmp) : "<none>")}");
var asset = _res.ResolveTexture(_scene, resourceId);
var image = asset != null ? Decode(asset) : null;
_slotDims[slot] = image != null ? (image.Width, image.Height) : (0, 0);
if (TraceOps) Godot.GD.Print($"[op] set-texture slot={slot} resId=0x{resourceId:x} -> {(asset?.Name ?? "<none>")}");
}
// 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.
// AGF is decoded synchronously on the VM thread so geometry queried immediately afterward sees real dims.
public (int Width, int Height) GetTextureSize(int slot)
=> _slotDims.TryGetValue(slot, out var d) ? (d.W, d.H) : (0, 0);
@@ -253,12 +252,28 @@ public sealed class GodotAdvHost : IHost
// the visible objects each frame in ascending-handle order. No immediate blit here.
public void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY) { }
/// <summary>Resolve a gfx surface's resId to its pre-converted BMP path (Main's per-frame compositor
/// resolves each visible object's surface through this).</summary>
public string? ResolveResIdTexture(long resId)
/// <summary>Resolve a gfx surface through scene-local or universal raw-id addressing and decode it
/// from the loose-first asset store.</summary>
public (RgbaImage Image, string Name, int AssetId)? ResolveResIdTexture(long resId)
{
var asset = _res.Resolve(_scene, resId);
return asset != null ? ResourceMap.TexturePath(asset) : null;
var asset = _res.ResolveTexture(_scene, resId);
var image = asset != null ? Decode(asset) : null;
return asset != null && image != null ? (image, asset.Name, asset.RawIndex) : null;
}
private RgbaImage? Decode(AssetEntry asset)
{
lock (_imageLock)
{
if (_images.TryGetValue(asset.RawIndex, out var cached)) return cached;
try { return _images[asset.RawIndex] = _res.DecodeTexture(asset); }
catch (System.Exception e)
{
Godot.GD.Print($"AGF decode failed {asset.Name}: {e.Message}");
_images[asset.RawIndex] = null;
return null;
}
}
}
// ---- audio ops (OGG plays natively in Godot) ----

View File

@@ -154,7 +154,8 @@ public partial class Main : Godot.Control
if (_selftest) (script, provider) = BuildSelfTestScene(table);
else { scripts = Sys4ScriptProvider.Load(table); script = scripts.RequireByName(scene + ".BIN"); provider = scripts; }
if (_timelineLogPath != null) _timeline = new GodotTimelineLog(_timelineLogPath);
_host = new GodotAdvHost(this, scripts != null ? new ResourceMap(scripts.Catalog) : ResourceMap.Load(), scene, _clock, _timeline) { SleepScale = sleepScale, TraceOps = _gfxLogPath != null };
var resources = scripts != null ? new ResourceMap(scripts.Catalog) : ResourceMap.Load();
_host = new GodotAdvHost(this, resources, scene, _clock, _timeline) { SleepScale = sleepScale, TraceOps = _gfxLogPath != null };
_trace = new GodotTraceSink(_timeline);
// --trace-histogram: aggregate op/call-site execution counts of the REAL Godot run (headless flow
// diverges — wait-for-input is a no-op there — so this is the only way to profile the live path).
@@ -164,6 +165,13 @@ public partial class Main : Godot.Control
if (histFile != null) { _hist = new Age.Engine.Diagnostics.HistogramTraceSink();
sink = new Age.Engine.Diagnostics.CompositeTraceSink(_trace, _hist); }
_vm = new VirtualMachine(script, table, _host, new VmOptions(MaxSteps: 20_000_000), provider, sink);
// SYSTEM4 loads the shared SO001 chrome sheet into surface slot 17 before any scene runs.
// Seed that inherited retained-surface state without replaying the entrypoint's unrelated UI flow.
if (!_selftest && resources.ResolveName("SO001.AGF") is { } systemChrome)
{
_host.SetTexture(systemChrome.RawIndex, 0x11);
_vm.Gfx.SetSurface(0x11, systemChrome.RawIndex, 0);
}
// --boot: run SYSTEM4's state prefix (INITCONFIG/INIT2/INIT) so the scene sees boot state — chiefly
// INIT2's gfx handle array 0x62455.. (skips the UI scripts LOGO/OP/TITLE). State carries via globals.
if (boot && !_selftest)
@@ -279,10 +287,10 @@ public partial class Main : Godot.Control
// ---- retained per-frame compositor (main thread, from _Process) ----
// Clear the screen and composite the VM's current VISIBLE gfx objects in ascending-handle order (= the
// engine's z-order), each blitting its live surface's rect at its position. Surfaces are cached by BMP
// path (this runs every frame). Native scale/translation matrix channels are sampled independently by
// engine's z-order), each blitting its live surface's rect at its position. Decoded AGF surfaces are cached
// by catalog identity (this runs every frame). Native scale/translation matrix channels are sampled independently by
// GfxState and applied here; object opacity comes only from the actual blend/color path.
private readonly System.Collections.Generic.Dictionary<(string Path, long Key), Image?> _imgCache = new();
private readonly System.Collections.Generic.Dictionary<(int AssetId, long Key), Image?> _imgCache = new();
private void Recomposite()
{
@@ -328,14 +336,14 @@ public partial class Main : Godot.Control
}
else
{
var bmp = _host.ResolveResIdTexture(v.SurfaceResId);
if (bmp == null) outcome = $"SKIP(resId=0x{v.SurfaceResId:x} UNRESOLVED)";
var texture = _host.ResolveResIdTexture(v.SurfaceResId);
if (texture == null) outcome = $"SKIP(resId=0x{v.SurfaceResId:x} UNRESOLVED)";
else
{
BlitLayer(bmp, v.ColorKey, v.Tint, strength, v.SrcX, v.SrcY, v.W, v.H,
BlitLayer(texture.Value.Image, texture.Value.AssetId, v.ColorKey, v.Tint, strength, v.SrcX, v.SrcY, v.W, v.H,
localToDest, opacity, v.MultiplyTint);
var raw = _vm.Gfx.TryGet(v.Handle);
outcome = $"slot={raw?.SourceSlot} DRAWN resId=0x{v.SurfaceResId:x} {System.IO.Path.GetFileName(bmp)} " +
outcome = $"slot={raw?.SourceSlot} DRAWN resId=0x{v.SurfaceResId:x} {texture.Value.Name} " +
$"src=({v.SrcX},{v.SrcY} {v.W}x{v.H}) base=({v.DstX},{v.DstY}) " +
$"anchor=({t.AnchorX:0.0},{t.AnchorY:0.0}) dst=({dstX},{dstY}) " +
$"scale=({t.ScaleX:0.00},{t.ScaleY:0.00}) trans=({t.TranslateX:0.0},{t.TranslateY:0.0}) " +
@@ -395,9 +403,9 @@ public partial class Main : Godot.Control
}
else
{
var bmp = _host.ResolveResIdTexture(source.SurfaceResId);
if (bmp == null) continue;
BlitLayer(bmp, source.ColorKey, source.Tint, source.TintStrength / 255f,
var texture = _host.ResolveResIdTexture(source.SurfaceResId);
if (texture == null) continue;
BlitLayer(texture.Value.Image, texture.Value.AssetId, source.ColorKey, source.Tint, source.TintStrength / 255f,
source.SrcX, source.SrcY, source.W, source.H, affine, opacity, source.MultiplyTint);
}
drawn++;
@@ -439,20 +447,14 @@ public partial class Main : Godot.Control
// matching the surface colorkey are made transparent (native bakes the key at load — engine-re.md §Blend).
// Mode 0 uses tintStrength to LERP texel RGB toward tint. Mode 1 sets multiplyTint and uses packed RGB as
// multiplicative modulation while alpha is object opacity.
private void BlitLayer(string bmpPath, long colorKey, long tint, float tintStrength, int srcX, int srcY, int w, int h,
private void BlitLayer(RgbaImage decoded, int assetId, long colorKey, long tint, float tintStrength, int srcX, int srcY, int w, int h,
Age.Engine.Model.Affine2D localToDest, float alpha = 1f, bool multiplyTint = false)
{
var cacheKey = (bmpPath, colorKey);
var cacheKey = (assetId, colorKey);
if (!_imgCache.TryGetValue(cacheKey, out var src))
{
src = new Image();
if (src.LoadBmpFromBuffer(System.IO.File.ReadAllBytes(bmpPath)) != Error.Ok)
{ GD.Print($"BMP load failed {bmpPath}"); src = null; }
else
{
if (src.GetFormat() != Image.Format.Rgba8) src.Convert(Image.Format.Rgba8);
if (Age.Engine.Model.BlendMath.HasColorKey(colorKey)) BakeColorKey(src, colorKey);
}
src = Image.CreateFromData(decoded.Width, decoded.Height, false, Image.Format.Rgba8, decoded.Pixels);
if (Age.Engine.Model.BlendMath.HasColorKey(colorKey)) BakeColorKey(src, colorKey);
_imgCache[cacheKey] = src;
}
if (src == null) return;