feat(gfx): Godot per-frame compositor on the surface/object model + restore --boot

Main._Process composites visible objects in ascending-handle order from their live
surface (resId->BMP); GodotAdvHost.DrawTexture no-op'd (retained), ResolveResIdTexture
added. Restored the --boot system-boot handling that the revert had dropped (Main.cs
was running cold, which is why nothing matched the oracle). Verified: booted opening
event CG renders correctly. Selftest green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-07-07 20:38:07 -04:00
parent 34ea2f30f9
commit eeaeabd5cc
2 changed files with 52 additions and 16 deletions

View File

@@ -58,10 +58,16 @@ public sealed class GodotAdvHost : IHost
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)
// Retained render model: draw-texture updates GfxState (object -> surface bind); Main._Process composites
// 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)
{
if (_slotBmp.TryGetValue(slot, out var bmp) && bmp != null)
_main.CallDeferred("BlitSlot", bmp, srcX, srcY, width, height, dstX, dstY);
var asset = _res.Resolve(_scene, resId);
return asset != null ? ResourceMap.TexturePath(asset) : null;
}
// ---- audio ops (OGG plays natively in Godot) ----

View File

@@ -76,6 +76,7 @@ public partial class Main : Godot.Control
var userArgs = OS.GetCmdlineUserArgs();
_selftest = System.Array.IndexOf(userArgs, "--selftest") >= 0;
bool boot = System.Array.IndexOf(userArgs, "--boot") >= 0; // run SYSTEM4's state prefix first
string scene = "SC0000"; // --scene <NAME>: which scene to play (default SC0000)
var seeds = new List<(int Addr, long Val)>(); // --seed 0xADDR=VAL (repeatable) — initial global state
for (int i = 0; i < userArgs.Length; i++)
@@ -104,8 +105,19 @@ public partial class Main : Godot.Control
else { script = Sys4Loader.Load(Paths.Scripts()[scene.ToUpperInvariant() + ".BIN"], table); provider = Sys4ScriptProvider.Load(table); }
_host = new GodotAdvHost(this, ResourceMap.Load(), scene);
_trace = new GodotTraceSink();
_vm = new VirtualMachine(script, table, _host, null, provider, _trace);
foreach (var (addr, val) in seeds) _vm.Globals[addr] = val; // seed initial state before running
_vm = new VirtualMachine(script, table, _host, new VmOptions(MaxSteps: 20_000_000), provider, _trace);
// --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)
{
var session = new GameSession();
foreach (var b in new[] { "INITCONFIG.BIN", "INIT2.BIN", "INIT.BIN" })
session.RunScene(Sys4Loader.Load(Paths.Scripts()[b], table), table, new CaptureHost(), null, provider);
foreach (var kv in session.Globals) _vm.Globals[kv.Key] = kv.Value;
foreach (var kv in session.GlobalStrings) _vm.GlobalStrings[kv.Key] = kv.Value;
GD.Print($"[boot] system boot done: {session.Globals.Count} globals seeded");
}
foreach (var (addr, val) in seeds) _vm.Globals[addr] = val; // --seed overrides boot state
_ = Task.Run(() => { _vm.Run(); _done = true; });
if (_selftest)
@@ -117,6 +129,7 @@ public partial class Main : Godot.Control
public override void _Process(double delta)
{
if (!_selftest && _vm != null) Recomposite(); // retained per-frame compositor (surface+object model)
// --shot: once the target page is composed and parked at wait-for-input, settle a few frames then grab it.
if (_shotPath != null && !_shotDone && (_host.Pages >= _shotPage && _host.IsWaiting || _done))
{
@@ -152,25 +165,42 @@ public partial class Main : Godot.Control
public override void _ExitTree() { _host?.SignalInput(); }
// ---- UI methods invoked on the main thread via CallDeferred ----
// 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);
// ---- 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). Alpha/colorkey/animation come in later phases (objects opaque here).
private readonly System.Collections.Generic.Dictionary<string, Image?> _imgCache = new();
private void Recomposite()
{
_screen.Fill(new Color(0, 0, 0, 0));
foreach (var v in _vm.Gfx.SnapshotVisibleObjects()) // already ascending-handle = z-order
{
if (v.SurfaceResId == 0) continue; // render-target/blank surface (no file) — later phase
var bmp = _host.ResolveResIdTexture(v.SurfaceResId);
if (bmp != null) BlitLayer(bmp, v.SrcX, v.SrcY, v.W, v.H, v.DstX, v.DstY);
}
_screenTex.Update(_screen);
}
private void BlitLayer(string bmpPath, int srcX, int srcY, int w, int h, int dstX, int dstY)
{
if (!_imgCache.TryGetValue(bmpPath, 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);
_imgCache[bmpPath] = src;
}
if (src == null) return;
// 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);
}
// Load an OGG off disk and play it. BGM loops; voice plays once, cutting off any prior line.