Complete native numbered-save round trips

This commit is contained in:
gamer147
2026-07-24 23:25:54 -04:00
parent 4db7e412bd
commit 0a4e200876
17 changed files with 907 additions and 89 deletions

View File

@@ -1754,8 +1754,9 @@ live surfaces and retained objects, and applies the 20-byte surface records' exp
load first replaces each serialized mutable bank prefix while preserving initialization-authored cells
beyond its count, restores history/gfx and retained audio, unwinds the obsolete managed call chain, runs
`CALLBACK_LOAD.BIN` when
the mounted script provider resolves it, starts the saved root at its `0xae` rendezvous, recursively
reconstructs child frames, resumes parents after their saved T2 call sites,
the mounted script provider resolves it, loads each saved script at its ordinary entry so its frame-local
prologue runs, lets that script reach its own `0xae` rendezvous, recursively reconstructs child frames,
resumes parents after their saved T2 call sites,
and finally resumes the terminal frame at its T1 boundary. Successful `0x19e` also flushes shared
`SAVE.DAT`/`RT.DAT`, matching `context_state_serialize`.
@@ -1791,6 +1792,11 @@ separately gated by both `set:CreateObject` and `set:AutoFreeTex`; the registere
path. Opcode `0x259` is also a real lifecycle operation:
`op_0x259_script_entry_clear_surface_persistence_flags@0x417660` clears record `+0x08` and `+0x0c`
across both native tables at every script entry.
Opcode `0x258` then declares the exceptions:
`op_0x258_set_surface_persistence_flags@0x4250a0` calls
`gfx_surface_set_persistence_flags@0x4159c0`, which replaces record `+0x08` from flags bit 0 and
record `+0x0c` from bit 1 in both tables. The port models bit 0 because layout-3 restoration consumes
that reload policy; the adjacent bit-1 consumer remains unnamed.
Installed slot 15 records resource `0x3383` with reload flag zero. SYSTEM4 loads that atlas before the
save UI, and BUNKI cuts its reusable choice-box corners, borders, and winged top ornament from it. The
port formerly released all 1,000 host surfaces unconditionally, so post-load BUNKI retained its black
@@ -1800,6 +1806,67 @@ native all-release branch. Port-authored saves emit the exact resource/reload/cr
`-1` for unused records, and opcode `0x259` clears the modeled reload policy without releasing textures.
The corrected native functions are renamed/commented in the saved `/v2` image.
#### Port-authored round trips: restored frame boundary and retained-object encoding (2026-07-24)
An immediate `installed SAVE00 -> port SAVE03` round trip isolated two independent writer defects.
The source file contains two frames (`SYSTEM4.BIN -> FORT.BIN`), while the first port rewrite contained
four: the same gameplay pair plus `SAVE.BIN` and its nested helper. Native opcode `0x1ad` selects the
terminal gameplay frame before opening modal helpers. Full restore through `0xae` reinstates that saved
terminal context as the effective boundary. The managed restore cleared its temporary restore state but
did not restore `_saveResumeFrame`, so the next `0x19e` fell back to the currently deepest frame. The port
now marks the terminal restored `ExecFrame` before resuming at T1; a regression loads two frames, saves
from a nested helper, and proves the rewrite still contains only the original two.
The first fresh rewrite (`SAVE04`) then exposed the other half of that managed/native representation gap.
It had two frames, but its restored SYSTEM4 ancestor changed from native `resume=8, call=8` to
`resume=-1, call=-1`. Native `op_0xae_continue_save_load_stack_restore@0x416790` restores the parent
context's real T1/T2/T3 coordinates before activating its child. The managed port instead leaves the
parent physically parked at the synthetic `0xae` rendezvous while `RunFrame` recursively runs FORT.
Recomputing a save record from that synthetic PC cannot find a T1 or T2 table entry. FORT itself loads
and runs, but when stage launch unwinds through SYSTEM4, the missing T2 continuation enters ordinary
Eushully boot.
Each restored `ExecFrame` now shadows its original `NativeSavedScriptFrame` while a restored descendant
is active. An intervening `0x19e` serializes those original T1/T2/T3 indices; T3 also reconstructs the
live local-return stack, and the saved T1 coordinate seeds the frame's read-message state. The shadow is
discarded only when that child genuinely returns and the parent resumes at its restored T2 continuation.
Restored root-reload and process-exit outcomes also propagate through the synthetic recursion. A focused
load -> nested save -> reload regression proves the parent indices survive and the child is not invoked
again through ordinary `CallScript`.
Manual slot 005 acceptance then proved that an immediate base rewrite could load and later launch a
stage. Slot 006, authored immediately after entering that stage, still restored a black dungeon with
visible but noninteractive UI. Its serialized FIELD range, 704 retained objects, map textures, and
native-looking 0.8 transform were intact. A software replay reproduced the black result because FIELD
had changed every restored map object's scale to `(0,0,1)`: global zoom selector `G[0x7682]` was 3, but
the frame-local lookup table that should yield 80 was still zero.
Ghidra resolves the ordering precisely. `script_frame_restore_saved_layout@0x40f2d0` calls
`script_frame_load_resource@0x40e980`; that loader allocates and zeroes locals, loads the script body,
and initializes its PC to the script codebase, not to `0xae`. The restored script therefore executes its
ordinary entry prologue before reaching the rendezvous. FIELD's prologue constructs the inline zoom
table `[40, 50, 64, 80, 100, 124, 156]`; the port's direct jump to `0xae` skipped those copies and let
FIELD apply zero scale after the gfx record restore.
Restored managed frames now likewise begin at script offset zero while retaining their saved T1/T2/T3
coordinates for `0xae`. Synthetic root and child regressions prove both prologues execute. Replaying the
unchanged real slot 006 now selects zoom 80, leaves the map at 0.8 scale, produces a nonblack dungeon
raster, and reaches the gameplay poll. No slot rewrite is required for this correction.
The same comparison exposed native retained-gfx pointer arithmetic. `context_state_serialize@0x40d320`
writes a handle and copies the meaningful `0xb5` DWORD / `0x2d4`-byte record, then advances its DWORD
cursor by `0x2d5`; `save_data_deserialize_and_begin_restore@0x40fd10` mirrors that stride. Early port
output packed entries contiguously. In addition, `gfx_object_init_default@0x472810` seeds six identity
matrices and three `0xffffffff` color defaults before the loader overwrites the complete record; the old
writer began with zero bytes and only filled modeled fields. It also wrote translation vectors over the
first row of the native 4x4 matrices instead of entries 12..14.
`NativeNumberedSaveCodec` now emits the native `0xb54`-byte entry stride and recognizes the old tight
experimental layout for compatibility. `NativeGfxPersistenceCodec` begins new records from the exact
native defaults, writes row-vector translation matrices correctly, and retains the original raw record
behind the semantic object so still-unnamed fields survive native load/save cycles. The `/v2` serializer
and deserializer comments record the corrected stride and overwrite behavior.
### Opcode `0xae` continues numbered-save stack restoration (2026-07-20)
Opcode `0xae` is the load-side rendezvous paired with serialized script-frame state. Its handler,
@@ -1816,6 +1883,12 @@ T1/T2/T3 indices back into live PC/call/return offsets. At the saved terminal co
the restore flag and reinstates the saved context/return state. The corpus placement supports that control-flow role: 305 calls overwhelmingly follow
coroutine-resume or call boundaries, including SC0000's main-loop resume sequence.
The load order is significant: `script_frame_restore_saved_layout` delegates script creation to
`script_frame_load_resource@0x40e980`, which initializes the new frame PC to the script codebase.
Consequently the script runs its normal frame-local initialization and reaches `0xae` itself. Starting
a restored frame directly at `0xae` is not native behavior; in FIELD it leaves the local zoom table
zeroed and collapses the restored dungeon to a black point.
The port now implements both branches: an ordinary one-instruction no-op outside restoration, and the
T1/T2/T3-driven managed-frame reconstruction described above while a full numbered load is active.
@@ -2534,7 +2607,24 @@ backbuffer zero instead and record current target `-1` at manager `+0xb530` (`En
`0x20e` then calls `d3d_clear_render_target_black@0x471460`, which invokes `IDirect3DDevice9::Clear` with no
rectangles, `D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER`, color zero, depth 1.0, and stencil zero. The port tracks
the selected target and forwards the clear to the host; the retained compositor reconstructs its main
backbuffer from black at publication boundaries, while offscreen clears also discard modeled text pixels.
backbuffer from black at publication boundaries, while offscreen clears discard both modeled pixels and
text draws.
Opcode `0x222` publishes its retained handle range into whichever target `0x20d` selected; it is not
merely an onscreen repaint request. SAVE.BIN provides a compact end-to-end example. It creates slot 2 at
800x600, selects and clears it, then publishes handles `[0,0x130b0)` to capture the underlying gameplay
without the save-menu objects. It next creates/selects/clears slot 192 at 112x84, binds slot 2 through
handle `0x15f90`, scales that object to 14%, publishes the two-handle range, and passes slot 192 to
`0x1ae`. Dungeon scripts use the same selected-target publication to construct map surfaces.
The port originally retained only an object-list snapshot for transitions and requested an ordinary
backbuffer repaint from `PresentObjectRange`; the selected surface's pixels remained its cleared black
allocation. That produced completely black port-authored `.STH` files and caused restored dungeon map
surfaces to turn black when their rebuild published offscreen. Godot now uses the platform-neutral
software affine compositor for selected-target `0x222`/`0x20c` publication, including source surfaces,
scaling, transforms, tint/opacity/blend, handle-range filtering, and real pixel clears. Backbuffer
publication remains on the existing renderer. `gfx_present_object_range@0x482230` records the refined
target ownership in the saved `/v2` image.
This trace also corrects an important base-pointer assumption in earlier graphics notes. Offsets `+0x408`
and `+0xb550` are relative to the retained-gfx manager at `EngineCtx+0x46614`, not to `EngineCtx` itself.