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.