From eeaeabd5cc17d2bb3c53fed56644bd28f9720f30 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Tue, 7 Jul 2026 20:38:07 -0400 Subject: [PATCH] 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 --- godot/GodotAdvHost.cs | 12 +++++++--- godot/Main.cs | 56 +++++++++++++++++++++++++++++++++---------- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/godot/GodotAdvHost.cs b/godot/GodotAdvHost.cs index e5b1704..6462850 100644 --- a/godot/GodotAdvHost.cs +++ b/godot/GodotAdvHost.cs @@ -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) { } + + /// 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). + 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) ---- diff --git a/godot/Main.cs b/godot/Main.cs index 9e6cbc3..1a06155 100644 --- a/godot/Main.cs +++ b/godot/Main.cs @@ -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 : 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 _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.