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
|
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
|
beyond its count, restores history/gfx and retained audio, unwinds the obsolete managed call chain, runs
|
||||||
`CALLBACK_LOAD.BIN` when
|
`CALLBACK_LOAD.BIN` when
|
||||||
the mounted script provider resolves it, starts the saved root at its `0xae` rendezvous, recursively
|
the mounted script provider resolves it, loads each saved script at its ordinary entry so its frame-local
|
||||||
reconstructs child frames, resumes parents after their saved T2 call sites,
|
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
|
and finally resumes the terminal frame at its T1 boundary. Successful `0x19e` also flushes shared
|
||||||
`SAVE.DAT`/`RT.DAT`, matching `context_state_serialize`.
|
`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:
|
path. Opcode `0x259` is also a real lifecycle operation:
|
||||||
`op_0x259_script_entry_clear_surface_persistence_flags@0x417660` clears record `+0x08` and `+0x0c`
|
`op_0x259_script_entry_clear_surface_persistence_flags@0x417660` clears record `+0x08` and `+0x0c`
|
||||||
across both native tables at every script entry.
|
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
|
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
|
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
|
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.
|
`-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.
|
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` continues numbered-save stack restoration (2026-07-20)
|
||||||
|
|
||||||
Opcode `0xae` is the load-side rendezvous paired with serialized script-frame state. Its handler,
|
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
|
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.
|
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
|
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.
|
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
|
`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
|
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
|
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`
|
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.
|
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)
|
### 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.
|
- **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
|
- **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)
|
### 0xc8 `sleep` (sleep, argc 1)
|
||||||
- **summary:** Pause the current script for <duration> milliseconds while retained presentation continues.
|
- **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).
|
- **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)
|
### 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
|
- **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)
|
### 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.
|
- **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
|
- **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.
|
- **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)
|
### 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
|
- **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
|
- **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
|
- **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
|
- **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
|
## unknown
|
||||||
|
|
||||||
### 0x2 `exit` (exit, argc 0)
|
### 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,
|
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.
|
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)
|
## 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.
|
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
|
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,
|
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.
|
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
|
namespaced mod data, and migrations remain additive extended-mode work rather than 1.0 compatibility
|
||||||
requirements.
|
requirements.
|
||||||
|
|
||||||
|
|||||||
@@ -245,13 +245,23 @@ Retained graphics then uses:
|
|||||||
```text
|
```text
|
||||||
0x00 gfx_record_size:u32 # 0x2d4
|
0x00 gfx_record_size:u32 # 0x2d4
|
||||||
+0x04 gfx_object_count:u32
|
+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_first:u32
|
||||||
... range_count:i32
|
... range_count:i32
|
||||||
... range_transform_record[0x2d4]
|
... range_transform_record[0x2d4]
|
||||||
... native allocation slack
|
... 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:
|
The 1,000 surface records preserve the native 20-byte metadata cells verbatim:
|
||||||
|
|
||||||
| Offset | Size | Meaning |
|
| 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.
|
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
|
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`
|
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
|
record is structurally complete but not every internal graphics field is semantically named. Native
|
||||||
allocation is larger than the records actually written (`0x2e1 + object_count * 0x2d8` DWORDs in the
|
`gfx_object_init_default` initializes six embedded 4x4 matrices (current/target scale, rotation, and
|
||||||
graphics sizing term), leaving zero/slack bytes after the meaningful range record.
|
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
|
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
|
`[402459,1,789,1,1,1]`, current BGM id `0x18`, retained SFX ids `0x3321` (channel 1) and
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ public class NativeNumberedSaveCodecTests
|
|||||||
new NativeSavedGfxObject(0xcf08,
|
new NativeSavedGfxObject(0xcf08,
|
||||||
Enumerable.Range(0, NativeNumberedSaveState.GfxRecordSize)
|
Enumerable.Range(0, NativeNumberedSaveState.GfxRecordSize)
|
||||||
.Select(i => unchecked((byte)i)).ToArray()),
|
.Select(i => unchecked((byte)i)).ToArray()),
|
||||||
|
new NativeSavedGfxObject(0xcf09,
|
||||||
|
Enumerable.Repeat((byte)0xa5, NativeNumberedSaveState.GfxRecordSize).ToArray()),
|
||||||
],
|
],
|
||||||
RangeTransformFirst = 100,
|
RangeTransformFirst = 100,
|
||||||
RangeTransformCount = 4,
|
RangeTransformCount = 4,
|
||||||
@@ -53,7 +55,16 @@ public class NativeNumberedSaveCodecTests
|
|||||||
Assert.Equal(state.PointerGlobals, decoded.PointerGlobals);
|
Assert.Equal(state.PointerGlobals, decoded.PointerGlobals);
|
||||||
Assert.Equal(state.GfxObjects[0].Handle, decoded.GfxObjects[0].Handle);
|
Assert.Equal(state.GfxObjects[0].Handle, decoded.GfxObjects[0].Handle);
|
||||||
Assert.Equal(state.GfxObjects[0].Record, decoded.GfxObjects[0].Record);
|
Assert.Equal(state.GfxObjects[0].Record, decoded.GfxObjects[0].Record);
|
||||||
|
Assert.Equal(state.GfxObjects[1].Handle, decoded.GfxObjects[1].Handle);
|
||||||
|
Assert.Equal(state.GfxObjects[1].Record, decoded.GfxObjects[1].Record);
|
||||||
|
Assert.False(decoded.LegacyTightGfxLayout);
|
||||||
Assert.Equal(state.RangeTransformRecord, decoded.RangeTransformRecord);
|
Assert.Equal(state.RangeTransformRecord, decoded.RangeTransformRecord);
|
||||||
|
|
||||||
|
const int nativeEntryStride = (1 + NativeNumberedSaveState.GfxRecordSize) * 4;
|
||||||
|
int objectsAt = FindGfxObjects(encoded, 2, 0xcf08);
|
||||||
|
Assert.Equal(0xcf08, BinaryPrimitives.ReadInt32LittleEndian(encoded.AsSpan(objectsAt)));
|
||||||
|
Assert.Equal(0xcf09, BinaryPrimitives.ReadInt32LittleEndian(
|
||||||
|
encoded.AsSpan(objectsAt + nativeEntryStride)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -153,4 +164,15 @@ public class NativeNumberedSaveCodecTests
|
|||||||
BinaryPrimitives.ReadInt32LittleEndian(
|
BinaryPrimitives.ReadInt32LittleEndian(
|
||||||
state.SurfaceRecords.AsSpan(slot * 20 + 8))));
|
state.SurfaceRecords.AsSpan(slot * 20 + 8))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int FindGfxObjects(byte[] payload, int count, int firstHandle)
|
||||||
|
{
|
||||||
|
for (int offset = 0; offset <= payload.Length - 12; offset += 4)
|
||||||
|
if (BinaryPrimitives.ReadInt32LittleEndian(payload.AsSpan(offset)) ==
|
||||||
|
NativeNumberedSaveState.GfxRecordSize
|
||||||
|
&& BinaryPrimitives.ReadInt32LittleEndian(payload.AsSpan(offset + 4)) == count
|
||||||
|
&& BinaryPrimitives.ReadInt32LittleEndian(payload.AsSpan(offset + 8)) == firstHandle)
|
||||||
|
return offset + 8;
|
||||||
|
throw new Xunit.Sdk.XunitException("Encoded gfx table was not found.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ public class NumberedSaveVmTests
|
|||||||
vm.Gfx.SetSurfaceReloadOnRestore(7, true);
|
vm.Gfx.SetSurfaceReloadOnRestore(7, true);
|
||||||
vm.Gfx.ReleaseSurfaceRange(7, 1);
|
vm.Gfx.ReleaseSurfaceRange(7, 1);
|
||||||
vm.Gfx.BindDraw(100, 3, 1, 2, 30, 40, 50, 60);
|
vm.Gfx.BindDraw(100, 3, 1, 2, 30, 40, 50, 60);
|
||||||
|
vm.Gfx.SetCurrentTranslation(100, (7, 8, 9));
|
||||||
|
vm.Gfx.SetRotationChannel(100, 0, 500, (0, 0, 1), 90);
|
||||||
|
|
||||||
vm.Run();
|
vm.Run();
|
||||||
|
|
||||||
@@ -58,7 +60,25 @@ public class NumberedSaveVmTests
|
|||||||
Assert.Equal(0x3321, state.SoundEffectResourceIds[1]);
|
Assert.Equal(0x3321, state.SoundEffectResourceIds[1]);
|
||||||
Assert.Equal("姫狩り", state.StringGlobals[4]);
|
Assert.Equal("姫狩り", state.StringGlobals[4]);
|
||||||
Assert.Equal(0x77u, state.Frames.Single().ScriptId);
|
Assert.Equal(0x77u, state.Frames.Single().ScriptId);
|
||||||
Assert.Contains(state.GfxObjects, item => item.Handle == 100);
|
NativeSavedGfxObject savedObject =
|
||||||
|
Assert.Single(state.GfxObjects, item => item.Handle == 100);
|
||||||
|
Assert.Equal(0, ReadGfxField(savedObject.Record, 0x34));
|
||||||
|
Assert.Equal(-1, ReadGfxField(savedObject.Record, 0x60));
|
||||||
|
Assert.Equal(0, ReadGfxField(savedObject.Record, 0x238));
|
||||||
|
Assert.Equal(-1, ReadGfxField(savedObject.Record, 0x240));
|
||||||
|
foreach (int matrixOffset in new[] { 0x6c, 0xac, 0xec, 0x16c, 0x1ac })
|
||||||
|
{
|
||||||
|
Assert.Equal(BitConverter.SingleToInt32Bits(1), ReadGfxField(savedObject.Record, matrixOffset));
|
||||||
|
Assert.Equal(BitConverter.SingleToInt32Bits(1), ReadGfxField(savedObject.Record, matrixOffset + 0x14));
|
||||||
|
Assert.Equal(BitConverter.SingleToInt32Bits(1), ReadGfxField(savedObject.Record, matrixOffset + 0x28));
|
||||||
|
Assert.Equal(BitConverter.SingleToInt32Bits(1), ReadGfxField(savedObject.Record, matrixOffset + 0x3c));
|
||||||
|
}
|
||||||
|
Assert.Equal(BitConverter.SingleToInt32Bits(1), ReadGfxField(savedObject.Record, 0x168));
|
||||||
|
Assert.Equal(BitConverter.SingleToInt32Bits(7), ReadGfxField(savedObject.Record, 0x19c));
|
||||||
|
Assert.Equal(BitConverter.SingleToInt32Bits(8), ReadGfxField(savedObject.Record, 0x1a0));
|
||||||
|
Assert.Equal(BitConverter.SingleToInt32Bits(9), ReadGfxField(savedObject.Record, 0x1a4));
|
||||||
|
Assert.Equal(BitConverter.SingleToInt32Bits(1), ReadGfxField(savedObject.Record, 0x130));
|
||||||
|
Assert.Equal(BitConverter.SingleToInt32Bits(-1), ReadGfxField(savedObject.Record, 0x13c));
|
||||||
Assert.Equal(0x1234, ReadSurfaceField(state, 3, 0));
|
Assert.Equal(0x1234, ReadSurfaceField(state, 3, 0));
|
||||||
Assert.Equal(0, ReadSurfaceField(state, 3, 8));
|
Assert.Equal(0, ReadSurfaceField(state, 3, 8));
|
||||||
Assert.Equal(-1, ReadSurfaceField(state, 4, 0));
|
Assert.Equal(-1, ReadSurfaceField(state, 4, 0));
|
||||||
@@ -88,14 +108,18 @@ public class NumberedSaveVmTests
|
|||||||
var store = new DirectoryNativeDatStore(root, Identity);
|
var store = new DirectoryNativeDatStore(root, Identity);
|
||||||
Script resumed = WithTables(WithPackedId(ScriptAssembler.Assemble(Table, "RESUMED.BIN",
|
Script resumed = WithTables(WithPackedId(ScriptAssembler.Assemble(Table, "RESUMED.BIN",
|
||||||
[
|
[
|
||||||
|
(Table.ByLabel("mov")!.Value,
|
||||||
|
[new Operand(GlobalInt, 0x510), new Operand(Immediate, 11)]),
|
||||||
(0xae, Array.Empty<Operand>()),
|
(0xae, Array.Empty<Operand>()),
|
||||||
(0x3, [new Operand(Immediate, 0x89)]),
|
(0x3, [new Operand(Immediate, 0x89)]),
|
||||||
(Table.ByLabel("mov")!.Value,
|
(Table.ByLabel("mov")!.Value,
|
||||||
[new Operand(GlobalInt, 0x500), new Operand(GlobalInt, 0x123)]),
|
[new Operand(GlobalInt, 0x500), new Operand(GlobalInt, 0x123)]),
|
||||||
(0x2, Array.Empty<Operand>()),
|
(0x2, Array.Empty<Operand>()),
|
||||||
], []), 0x88), scriptCallOffsets: [1]);
|
], []), 0x88), scriptCallOffsets: [6]);
|
||||||
Script child = WithPackedId(ScriptAssembler.Assemble(Table, "CHILD.BIN",
|
Script child = WithPackedId(ScriptAssembler.Assemble(Table, "CHILD.BIN",
|
||||||
[
|
[
|
||||||
|
(Table.ByLabel("mov")!.Value,
|
||||||
|
[new Operand(GlobalInt, 0x511), new Operand(Immediate, 22)]),
|
||||||
(0xae, Array.Empty<Operand>()),
|
(0xae, Array.Empty<Operand>()),
|
||||||
(Table.ByLabel("mov")!.Value,
|
(Table.ByLabel("mov")!.Value,
|
||||||
[new Operand(GlobalInt, 0x501), new Operand(GlobalInt, 0x123)]),
|
[new Operand(GlobalInt, 0x501), new Operand(GlobalInt, 0x123)]),
|
||||||
@@ -168,6 +192,8 @@ public class NumberedSaveVmTests
|
|||||||
Assert.Equal(777, vm.Globals[0x124]);
|
Assert.Equal(777, vm.Globals[0x124]);
|
||||||
Assert.Equal(456, vm.Globals[0x500]);
|
Assert.Equal(456, vm.Globals[0x500]);
|
||||||
Assert.Equal(456, vm.Globals[0x501]);
|
Assert.Equal(456, vm.Globals[0x501]);
|
||||||
|
Assert.Equal(11, vm.Globals[0x510]);
|
||||||
|
Assert.Equal(22, vm.Globals[0x511]);
|
||||||
Assert.Equal(1, vm.Globals[0x502]);
|
Assert.Equal(1, vm.Globals[0x502]);
|
||||||
Assert.Equal("復帰", vm.GlobalStrings[0]);
|
Assert.Equal("復帰", vm.GlobalStrings[0]);
|
||||||
Assert.Equal(0x123, vm.GlobalPointers[0]);
|
Assert.Equal(0x123, vm.GlobalPointers[0]);
|
||||||
@@ -204,6 +230,163 @@ public class NumberedSaveVmTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FullLoadReestablishesSaveBoundaryBeforeNestedSaveUiWritesAnotherSlot()
|
||||||
|
{
|
||||||
|
string root = NewTemporaryDirectory();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var store = new DirectoryNativeDatStore(root, Identity);
|
||||||
|
int callScript = Table.ByLabel("call-script")!.Value;
|
||||||
|
Script resumed = WithTables(WithPackedId(ScriptAssembler.Assemble(Table, "RESUMED.BIN",
|
||||||
|
[
|
||||||
|
(0xae, Array.Empty<Operand>()),
|
||||||
|
(callScript, [new Operand(Immediate, 0x89)]),
|
||||||
|
(0x2, Array.Empty<Operand>()),
|
||||||
|
], []), 0x88), scriptCallOffsets: [1]);
|
||||||
|
Script child = WithPackedId(ScriptAssembler.Assemble(Table, "CHILD.BIN",
|
||||||
|
[
|
||||||
|
(0xae, Array.Empty<Operand>()),
|
||||||
|
(callScript, [new Operand(Immediate, 0x91)]),
|
||||||
|
(0x2, Array.Empty<Operand>()),
|
||||||
|
], []), 0x89);
|
||||||
|
Script saveUi = WithPackedId(ScriptAssembler.Assemble(Table, "SAVE_UI.BIN",
|
||||||
|
[
|
||||||
|
(0x19e, [new Operand(LocalInt, 0), new Operand(Immediate, 2)]),
|
||||||
|
(0x2, Array.Empty<Operand>()),
|
||||||
|
], []), 0x91);
|
||||||
|
Script loader = WithPackedId(ScriptAssembler.Assemble(Table, "LOADER.BIN",
|
||||||
|
[
|
||||||
|
(0x1a1, [new Operand(LocalInt, 0), new Operand(Immediate, 1)]),
|
||||||
|
(0x2, Array.Empty<Operand>()),
|
||||||
|
], []), 0x99);
|
||||||
|
byte[] opaqueRecord = NativeGfxRecord(3);
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(opaqueRecord.AsSpan(0x68), 0x12345678);
|
||||||
|
NativeNumberedSaveState source = NativeNumberedSaveCodec.Empty(
|
||||||
|
[
|
||||||
|
new NativeSavedScriptFrame(-1, 0x88, [], 8, 0),
|
||||||
|
new NativeSavedScriptFrame(0, 0x89, [], -1, -1),
|
||||||
|
]) with
|
||||||
|
{
|
||||||
|
GfxObjects = [new NativeSavedGfxObject(100, opaqueRecord)],
|
||||||
|
};
|
||||||
|
store.SaveNumberedFile(
|
||||||
|
1, NativeNumberedSaveCodec.Encode(source), [],
|
||||||
|
NativeSystemTime.FromLocalDateTime(DateTime.Now), 0);
|
||||||
|
var vm = new VirtualMachine(
|
||||||
|
loader, Table, new RecordingHost(),
|
||||||
|
provider: new MapProvider(new()
|
||||||
|
{
|
||||||
|
[0x88] = resumed,
|
||||||
|
[0x89] = child,
|
||||||
|
[0x91] = saveUi,
|
||||||
|
}),
|
||||||
|
nativeDatStore: store);
|
||||||
|
|
||||||
|
vm.Run();
|
||||||
|
|
||||||
|
NativeNumberedSaveFile? resaved = store.LoadNumberedFile(2);
|
||||||
|
Assert.NotNull(resaved);
|
||||||
|
NativeNumberedSaveState state =
|
||||||
|
NativeNumberedSaveCodec.Decode(resaved!.Document.Payload);
|
||||||
|
Assert.Equal([0x88u, 0x89u], state.Frames.Select(frame => frame.ScriptId));
|
||||||
|
Assert.Equal(8, state.Frames[0].ResumeIndex);
|
||||||
|
Assert.Equal(0, state.Frames[0].CallTargetIndex);
|
||||||
|
Assert.Equal(
|
||||||
|
0x12345678,
|
||||||
|
ReadGfxField(Assert.Single(state.GfxObjects).Record, 0x68));
|
||||||
|
|
||||||
|
Script reloader = WithPackedId(ScriptAssembler.Assemble(Table, "RELOADER.BIN",
|
||||||
|
[
|
||||||
|
(0x1a1, [new Operand(LocalInt, 0), new Operand(Immediate, 2)]),
|
||||||
|
(0x2, Array.Empty<Operand>()),
|
||||||
|
], []), 0x9a);
|
||||||
|
var trace = new RecordingTraceSink();
|
||||||
|
var roundTrippedVm = new VirtualMachine(
|
||||||
|
reloader, Table, new RecordingHost(),
|
||||||
|
provider: new MapProvider(new()
|
||||||
|
{
|
||||||
|
[0x88] = resumed,
|
||||||
|
[0x89] = child,
|
||||||
|
[0x91] = saveUi,
|
||||||
|
}),
|
||||||
|
sink: trace,
|
||||||
|
nativeDatStore: store);
|
||||||
|
|
||||||
|
roundTrippedVm.Run();
|
||||||
|
|
||||||
|
Assert.DoesNotContain(trace.Events, item =>
|
||||||
|
item.Kind == Age.Engine.Diagnostics.TraceEventKind.FrameEnter
|
||||||
|
&& item.Name == "CHILD.BIN"
|
||||||
|
&& item.Cause == Age.Engine.Diagnostics.FrameCause.CallScript);
|
||||||
|
Assert.Equal("exit", roundTrippedVm.HaltReason);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(root, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RestoredChildRootReloadDiscardsSyntheticAncestorChain()
|
||||||
|
{
|
||||||
|
string root = NewTemporaryDirectory();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var store = new DirectoryNativeDatStore(root, Identity);
|
||||||
|
int callScript = Table.ByLabel("call-script")!.Value;
|
||||||
|
int move = Table.ByLabel("mov")!.Value;
|
||||||
|
Script resumed = WithTables(WithPackedId(ScriptAssembler.Assemble(Table, "RESUMED.BIN",
|
||||||
|
[
|
||||||
|
(0xae, Array.Empty<Operand>()),
|
||||||
|
(callScript, [new Operand(Immediate, 0x89)]),
|
||||||
|
(move, [new Operand(GlobalInt, 0x600), new Operand(Immediate, 1)]),
|
||||||
|
(0x2, Array.Empty<Operand>()),
|
||||||
|
], []), 0x88), scriptCallOffsets: [1]);
|
||||||
|
Script child = WithPackedId(ScriptAssembler.Assemble(Table, "CHILD_RELOAD.BIN",
|
||||||
|
[
|
||||||
|
(0xae, Array.Empty<Operand>()),
|
||||||
|
(0x9, Array.Empty<Operand>()),
|
||||||
|
], []), 0x89);
|
||||||
|
Script reloaded = WithPackedId(ScriptAssembler.Assemble(Table, "RELOADED.BIN",
|
||||||
|
[
|
||||||
|
(move, [new Operand(GlobalInt, 0x601), new Operand(Immediate, 1)]),
|
||||||
|
(0x2, Array.Empty<Operand>()),
|
||||||
|
], []), 0);
|
||||||
|
Script loader = WithPackedId(ScriptAssembler.Assemble(Table, "LOADER.BIN",
|
||||||
|
[
|
||||||
|
(0x1a1, [new Operand(LocalInt, 0), new Operand(Immediate, 1)]),
|
||||||
|
(0x2, Array.Empty<Operand>()),
|
||||||
|
], []), 0x99);
|
||||||
|
NativeNumberedSaveState source = NativeNumberedSaveCodec.Empty(
|
||||||
|
[
|
||||||
|
new NativeSavedScriptFrame(-1, 0x88, [], -1, 0),
|
||||||
|
new NativeSavedScriptFrame(0, 0x89, [], -1, -1),
|
||||||
|
]);
|
||||||
|
store.SaveNumberedFile(
|
||||||
|
1, NativeNumberedSaveCodec.Encode(source), [],
|
||||||
|
NativeSystemTime.FromLocalDateTime(DateTime.Now), 0);
|
||||||
|
var vm = new VirtualMachine(
|
||||||
|
loader, Table, new RecordingHost(),
|
||||||
|
provider: new MapProvider(new()
|
||||||
|
{
|
||||||
|
[0] = reloaded,
|
||||||
|
[0x88] = resumed,
|
||||||
|
[0x89] = child,
|
||||||
|
}),
|
||||||
|
nativeDatStore: store);
|
||||||
|
|
||||||
|
vm.Run();
|
||||||
|
|
||||||
|
Assert.False(vm.Globals.ContainsKey(0x600));
|
||||||
|
Assert.Equal(1, vm.Globals[0x601]);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(root, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DataOnlyLoadReleasesAllSurfacesWhenBothNativeSettingsEnableIt()
|
public void DataOnlyLoadReleasesAllSurfacesWhenBothNativeSettingsEnableIt()
|
||||||
{
|
{
|
||||||
@@ -272,6 +455,28 @@ public class NumberedSaveVmTests
|
|||||||
Assert.False(surface.ReloadOnRestore);
|
Assert.False(surface.ReloadOnRestore);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SurfacePersistenceDeclarationSetsAndClearsReloadBit()
|
||||||
|
{
|
||||||
|
Script script = ScriptAssembler.Assemble(Table, "SURFACE_POLICY.BIN",
|
||||||
|
[
|
||||||
|
(0x258, [new Operand(Immediate, 15), new Operand(Immediate, 1)]),
|
||||||
|
(0x258, [new Operand(Immediate, 16), new Operand(Immediate, 3)]),
|
||||||
|
(0x258, [new Operand(Immediate, 15), new Operand(Immediate, 0)]),
|
||||||
|
(0x2, Array.Empty<Operand>()),
|
||||||
|
], []);
|
||||||
|
var vm = new VirtualMachine(script, Table, new RecordingHost());
|
||||||
|
vm.Gfx.SetSurface(15, 0x3383, 0);
|
||||||
|
vm.Gfx.SetSurface(16, 0x3384, 0);
|
||||||
|
|
||||||
|
vm.Run();
|
||||||
|
|
||||||
|
GfxSurfacePersistenceState[] surfaces =
|
||||||
|
vm.Gfx.CapturePersistenceSnapshot().Surfaces.ToArray();
|
||||||
|
Assert.False(Assert.Single(surfaces, item => item.Slot == 15).ReloadOnRestore);
|
||||||
|
Assert.True(Assert.Single(surfaces, item => item.Slot == 16).ReloadOnRestore);
|
||||||
|
}
|
||||||
|
|
||||||
private static Script WithPackedId(Script source, uint packedId)
|
private static Script WithPackedId(Script source, uint packedId)
|
||||||
=> new()
|
=> new()
|
||||||
{
|
{
|
||||||
@@ -342,11 +547,21 @@ public class NumberedSaveVmTests
|
|||||||
Write(0x24, 50);
|
Write(0x24, 50);
|
||||||
Write(0x28, 60);
|
Write(0x28, 60);
|
||||||
Write(0x60, -1);
|
Write(0x60, -1);
|
||||||
Write(0x238, 1);
|
Write(0x64, -1);
|
||||||
Write(0x23c, 1);
|
foreach (int matrixOffset in new[] { 0x6c, 0xac, 0xec, 0x12c, 0x16c, 0x1ac })
|
||||||
|
{
|
||||||
|
Write(matrixOffset, BitConverter.SingleToInt32Bits(1));
|
||||||
|
Write(matrixOffset + 0x14, BitConverter.SingleToInt32Bits(1));
|
||||||
|
Write(matrixOffset + 0x28, BitConverter.SingleToInt32Bits(1));
|
||||||
|
Write(matrixOffset + 0x3c, BitConverter.SingleToInt32Bits(1));
|
||||||
|
}
|
||||||
|
Write(0x240, -1);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int ReadGfxField(byte[] record, int offset)
|
||||||
|
=> BinaryPrimitives.ReadInt32LittleEndian(record.AsSpan(offset));
|
||||||
|
|
||||||
private static string NewTemporaryDirectory()
|
private static string NewTemporaryDirectory()
|
||||||
{
|
{
|
||||||
string path = Path.Combine(Path.GetTempPath(), "age-save-vm-" + Guid.NewGuid().ToString("N"));
|
string path = Path.Combine(Path.GetTempPath(), "age-save-vm-" + Guid.NewGuid().ToString("N"));
|
||||||
|
|||||||
41
engine/Age.Engine.Tests/RetainedSurfaceRasterizerTests.cs
Normal file
41
engine/Age.Engine.Tests/RetainedSurfaceRasterizerTests.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using Age.Engine.Model;
|
||||||
|
using Age.Engine.Sys4;
|
||||||
|
|
||||||
|
public class RetainedSurfaceRasterizerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CompositeRangePublishesScaledRetainedTextureIntoOffscreenSurface()
|
||||||
|
{
|
||||||
|
var gfx = new GfxState();
|
||||||
|
gfx.SetSurface(2, 0x1234, -1);
|
||||||
|
gfx.BindDraw(100, 2, 0, 0, 4, 4, 0, 0);
|
||||||
|
gfx.SetCurrentScale(100, (50, 50, 100));
|
||||||
|
gfx.SetSurface(3, 0x5678, -1);
|
||||||
|
gfx.BindDraw(200, 3, 0, 0, 2, 2, 0, 0);
|
||||||
|
|
||||||
|
byte[] sourcePixels = new byte[4 * 4 * 4];
|
||||||
|
for (int index = 0; index < sourcePixels.Length; index += 4)
|
||||||
|
{
|
||||||
|
sourcePixels[index] = 0x44;
|
||||||
|
sourcePixels[index + 1] = 0x88;
|
||||||
|
sourcePixels[index + 2] = 0xcc;
|
||||||
|
sourcePixels[index + 3] = 0xff;
|
||||||
|
}
|
||||||
|
var source = new RgbaImage(4, 4, sourcePixels);
|
||||||
|
var excluded = new RgbaImage(2, 2, Enumerable.Repeat((byte)0xff, 16).ToArray());
|
||||||
|
var destination = new RgbaImage(2, 2, new byte[16]);
|
||||||
|
|
||||||
|
int rendered = RetainedSurfaceRasterizer.CompositeRange(
|
||||||
|
destination, gfx.SnapshotVisibleObjects(), 100, 1,
|
||||||
|
item => item.Handle == 100 ? source : excluded);
|
||||||
|
|
||||||
|
Assert.Equal(1, rendered);
|
||||||
|
for (int index = 0; index < destination.Pixels.Length; index += 4)
|
||||||
|
{
|
||||||
|
Assert.Equal(0x44, destination.Pixels[index]);
|
||||||
|
Assert.Equal(0x88, destination.Pixels[index + 1]);
|
||||||
|
Assert.Equal(0xcc, destination.Pixels[index + 2]);
|
||||||
|
Assert.Equal(0xff, destination.Pixels[index + 3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -101,6 +101,10 @@ public sealed class GfxState
|
|||||||
}
|
}
|
||||||
public sealed class GfxObject
|
public sealed class GfxObject
|
||||||
{
|
{
|
||||||
|
// The native numbered-save record contains several full transform matrices and reserved fields
|
||||||
|
// beyond the port's semantic model. Preserve the decoded bytes so a native load/save round trip
|
||||||
|
// does not erase state that the port has not modeled yet; known fields are patched by the codec.
|
||||||
|
public byte[]? NativePersistenceRecord;
|
||||||
public (long X, long Y, long Z) V18, V24, V16c;
|
public (long X, long Y, long Z) V18, V24, V16c;
|
||||||
public long Field64, Field68, Field6c;
|
public long Field64, Field68, Field6c;
|
||||||
public long Color = 0xffffffff; // native gfx_object_init_default obj+0x60: identity packed ARGB
|
public long Color = 0xffffffff; // native gfx_object_init_default obj+0x60: identity packed ARGB
|
||||||
@@ -464,6 +468,7 @@ public sealed class GfxState
|
|||||||
private static GfxObject CloneState(GfxObject s)
|
private static GfxObject CloneState(GfxObject s)
|
||||||
=> new()
|
=> new()
|
||||||
{
|
{
|
||||||
|
NativePersistenceRecord = s.NativePersistenceRecord?.ToArray(),
|
||||||
V18 = s.V18, V24 = s.V24, V16c = s.V16c,
|
V18 = s.V18, V24 = s.V24, V16c = s.V16c,
|
||||||
Field64 = s.Field64, Field68 = s.Field68, Field6c = s.Field6c,
|
Field64 = s.Field64, Field68 = s.Field68, Field6c = s.Field6c,
|
||||||
Color = s.Color, HasColor = s.HasColor, StaticColorMode = s.StaticColorMode,
|
Color = s.Color, HasColor = s.HasColor, StaticColorMode = s.StaticColorMode,
|
||||||
|
|||||||
@@ -55,18 +55,27 @@ internal static class NativeGfxPersistenceCodec
|
|||||||
reload));
|
reload));
|
||||||
}
|
}
|
||||||
var objects = state.GfxObjects
|
var objects = state.GfxObjects
|
||||||
.Select(item => (item.Handle, DecodeObject(item.Record)))
|
.Select(item => (item.Handle, DecodeObject(item.Record, state.LegacyTightGfxLayout)))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
return new GfxPersistenceSnapshot(
|
return new GfxPersistenceSnapshot(
|
||||||
surfaces, objects, state.RangeTransformFirst, state.RangeTransformCount,
|
surfaces, objects, state.RangeTransformFirst, state.RangeTransformCount,
|
||||||
DecodeObject(state.RangeTransformRecord));
|
DecodeObject(state.RangeTransformRecord, state.LegacyTightGfxLayout));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] EncodeObject(GfxState.GfxObject value)
|
private static byte[] EncodeObject(GfxState.GfxObject value)
|
||||||
{
|
{
|
||||||
byte[] raw = new byte[NativeNumberedSaveState.GfxRecordSize];
|
byte[] raw = value.NativePersistenceRecord is { Length: NativeNumberedSaveState.GfxRecordSize }
|
||||||
int flags = value.Visible ? 1 : 0;
|
? value.NativePersistenceRecord.ToArray()
|
||||||
if (value.RotationEnabled || value.SrcAnim || value.ColorAnim) flags |= 4;
|
: CreateDefaultObjectRecord();
|
||||||
|
int flags = ReadInt(raw, 0);
|
||||||
|
flags = value.Visible ? flags | 1 : flags & ~1;
|
||||||
|
flags = value.OneShotColorEnabled || value.ScaleEnabled
|
||||||
|
|| value.RotationChannelEnabled || value.TranslationEnabled
|
||||||
|
? flags | 2
|
||||||
|
: flags & ~2;
|
||||||
|
flags = value.RotationEnabled || value.SrcAnim || value.ColorAnim
|
||||||
|
? flags | 4
|
||||||
|
: flags & ~4;
|
||||||
WriteInt(raw, 0, flags);
|
WriteInt(raw, 0, flags);
|
||||||
WriteInt(raw, 4, value.SourceSlot);
|
WriteInt(raw, 4, value.SourceSlot);
|
||||||
WriteInt(raw, 8, value.SrcRect.X);
|
WriteInt(raw, 8, value.SrcRect.X);
|
||||||
@@ -76,7 +85,7 @@ internal static class NativeGfxPersistenceCodec
|
|||||||
WriteVector(raw, 0x18, value.V18);
|
WriteVector(raw, 0x18, value.V18);
|
||||||
WriteVector(raw, 0x24, value.V24);
|
WriteVector(raw, 0x24, value.V24);
|
||||||
WriteInt(raw, 0x30, unchecked((int)value.StaticColorMode));
|
WriteInt(raw, 0x30, unchecked((int)value.StaticColorMode));
|
||||||
WriteInt(raw, 0x34, unchecked((int)value.OneShotStartMs));
|
WriteInt(raw, 0x34, EncodeNativeStart(value.OneShotStartMs));
|
||||||
WriteInt(raw, 0x38, unchecked((int)value.ColorDelayMs));
|
WriteInt(raw, 0x38, unchecked((int)value.ColorDelayMs));
|
||||||
WriteInt(raw, 0x3c, unchecked((int)value.ScaleDelayMs));
|
WriteInt(raw, 0x3c, unchecked((int)value.ScaleDelayMs));
|
||||||
WriteInt(raw, 0x40, unchecked((int)value.RotationDelayMs));
|
WriteInt(raw, 0x40, unchecked((int)value.RotationDelayMs));
|
||||||
@@ -89,27 +98,34 @@ internal static class NativeGfxPersistenceCodec
|
|||||||
WriteInt(raw, 0x64, unchecked((int)value.OneShotColorTarget));
|
WriteInt(raw, 0x64, unchecked((int)value.OneShotColorTarget));
|
||||||
WriteScaleMatrix(raw, 0x6c, value.ScaleCurrent);
|
WriteScaleMatrix(raw, 0x6c, value.ScaleCurrent);
|
||||||
WriteScaleMatrix(raw, 0xac, value.ScaleTarget);
|
WriteScaleMatrix(raw, 0xac, value.ScaleTarget);
|
||||||
WriteFloat(raw, 0x16c, value.TranslationCurrent.X);
|
WriteRotationMatrix(raw, 0xec, value.RotationCurrent);
|
||||||
WriteFloat(raw, 0x170, value.TranslationCurrent.Y);
|
WriteRotationMatrix(raw, 0x12c, value.RotationTarget);
|
||||||
WriteFloat(raw, 0x174, value.TranslationCurrent.Z);
|
WriteTranslationMatrix(raw, 0x16c, value.TranslationCurrent);
|
||||||
WriteFloat(raw, 0x1ac, value.TranslationTarget.X);
|
WriteTranslationMatrix(raw, 0x1ac, value.TranslationTarget);
|
||||||
WriteFloat(raw, 0x1b0, value.TranslationTarget.Y);
|
WriteFloat(raw, 0x1ec, value.RotationCurrent.X);
|
||||||
WriteFloat(raw, 0x1b4, value.TranslationTarget.Z);
|
WriteFloat(raw, 0x1f0, value.RotationCurrent.Y);
|
||||||
WriteInt(raw, 0x20c, unchecked((int)value.ColorStart));
|
WriteFloat(raw, 0x1f4, value.RotationCurrent.Z);
|
||||||
WriteInt(raw, 0x214, unchecked((int)value.RotationStartMs));
|
WriteFloat(raw, 0x1f8, value.RotationTarget.X);
|
||||||
|
WriteFloat(raw, 0x1fc, value.RotationTarget.Y);
|
||||||
|
WriteFloat(raw, 0x200, value.RotationTarget.Z);
|
||||||
|
WriteFloat(raw, 0x204, value.RotationCurrent.Angle);
|
||||||
|
WriteFloat(raw, 0x208, value.RotationTarget.Angle);
|
||||||
|
WriteInt(raw, 0x20c, EncodeNativeStart(value.ColorStart));
|
||||||
|
WriteInt(raw, 0x214, EncodeNativeStart(value.RotationStartMs));
|
||||||
WriteInt(raw, 0x220, unchecked((int)value.ColorPeriod));
|
WriteInt(raw, 0x220, unchecked((int)value.ColorPeriod));
|
||||||
WriteInt(raw, 0x228, unchecked((int)value.RotationPeriodMs));
|
WriteInt(raw, 0x228, unchecked((int)value.RotationPeriodMs));
|
||||||
WriteInt(raw, 0x230, unchecked((int)value.SrcPeriod));
|
WriteInt(raw, 0x230, unchecked((int)value.SrcPeriod));
|
||||||
WriteInt(raw, 0x234, unchecked((int)value.SrcCell));
|
WriteInt(raw, 0x234, unchecked((int)value.SrcCell));
|
||||||
WriteInt(raw, 0x238, unchecked((int)value.SrcFrameCount));
|
WriteInt(raw, 0x238, value.SrcAnim ? unchecked((int)value.SrcFrameCount) : 0);
|
||||||
WriteInt(raw, 0x23c, unchecked((int)value.SrcColumns));
|
WriteInt(raw, 0x23c, value.SrcAnim ? unchecked((int)value.SrcColumns) : 0);
|
||||||
WriteInt(raw, 0x240, unchecked((int)value.ColorTarget));
|
if (value.ColorAnim)
|
||||||
|
WriteInt(raw, 0x240, unchecked((int)value.ColorTarget));
|
||||||
WriteVector(raw, 0x244, value.RotationAxis);
|
WriteVector(raw, 0x244, value.RotationAxis);
|
||||||
WriteInt(raw, 0x2d0, unchecked((int)value.OneShotAnimationControlFlags));
|
WriteInt(raw, 0x2d0, unchecked((int)value.OneShotAnimationControlFlags));
|
||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GfxState.GfxObject DecodeObject(ReadOnlySpan<byte> raw)
|
private static GfxState.GfxObject DecodeObject(ReadOnlySpan<byte> raw, bool legacyTightLayout)
|
||||||
{
|
{
|
||||||
if (raw.Length != NativeNumberedSaveState.GfxRecordSize)
|
if (raw.Length != NativeNumberedSaveState.GfxRecordSize)
|
||||||
throw new InvalidDataException("Native retained-gfx record has the wrong size.");
|
throw new InvalidDataException("Native retained-gfx record has the wrong size.");
|
||||||
@@ -118,13 +134,16 @@ internal static class NativeGfxPersistenceCodec
|
|||||||
int flags = ReadInt(raw, 0);
|
int flags = ReadInt(raw, 0);
|
||||||
return new GfxState.GfxObject
|
return new GfxState.GfxObject
|
||||||
{
|
{
|
||||||
|
NativePersistenceRecord = legacyTightLayout
|
||||||
|
? CreateDefaultObjectRecord()
|
||||||
|
: raw.ToArray(),
|
||||||
Visible = (flags & 1) != 0,
|
Visible = (flags & 1) != 0,
|
||||||
SourceSlot = ReadInt(raw, 4),
|
SourceSlot = ReadInt(raw, 4),
|
||||||
SrcRect = (left, top, right - left, bottom - top),
|
SrcRect = (left, top, right - left, bottom - top),
|
||||||
V18 = ReadLongVector(raw, 0x18),
|
V18 = ReadLongVector(raw, 0x18),
|
||||||
V24 = ReadLongVector(raw, 0x24),
|
V24 = ReadLongVector(raw, 0x24),
|
||||||
StaticColorMode = ReadInt(raw, 0x30),
|
StaticColorMode = ReadInt(raw, 0x30),
|
||||||
OneShotStartMs = ReadInt(raw, 0x34),
|
OneShotStartMs = DecodeNativeStart(ReadInt(raw, 0x34)),
|
||||||
ColorDelayMs = ReadInt(raw, 0x38),
|
ColorDelayMs = ReadInt(raw, 0x38),
|
||||||
ScaleDelayMs = ReadInt(raw, 0x3c),
|
ScaleDelayMs = ReadInt(raw, 0x3c),
|
||||||
RotationDelayMs = ReadInt(raw, 0x40),
|
RotationDelayMs = ReadInt(raw, 0x40),
|
||||||
@@ -138,11 +157,17 @@ internal static class NativeGfxPersistenceCodec
|
|||||||
OneShotColorTarget = unchecked((uint)ReadInt(raw, 0x64)),
|
OneShotColorTarget = unchecked((uint)ReadInt(raw, 0x64)),
|
||||||
ScaleCurrent = ReadScale(raw, 0x6c),
|
ScaleCurrent = ReadScale(raw, 0x6c),
|
||||||
ScaleTarget = ReadScale(raw, 0xac),
|
ScaleTarget = ReadScale(raw, 0xac),
|
||||||
TranslationCurrent = ReadDoubleVector(raw, 0x16c),
|
TranslationCurrent = ReadMatrixTranslation(raw, 0x16c),
|
||||||
V16c = ReadLongFloatVector(raw, 0x16c),
|
V16c = ReadLongMatrixTranslation(raw, 0x16c),
|
||||||
TranslationTarget = ReadDoubleVector(raw, 0x1ac),
|
TranslationTarget = ReadMatrixTranslation(raw, 0x1ac),
|
||||||
ColorStart = ReadInt(raw, 0x20c),
|
RotationCurrent = (
|
||||||
RotationStartMs = ReadInt(raw, 0x214),
|
ReadFloat(raw, 0x1ec), ReadFloat(raw, 0x1f0), ReadFloat(raw, 0x1f4),
|
||||||
|
ReadFloat(raw, 0x204)),
|
||||||
|
RotationTarget = (
|
||||||
|
ReadFloat(raw, 0x1f8), ReadFloat(raw, 0x1fc), ReadFloat(raw, 0x200),
|
||||||
|
ReadFloat(raw, 0x208)),
|
||||||
|
ColorStart = DecodeNativeStart(ReadInt(raw, 0x20c)),
|
||||||
|
RotationStartMs = DecodeNativeStart(ReadInt(raw, 0x214)),
|
||||||
ColorPeriod = ReadInt(raw, 0x220),
|
ColorPeriod = ReadInt(raw, 0x220),
|
||||||
RotationPeriodMs = ReadInt(raw, 0x228),
|
RotationPeriodMs = ReadInt(raw, 0x228),
|
||||||
SrcPeriod = ReadInt(raw, 0x230),
|
SrcPeriod = ReadInt(raw, 0x230),
|
||||||
@@ -170,6 +195,7 @@ internal static class NativeGfxPersistenceCodec
|
|||||||
|
|
||||||
private static void WriteScaleMatrix(Span<byte> raw, int offset, (double X, double Y, double Z) scale)
|
private static void WriteScaleMatrix(Span<byte> raw, int offset, (double X, double Y, double Z) scale)
|
||||||
{
|
{
|
||||||
|
ClearMatrix(raw, offset);
|
||||||
WriteFloat(raw, offset, scale.X);
|
WriteFloat(raw, offset, scale.X);
|
||||||
WriteFloat(raw, offset + 0x14, scale.Y);
|
WriteFloat(raw, offset + 0x14, scale.Y);
|
||||||
WriteFloat(raw, offset + 0x28, scale.Z);
|
WriteFloat(raw, offset + 0x28, scale.Z);
|
||||||
@@ -179,6 +205,40 @@ internal static class NativeGfxPersistenceCodec
|
|||||||
private static (double X, double Y, double Z) ReadScale(ReadOnlySpan<byte> raw, int offset)
|
private static (double X, double Y, double Z) ReadScale(ReadOnlySpan<byte> raw, int offset)
|
||||||
=> (ReadFloat(raw, offset), ReadFloat(raw, offset + 0x14), ReadFloat(raw, offset + 0x28));
|
=> (ReadFloat(raw, offset), ReadFloat(raw, offset + 0x14), ReadFloat(raw, offset + 0x28));
|
||||||
|
|
||||||
|
private static void WriteRotationMatrix(
|
||||||
|
Span<byte> raw, int offset, (double X, double Y, double Z, double Angle) rotation)
|
||||||
|
{
|
||||||
|
ClearMatrix(raw, offset);
|
||||||
|
double length = Math.Sqrt(
|
||||||
|
rotation.X * rotation.X + rotation.Y * rotation.Y + rotation.Z * rotation.Z);
|
||||||
|
if (length <= double.Epsilon || rotation.Angle == 0)
|
||||||
|
{
|
||||||
|
WriteFloat(raw, offset, 1);
|
||||||
|
WriteFloat(raw, offset + 0x14, 1);
|
||||||
|
WriteFloat(raw, offset + 0x28, 1);
|
||||||
|
WriteFloat(raw, offset + 0x3c, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
double x = rotation.X / length;
|
||||||
|
double y = rotation.Y / length;
|
||||||
|
double z = rotation.Z / length;
|
||||||
|
double radians = rotation.Angle * Math.PI / 180.0;
|
||||||
|
double cosine = Math.Cos(radians);
|
||||||
|
double sine = Math.Sin(radians);
|
||||||
|
double complement = 1.0 - cosine;
|
||||||
|
WriteFloat(raw, offset, x * x * complement + cosine);
|
||||||
|
WriteFloat(raw, offset + 0x04, x * y * complement + z * sine);
|
||||||
|
WriteFloat(raw, offset + 0x08, x * z * complement - y * sine);
|
||||||
|
WriteFloat(raw, offset + 0x10, x * y * complement - z * sine);
|
||||||
|
WriteFloat(raw, offset + 0x14, y * y * complement + cosine);
|
||||||
|
WriteFloat(raw, offset + 0x18, x * sine + y * z * complement);
|
||||||
|
WriteFloat(raw, offset + 0x20, y * sine + x * z * complement);
|
||||||
|
WriteFloat(raw, offset + 0x24, y * z * complement - x * sine);
|
||||||
|
WriteFloat(raw, offset + 0x28, z * z * complement + cosine);
|
||||||
|
WriteFloat(raw, offset + 0x3c, 1);
|
||||||
|
}
|
||||||
|
|
||||||
private static void WriteVector(Span<byte> raw, int offset, (long X, long Y, long Z) vector)
|
private static void WriteVector(Span<byte> raw, int offset, (long X, long Y, long Z) vector)
|
||||||
{
|
{
|
||||||
WriteInt(raw, offset, unchecked((int)vector.X));
|
WriteInt(raw, offset, unchecked((int)vector.X));
|
||||||
@@ -189,11 +249,55 @@ internal static class NativeGfxPersistenceCodec
|
|||||||
private static (long X, long Y, long Z) ReadLongVector(ReadOnlySpan<byte> raw, int offset)
|
private static (long X, long Y, long Z) ReadLongVector(ReadOnlySpan<byte> raw, int offset)
|
||||||
=> (ReadInt(raw, offset), ReadInt(raw, offset + 4), ReadInt(raw, offset + 8));
|
=> (ReadInt(raw, offset), ReadInt(raw, offset + 4), ReadInt(raw, offset + 8));
|
||||||
|
|
||||||
private static (long X, long Y, long Z) ReadLongFloatVector(ReadOnlySpan<byte> raw, int offset)
|
private static void WriteTranslationMatrix(
|
||||||
=> ((long)ReadFloat(raw, offset), (long)ReadFloat(raw, offset + 4), (long)ReadFloat(raw, offset + 8));
|
Span<byte> raw, int offset, (double X, double Y, double Z) translation)
|
||||||
|
{
|
||||||
|
ClearMatrix(raw, offset);
|
||||||
|
WriteFloat(raw, offset, 1);
|
||||||
|
WriteFloat(raw, offset + 0x14, 1);
|
||||||
|
WriteFloat(raw, offset + 0x28, 1);
|
||||||
|
WriteFloat(raw, offset + 0x30, translation.X);
|
||||||
|
WriteFloat(raw, offset + 0x34, translation.Y);
|
||||||
|
WriteFloat(raw, offset + 0x38, translation.Z);
|
||||||
|
WriteFloat(raw, offset + 0x3c, 1);
|
||||||
|
}
|
||||||
|
|
||||||
private static (double X, double Y, double Z) ReadDoubleVector(ReadOnlySpan<byte> raw, int offset)
|
private static (long X, long Y, long Z) ReadLongMatrixTranslation(
|
||||||
=> (ReadFloat(raw, offset), ReadFloat(raw, offset + 4), ReadFloat(raw, offset + 8));
|
ReadOnlySpan<byte> raw, int offset)
|
||||||
|
=> ((long)ReadFloat(raw, offset + 0x30),
|
||||||
|
(long)ReadFloat(raw, offset + 0x34),
|
||||||
|
(long)ReadFloat(raw, offset + 0x38));
|
||||||
|
|
||||||
|
private static (double X, double Y, double Z) ReadMatrixTranslation(
|
||||||
|
ReadOnlySpan<byte> raw, int offset)
|
||||||
|
=> (ReadFloat(raw, offset + 0x30),
|
||||||
|
ReadFloat(raw, offset + 0x34),
|
||||||
|
ReadFloat(raw, offset + 0x38));
|
||||||
|
|
||||||
|
private static void ClearMatrix(Span<byte> raw, int offset)
|
||||||
|
=> raw.Slice(offset, 0x40).Clear();
|
||||||
|
|
||||||
|
private static int EncodeNativeStart(long value)
|
||||||
|
=> value < 0 ? 0 : unchecked((int)value);
|
||||||
|
|
||||||
|
private static long DecodeNativeStart(int value)
|
||||||
|
=> value == 0 ? -1 : value;
|
||||||
|
|
||||||
|
private static byte[] CreateDefaultObjectRecord()
|
||||||
|
{
|
||||||
|
byte[] raw = new byte[NativeNumberedSaveState.GfxRecordSize];
|
||||||
|
WriteInt(raw, 0x60, -1);
|
||||||
|
WriteInt(raw, 0x64, -1);
|
||||||
|
foreach (int matrixOffset in new[] { 0x6c, 0xac, 0xec, 0x12c, 0x16c, 0x1ac })
|
||||||
|
{
|
||||||
|
WriteFloat(raw, matrixOffset, 1);
|
||||||
|
WriteFloat(raw, matrixOffset + 0x14, 1);
|
||||||
|
WriteFloat(raw, matrixOffset + 0x28, 1);
|
||||||
|
WriteFloat(raw, matrixOffset + 0x3c, 1);
|
||||||
|
}
|
||||||
|
WriteInt(raw, 0x240, -1);
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
private static void WriteFloat(Span<byte> raw, int offset, double value)
|
private static void WriteFloat(Span<byte> raw, int offset, double value)
|
||||||
=> WriteInt(raw, offset, BitConverter.SingleToInt32Bits((float)value));
|
=> WriteInt(raw, offset, BitConverter.SingleToInt32Bits((float)value));
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ public sealed record NativeNumberedSaveState(
|
|||||||
IReadOnlyList<NativeSavedGfxObject> GfxObjects,
|
IReadOnlyList<NativeSavedGfxObject> GfxObjects,
|
||||||
long RangeTransformFirst,
|
long RangeTransformFirst,
|
||||||
int RangeTransformCount,
|
int RangeTransformCount,
|
||||||
byte[] RangeTransformRecord)
|
byte[] RangeTransformRecord,
|
||||||
|
bool LegacyTightGfxLayout = false)
|
||||||
{
|
{
|
||||||
public const int SoundEffectChannelCount = 10;
|
public const int SoundEffectChannelCount = 10;
|
||||||
public const int ResourceRecordsSize = 300 * 4;
|
public const int ResourceRecordsSize = 300 * 4;
|
||||||
@@ -45,6 +46,9 @@ public static class NativeNumberedSaveCodec
|
|||||||
private const int FrameReturnCapacity = 256;
|
private const int FrameReturnCapacity = 256;
|
||||||
private const int GfxAllocationDwords = 0x2d8;
|
private const int GfxAllocationDwords = 0x2d8;
|
||||||
private const int GfxConstantDwords = 0x2e1;
|
private const int GfxConstantDwords = 0x2e1;
|
||||||
|
// AGE stores a byte-sized 0x2d4 object record at the front of a 0x2d4-DWORD region.
|
||||||
|
// The handle consumes one preceding DWORD, so consecutive entries start 0x2d5 DWORDs apart.
|
||||||
|
private const int GfxEntryStrideBytes = (1 + NativeNumberedSaveState.GfxRecordSize) * 4;
|
||||||
private static readonly Encoding NativeEncoding = CreateNativeEncoding();
|
private static readonly Encoding NativeEncoding = CreateNativeEncoding();
|
||||||
|
|
||||||
public static byte[] Encode(NativeNumberedSaveState state)
|
public static byte[] Encode(NativeNumberedSaveState state)
|
||||||
@@ -103,7 +107,7 @@ public static class NativeNumberedSaveCodec
|
|||||||
{
|
{
|
||||||
WriteInt(payload, at, unchecked((int)gfx.Handle));
|
WriteInt(payload, at, unchecked((int)gfx.Handle));
|
||||||
gfx.Record.CopyTo(payload, at + 4);
|
gfx.Record.CopyTo(payload, at + 4);
|
||||||
at += 4 + NativeNumberedSaveState.GfxRecordSize;
|
at += GfxEntryStrideBytes;
|
||||||
}
|
}
|
||||||
WriteInt(payload, at, unchecked((int)state.RangeTransformFirst));
|
WriteInt(payload, at, unchecked((int)state.RangeTransformFirst));
|
||||||
WriteInt(payload, at + 4, state.RangeTransformCount);
|
WriteInt(payload, at + 4, state.RangeTransformCount);
|
||||||
@@ -154,14 +158,19 @@ public static class NativeNumberedSaveCodec
|
|||||||
if (gfxRecordSize != NativeNumberedSaveState.GfxRecordSize)
|
if (gfxRecordSize != NativeNumberedSaveState.GfxRecordSize)
|
||||||
throw new InvalidDataException($"Unsupported native gfx record size 0x{gfxRecordSize:x}.");
|
throw new InvalidDataException($"Unsupported native gfx record size 0x{gfxRecordSize:x}.");
|
||||||
at += 8;
|
at += 8;
|
||||||
var objects = new NativeSavedGfxObject[gfxCount];
|
int objectsAt = at;
|
||||||
for (int i = 0; i < objects.Length; i++)
|
(NativeSavedGfxObject[] objects, int afterObjects) =
|
||||||
|
ReadGfxObjects(payload, objectsAt, gfxCount, GfxEntryStrideBytes);
|
||||||
|
bool legacyTightGfxLayout = false;
|
||||||
|
// Compatibility for experimental files written by the port before the native DWORD stride was
|
||||||
|
// understood. A native retained-object map cannot contain duplicate handles.
|
||||||
|
if (objects.Select(item => item.Handle).Distinct().Count() != objects.Length)
|
||||||
{
|
{
|
||||||
Require(payload, at, 4 + gfxRecordSize, "numbered-save gfx object");
|
(objects, afterObjects) =
|
||||||
long handle = ReadInt(payload, at);
|
ReadGfxObjects(payload, objectsAt, gfxCount, 4 + gfxRecordSize);
|
||||||
objects[i] = new NativeSavedGfxObject(handle, payload.Slice(at + 4, gfxRecordSize).ToArray());
|
legacyTightGfxLayout = true;
|
||||||
at += 4 + gfxRecordSize;
|
|
||||||
}
|
}
|
||||||
|
at = afterObjects;
|
||||||
Require(payload, at, 8 + gfxRecordSize, "numbered-save range transform");
|
Require(payload, at, 8 + gfxRecordSize, "numbered-save range transform");
|
||||||
long rangeFirst = ReadInt(payload, at);
|
long rangeFirst = ReadInt(payload, at);
|
||||||
int rangeCount = ReadInt(payload, at + 4);
|
int rangeCount = ReadInt(payload, at + 4);
|
||||||
@@ -170,7 +179,25 @@ public static class NativeNumberedSaveCodec
|
|||||||
return new NativeNumberedSaveState(
|
return new NativeNumberedSaveState(
|
||||||
ReadInt(payload, 4), ReadInt(payload, 8), soundEffects, resources, surfaces, frames,
|
ReadInt(payload, 4), ReadInt(payload, 8), soundEffects, resources, surfaces, frames,
|
||||||
integers, floats, strings, pointers, pointerStrings, localPointerScratch, objects,
|
integers, floats, strings, pointers, pointerStrings, localPointerScratch, objects,
|
||||||
rangeFirst, rangeCount, rangeRecord);
|
rangeFirst, rangeCount, rangeRecord, legacyTightGfxLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (NativeSavedGfxObject[] Objects, int After) ReadGfxObjects(
|
||||||
|
ReadOnlySpan<byte> payload, int offset, int count, int strideBytes)
|
||||||
|
{
|
||||||
|
var objects = new NativeSavedGfxObject[count];
|
||||||
|
int at = offset;
|
||||||
|
for (int i = 0; i < objects.Length; i++)
|
||||||
|
{
|
||||||
|
Require(payload, at, 4 + NativeNumberedSaveState.GfxRecordSize,
|
||||||
|
"numbered-save gfx object");
|
||||||
|
long handle = ReadInt(payload, at);
|
||||||
|
objects[i] = new NativeSavedGfxObject(
|
||||||
|
handle,
|
||||||
|
payload.Slice(at + 4, NativeNumberedSaveState.GfxRecordSize).ToArray());
|
||||||
|
at = checked(at + strideBytes);
|
||||||
|
}
|
||||||
|
return (objects, at);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static NativeNumberedSaveState Empty(IReadOnlyList<NativeSavedScriptFrame> frames)
|
public static NativeNumberedSaveState Empty(IReadOnlyList<NativeSavedScriptFrame> frames)
|
||||||
|
|||||||
65
engine/Age.Engine/Sys4/RetainedSurfaceRasterizer.cs
Normal file
65
engine/Age.Engine/Sys4/RetainedSurfaceRasterizer.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
using Age.Engine.Model;
|
||||||
|
|
||||||
|
namespace Age.Engine.Sys4;
|
||||||
|
|
||||||
|
/// <summary>Platform-neutral software publication of retained gfx objects into an AGE surface.
|
||||||
|
/// Native scripts use the same object list for the backbuffer and selected offscreen render targets.</summary>
|
||||||
|
public static class RetainedSurfaceRasterizer
|
||||||
|
{
|
||||||
|
public static int CompositeRange(
|
||||||
|
RgbaImage destination,
|
||||||
|
IReadOnlyList<RenderObject> visible,
|
||||||
|
long firstHandle,
|
||||||
|
long count,
|
||||||
|
Func<RenderObject, RgbaImage?> resolveSource)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(destination);
|
||||||
|
ArgumentNullException.ThrowIfNull(visible);
|
||||||
|
ArgumentNullException.ThrowIfNull(resolveSource);
|
||||||
|
if (destination.Width <= 0 || destination.Height <= 0
|
||||||
|
|| destination.Pixels.Length != checked(destination.Width * destination.Height * 4)
|
||||||
|
|| count <= 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
int rendered = 0;
|
||||||
|
foreach (RenderObject item in visible)
|
||||||
|
{
|
||||||
|
if (item.Handle < firstHandle || item.Handle - firstHandle >= count) continue;
|
||||||
|
|
||||||
|
TransformState transform = item.Transform;
|
||||||
|
Affine2D localToDestination =
|
||||||
|
Transform2DMath.Build(transform, item.Rotation).FromLocalOrigin(item.DstX, item.DstY);
|
||||||
|
if (item.RangeTransform is { } rangeTransform)
|
||||||
|
localToDestination = localToDestination.Then(rangeTransform);
|
||||||
|
|
||||||
|
RgbaImage? source = resolveSource(item);
|
||||||
|
float opacity = item.Alpha / 255f;
|
||||||
|
float tintStrength = item.TintStrength / 255f;
|
||||||
|
if (source == null)
|
||||||
|
{
|
||||||
|
if (item.SurfaceResId != 0 || item.Blend == BlendKind.Opaque) continue;
|
||||||
|
int width = item.W > 0 ? item.W : 800;
|
||||||
|
int height = item.H > 0 ? item.H : 600;
|
||||||
|
float fillOpacity = item.MultiplyTint ? opacity : opacity * tintStrength;
|
||||||
|
SoftwareAffineRasterizer.FillRgba(
|
||||||
|
destination.Pixels, destination.Width, destination.Height,
|
||||||
|
width, height, localToDestination, item.Tint, fillOpacity);
|
||||||
|
rendered++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.W <= 0 || item.H <= 0 || item.SrcX < 0 || item.SrcY < 0) continue;
|
||||||
|
int widthToDraw = System.Math.Min(item.W, source.Width - item.SrcX);
|
||||||
|
int heightToDraw = System.Math.Min(item.H, source.Height - item.SrcY);
|
||||||
|
if (widthToDraw <= 0 || heightToDraw <= 0) continue;
|
||||||
|
SoftwareAffineRasterizer.BlitRgba(
|
||||||
|
destination.Pixels, destination.Width, destination.Height,
|
||||||
|
source.Pixels, source.Width, source.Height,
|
||||||
|
item.SrcX, item.SrcY, widthToDraw, heightToDraw,
|
||||||
|
localToDestination, item.Tint, tintStrength, opacity,
|
||||||
|
item.MultiplyTint, item.Blend);
|
||||||
|
rendered++;
|
||||||
|
}
|
||||||
|
return rendered;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Age.Engine.Model;
|
using Age.Engine.Model;
|
||||||
|
using Age.Engine.Persistence;
|
||||||
namespace Age.Engine.Vm;
|
namespace Age.Engine.Vm;
|
||||||
|
|
||||||
/// <summary>One script activation: the running script, its instruction cursor, its local slots,
|
/// <summary>One script activation: the running script, its instruction cursor, its local slots,
|
||||||
@@ -10,6 +11,10 @@ internal sealed class ExecFrame
|
|||||||
|
|
||||||
public readonly Script Script;
|
public readonly Script Script;
|
||||||
public int Pc; // entry instruction index
|
public int Pc; // entry instruction index
|
||||||
|
// While a restored descendant is running, this activation is parked at the synthetic op-0xae
|
||||||
|
// rendezvous rather than its native T1/T2/T3 coordinate. Retain the serialized coordinates until
|
||||||
|
// the descendant returns so an intervening save can reproduce the native parent frame.
|
||||||
|
public NativeSavedScriptFrame? RestoredSaveFrame;
|
||||||
public int ReadMessageOffset = -1; // latest op-0x71 code DWORD coordinate
|
public int ReadMessageOffset = -1; // latest op-0x71 code DWORD coordinate
|
||||||
public readonly Frame Locals = new();
|
public readonly Frame Locals = new();
|
||||||
public readonly List<int> CallStack = new(); // intra-script `call` (op 0x8f) returns
|
public readonly List<int> CallStack = new(); // intra-script `call` (op 0x8f) returns
|
||||||
|
|||||||
@@ -595,12 +595,17 @@ public sealed class VirtualMachine
|
|||||||
Script root = _s;
|
Script root = _s;
|
||||||
int rootEntry = root.IndexByOffset.TryGetValue(entryOffset, out var idx) ? idx : 0;
|
int rootEntry = root.IndexByOffset.TryGetValue(entryOffset, out var idx) ? idx : 0;
|
||||||
FrameCause cause = FrameCause.TopScene;
|
FrameCause cause = FrameCause.TopScene;
|
||||||
|
NativeSavedScriptFrame? restoredRootFrame = null;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
FrameOutcome outcome;
|
FrameOutcome outcome;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
outcome = RunFrame(new ExecFrame(root, rootEntry), cause);
|
ExecFrame rootFrame = restoredRootFrame == null
|
||||||
|
? new ExecFrame(root, rootEntry)
|
||||||
|
: CreateRestoredFrame(root, restoredRootFrame);
|
||||||
|
restoredRootFrame = null;
|
||||||
|
outcome = RunFrame(rootFrame, cause);
|
||||||
}
|
}
|
||||||
catch (NumberedRestoreRequestedException)
|
catch (NumberedRestoreRequestedException)
|
||||||
{
|
{
|
||||||
@@ -618,8 +623,9 @@ public sealed class VirtualMachine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
root = ResolveSavedScript(_loadedNumberedState.Frames[0]);
|
root = ResolveSavedScript(_loadedNumberedState.Frames[0]);
|
||||||
rootEntry = FindRestoreRendezvous(root);
|
rootEntry = 0;
|
||||||
_restoreFrameIndex = 0;
|
_restoreFrameIndex = 0;
|
||||||
|
restoredRootFrame = _loadedNumberedState.Frames[0];
|
||||||
cause = FrameCause.SaveRestore;
|
cause = FrameCause.SaveRestore;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -800,19 +806,26 @@ public sealed class VirtualMachine
|
|||||||
for (int i = 0; i <= cutoff; i++)
|
for (int i = 0; i <= cutoff; i++)
|
||||||
{
|
{
|
||||||
ExecFrame frame = active[i];
|
ExecFrame frame = active[i];
|
||||||
int[] returns = frame.CallStack
|
NativeSavedScriptFrame? restored = frame.RestoredSaveFrame;
|
||||||
.Where(returnPc => (uint)returnPc < (uint)frame.Script.Instructions.Count)
|
int[] returns = restored?.ReturnIndices.ToArray()
|
||||||
.Select(returnPc =>
|
?? frame.CallStack
|
||||||
{
|
.Where(returnPc => (uint)returnPc < (uint)frame.Script.Instructions.Count)
|
||||||
int returnOffset = frame.Script.Instructions[returnPc].Offset;
|
.Select(returnPc =>
|
||||||
return FindTableIndex(frame.Script.LocalCallOffsets, returnOffset - 3);
|
{
|
||||||
})
|
int returnOffset = frame.Script.Instructions[returnPc].Offset;
|
||||||
.Where(index => index >= 0)
|
return FindTableIndex(frame.Script.LocalCallOffsets, returnOffset - 3);
|
||||||
.ToArray();
|
})
|
||||||
int resumeIndex = CurrentReadMessageIndex(frame);
|
.Where(index => index >= 0)
|
||||||
int callTargetIndex = i == cutoff || (uint)frame.Pc >= (uint)frame.Script.Instructions.Count
|
.ToArray();
|
||||||
|
int resumeIndex = restored?.ResumeIndex ?? CurrentReadMessageIndex(frame);
|
||||||
|
int callTargetIndex = i == cutoff
|
||||||
? -1
|
? -1
|
||||||
: FindTableIndex(frame.Script.ScriptCallOffsets, frame.Script.Instructions[frame.Pc].Offset);
|
: restored?.CallTargetIndex
|
||||||
|
?? ((uint)frame.Pc >= (uint)frame.Script.Instructions.Count
|
||||||
|
? -1
|
||||||
|
: FindTableIndex(
|
||||||
|
frame.Script.ScriptCallOffsets,
|
||||||
|
frame.Script.Instructions[frame.Pc].Offset));
|
||||||
frames[i] = new NativeSavedScriptFrame(
|
frames[i] = new NativeSavedScriptFrame(
|
||||||
i - 1, frame.Script.PackedId, returns, resumeIndex, callTargetIndex);
|
i - 1, frame.Script.PackedId, returns, resumeIndex, callTargetIndex);
|
||||||
}
|
}
|
||||||
@@ -941,6 +954,29 @@ public sealed class VirtualMachine
|
|||||||
$"Saved script {script.Name} has no opcode 0xae restore rendezvous.");
|
$"Saved script {script.Name} has no opcode 0xae restore rendezvous.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ExecFrame CreateRestoredFrame(Script script, NativeSavedScriptFrame saved)
|
||||||
|
{
|
||||||
|
// Native creates each saved script context at its ordinary entrypoint. The script runs its
|
||||||
|
// local-array/constants/resource prologue and rendezvouses at 0xae itself; jumping directly
|
||||||
|
// to 0xae leaves those frame-local tables zeroed (FIELD then collapses its map zoom to 0%).
|
||||||
|
_ = FindRestoreRendezvous(script);
|
||||||
|
int entry = script.IndexByOffset.TryGetValue(0, out int index) ? index : 0;
|
||||||
|
var frame = new ExecFrame(script, entry)
|
||||||
|
{
|
||||||
|
RestoredSaveFrame = saved,
|
||||||
|
};
|
||||||
|
if ((uint)saved.ResumeIndex < (uint)script.ReadMessageOffsets.Count)
|
||||||
|
frame.ReadMessageOffset = script.ReadMessageOffsets[saved.ResumeIndex];
|
||||||
|
foreach (int returnIndex in saved.ReturnIndices)
|
||||||
|
{
|
||||||
|
if ((uint)returnIndex >= (uint)script.LocalCallOffsets.Count) continue;
|
||||||
|
int returnOffset = checked(script.LocalCallOffsets[returnIndex] + 3);
|
||||||
|
if (script.IndexByOffset.TryGetValue(returnOffset, out int returnPc))
|
||||||
|
frame.CallStack.Add(returnPc);
|
||||||
|
}
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
private static int ResolveTableOffset(
|
private static int ResolveTableOffset(
|
||||||
Script script, IReadOnlyList<int> table, int index, int fallback)
|
Script script, IReadOnlyList<int> table, int index, int fallback)
|
||||||
{
|
{
|
||||||
@@ -1039,6 +1075,9 @@ public sealed class VirtualMachine
|
|||||||
{
|
{
|
||||||
case "script-entry":
|
case "script-entry":
|
||||||
Gfx.ClearSurfaceReloadPolicies(); return pc + 1;
|
Gfx.ClearSurfaceReloadPolicies(); return pc + 1;
|
||||||
|
case "set-surface-persistence-flags": // 0x258 (slot)(flags): bit 0 = numbered-load reload
|
||||||
|
Gfx.SetSurfaceReloadOnRestore(unchecked((int)Read(a[0])), (Read(a[1]) & 1) != 0);
|
||||||
|
return pc + 1;
|
||||||
case "add": Write(a[0], Read(a[1]) + Read(a[2])); return pc + 1;
|
case "add": Write(a[0], Read(a[1]) + Read(a[2])); return pc + 1;
|
||||||
case "sub": Write(a[0], Read(a[1]) - Read(a[2])); return pc + 1;
|
case "sub": Write(a[0], Read(a[1]) - Read(a[2])); return pc + 1;
|
||||||
case "mul": Write(a[0], Read(a[1]) * Read(a[2])); return pc + 1;
|
case "mul": Write(a[0], Read(a[1]) * Read(a[2])); return pc + 1;
|
||||||
@@ -1139,6 +1178,11 @@ public sealed class VirtualMachine
|
|||||||
bool terminal = _restoreFrameIndex == _loadedNumberedState.Frames.Count - 1;
|
bool terminal = _restoreFrameIndex == _loadedNumberedState.Frames.Count - 1;
|
||||||
if (terminal)
|
if (terminal)
|
||||||
{
|
{
|
||||||
|
// Native restores the saved top frame as the numbered-save boundary selected by
|
||||||
|
// opcode 0x1ad. Re-establish that identity so a subsequent save excludes transient
|
||||||
|
// SAVE/menu helper frames instead of serializing the currently open modal stack.
|
||||||
|
lock (_debugControlLock) _saveResumeFrame = _cur;
|
||||||
|
_cur.RestoredSaveFrame = null;
|
||||||
_loadedNumberedState = null;
|
_loadedNumberedState = null;
|
||||||
_restoreFrameIndex = -1;
|
_restoreFrameIndex = -1;
|
||||||
return ResolveTableOffset(_cur.Script, _cur.Script.ReadMessageOffsets, saved.ResumeIndex, pc + 1);
|
return ResolveTableOffset(_cur.Script, _cur.Script.ReadMessageOffsets, saved.ResumeIndex, pc + 1);
|
||||||
@@ -1149,11 +1193,14 @@ public sealed class VirtualMachine
|
|||||||
Script child = ResolveSavedScript(childSaved);
|
Script child = ResolveSavedScript(childSaved);
|
||||||
_restoreFrameIndex = parentIndex + 1;
|
_restoreFrameIndex = parentIndex + 1;
|
||||||
FrameOutcome childOutcome = RunFrame(
|
FrameOutcome childOutcome = RunFrame(
|
||||||
new ExecFrame(child, FindRestoreRendezvous(child)), FrameCause.SaveRestore,
|
CreateRestoredFrame(child, childSaved), FrameCause.SaveRestore,
|
||||||
childSaved.ScriptId);
|
childSaved.ScriptId);
|
||||||
_restoreFrameIndex = parentIndex;
|
_restoreFrameIndex = parentIndex;
|
||||||
if (childOutcome is FrameOutcome.Halted or FrameOutcome.ExitRequested)
|
if (childOutcome == FrameOutcome.Halted) return HALT;
|
||||||
return HALT;
|
if (childOutcome == FrameOutcome.RootReload) return ROOT_RELOAD;
|
||||||
|
if (childOutcome == FrameOutcome.ExitRequested)
|
||||||
|
throw new ProcessExitRequestedException();
|
||||||
|
_cur.RestoredSaveFrame = null;
|
||||||
return ResolveTableOffset(
|
return ResolveTableOffset(
|
||||||
_cur.Script, _cur.Script.ScriptCallOffsets, saved.CallTargetIndex, pc) + 1;
|
_cur.Script, _cur.Script.ScriptCallOffsets, saved.CallTargetIndex, pc) + 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -274,6 +274,11 @@ public sealed class GodotAdvHost : IHost
|
|||||||
|
|
||||||
public void PresentObjectRange(GfxState gfx, long firstHandle, long count)
|
public void PresentObjectRange(GfxState gfx, long firstHandle, long count)
|
||||||
{
|
{
|
||||||
|
if (gfx.CurrentRenderTargetSlot >= 0)
|
||||||
|
{
|
||||||
|
PublishObjectRangeToSurface(gfx, firstHandle, count);
|
||||||
|
return;
|
||||||
|
}
|
||||||
System.Threading.Interlocked.Exchange(ref _presentRequested, 1);
|
System.Threading.Interlocked.Exchange(ref _presentRequested, 1);
|
||||||
_timeline?.Event("present-object-range", new() { ["first"] = firstHandle, ["count"] = count });
|
_timeline?.Event("present-object-range", new() { ["first"] = firstHandle, ["count"] = count });
|
||||||
}
|
}
|
||||||
@@ -573,6 +578,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
{
|
{
|
||||||
int slot = gfx.CurrentRenderTargetSlot;
|
int slot = gfx.CurrentRenderTargetSlot;
|
||||||
var snapshot = gfx.SnapshotVisibleObjects(_clock.NowMs);
|
var snapshot = gfx.SnapshotVisibleObjects(_clock.NowMs);
|
||||||
|
PublishObjectRangeToSurface(gfx, 0, long.MaxValue, snapshot);
|
||||||
lock (_screenTransitionLock) _renderTargetSnapshots[slot] = snapshot;
|
lock (_screenTransitionLock) _renderTargetSnapshots[slot] = snapshot;
|
||||||
_timeline?.Event("render-target-snapshot", new()
|
_timeline?.Event("render-target-snapshot", new()
|
||||||
{
|
{
|
||||||
@@ -1110,10 +1116,67 @@ public sealed class GodotAdvHost : IHost
|
|||||||
// For an offscreen target, discard separately retained text draws so its modeled pixel contents
|
// For an offscreen target, discard separately retained text draws so its modeled pixel contents
|
||||||
// observe the native D3D clear as well.
|
// observe the native D3D clear as well.
|
||||||
if (surfaceSlot >= 0)
|
if (surfaceSlot >= 0)
|
||||||
|
{
|
||||||
lock (_textLock) _surfaceText.Remove(surfaceSlot);
|
lock (_textLock) _surfaceText.Remove(surfaceSlot);
|
||||||
|
lock (_imageLock)
|
||||||
|
{
|
||||||
|
if (_surfaceImages.TryGetValue(surfaceSlot, out var image))
|
||||||
|
System.Array.Clear(image.Pixels);
|
||||||
|
else if (_slotDims.TryGetValue(surfaceSlot, out var dimensions)
|
||||||
|
&& dimensions.W > 0 && dimensions.H > 0)
|
||||||
|
_surfaceImages[surfaceSlot] = new RgbaImage(
|
||||||
|
dimensions.W, dimensions.H,
|
||||||
|
new byte[checked(dimensions.W * dimensions.H * 4)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
_timeline?.Event("render-target-clear", new() { ["surface"] = surfaceSlot });
|
_timeline?.Event("render-target-clear", new() { ["surface"] = surfaceSlot });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void PublishObjectRangeToSurface(
|
||||||
|
GfxState gfx, long firstHandle, long count,
|
||||||
|
IReadOnlyList<RenderObject>? sampled = null)
|
||||||
|
{
|
||||||
|
int targetSlot = gfx.CurrentRenderTargetSlot;
|
||||||
|
if (targetSlot < 0 || !_slotDims.TryGetValue(targetSlot, out var dimensions)
|
||||||
|
|| dimensions.W <= 0 || dimensions.H <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
RgbaImage destination;
|
||||||
|
lock (_imageLock)
|
||||||
|
destination = _surfaceImages.TryGetValue(targetSlot, out var current)
|
||||||
|
? new RgbaImage(current.Width, current.Height, (byte[])current.Pixels.Clone())
|
||||||
|
: new RgbaImage(dimensions.W, dimensions.H,
|
||||||
|
new byte[checked(dimensions.W * dimensions.H * 4)]);
|
||||||
|
|
||||||
|
IReadOnlyList<RenderObject> visible = sampled ?? gfx.SnapshotVisibleObjects(_clock.NowMs);
|
||||||
|
int rendered = RetainedSurfaceRasterizer.CompositeRange(
|
||||||
|
destination, visible, firstHandle, count,
|
||||||
|
item =>
|
||||||
|
{
|
||||||
|
var raw = gfx.TryGet(item.Handle);
|
||||||
|
var resolved = raw != null
|
||||||
|
? ResolveSurfaceTexture(raw.SourceSlot, item.SurfaceResId)
|
||||||
|
: ResolveResIdTexture(item.SurfaceResId);
|
||||||
|
return resolved == null
|
||||||
|
? null
|
||||||
|
: RgbaSurfaceOps.WithColorKey(resolved.Value.Image, item.ColorKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
lock (_imageLock) _surfaceImages[targetSlot] = destination;
|
||||||
|
IReadOnlyList<RenderObject> retained = visible
|
||||||
|
.Where(item => item.Handle >= firstHandle && item.Handle - firstHandle < count)
|
||||||
|
.ToArray();
|
||||||
|
lock (_screenTransitionLock) _renderTargetSnapshots[targetSlot] = retained;
|
||||||
|
_timeline?.Event("render-target-publish", new()
|
||||||
|
{
|
||||||
|
["surface"] = targetSlot,
|
||||||
|
["first"] = firstHandle,
|
||||||
|
["count"] = count,
|
||||||
|
["objects"] = retained.Count,
|
||||||
|
["rendered"] = rendered,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public void ReleaseSurfaceRange(int firstSlot, int count)
|
public void ReleaseSurfaceRange(int firstSlot, int count)
|
||||||
{
|
{
|
||||||
IReadOnlyList<MovieSurfaceBinding> stoppedMovies = _movieSurfaces.ReleaseRange(firstSlot, count);
|
IReadOnlyList<MovieSurfaceBinding> stoppedMovies = _movieSurfaces.ReleaseRange(firstSlot, count);
|
||||||
|
|||||||
@@ -76,6 +76,5 @@ INFERRED: dict[int, dict] = {
|
|||||||
0x239: dict(name='animate-gfx-srcrect-target', category='draw', noop=False, confidence='high', source='investigation', summary='(handle)(delay_ms)(duration_ms)(frame_count)(column_count)(target_frame) — one-shot row-major source-rectangle cell channel. Worker gfx_worker_set_srcrect_cell @0x47ed90 stores timing at obj+0x48/+0x5c, layout at +0x238/+0x23c, and target at +0x234. C# currently retains the endpoint cell immediately.'),
|
0x239: dict(name='animate-gfx-srcrect-target', category='draw', noop=False, confidence='high', source='investigation', summary='(handle)(delay_ms)(duration_ms)(frame_count)(column_count)(target_frame) — one-shot row-major source-rectangle cell channel. Worker gfx_worker_set_srcrect_cell @0x47ed90 stores timing at obj+0x48/+0x5c, layout at +0x238/+0x23c, and target at +0x234. C# currently retains the endpoint cell immediately.'),
|
||||||
0x23b: dict(name='draw-decimal-glyphs', category='draw', noop=False, confidence='high', source='investigation', summary='Draw an integer as decimal glyph objects from a style registered by opcode 0x13a.'),
|
0x23b: dict(name='draw-decimal-glyphs', category='draw', noop=False, confidence='high', source='investigation', summary='Draw an integer as decimal glyph objects from a style registered by opcode 0x13a.'),
|
||||||
0x23f: dict(name='query-surface-stop-time-ms', category='draw', noop=False, confidence='high', source='investigation', summary='(out_stop_time_ms)(surface_slot) — query the DirectShow stop position retained by a loaded movie surface, convert seconds to integer milliseconds by truncating toward zero, and write -1 when the movie slot is empty. Port-only host decoder failure is modeled as an explicitly completed, zero-duration movie.'),
|
0x23f: dict(name='query-surface-stop-time-ms', category='draw', noop=False, confidence='high', source='investigation', summary='(out_stop_time_ms)(surface_slot) — query the DirectShow stop position retained by a loaded movie surface, convert seconds to integer milliseconds by truncating toward zero, and write -1 when the movie slot is empty. Port-only host decoder failure is modeled as an explicitly completed, zero-duration movie.'),
|
||||||
0x258: dict(name='decl?', category='marker', noop=True, confidence='low', source='harness', summary='2 imm; runs in a chain right after script-entry 0x259, enumerating ids — prologue declaration/registration?'),
|
|
||||||
0x2c5: dict(name='byte-string-length', category='compute', noop=False, confidence='high', source='investigation', summary="Write the resolved NUL-terminated engine string's raw byte length."),
|
0x2c5: dict(name='byte-string-length', category='compute', noop=False, confidence='high', source='investigation', summary="Write the resolved NUL-terminated engine string's raw byte length."),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1724,12 +1724,12 @@ abi_source = "kelebek+decode-validated"
|
|||||||
name = "continue-save-load-stack-restore"
|
name = "continue-save-load-stack-restore"
|
||||||
category = "control"
|
category = "control"
|
||||||
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."
|
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."
|
||||||
details = "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."
|
details = "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."
|
||||||
noop_headless = false
|
noop_headless = false
|
||||||
source = "investigation"
|
source = "investigation"
|
||||||
confidence = "high"
|
confidence = "high"
|
||||||
depends_on = []
|
depends_on = []
|
||||||
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."
|
||||||
|
|
||||||
[[opcode]]
|
[[opcode]]
|
||||||
op = 0xb4
|
op = 0xb4
|
||||||
@@ -5742,12 +5742,12 @@ abi_source = "kelebek+decode-validated"
|
|||||||
[opcode.semantics]
|
[opcode.semantics]
|
||||||
name = "present-gfx-object-range"
|
name = "present-gfx-object-range"
|
||||||
category = "draw"
|
category = "draw"
|
||||||
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."
|
||||||
noop_headless = false
|
noop_headless = false
|
||||||
source = "investigation"
|
source = "investigation"
|
||||||
confidence = "high"
|
confidence = "high"
|
||||||
depends_on = []
|
depends_on = []
|
||||||
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."
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
@@ -6709,28 +6709,29 @@ observed_types = ["imm"]
|
|||||||
|
|
||||||
[[opcode]]
|
[[opcode]]
|
||||||
op = 0x258
|
op = 0x258
|
||||||
label = "u00422FE0"
|
label = "set-surface-persistence-flags"
|
||||||
argc = 2
|
argc = 2
|
||||||
abi_source = "kelebek+decode-validated"
|
abi_source = "kelebek+decode-validated"
|
||||||
|
|
||||||
[opcode.semantics]
|
[opcode.semantics]
|
||||||
name = "decl?"
|
name = "set-surface-persistence-flags"
|
||||||
category = "marker"
|
category = "draw"
|
||||||
summary = "2 imm; runs in a chain right after script-entry 0x259, enumerating ids — prologue declaration/registration?"
|
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."
|
||||||
noop_headless = true
|
details = "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."
|
||||||
source = "harness"
|
noop_headless = false
|
||||||
confidence = "low"
|
source = "investigation"
|
||||||
|
confidence = "high"
|
||||||
depends_on = []
|
depends_on = []
|
||||||
evidence = ""
|
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."
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
role = ""
|
role = "surface slot"
|
||||||
observed_types = ["imm"]
|
observed_types = ["imm"]
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 2
|
i = 2
|
||||||
role = ""
|
role = "persistence flags"
|
||||||
observed_types = ["imm"]
|
observed_types = ["imm"]
|
||||||
|
|
||||||
[[opcode]]
|
[[opcode]]
|
||||||
|
|||||||
Reference in New Issue
Block a user