Complete native numbered-save round trips
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -405,9 +405,9 @@ Implemented as a whole-stack root-reload boundary in the persistent VM. A reques
|
||||
### 0xae `continue-save-load-stack-restore` (continue-save-load-stack-restore, argc 0)
|
||||
- **summary:** () - during serialized save restoration, replace the current frame PC with its saved resume/call target and advance through the saved script-context stack; otherwise a no-op.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0xae_continue_save_load_stack_restore@0x416790 first tests ctx+0x53d24 (set by save_data_deserialize_and_begin_restore@0x40fd10). When clear it returns. When set, it selects the serialized frame layout through set:SaveVersion1/2, restores the current PC from that layout's saved return/call target, advances through contexts with script_frame_restore_saved_layout@0x40f2d0, and clears the restore flag on reaching the saved terminal context. Its 305 corpus sites overwhelmingly follow coroutine-resume/call boundaries, which provide the rendezvous points used while reconstructing the stack.
|
||||
- **evidence:** Ghidra /v2: op_0xae_continue_save_load_stack_restore@0x416790 first tests ctx+0x53d24 (set by save_data_deserialize_and_begin_restore@0x40fd10). When clear it returns. When set, it selects the serialized frame layout through set:SaveVersion1/2, restores the current PC from that layout's saved return/call target, advances through contexts with script_frame_restore_saved_layout@0x40f2d0, and clears the restore flag on reaching the saved terminal context. The restore helper calls script_frame_load_resource@0x40e980, which initializes each new frame PC to its script codebase; the script therefore executes its ordinary prologue before reaching 0xae. Native parent contexts retain restored coordinates while the child runs. SAVE00 -> SAVE03 proved the port must retain the terminal 0x1ad boundary; SAVE00 -> SAVE04 proved a managed parent parked at synthetic 0xae must serialize original resume=8/call=8 rather than synthetic -1/-1; slot 006 proved direct entry at 0xae skips FIELD's zoom-table prologue and collapses the dungeon map. Its 305 corpus sites overwhelmingly follow coroutine-resume/call boundaries.
|
||||
|
||||
Layout 3 frame d259 indexes SYS4 T1 read-message reset sites, d260 indexes T2 call-script sites, and the saved local return stack indexes T3 local-call sites. Port status (2026-07-24): the active path reconstructs the saved recursive frame chain and resumes the terminal frame at its T1 boundary. The installed SAVE00 continuation gate proves SYSTEM4 -> FORT restoration reaches FORT's CHMENU gameplay poll; the synthetic gate asserts the child frame enters with SaveRestore rather than ordinary CallScript cause.
|
||||
Layout 3 frame d259 indexes SYS4 T1 read-message reset sites, d260 indexes T2 call-script sites, and the saved local return stack indexes T3 local-call sites. Port status (2026-07-24): each saved script is loaded at its ordinary entry, runs its frame-local prologue, and reaches 0xae itself; the active path then reconstructs the saved recursive frame chain, preserves each synthetic ancestor's original T1/T2/T3 coordinates while its restored child is active, reinstates the terminal restored frame as opcode 0x1ad's save boundary, and resumes it at T1. The installed SAVE00 continuation gate proves SYSTEM4 -> FORT restoration reaches FORT's CHMENU gameplay poll; the unchanged port-authored dungeon slot 006 proves FIELD's pre-rendezvous zoom table is initialized; synthetic gates assert both root/child prologue execution, SaveRestore rather than ordinary CallScript entry, exclusion of nested SAVE helpers from a re-save, and reload of that rewrite without ordinary boot re-entry.
|
||||
|
||||
### 0xc8 `sleep` (sleep, argc 1)
|
||||
- **summary:** Pause the current script for <duration> milliseconds while retained presentation continues.
|
||||
@@ -790,9 +790,9 @@ Implemented through IHost.PlayModalMovieToSurface. Its operand uses the same nat
|
||||
- **evidence:** Ghidra 0x47ecc0 calls matrix builder 0x48afb1 for target obj+0x1ac. Consumer 0x472f00 uses delay obj+0x44, duration obj+0x58, current obj+0x16c, target obj+0x1ac, shared start obj+0x34, and retained-gfx frame time owner+0xb550 (EngineCtx+0x51b64).
|
||||
|
||||
### 0x222 `present-gfx-object-range` (present-gfx-object-range, argc 2)
|
||||
- **summary:** (first_handle)(count) - flush/present retained graphics objects in the selected handle range and clear their pending update flags.
|
||||
- **summary:** (first_handle)(count) - flush/present retained graphics objects in the selected handle range into the currently selected backbuffer or offscreen render target, then clear their pending update flags.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x222_handler@0x4235e0 calls gfx_present_object_range@0x482230. The worker enters the graphics service, walks the retained-object map, processes flagged objects whose handles fall in [first,first+count), clears pending flags, and finalizes the render batch. HISTORY.BIN uses (0,60000) after rebuilding its retained presentation.
|
||||
- **evidence:** Ghidra /v2: op_0x222_handler@0x4235e0 calls gfx_present_object_range@0x482230. The worker enters the graphics service, walks the retained-object map, processes flagged objects whose handles fall in [first,first+count), clears pending flags, and finalizes the render batch in the D3D target previously selected by op 0x20d. HISTORY.BIN uses (0,60000) for the backbuffer. SAVE.BIN instead renders [0,0x130b0) into 800x600 slot 2, then handle 0x15f90 at 14% scale into 112x84 slot 192; op 0x1ae writes slot 192 as the numbered .STH thumbnail.
|
||||
|
||||
### 0x223 `queue-surface-alpha-transition` (queue-surface-alpha-transition, argc 8)
|
||||
- **summary:** (command_key)(target_slot)(range_a_start)(range_a_count)(range_b_start)(range_b_count)(delay_ms)(duration_ms) — queue a type-0 timed alpha transition command in the separate ctx+0x414 command map. This is render-target/surface presentation state, not an object affine matrix. The render frame composites the two handle ranges into target_slot and ramps alpha 0->1 after delay over duration.
|
||||
@@ -924,6 +924,13 @@ The setter get-or-creates the object and writes the complete operand. During ret
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x24e_handler@0x425070 writes operand 1 directly to EngineCtx.gfx_animation_service_flags at +0x51b80. The mapped field is also read by op 0x243: bit 1 suppresses its force-complete/clock-reset request.
|
||||
|
||||
### 0x258 `set-surface-persistence-flags` (set-surface-persistence-flags, argc 2)
|
||||
- **summary:** (surface_slot)(flags) - replace the native surface record's numbered-save persistence flags; bit 0 controls asset reload on restore and bit 1 controls the adjacent still-unnamed field.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x258_set_surface_persistence_flags@0x4250a0 reads the slot and flags operands and calls gfx_surface_set_persistence_flags@0x4159c0. The worker writes flags&1 to record +0x08 and (flags>>1)&1 to +0x0c in both 1,000-record tables. This corrects the old upstream address/name association: 0x422fe0 is opcode 0x20f movie playback, not 0x258. Corpus declaration chains follow opcode 0x259 at script entry.
|
||||
|
||||
Scripts place declaration chains immediately after opcode 0x259 clears both fields. The port models bit 0 because it is consumed by layout-3 restoration; bit 1 is retained as an identified native field but has no known runtime consumer yet.
|
||||
|
||||
### 0x259 `script-entry` (script-entry, argc 0)
|
||||
- **summary:** zero-arg script/prologue entry; clears surface-record persistence fields +0x08 and +0x0c across both 1,000-record native tables before the declaration chain continues
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
@@ -1150,10 +1157,6 @@ Port status (2026-07-24): implemented through the same profile-lifetime setting
|
||||
- **summary:** 1 imm; mov->0x21b->stmt-end; near save/load-messkip — likely line/stmt id, verify not msg-control
|
||||
- **grounding:** source=harness, confidence=med, noop_headless=True
|
||||
|
||||
### 0x258 `decl?` (u00422FE0, argc 2)
|
||||
- **summary:** 2 imm; runs in a chain right after script-entry 0x259, enumerating ids — prologue declaration/registration?
|
||||
- **grounding:** source=harness, confidence=low, noop_headless=True
|
||||
|
||||
## unknown
|
||||
|
||||
### 0x2 `exit` (exit, argc 0)
|
||||
|
||||
@@ -3628,6 +3628,111 @@ mapped and implemented as the script-entry clear of record `+0x08`/`+0x0c` (the
|
||||
Regressions cover native-default preservation, opt-in all-release, flag-for-flag port-authored output,
|
||||
script-entry clearing, and the installed all-zero reload-bit oracle.
|
||||
|
||||
### Persistence implementation step 10 — publish selected offscreen render targets (2026-07-24)
|
||||
|
||||
The first port-authored dungeon save exposed two symptoms with one graphics cause: its 112x84 `.STH`
|
||||
was completely black, and loading it showed the dungeon map for one frame before the map surface became
|
||||
black while the UI/minimap remained live.
|
||||
|
||||
SAVE.BIN's bytecode makes the missing contract explicit. It renders the underlying gameplay handle range
|
||||
into created 800x600 surface 2, binds that surface to handle `0x15f90`, scales it to 14% into created
|
||||
112x84 surface 192, then asks opcode `0x1ae` to serialize surface 192. Both render passes use `0x20d`
|
||||
target selection, `0x20e` clear, and `0x222` range publication. Dungeon map construction uses the same
|
||||
offscreen path. Godot had modeled selection and black allocation, but `PresentObjectRange` only requested
|
||||
an onscreen recomposition, so neither target ever received pixels.
|
||||
|
||||
Selected-target `0x222` and `0x20c` now publish through a platform-neutral retained-surface rasterizer.
|
||||
It filters the requested handle range and reproduces affine/range transforms, scaling, tint, opacity,
|
||||
blend mode, color keys, created/source surfaces, and black clears before storing the target pixels.
|
||||
Backbuffer publication is unchanged. A focused regression reproduces SAVE.BIN's scaled texture capture
|
||||
and proves objects outside the range cannot leak into the target. The original black SAVE01 thumbnail is
|
||||
a preserved diagnostic artifact; a newly overwritten save is required for visual acceptance of the
|
||||
fixed `.STH`. The subsequent immediate-resave test showed that a separate numbered-state round-trip defect
|
||||
still prevented the rewritten `.DAT` from resuming interactively.
|
||||
|
||||
### Persistence implementation step 11 — repair port-authored numbered-state round trips (2026-07-24)
|
||||
|
||||
The clean comparison target was `SAVE03`: load native slot 0 and save immediately, without entering a
|
||||
dungeon. Loading that rewrite was completely black, proving the remaining failure was not dungeon-map
|
||||
construction. The source slot has two saved frames; the rewrite had four and incorrectly included the
|
||||
currently open save-menu/helper frames. Full `0xae` restoration had not re-established the terminal saved
|
||||
frame as opcode `0x1ad`'s ongoing save cutoff, so the later `0x19e` fell back to the deepest active helper.
|
||||
The terminal restore now retains its `ExecFrame` as `_saveResumeFrame`. A regression reconstructs a
|
||||
two-frame save, invokes `0x19e` from a nested helper, and proves the new slot still contains only the two
|
||||
gameplay frames.
|
||||
|
||||
Binary comparison also corrected the retained-gfx writer. Native records are not tightly packed:
|
||||
each handle plus meaningful `0x2d4`-byte record begins on a `0x2d5`-DWORD / `0xb54`-byte stride. The port
|
||||
now emits that sparse layout and imports its earlier packed experimental files. New records start from
|
||||
the exact `gfx_object_init_default` identity-matrix/color state; decoded native records retain an opaque
|
||||
byte template so unnamed fields survive rewrites; and translation is encoded at row-vector matrix
|
||||
entries 12..14 rather than over the matrix diagonal. Focused tests cover the native stride, all six
|
||||
identity matrices, translation placement, opaque-byte preservation, and the restored save boundary.
|
||||
|
||||
The `/v2` serializer/deserializer annotations and canonical format notes are updated. The next acceptance
|
||||
action is to rebuild, load native slot 0, immediately overwrite a fresh slot, and load that fresh rewrite;
|
||||
older slot 1/2/3 files remain useful compatibility inputs but retain the already-written bad frame chain.
|
||||
|
||||
Validation: all 391 engine tests pass; opcode lint/tooling tests are clean; the Godot C# build has zero
|
||||
warnings; the 211-object SAVE03 compatibility rewrite decodes with the native stride; and threaded
|
||||
headless execution reports `SELFTEST OK`.
|
||||
|
||||
### Persistence implementation step 12 — preserve restored ancestor continuations (2026-07-24)
|
||||
|
||||
Fresh slot 004 passed the immediate load gate, but selecting a stage played the Eushully intro. Its frame
|
||||
count was fixed at two, yet a decoded comparison found the decisive remaining difference:
|
||||
native slot 000's SYSTEM4 parent stores `resume=8, call=8`, while slot 004 stored `-1,-1`.
|
||||
|
||||
Native `0xae` restores each parent context to its real T1/T2/T3 coordinates before activating the next
|
||||
saved child. The managed reconstruction instead recursively runs FORT while its SYSTEM4 `ExecFrame` is
|
||||
physically parked at the synthetic `0xae` instruction. Re-save capture derived coordinates from that
|
||||
synthetic PC, found neither a T1 nor T2 table entry, and wrote `-1`. FORT could initially run, but the
|
||||
later stage-launch unwind had no SYSTEM4 T2 continuation and fell into ordinary boot.
|
||||
|
||||
Restored frames now shadow their original serialized coordinates while a restored descendant remains
|
||||
active. Re-saves use those T1/T2/T3 values; T3 rebuilds the live local-return stack; T1 initializes the
|
||||
read-message coordinate; and the shadow clears only when the child genuinely returns to its parent.
|
||||
Root-reload and process-exit results now propagate through this synthetic recursion as they do through
|
||||
ordinary `call-script`. The existing two-frame load/re-save regression now asserts the ancestor's resume
|
||||
and call indices and loads the rewrite again, rejecting any ordinary re-entry into the restored child.
|
||||
|
||||
The `0xae` opcode source/reference, engine RE, roadmap, status memory, and `/v2` plate comment record the
|
||||
refinement. Existing slot 004 retains its already-written `-1,-1`; acceptance requires another fresh
|
||||
slot created from native slot 000, followed by load and stage launch.
|
||||
|
||||
Validation: engine **392/392**, clean opcode lint/tooling, zero-warning Godot build, and threaded
|
||||
`SELFTEST OK`. The remaining gate is interactive confirmation with a newly authored slot; the fix cannot
|
||||
retroactively repair slot 004's serialized parent coordinates.
|
||||
|
||||
### Persistence implementation step 13 — run restored script prologues before `0xae` (2026-07-24)
|
||||
|
||||
Manual slot 005 acceptance passed both immediate base restoration and later stage launch. The next
|
||||
dungeon-authored rewrite, slot 006, loaded with its UI present but a black, noninteractive map. Binary
|
||||
and software-renderer inspection showed that the slot itself was healthy: it contained FIELD's saved
|
||||
range, 704 retained objects, map textures, and a native-looking 0.8 transform. During restore, however,
|
||||
FIELD overwrote the map objects with zero scale. Global zoom selector `G[0x7682]` was 3, while the
|
||||
frame-local zoom table entry that should have returned 80 was zero.
|
||||
|
||||
Native `script_frame_restore_saved_layout@0x40f2d0` calls
|
||||
`script_frame_load_resource@0x40e980`, which creates each saved script frame at its ordinary codebase.
|
||||
The script runs its entry prologue and reaches `0xae` itself. The managed restore instead created frames
|
||||
directly at `0xae`, skipping FIELD's inline initialization of `[40, 50, 64, 80, 100, 124, 156]`.
|
||||
|
||||
Restored frames now start at script offset zero while retaining the serialized T1/T2/T3 coordinates
|
||||
consumed at `0xae`. Synthetic root and child tests require their pre-rendezvous prologues to execute.
|
||||
The unchanged real slot 006 then replays with zoom 80, 0.8 object scale, a nonblack dungeon raster, and
|
||||
the gameplay poll; the fix does not require rewriting that save.
|
||||
|
||||
The adjacent surface declaration gap is also closed. Native opcode `0x258` sets surface-record `+0x08`
|
||||
from flags bit 0 and `+0x0c` from bit 1 after `0x259` clears both tables. The port now applies bit 0 as
|
||||
the numbered-load reload policy; bit 1 remains identified but has no known consumer.
|
||||
|
||||
Validation: engine **393/393**, clean opcode lint/tooling, zero-warning Godot build, threaded
|
||||
`SELFTEST OK`, and exact slot-006 software replay with zoom 80 and a nonblack dungeon raster.
|
||||
|
||||
Acceptance: rebuild and interactively load existing slot 006. The expected result is the restored
|
||||
dungeon map and controls without a re-save.
|
||||
|
||||
## Data-semantics sidebar: focused append EBINIT inspection (2026-07-24)
|
||||
|
||||
The static INIT surface now accepts a universal packed script id for focused append inspection.
|
||||
|
||||
@@ -480,7 +480,19 @@ serialized mutable bank prefixes (preserving initialized unit/stage/string defin
|
||||
retained BGM/SFX state, preserves initialized flag-zero system surfaces while overlaying explicit saved
|
||||
reload records, reproduces the native configuration-gated all-surface release when explicitly enabled,
|
||||
and writes native per-slot reload/created metadata rather than treating every texture as reloadable.
|
||||
Opcode `0x259` supplies its real script-entry reload-policy clear. JSON inspection/export,
|
||||
Opcode `0x259` supplies its real script-entry reload-policy clear. Selected offscreen render targets now
|
||||
receive actual retained-object pixels, closing the black-thumbnail half of the first port-authored-save
|
||||
failure. The following immediate-resave oracle exposed and fixed two separate numbered-state defects:
|
||||
`0xae` now re-establishes the saved `0x1ad` gameplay-frame boundary before a later save UI opens, and
|
||||
retained objects use AGE's sparse stride plus native-initialized matrix/default bytes while preserving
|
||||
unnamed fields. The next fresh rewrite also proved that reconstructed managed ancestor frames must retain
|
||||
their original T1/T2/T3 coordinates while parked at synthetic `0xae`; preserving those coordinates fixes
|
||||
the later SYSTEM4 unwind that otherwise re-entered the Eushully intro. Slot 005 subsequently passed the
|
||||
base-load and stage-launch round trip. Dungeon-authored slot 006 exposed one further native-ordering
|
||||
requirement: restored scripts must begin at their ordinary entry, run frame-local prologues, and reach
|
||||
`0xae` themselves. Matching that order restores FIELD's 80% zoom table entry and produces the dungeon
|
||||
map from the unchanged slot; interactive slot-006 confirmation is the remaining visual gate.
|
||||
JSON inspection/export,
|
||||
namespaced mod data, and migrations remain additive extended-mode work rather than 1.0 compatibility
|
||||
requirements.
|
||||
|
||||
|
||||
@@ -245,13 +245,23 @@ Retained graphics then uses:
|
||||
```text
|
||||
0x00 gfx_record_size:u32 # 0x2d4
|
||||
+0x04 gfx_object_count:u32
|
||||
+0x08 repeated { handle:u32, record[0x2d4] }
|
||||
+0x08 repeated sparse entries:
|
||||
handle:u32
|
||||
record[0x2d4] # 0xb5 meaningful DWORDs
|
||||
padding[0x87c]
|
||||
entry stride = 0x2d5 DWORDs = 0xb54 bytes
|
||||
... range_first:u32
|
||||
... range_count:i32
|
||||
... range_transform_record[0x2d4]
|
||||
... native allocation slack
|
||||
```
|
||||
|
||||
The sparse stride is not an allocator-only artifact. Both native writer and reader hold a DWORD pointer:
|
||||
after the handle, they copy `0x2d4` **bytes** but advance by `0x2d4` **DWORDs**. Consecutive handles are
|
||||
therefore `0x2d5` DWORDs apart. Early experimental port saves incorrectly packed
|
||||
`{handle,record}` contiguously; compatibility import recognizes that layout, while all new output uses
|
||||
the native sparse form.
|
||||
|
||||
The 1,000 surface records preserve the native 20-byte metadata cells verbatim:
|
||||
|
||||
| Offset | Size | Meaning |
|
||||
@@ -270,9 +280,13 @@ surfaces expected to remain live across the load. The installed file is decisive
|
||||
are zero, while SYSTEM4's reusable choice-frame atlas remains recorded as resource `0x3383` in slot 15.
|
||||
A separate configuration-gated path can release all 1,000 surfaces before this loop, but Himegari's
|
||||
registered `CreateObject=1`, `AutoFreeTex=0` defaults leave it inactive. A retained `0x2d4`
|
||||
record is structurally complete but not every internal graphics field is semantically named. The native
|
||||
allocation is larger than the records actually written (`0x2e1 + object_count * 0x2d8` DWORDs in the
|
||||
graphics sizing term), leaving zero/slack bytes after the meaningful range record.
|
||||
record is structurally complete but not every internal graphics field is semantically named. Native
|
||||
`gfx_object_init_default` initializes six embedded 4x4 matrices (current/target scale, rotation, and
|
||||
translation) to identity, packed colors at `+0x60/+0x64/+0x240` to `0xffffffff`, and other fields to zero.
|
||||
Deserialization initializes an object and then overwrites all `0x2d4` bytes, so a compatibility writer
|
||||
must emit or preserve those defaults rather than zero-fill unnamed fields. The graphics sizing term remains
|
||||
larger than the meaningful sparse entries and range record
|
||||
(`0x2e1 + object_count * 0x2d8` DWORDs), leaving final allocation slack as well.
|
||||
|
||||
The installed `SAVE00.DAT` validates the complete layout-3 decode: cutoff 1, global-bank counts
|
||||
`[402459,1,789,1,1,1]`, current BGM id `0x18`, retained SFX ids `0x3321` (channel 1) and
|
||||
|
||||
Reference in New Issue
Block a user