Fix retained background lifecycle
This commit is contained in:
@@ -403,9 +403,31 @@ correct `dst`/`w`/`h`. Ancillary per-object tables observed: `ctx+0x14d54` (obj
|
|||||||
Worker functions decoded + annotated in the Ghidra project (updated 2026-07-09): `gfx_object_erase`(`0x47d850`),
|
Worker functions decoded + annotated in the Ghidra project (updated 2026-07-09): `gfx_object_erase`(`0x47d850`),
|
||||||
`gfx_object_erase_range`(`0x47d8b0`), `gfx_object_query_source_slot`(`0x47f280`),
|
`gfx_object_erase_range`(`0x47d8b0`), `gfx_object_query_source_slot`(`0x47f280`),
|
||||||
`gfx_object_get_or_create`(`0x47ddb0`, inserts a zeroed default via
|
`gfx_object_get_or_create`(`0x47ddb0`, inserts a zeroed default via
|
||||||
`gfx_object_init_default`@`0x472810`), the setters `gfx_set_vec18/24/16c`(`0x47e960/e910/e800`), the getters
|
`gfx_object_init_default`@`0x472810`; critically, source slot `obj+4` defaults to **0**, while only an absent
|
||||||
|
map entry queries as `-1`), the setters `gfx_set_vec18/24/16c`(`0x47e960/e910/e800`), the getters
|
||||||
`gfx_get_vec18/24`(`0x47f360/f2e0`).
|
`gfx_get_vec18/24`(`0x47f360/f2e0`).
|
||||||
|
|
||||||
|
**Page-58 lifecycle correction (2026-07-11).** Locator `SC0000 P058` resolves to `wait@0x6545`; the loader at
|
||||||
|
`0x6470..0x6478` resolves resource `0x7a` to `EV050EA.AGF`. The port loaded and bound it correctly, but handle
|
||||||
|
`0xcb2a` inherited an earlier translation target `(-100,0)`, rotation `-90 degrees`, and alpha endpoint zero.
|
||||||
|
The preceding query-gated cleanup had skipped this created-but-unbound object because the port initialized
|
||||||
|
`SourceSlot=-1`. Native `gfx_object_init_default` zeroes `obj+4`, so op `0x215` returns slot 0, the cleanup
|
||||||
|
erases the object, and the later bind recreates identity state. Matching that default leaves EV050EA centered,
|
||||||
|
unrotated, and opaque in the synchronized page-58 compositor trace. The adjacent op `0x242(handle,0)` is not
|
||||||
|
a reset: `op_0x242_set_object_field2d0@0x4249d0` calls `gfx_object_set_field2d0@0x47f1a0`, which only writes
|
||||||
|
the supplied value at `obj+0x2d0`.
|
||||||
|
|
||||||
|
**Page-89 surface-lifetime correction (2026-07-11).** Locator `SC0000 P089` resolves to
|
||||||
|
`wait@0x89e9`; the loader at `0x8976..0x898a` resolves resource `0x6a` to `BG004D.AGF`. The background
|
||||||
|
was bound but composited at `(400,650)`. The remaining 550 pixels were intentional retained motion from
|
||||||
|
BG001A; the incorrect `(400,600)` component came from the host keeping slot 0's boot-time 800x600 dimensions
|
||||||
|
after op `0x1fa` released that surface. Native op `0x1fa` frees and nulls `ctx+0x52bd4[slot]`, so a subsequent
|
||||||
|
op `0x208` size query of the released slot yields no surface dimensions. Clearing the host slot resource and
|
||||||
|
dimensions on release makes the script's existing-object geometry path compute base `(0,-500)`; after the
|
||||||
|
retained `(0,550)` translation, BG004D lands at `(0,50)`. A synchronized run at the exact wait confirms the
|
||||||
|
background and character layers together. This path also established that op `0x1ff` directly replaces the
|
||||||
|
current translation matrix at `obj+0x16c`; it is now modeled rather than stored in an inert side vector.
|
||||||
|
|
||||||
#### The `0x21c–0x243` sprite transform / ANIMATION cluster (2026-07-10, partial implementation)
|
#### The `0x21c–0x243` sprite transform / ANIMATION cluster (2026-07-10, partial implementation)
|
||||||
|
|
||||||
The scene-completeness tracker (`tools/scene_opcode_coverage.py`) flagged a dense band of GAP ops in
|
The scene-completeness tracker (`tools/scene_opcode_coverage.py`) flagged a dense band of GAP ops in
|
||||||
@@ -918,8 +940,8 @@ negative operands in `SetColorAnimResolved`, samples packed ARGB before blend se
|
|||||||
the existing mode-specific path. Exact AE001H, mode-0 RGB-modulation, and mode-1 alpha regressions cover the
|
the existing mode-specific path. Exact AE001H, mode-0 RGB-modulation, and mode-1 alpha regressions cover the
|
||||||
contract; the white pulse is removed without suppressing the scripted channel.
|
contract; the white pulse is removed without suppressing the scripted channel.
|
||||||
|
|
||||||
**Resolved 2026-07-11:** `0x236` is the movie-to-retained-surface path described below. The unclassified
|
**Resolved 2026-07-11:** `0x236` is the movie-to-retained-surface path described below. `0x242` is now
|
||||||
`0x242/0x23d/0x20a/0x20e` tail (2-arg flags / inline) remains GAP and outside this slice.
|
classified as the retained-object `+0x2d0` field setter described above; `0x23d/0x20a/0x20e` remain GAP.
|
||||||
|
|
||||||
### Movie-to-surface opcode `0x236` (2026-07-11)
|
### Movie-to-surface opcode `0x236` (2026-07-11)
|
||||||
|
|
||||||
|
|||||||
@@ -166,10 +166,10 @@ SC0000 label_1235a reaches this when 0x1c7/0x1cc are zero. Native run-state bit
|
|||||||
- **grounding:** source=investigation, confidence=high
|
- **grounding:** source=investigation, confidence=high
|
||||||
- **evidence:** Ghidra /v2: handler gfx_op_0x1fd_set_vec_scaled@0x422650 fetches operands 2..4, divides each by the 100.0 constant, and calls gfx_object_set_scale_current@0x47e6b0. The worker gets/creates the object, marks obj+0x68, and calls matrix4_make_scale on obj+0x6c. SC0000 sets AE001D handles to 210/210/100 and 240/240/100; without this setter their 800x800 alpha circles remain below the viewport. Both functions annotated and /v2 saved 2026-07-11.
|
- **evidence:** Ghidra /v2: handler gfx_op_0x1fd_set_vec_scaled@0x422650 fetches operands 2..4, divides each by the 100.0 constant, and calls gfx_object_set_scale_current@0x47e6b0. The worker gets/creates the object, marks obj+0x68, and calls matrix4_make_scale on obj+0x6c. SC0000 sets AE001D handles to 210/210/100 and 240/240/100; without this setter their 800x800 alpha circles remain below the viewport. Both functions annotated and /v2 saved 2026-07-11.
|
||||||
|
|
||||||
### 0x1ff `set-gfx-geom3-c` (set-gfx-geom3-c, argc 4)
|
### 0x1ff `set-current-translation` (set-gfx-geom3-c, argc 4)
|
||||||
- **summary:** 0x1ff (handle)(a)(b)(c) — gfx cmd-type 9. Handler gfx_op_0x1ff_set_geom3 @0x4227b0: SETS a 3-vector (int→float a,b,c) on object `handle` via native worker FUN_0047e800 (sibling of 0x217/0x219, a distinct per-object vector). See docs/engine-re.md gfx op-contract table.
|
- **summary:** 0x1ff (handle)(x)(y)(z) — immediately replace the retained object's current translation matrix at obj+0x16c. This is the direct-current companion to 0x220's delayed target at obj+0x1ac.
|
||||||
- **grounding:** source=investigation, confidence=high
|
- **grounding:** source=investigation, confidence=high
|
||||||
- **evidence:** Ghidra handler 0x4227b0 (dispatch ctx[0x26c93+0x1ff]); FUN_0047e800(op1,(float)op2,(float)op3,(float)op4).
|
- **evidence:** Ghidra /v2: handler gfx_op_0x1ff_set_geom3@0x4227b0 converts operands 2..4 to float and calls gfx_object_set_translation_current@0x47e800. The worker gets/creates the object, marks obj+0x168, and calls matrix4_make_translation on obj+0x16c. SC0000 uses (0,0,0) before animating BG001A. Annotated and saved 2026-07-11.
|
||||||
|
|
||||||
### 0x202 `gfx-blit-color` (gfx-blit-color, argc 5)
|
### 0x202 `gfx-blit-color` (gfx-blit-color, argc 5)
|
||||||
- **summary:** 0x202 (handle)(delay_ms)(duration_ms)(alpha)(color) — arm the one-shot packed-ARGB channel. Worker gfx_op_0x202_worker_set_color_anim @0x47ea00 resets shared start obj+0x34, writes delay +0x38, duration +0x4c, and target +0x64. gfx_object_apply_transform_channels @0x472f00 linearly interpolates each byte from current +0x60 on frame clock ctx+0xb550, commits the target, clears timing, writes target -1, and clears the one-shot active bit when all sibling channels finish. Negative alpha/RGB independently preserve current bytes. Implemented in GfxState with synchronized current/target timeline evidence; draw-string 0x204/0x7a remains separate.
|
- **summary:** 0x202 (handle)(delay_ms)(duration_ms)(alpha)(color) — arm the one-shot packed-ARGB channel. Worker gfx_op_0x202_worker_set_color_anim @0x47ea00 resets shared start obj+0x34, writes delay +0x38, duration +0x4c, and target +0x64. gfx_object_apply_transform_channels @0x472f00 linearly interpolates each byte from current +0x60 on frame clock ctx+0xb550, commits the target, clears timing, writes target -1, and clears the one-shot active bit when all sibling channels finish. Negative alpha/RGB independently preserve current bytes. Implemented in GfxState with synchronized current/target timeline evidence; draw-string 0x204/0x7a remains separate.
|
||||||
@@ -206,9 +206,9 @@ Native handler gfx_op_0x20c_present_frame -> gfx_render_frame @0x4820b0. This is
|
|||||||
- **evidence:** Ghidra handler 0x423110; writes obj+0x68/+0x6c from operands 2/3, obj from ctx+0x14d54[operand1*4].
|
- **evidence:** Ghidra handler 0x423110; writes obj+0x68/+0x6c from operands 2/3, obj from ctx+0x14d54[operand1*4].
|
||||||
|
|
||||||
### 0x215 `query-gfx-object?` (query-gfx-object?, argc 2)
|
### 0x215 `query-gfx-object?` (query-gfx-object?, argc 2)
|
||||||
- **summary:** 0x215 (out_slot)(handle) — query the retained gfx-object map. Handler gfx_op_0x215_query_source_slot@0x42a0b0 calls gfx_object_query_source_slot@0x47f280 with owner ctx+0x46614. The worker searches owner+0x408, the same map populated by geometry/draw workers, and returns obj+4: the live source-surface slot written by draw-texture, or -1 if absent. A geometry-only object remains unbound and returns -1. SC0000 uses a successful result for existing-object geometry and query-guarded teardown: op 0x1f7 erases the object group and op 0x1fa releases this slot. VM fix 2026-07-09 restores AE001H magic-circle cleanup.
|
- **summary:** 0x215 (out_slot)(handle) — query the retained gfx-object map. Handler gfx_op_0x215_query_source_slot@0x42a0b0 calls gfx_object_query_source_slot@0x47f280 with owner ctx+0x46614. The worker searches owner+0x408, the same map populated by geometry/draw workers, and returns obj+4: the live source-surface slot written by draw-texture, or -1 if absent. gfx_object_init_default zeroes obj+4, so a geometry/animation-created but draw-unbound object returns slot 0, not -1. SC0000 uses a successful result for query-guarded teardown: op 0x1f7 erases the object group and op 0x1fa releases this slot. Matching the zero default prevents stale transforms from surviving into the EV050EA CG at SC0000 page 58.
|
||||||
- **grounding:** source=investigation, confidence=high
|
- **grounding:** source=investigation, confidence=high
|
||||||
- **evidence:** Ghidra asm: handler 0x42a0b0 passes ECX=ctx+0x46614 to 0x47f280. That worker searches ECX+0x408 and returns resolved object+4. gfx_object_bind_draw@0x47e870, called with the same owner, get-or-creates in owner+0x408 and writes source slot to object+4. SC0000 post-effect cleanup 0x3321 queries G[0x62457], sign-tests, then executes 0x1f7(handle,10) + 0x1fa(returned_slot); AE001H res 0x37 was bound to that handle.
|
- **evidence:** Ghidra: handler 0x42a0b0 passes ECX=ctx+0x46614 to 0x47f280, which searches ECX+0x408 and returns object+4. gfx_object_bind_draw@0x47e870 writes the bound slot there; gfx_object_init_default@0x472810 explicitly writes zero to dword index 1. SC0000 page-58 trace: the old -1 default skipped cleanup of transform-created handle 0xcb2a, then EV050EA inherited translation (-100,0), rotation -90, and alpha 0; the native zero default makes the guard succeed and the reused object is identity/opaque.
|
||||||
|
|
||||||
### 0x216 `query-gfx-field?` (query-gfx-field?, argc 2)
|
### 0x216 `query-gfx-field?` (query-gfx-field?, argc 2)
|
||||||
- **summary:** 0x216 (out)(idx) — gfx cmd-type 5. Handler gfx_op_0x216_query_table46d14 @0x42a0f0: out = *(ctx+0x46d14 + idx*0x14). A per-object field query over a stride-0x14 table. See docs/engine-re.md gfx op-contract table.
|
- **summary:** 0x216 (out)(idx) — gfx cmd-type 5. Handler gfx_op_0x216_query_table46d14 @0x42a0f0: out = *(ctx+0x46d14 + idx*0x14). A per-object field query over a stride-0x14 table. See docs/engine-re.md gfx op-contract table.
|
||||||
@@ -316,6 +316,11 @@ The handler requires an existing destination texture, allocates/reuses a 0x478-b
|
|||||||
- **summary:** 0x23f query-object (out)(handle): return object status (FUN_0042a520; -1 if none). C# VM: 0 if the object exists else -1. See docs/engine-re.md §SC0000 anim cluster.
|
- **summary:** 0x23f query-object (out)(handle): return object status (FUN_0042a520; -1 if none). C# VM: 0 if the object exists else -1. See docs/engine-re.md §SC0000 anim cluster.
|
||||||
- **grounding:** source=kelebek, confidence=low
|
- **grounding:** source=kelebek, confidence=low
|
||||||
|
|
||||||
|
### 0x242 `set-gfx-field2d0` (set-gfx-field2d0, argc 2)
|
||||||
|
- **summary:** 0x242 (handle)(value) — command type 5. Get-or-create the retained gfx object and write value to obj+0x2d0. SC0000's common CG loader passes zero after draw binding. This field does not reset transform or color channels; its downstream purpose remains unknown.
|
||||||
|
- **grounding:** source=investigation, confidence=high
|
||||||
|
- **evidence:** Ghidra /v2: op_0x242_set_object_field2d0@0x4249d0 fetches operands 2 and 1 and calls gfx_object_set_field2d0@0x47f1a0; the worker calls gfx_object_get_or_create then stores operand 2 at returned object+0x2d0.
|
||||||
|
|
||||||
### 0x243 `reset-anim-clock` (reset-anim-clock, argc 0)
|
### 0x243 `reset-anim-clock` (reset-anim-clock, argc 0)
|
||||||
- **summary:** Reset the native global animation-service elapsed and duration fields to zero when service flag bit 1 is clear.
|
- **summary:** Reset the native global animation-service elapsed and duration fields to zero when service flag bit 1 is clear.
|
||||||
- **grounding:** source=investigation, confidence=high
|
- **grounding:** source=investigation, confidence=high
|
||||||
@@ -1077,10 +1082,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
|
|||||||
- **summary:** —
|
- **summary:** —
|
||||||
- **grounding:** source=kelebek, confidence=low
|
- **grounding:** source=kelebek, confidence=low
|
||||||
|
|
||||||
### 0x242 `u00422D60` (u00422D60, argc 2)
|
|
||||||
- **summary:** —
|
|
||||||
- **grounding:** source=kelebek, confidence=low
|
|
||||||
|
|
||||||
### 0x248 `u00422E80` (u00422E80, argc 1)
|
### 0x248 `u00422E80` (u00422E80, argc 1)
|
||||||
- **summary:** —
|
- **summary:** —
|
||||||
- **grounding:** source=kelebek, confidence=low
|
- **grounding:** source=kelebek, confidence=low
|
||||||
|
|||||||
@@ -1445,3 +1445,27 @@ around the wait. Page number is deliberately only the friendly coordinate becaus
|
|||||||
shift ordinals; the script/offset remains authoritative. A live SC0000 run verified page 1 as
|
shift ordinals; the script/offset remains authoritative. A live SC0000 run verified page 1 as
|
||||||
`show-text@0x834`, string `0x14963`, and `wait-for-input@0x83c`. Validation: focused Python tests,
|
`show-text@0x834`, string `0x14963`, and `wait-for-input@0x83c`. Validation: focused Python tests,
|
||||||
engine **152/152**, zero-warning Godot build, threaded `SELFTEST OK`, and the live lookup all pass.
|
engine **152/152**, zero-warning Godot build, threaded `SELFTEST OK`, and the live lookup all pass.
|
||||||
|
|
||||||
|
### SC0000 page-58 EV050EA background restored (2026-07-11)
|
||||||
|
|
||||||
|
The first user-supplied locator, `SC0000 P058 / wait@0x6545 / text@0x653d`, led directly to the missing
|
||||||
|
full-screen CG. Resource `0x7a` resolves to `EV050EA.AGF` and was loaded into slot 5, but retained handle
|
||||||
|
`0xcb2a` inherited an older `(-100,0)` translation, `-90 degree` rotation, and alpha-zero endpoint. Native
|
||||||
|
RE showed why cleanup diverged: `gfx_object_init_default` zeroes source slot `obj+4`, so op `0x215` returns
|
||||||
|
0 for a created-but-unbound object; only a missing handle returns -1. The port's `SourceSlot=-1` default made
|
||||||
|
the sign-tested cleanup skip that object. The model now uses native slot 0, allowing erase/recreate to restore
|
||||||
|
identity state. A lifecycle regression reproduces the transform-create, query-cleanup, and CG-reuse sequence.
|
||||||
|
Validation: engine **153/153**, zero-warning Godot build, and a synchronized real-scene compositor trace at
|
||||||
|
page 58 showing `EV050EA.AGF` at `(0,0)`, scale 1, translation 0, rotation 0, and opacity 1.
|
||||||
|
|
||||||
|
### SC0000 page-89 BG004D background restored (2026-07-11)
|
||||||
|
|
||||||
|
Locator `SC0000 P089 / wait@0x89e9 / text@0x89e1` identified `BG004D.AGF` as loaded but offscreen at
|
||||||
|
`(400,650)`. The prior BG001A animation intentionally leaves translation y=550; the erroneous extra
|
||||||
|
`(400,600)` came from `GodotAdvHost` retaining slot 0's seeded 800x600 dimensions after op `0x1fa` released
|
||||||
|
the surface. Native release frees and nulls the slot. Static releases now remove the host resource and size
|
||||||
|
state (while the existing in-flight movie retention remains unchanged), and the CLI graphics trace mirrors
|
||||||
|
that lifecycle. The loader consequently builds base `(0,-500)`, so the retained translation places BG004D
|
||||||
|
at `(0,50)`. Op `0x1ff` was also completed as the native immediate current-translation setter at obj+0x16c.
|
||||||
|
Validation: engine **155/155**, zero-warning Godot and CLI builds, and a full synchronized run reaching the
|
||||||
|
exact page-89 wait with `BG004D.AGF` drawn at `(0,50)` alongside the character sprite.
|
||||||
|
|||||||
@@ -373,8 +373,7 @@ sealed class GfxTraceHost : IHost
|
|||||||
private readonly ResourceMap _res;
|
private readonly ResourceMap _res;
|
||||||
private readonly string _scene;
|
private readonly string _scene;
|
||||||
private readonly Dictionary<int, string?> _slotAsset = new(); // slot -> resolved AGF name (or null)
|
private readonly Dictionary<int, string?> _slotAsset = new(); // slot -> resolved AGF name (or null)
|
||||||
// slot -> dims. Slot 0 is the primary/screen surface (800x600), normally created at engine boot which
|
// slot -> dimensions of the currently allocated surface. Slot 0 starts as the engine's primary surface.
|
||||||
// the single-scene harness skips; seed it so the first CG's anchor math stays correct (not 0x0).
|
|
||||||
private readonly Dictionary<int, (int W, int H)> _slotDims = new() { { 0, (800, 600) } };
|
private readonly Dictionary<int, (int W, int H)> _slotDims = new() { { 0, (800, 600) } };
|
||||||
public List<string> Events { get; } = new();
|
public List<string> Events { get; } = new();
|
||||||
public GfxTraceHost(ResourceMap res, string scene) { _res = res; _scene = scene; }
|
public GfxTraceHost(ResourceMap res, string scene) { _res = res; _scene = scene; }
|
||||||
@@ -408,6 +407,12 @@ sealed class GfxTraceHost : IHost
|
|||||||
_slotDims[slot] = (width, height);
|
_slotDims[slot] = (width, height);
|
||||||
Events.Add($"create-texture slot={slot} {width}x{height}");
|
Events.Add($"create-texture slot={slot} {width}x{height}");
|
||||||
}
|
}
|
||||||
|
public void ReleaseSurface(int slot)
|
||||||
|
{
|
||||||
|
_slotAsset.Remove(slot);
|
||||||
|
_slotDims.Remove(slot);
|
||||||
|
Events.Add($"release-surface slot={slot}");
|
||||||
|
}
|
||||||
public void ShowText(int offset, string text) { }
|
public void ShowText(int offset, string text) { }
|
||||||
public void WaitForInput() { }
|
public void WaitForInput() { }
|
||||||
public void Sleep(long duration) { }
|
public void Sleep(long duration) { }
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public class AnimChannelTests
|
|||||||
{
|
{
|
||||||
(0x55, new[]{G(1), I(0xcf3a)}), (0x55, new[]{G(2), I(0)}),
|
(0x55, new[]{G(1), I(0xcf3a)}), (0x55, new[]{G(2), I(0)}),
|
||||||
(0x55, new[]{G(3), I(360)}), (0x55, new[]{G(4), I(20)}), (0x55, new[]{G(5), I(0)}),
|
(0x55, new[]{G(3), I(360)}), (0x55, new[]{G(4), I(20)}), (0x55, new[]{G(5), I(0)}),
|
||||||
|
(0x1fb, new[]{G(1), I(0), I(0), I(0), I(1), I(1), I(0), I(0)}),
|
||||||
(0x22f, new[]{G(1), G(2), G(3), G(4), G(5)}),
|
(0x22f, new[]{G(1), G(2), G(3), G(4), G(5)}),
|
||||||
(0x55, new[]{G(6), I(300)}), (0x55, new[]{G(7), I(40)}),
|
(0x55, new[]{G(6), I(300)}), (0x55, new[]{G(7), I(40)}),
|
||||||
(0x55, new[]{G(8), I(20)}), (0x51, new[]{G(8), I(0), G(8)}),
|
(0x55, new[]{G(8), I(20)}), (0x51, new[]{G(8), I(0), G(8)}),
|
||||||
@@ -62,6 +63,7 @@ public class AnimChannelTests
|
|||||||
(0x55, new[]{G(4), I(360)}), (0x55, new[]{G(5), I(20)}), (0x55, new[]{G(6), I(0)}),
|
(0x55, new[]{G(4), I(360)}), (0x55, new[]{G(5), I(20)}), (0x55, new[]{G(6), I(0)}),
|
||||||
(0x55, new[]{G(40), I(20)}), (0x51, new[]{G(40), I(0), G(40)}),
|
(0x55, new[]{G(40), I(20)}), (0x51, new[]{G(40), I(0), G(40)}),
|
||||||
(0x55, new[]{G(41), I(60)}), (0x51, new[]{G(41), I(0), G(41)}),
|
(0x55, new[]{G(41), I(60)}), (0x51, new[]{G(41), I(0), G(41)}),
|
||||||
|
(0x1fb, new[]{G(1), I(0), I(0), I(0), I(1), I(1), I(0), I(0)}),
|
||||||
(0x22f, new[]{G(1), G(2), G(4), G(5), G(6)}),
|
(0x22f, new[]{G(1), G(2), G(4), G(5), G(6)}),
|
||||||
|
|
||||||
(0x228, new[]{G(10), G(1), G(11), G(12), G(13)}),
|
(0x228, new[]{G(10), G(1), G(11), G(12), G(13)}),
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public class GfxAnimationTests
|
|||||||
public void ScaleAndTranslationChannels_AreIndependent()
|
public void ScaleAndTranslationChannels_AreIndependent()
|
||||||
{
|
{
|
||||||
var g = new GfxState();
|
var g = new GfxState();
|
||||||
|
g.BindDraw(0x1000, 1, 0, 0, 1, 1, 0, 0);
|
||||||
g.SetScaleChannel(0x1000, delayMs: 7, durationMs: 900, percent: (200, 50, 100));
|
g.SetScaleChannel(0x1000, delayMs: 7, durationMs: 900, percent: (200, 50, 100));
|
||||||
g.SetTranslationChannel(0x1000, delayMs: 9, durationMs: 1200, target: (80, -25, 6));
|
g.SetTranslationChannel(0x1000, delayMs: 9, durationMs: 1200, target: (80, -25, 6));
|
||||||
var o = g.TryGet(0x1000)!;
|
var o = g.TryGet(0x1000)!;
|
||||||
@@ -44,6 +45,7 @@ public class GfxAnimationTests
|
|||||||
public void RotationCycle_DoesNotOverwriteMatrixChannels()
|
public void RotationCycle_DoesNotOverwriteMatrixChannels()
|
||||||
{
|
{
|
||||||
var g = new GfxState();
|
var g = new GfxState();
|
||||||
|
g.BindDraw(0x1000, 1, 0, 0, 1, 1, 0, 0);
|
||||||
g.SetScaleChannel(0x1000, 0, 100, (150, 150, 100));
|
g.SetScaleChannel(0x1000, 0, 100, (150, 150, 100));
|
||||||
g.SetTranslationChannel(0x1000, 0, 100, (10, 20, 0));
|
g.SetTranslationChannel(0x1000, 0, 100, (10, 20, 0));
|
||||||
g.SetRotationCycle(0x1000, periodMs: 30, axis: (0, 0, 5));
|
g.SetRotationCycle(0x1000, periodMs: 30, axis: (0, 0, 5));
|
||||||
@@ -55,6 +57,30 @@ public class GfxAnimationTests
|
|||||||
Assert.True(o.RotationEnabled);
|
Assert.True(o.RotationEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreBindMatrixChannels_CreateNeutralPlaceholderWithoutLatentMotion()
|
||||||
|
{
|
||||||
|
// SC0000 calls op 0x220 on background handle 0xcb20 before BG001A is bound. Native creates the
|
||||||
|
// object (so op 0x215 returns slot 0) but ignores all three matrix setters while visible bit 0 is clear.
|
||||||
|
var g = new GfxState();
|
||||||
|
g.SetScaleChannel(0xcb20, 0, 500, (200, 200, 100));
|
||||||
|
g.SetRotationChannel(0xcb20, 0, 500, (0, 0, 1), 90);
|
||||||
|
g.SetTranslationChannel(0xcb20, 0, 500, (0, 600, 0));
|
||||||
|
|
||||||
|
var placeholder = g.TryGet(0xcb20)!;
|
||||||
|
Assert.Equal(0, g.QuerySlot(0xcb20));
|
||||||
|
Assert.False(placeholder.Visible);
|
||||||
|
Assert.False(placeholder.ScaleEnabled);
|
||||||
|
Assert.False(placeholder.RotationChannelEnabled);
|
||||||
|
Assert.False(placeholder.TranslationEnabled);
|
||||||
|
Assert.Equal((0.0, 0.0, 0.0), placeholder.TranslationTarget);
|
||||||
|
|
||||||
|
g.BindDraw(0xcb20, 4, 0, 0, 800, 500, 0, -500);
|
||||||
|
g.SetTranslationChannel(0xcb20, 0, 500, (0, 600, 0));
|
||||||
|
Assert.True(placeholder.TranslationEnabled);
|
||||||
|
Assert.Equal((0.0, 600.0, 0.0), placeholder.TranslationTarget);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ActiveVisualPresentation_ExcludesStaticWaits_ButIncludesAmbientChannels()
|
public void ActiveVisualPresentation_ExcludesStaticWaits_ButIncludesAmbientChannels()
|
||||||
{
|
{
|
||||||
@@ -159,6 +185,7 @@ public class GfxAnimationTests
|
|||||||
var scene = ScriptAssembler.Assemble(t, "ANIM", new List<(int, Operand[])>
|
var scene = ScriptAssembler.Assemble(t, "ANIM", new List<(int, Operand[])>
|
||||||
{
|
{
|
||||||
MovGI(1, 0x1000), MovGI(2, 7), MovGI(3, 9), MovGI(4, 800), MovGI(5, 500), MovGI(6, 0),
|
MovGI(1, 0x1000), MovGI(2, 7), MovGI(3, 9), MovGI(4, 800), MovGI(5, 500), MovGI(6, 0),
|
||||||
|
(0x1fb, new[] { G(1), I(0), I(0), I(0), I(1), I(1), I(0), I(0) }),
|
||||||
(0x220, new[] { G(1), G(2), G(3), G(4), G(5), G(6) }),
|
(0x220, new[] { G(1), G(2), G(3), G(4), G(5), G(6) }),
|
||||||
MovGI(4, 200), MovGI(5, 50), MovGI(6, 100),
|
MovGI(4, 200), MovGI(5, 50), MovGI(6, 100),
|
||||||
(0x21e, new[] { G(1), G(2), G(3), G(4), G(5), G(6) }),
|
(0x21e, new[] { G(1), G(2), G(3), G(4), G(5), G(6) }),
|
||||||
@@ -215,6 +242,23 @@ public class GfxAnimationTests
|
|||||||
Assert.Equal(1, vm.Gfx.AnimClockGeneration);
|
Assert.Equal(1, vm.Gfx.AnimClockGeneration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CurrentTranslationSetter_ReplacesTheLiveMatrixImmediately()
|
||||||
|
{
|
||||||
|
var t = T();
|
||||||
|
var scene = ScriptAssembler.Assemble(t, "CURRENTTRANSLATION", new List<(int, Operand[])>
|
||||||
|
{
|
||||||
|
MovGI(1, 0xcb20), MovGI(2, 12), MovGI(3, 34), MovGI(4, 5),
|
||||||
|
(0x1ff, new[] { G(1), G(2), G(3), G(4) }),
|
||||||
|
Exit(),
|
||||||
|
}, System.Array.Empty<string>());
|
||||||
|
var vm = new VirtualMachine(scene, t, new RecordingHost());
|
||||||
|
vm.Run();
|
||||||
|
var o = vm.Gfx.TryGet(0xcb20)!;
|
||||||
|
Assert.Equal((12.0, 34.0, 5.0), o.TranslationCurrent);
|
||||||
|
Assert.Equal((12L, 34L, 5L), o.V16c);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResetAnimClock_DispatchClearsOnlyGlobalServiceClock()
|
public void ResetAnimClock_DispatchClearsOnlyGlobalServiceClock()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -39,10 +39,10 @@ public class GfxCommandBufferTests
|
|||||||
private static (int, Operand[]) Register(int handle) => (0x1a2, new[] { G(handle) });
|
private static (int, Operand[]) Register(int handle) => (0x1a2, new[] { G(handle) });
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void QueryReturnsMinusOneUntilDrawBound_ThenSourceSlot()
|
public void QueryReturnsZeroForCreatedUnboundObject_ThenBoundSourceSlot()
|
||||||
{
|
{
|
||||||
// Op 0x215 returns obj+4 from the retained gfx object. Geometry creates the object but leaves it unbound;
|
// Op 0x215 returns obj+4 from the retained gfx object. Native initialization leaves an unbound object's
|
||||||
// op 0x1a2's descriptor registry is unrelated. Draw-texture binds the source slot returned by the query.
|
// field at zero; op 0x1a2 is unrelated. Draw-texture replaces it with the bound source slot.
|
||||||
var t = T();
|
var t = T();
|
||||||
var scene = ScriptAssembler.Assemble(t, "GFX", new List<(int, Operand[])>
|
var scene = ScriptAssembler.Assemble(t, "GFX", new List<(int, Operand[])>
|
||||||
{
|
{
|
||||||
@@ -55,7 +55,7 @@ public class GfxCommandBufferTests
|
|||||||
}, System.Array.Empty<string>());
|
}, System.Array.Empty<string>());
|
||||||
var vm = new VirtualMachine(scene, t, new RecordingHost());
|
var vm = new VirtualMachine(scene, t, new RecordingHost());
|
||||||
vm.Run();
|
vm.Run();
|
||||||
Assert.Equal(-1, vm.Globals[10]);
|
Assert.Equal(0, vm.Globals[10]);
|
||||||
Assert.Equal(6, vm.Globals[11]);
|
Assert.Equal(6, vm.Globals[11]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,20 +6,41 @@ public class GfxStateTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void QueryReturnsBoundSourceSlot_NotOperandRegistryValue()
|
public void QueryReturnsBoundSourceSlot_NotOperandRegistryValue()
|
||||||
{
|
{
|
||||||
// Native op 0x215 queries the retained-object map and returns obj+4, the source slot set by draw-texture.
|
// Native op 0x215 queries the retained-object map and returns obj+4. The default initializer zeroes
|
||||||
// Geometry alone creates an object but leaves obj+4 at -1. Op 0x1a2 is a separate descriptor registry.
|
// that field, while draw-texture replaces it with the bound slot. Op 0x1a2 is a separate registry.
|
||||||
var g = new GfxState();
|
var g = new GfxState();
|
||||||
g.GetOrCreate(0xcb2a).V18 = (400, 600, 0);
|
g.GetOrCreate(0xcb2a).V18 = (400, 600, 0);
|
||||||
Assert.Equal(-1, g.QuerySlot(0xcb2a));
|
Assert.Equal(0, g.QuerySlot(0xcb2a));
|
||||||
|
|
||||||
g.Register(0xcb2a);
|
g.Register(0xcb2a);
|
||||||
Assert.Equal(-1, g.QuerySlot(0xcb2a));
|
Assert.Equal(0, g.QuerySlot(0xcb2a));
|
||||||
|
|
||||||
g.BindDraw(0xcb2a, 6, 0, 0, 200, 200, 10, 20);
|
g.BindDraw(0xcb2a, 6, 0, 0, 200, 200, 10, 20);
|
||||||
Assert.Equal(6, g.QuerySlot(0xcb2a));
|
Assert.Equal(6, g.QuerySlot(0xcb2a));
|
||||||
Assert.Equal(-1, g.QuerySlot(0x9999));
|
Assert.Equal(-1, g.QuerySlot(0x9999));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UnboundMatrixSetterCreatesQueryableObjectForCleanupBeforeReuse()
|
||||||
|
{
|
||||||
|
// Native matrix setters create a neutral placeholder even when they ignore the channel because it is
|
||||||
|
// not draw-bound. obj+4 still defaults to slot 0, so the later query >= 0 cleanup erases the object.
|
||||||
|
var g = new GfxState();
|
||||||
|
g.SetTranslationChannel(0xcb2a, 0, 150, (-100, 0, 0));
|
||||||
|
g.SetRotationChannel(0xcb2a, 0, 150, (0, 0, 1), -90);
|
||||||
|
Assert.Equal(0, g.QuerySlot(0xcb2a));
|
||||||
|
|
||||||
|
if (g.QuerySlot(0xcb2a) >= 0)
|
||||||
|
g.EraseRange(0xcb2a, 10);
|
||||||
|
|
||||||
|
g.SetSurface(5, 0x7a, 0);
|
||||||
|
g.BindDraw(0xcb2a, 5, 0, 0, 800, 600, 0, 0);
|
||||||
|
var reused = Assert.Single(g.SnapshotVisibleObjects(1000));
|
||||||
|
Assert.Equal((0.0, 0.0, 0.0),
|
||||||
|
(reused.Transform.TranslateX, reused.Transform.TranslateY, reused.Transform.RotationAngleDegrees));
|
||||||
|
Assert.Equal(255, reused.Alpha);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void VectorsRoundTripPerObject()
|
public void VectorsRoundTripPerObject()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -81,7 +81,9 @@ public sealed class GfxState
|
|||||||
public long ColorPeriod, ColorStart = -1, ColorTarget;
|
public long ColorPeriod, ColorStart = -1, ColorTarget;
|
||||||
public bool ColorAnim;
|
public bool ColorAnim;
|
||||||
// draw-texture bind (gfx_object_bind_draw): the surface to draw + its source rect + the visible flag.
|
// draw-texture bind (gfx_object_bind_draw): the surface to draw + its source rect + the visible flag.
|
||||||
public int SourceSlot = -1;
|
// Native gfx_object_init_default zeroes obj+4. Querying an object created by a geometry/animation
|
||||||
|
// setter therefore returns slot 0 even before draw-texture binds it; only an absent object returns -1.
|
||||||
|
public int SourceSlot;
|
||||||
public (int X, int Y, int W, int H) SrcRect;
|
public (int X, int Y, int W, int H) SrcRect;
|
||||||
public bool Visible;
|
public bool Visible;
|
||||||
|
|
||||||
@@ -441,12 +443,26 @@ public sealed class GfxState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Op 0x1ff: immediately replace the object's current translation matrix at obj+0x16c.</summary>
|
||||||
|
public void SetCurrentTranslation(long handle, (long X, long Y, long Z) translation)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
var o = GetOrCreate(handle);
|
||||||
|
o.V16c = translation;
|
||||||
|
o.TranslationCurrent = translation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Op 0x21e: normalized scale target (100 = identity), with independent delay/duration.</summary>
|
/// <summary>Op 0x21e: normalized scale target (100 = identity), with independent delay/duration.</summary>
|
||||||
public void SetScaleChannel(long handle, long delayMs, long durationMs, (long X, long Y, long Z) percent)
|
public void SetScaleChannel(long handle, long delayMs, long durationMs, (long X, long Y, long Z) percent)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
var o = GetOrCreate(handle);
|
var o = GetOrCreate(handle);
|
||||||
|
// Native setters get-or-create first, then require object flag bit 0 (draw-bound/visible).
|
||||||
|
// Pre-bind calls leave a neutral, queryable placeholder and do not queue latent motion.
|
||||||
|
if (!o.Visible) return;
|
||||||
o.ScaleDelayMs = delayMs; o.ScaleDurationMs = durationMs;
|
o.ScaleDelayMs = delayMs; o.ScaleDurationMs = durationMs;
|
||||||
o.ScaleTarget = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0);
|
o.ScaleTarget = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0);
|
||||||
o.ScaleEnabled = durationMs > 0; o.OneShotStartMs = -1;
|
o.ScaleEnabled = durationMs > 0; o.OneShotStartMs = -1;
|
||||||
@@ -459,6 +475,7 @@ public sealed class GfxState
|
|||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
var o = GetOrCreate(handle);
|
var o = GetOrCreate(handle);
|
||||||
|
if (!o.Visible) return;
|
||||||
o.TranslationDelayMs = delayMs; o.TranslationDurationMs = durationMs;
|
o.TranslationDelayMs = delayMs; o.TranslationDurationMs = durationMs;
|
||||||
o.TranslationTarget = target;
|
o.TranslationTarget = target;
|
||||||
o.TranslationEnabled = durationMs > 0; o.OneShotStartMs = -1;
|
o.TranslationEnabled = durationMs > 0; o.OneShotStartMs = -1;
|
||||||
@@ -488,6 +505,7 @@ public sealed class GfxState
|
|||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
var o = GetOrCreate(handle);
|
var o = GetOrCreate(handle);
|
||||||
|
if (!o.Visible) return;
|
||||||
o.RotationDelayMs = delayMs; o.RotationDurationMs = durationMs;
|
o.RotationDelayMs = delayMs; o.RotationDurationMs = durationMs;
|
||||||
o.RotationTarget = (axis.X, axis.Y, axis.Z, angleDegrees);
|
o.RotationTarget = (axis.X, axis.Y, axis.Z, angleDegrees);
|
||||||
o.RotationChannelEnabled = durationMs > 0; o.OneShotStartMs = -1;
|
o.RotationChannelEnabled = durationMs > 0; o.OneShotStartMs = -1;
|
||||||
|
|||||||
@@ -411,8 +411,8 @@ public sealed class VirtualMachine
|
|||||||
}
|
}
|
||||||
case "u00422930": // 0x23f query-object: (out)(handle) <- 0 if the object exists, else -1
|
case "u00422930": // 0x23f query-object: (out)(handle) <- 0 if the object exists, else -1
|
||||||
Write(a[0], Gfx.TryGet(Read(a[1])) != null ? 0 : -1); return pc + 1;
|
Write(a[0], Gfx.TryGet(Read(a[1])) != null ? 0 : -1); return pc + 1;
|
||||||
case "set-gfx-geom3-c": // 0x1ff (handle)(a)(b)(c) -> V16c
|
case "set-gfx-geom3-c": // 0x1ff: set current translation matrix
|
||||||
Gfx.GetOrCreate(Read(a[0])).V16c = (Read(a[1]), Read(a[2]), Read(a[3])); return pc + 1;
|
Gfx.SetCurrentTranslation(Read(a[0]), (Read(a[1]), Read(a[2]), Read(a[3]))); return pc + 1;
|
||||||
case "u00420620": // upstream ABI label
|
case "u00420620": // upstream ABI label
|
||||||
case "gfx-set-scale-current": // 0x1fd (handle)(sx%)(sy%)(sz%) -> current scale matrix
|
case "gfx-set-scale-current": // 0x1fd (handle)(sx%)(sy%)(sz%) -> current scale matrix
|
||||||
Gfx.SetCurrentScale(Read(a[0]), (Read(a[1]), Read(a[2]), Read(a[3]))); return pc + 1;
|
Gfx.SetCurrentScale(Read(a[0]), (Read(a[1]), Read(a[2]), Read(a[3]))); return pc + 1;
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ public sealed class GodotAdvHost : IHost
|
|||||||
private readonly Dictionary<int, long> _movieBySurface = new();
|
private readonly Dictionary<int, long> _movieBySurface = new();
|
||||||
private readonly HashSet<long> _completedMovies = new();
|
private readonly HashSet<long> _completedMovies = new();
|
||||||
private readonly string?[] _sfxNames = new string?[10]; // SC0000 native channel subset
|
private readonly string?[] _sfxNames = new string?[10]; // SC0000 native channel subset
|
||||||
// slot -> dims. Slot 0 is the primary/screen surface (800x600), normally created at engine boot which
|
// slot -> dimensions of the currently allocated surface. Slot 0 begins as the engine's 800x600
|
||||||
// the single-scene harness skips; seed it so the first CG's anchor math stays correct (not 0x0).
|
// primary surface, but op 0x1fa releases it like any other slot; subsequent size queries must return 0x0.
|
||||||
private readonly Dictionary<int, (int W, int H)> _slotDims = new() { { 0, (800, 600) } };
|
private readonly Dictionary<int, (int W, int H)> _slotDims = new() { { 0, (800, 600) } };
|
||||||
private readonly SemaphoreSlim _gate = new(0, 1);
|
private readonly SemaphoreSlim _gate = new(0, 1);
|
||||||
private readonly Age.Engine.Hosting.FrameClock _clock;
|
private readonly Age.Engine.Hosting.FrameClock _clock;
|
||||||
@@ -359,7 +359,16 @@ public sealed class GodotAdvHost : IHost
|
|||||||
long resourceId;
|
long resourceId;
|
||||||
lock (_imageLock)
|
lock (_imageLock)
|
||||||
{
|
{
|
||||||
if (!_movieBySurface.Remove(slot, out resourceId)) return;
|
if (!_movieBySurface.Remove(slot, out resourceId))
|
||||||
|
{
|
||||||
|
lock (_textLock)
|
||||||
|
{
|
||||||
|
_surfaceText.Remove(slot);
|
||||||
|
_surfaceResources.Remove(slot);
|
||||||
|
}
|
||||||
|
_slotDims.Remove(slot);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!_completedMovies.Contains(resourceId))
|
if (!_completedMovies.Contains(resourceId))
|
||||||
{
|
{
|
||||||
_movieBySurface[slot] = resourceId;
|
_movieBySurface[slot] = resourceId;
|
||||||
@@ -368,6 +377,12 @@ public sealed class GodotAdvHost : IHost
|
|||||||
_movieFrames.Remove(resourceId);
|
_movieFrames.Remove(resourceId);
|
||||||
_completedMovies.Remove(resourceId);
|
_completedMovies.Remove(resourceId);
|
||||||
}
|
}
|
||||||
|
lock (_textLock)
|
||||||
|
{
|
||||||
|
_surfaceText.Remove(slot);
|
||||||
|
_surfaceResources.Remove(slot);
|
||||||
|
}
|
||||||
|
_slotDims.Remove(slot);
|
||||||
_timeline?.Event("movie-stop", new() { ["resource"] = resourceId, ["surface"] = slot });
|
_timeline?.Event("movie-stop", new() { ["resource"] = resourceId, ["surface"] = slot });
|
||||||
_main.CallDeferred("StopMovie", resourceId);
|
_main.CallDeferred("StopMovie", resourceId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ INFERRED: dict[int, dict] = {
|
|||||||
0x1f4: dict(name='stmt-begin', category='marker', noop=True, confidence='high', source='investigation', summary='zero-arg; opens scripts, pairs with stmt-end 0x1f5'),
|
0x1f4: dict(name='stmt-begin', category='marker', noop=True, confidence='high', source='investigation', summary='zero-arg; opens scripts, pairs with stmt-end 0x1f5'),
|
||||||
0x1f5: dict(name='stmt-end', category='marker', noop=True, confidence='high', source='investigation', summary='zero-arg; precedes exit/next-stmt, pairs with 0x1f4'),
|
0x1f5: dict(name='stmt-end', category='marker', noop=True, confidence='high', source='investigation', summary='zero-arg; precedes exit/next-stmt, pairs with 0x1f4'),
|
||||||
0x1fd: dict(name='gfx-set-scale-current', category='draw', noop=False, confidence='high', source='investigation', summary="(handle)(scale_x_percent)(scale_y_percent)(scale_z_percent) — immediately replace the retained object's current scale matrix at obj+0x6c. The handler divides each integer operand by 100.0 before calling matrix4_make_scale; this is distinct from 0x21e's delayed one-shot target scale."),
|
0x1fd: dict(name='gfx-set-scale-current', category='draw', noop=False, confidence='high', source='investigation', summary="(handle)(scale_x_percent)(scale_y_percent)(scale_z_percent) — immediately replace the retained object's current scale matrix at obj+0x6c. The handler divides each integer operand by 100.0 before calling matrix4_make_scale; this is distinct from 0x21e's delayed one-shot target scale."),
|
||||||
|
0x1ff: dict(name='set-current-translation', category='draw', noop=False, confidence='high', source='investigation', summary="0x1ff (handle)(x)(y)(z) — immediately replace the retained object's current translation matrix at obj+0x16c. This is the direct-current companion to 0x220's delayed target at obj+0x1ac."),
|
||||||
0x21b: dict(name='line-id?', category='marker', noop=True, confidence='med', source='harness', summary='1 imm; mov->0x21b->stmt-end; near save/load-messkip — likely line/stmt id, verify not msg-control'),
|
0x21b: dict(name='line-id?', category='marker', noop=True, confidence='med', source='harness', summary='1 imm; mov->0x21b->stmt-end; near save/load-messkip — likely line/stmt id, verify not msg-control'),
|
||||||
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?'),
|
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?'),
|
||||||
0x259: dict(name='script-entry', category='marker', noop=True, confidence='low', source='harness', summary='zero-arg; the first instruction of a script (offset 0), opens the decl chain that 0x258 continues — script/prologue entry marker, structural'),
|
0x259: dict(name='script-entry', category='marker', noop=True, confidence='low', source='harness', summary='zero-arg; the first instruction of a script (offset 0), opens the decl chain that 0x258 continues — script/prologue entry marker, structural'),
|
||||||
|
|||||||
@@ -4753,14 +4753,14 @@ argc = 4
|
|||||||
abi_source = "kelebek+decode-validated"
|
abi_source = "kelebek+decode-validated"
|
||||||
|
|
||||||
[opcode.semantics]
|
[opcode.semantics]
|
||||||
name = "set-gfx-geom3-c"
|
name = "set-current-translation"
|
||||||
category = "draw"
|
category = "draw"
|
||||||
summary = "0x1ff (handle)(a)(b)(c) — gfx cmd-type 9. Handler gfx_op_0x1ff_set_geom3 @0x4227b0: SETS a 3-vector (int→float a,b,c) on object `handle` via native worker FUN_0047e800 (sibling of 0x217/0x219, a distinct per-object vector). See docs/engine-re.md gfx op-contract table."
|
summary = "0x1ff (handle)(x)(y)(z) — immediately replace the retained object's current translation matrix at obj+0x16c. This is the direct-current companion to 0x220's delayed target at obj+0x1ac."
|
||||||
noop_headless = false
|
noop_headless = false
|
||||||
source = "investigation"
|
source = "investigation"
|
||||||
confidence = "high"
|
confidence = "high"
|
||||||
depends_on = []
|
depends_on = []
|
||||||
evidence = "Ghidra handler 0x4227b0 (dispatch ctx[0x26c93+0x1ff]); FUN_0047e800(op1,(float)op2,(float)op3,(float)op4)."
|
evidence = "Ghidra /v2: handler gfx_op_0x1ff_set_geom3@0x4227b0 converts operands 2..4 to float and calls gfx_object_set_translation_current@0x47e800. The worker gets/creates the object, marks obj+0x168, and calls matrix4_make_translation on obj+0x16c. SC0000 uses (0,0,0) before animating BG001A. Annotated and saved 2026-07-11."
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
@@ -5251,12 +5251,12 @@ abi_source = "kelebek+decode-validated"
|
|||||||
[opcode.semantics]
|
[opcode.semantics]
|
||||||
name = "query-gfx-object?"
|
name = "query-gfx-object?"
|
||||||
category = "draw"
|
category = "draw"
|
||||||
summary = "0x215 (out_slot)(handle) — query the retained gfx-object map. Handler gfx_op_0x215_query_source_slot@0x42a0b0 calls gfx_object_query_source_slot@0x47f280 with owner ctx+0x46614. The worker searches owner+0x408, the same map populated by geometry/draw workers, and returns obj+4: the live source-surface slot written by draw-texture, or -1 if absent. A geometry-only object remains unbound and returns -1. SC0000 uses a successful result for existing-object geometry and query-guarded teardown: op 0x1f7 erases the object group and op 0x1fa releases this slot. VM fix 2026-07-09 restores AE001H magic-circle cleanup."
|
summary = "0x215 (out_slot)(handle) — query the retained gfx-object map. Handler gfx_op_0x215_query_source_slot@0x42a0b0 calls gfx_object_query_source_slot@0x47f280 with owner ctx+0x46614. The worker searches owner+0x408, the same map populated by geometry/draw workers, and returns obj+4: the live source-surface slot written by draw-texture, or -1 if absent. gfx_object_init_default zeroes obj+4, so a geometry/animation-created but draw-unbound object returns slot 0, not -1. SC0000 uses a successful result for query-guarded teardown: op 0x1f7 erases the object group and op 0x1fa releases this slot. Matching the zero default prevents stale transforms from surviving into the EV050EA CG at SC0000 page 58."
|
||||||
noop_headless = false
|
noop_headless = false
|
||||||
source = "investigation"
|
source = "investigation"
|
||||||
confidence = "high"
|
confidence = "high"
|
||||||
depends_on = []
|
depends_on = []
|
||||||
evidence = "Ghidra asm: handler 0x42a0b0 passes ECX=ctx+0x46614 to 0x47f280. That worker searches ECX+0x408 and returns resolved object+4. gfx_object_bind_draw@0x47e870, called with the same owner, get-or-creates in owner+0x408 and writes source slot to object+4. SC0000 post-effect cleanup 0x3321 queries G[0x62457], sign-tests, then executes 0x1f7(handle,10) + 0x1fa(returned_slot); AE001H res 0x37 was bound to that handle."
|
evidence = "Ghidra: handler 0x42a0b0 passes ECX=ctx+0x46614 to 0x47f280, which searches ECX+0x408 and returns object+4. gfx_object_bind_draw@0x47e870 writes the bound slot there; gfx_object_init_default@0x472810 explicitly writes zero to dword index 1. SC0000 page-58 trace: the old -1 default skipped cleanup of transform-created handle 0xcb2a, then EV050EA inherited translation (-100,0), rotation -90, and alpha 0; the native zero default makes the guard succeed and the reused object is identity/opaque."
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
@@ -6426,28 +6426,28 @@ observed_types = ["l-int"]
|
|||||||
|
|
||||||
[[opcode]]
|
[[opcode]]
|
||||||
op = 0x242
|
op = 0x242
|
||||||
label = "u00422D60"
|
label = "set-gfx-field2d0"
|
||||||
argc = 2
|
argc = 2
|
||||||
abi_source = "kelebek+decode-validated"
|
abi_source = "kelebek+decode-validated"
|
||||||
|
|
||||||
[opcode.semantics]
|
[opcode.semantics]
|
||||||
name = "u00422D60"
|
name = "set-gfx-field2d0"
|
||||||
category = "unknown"
|
category = "draw"
|
||||||
summary = ""
|
summary = "0x242 (handle)(value) — command type 5. Get-or-create the retained gfx object and write value to obj+0x2d0. SC0000's common CG loader passes zero after draw binding. This field does not reset transform or color channels; its downstream purpose remains unknown."
|
||||||
noop_headless = false
|
noop_headless = false
|
||||||
source = "kelebek"
|
source = "investigation"
|
||||||
confidence = "low"
|
confidence = "high"
|
||||||
depends_on = []
|
depends_on = []
|
||||||
evidence = ""
|
evidence = "Ghidra /v2: op_0x242_set_object_field2d0@0x4249d0 fetches operands 2 and 1 and calls gfx_object_set_field2d0@0x47f1a0; the worker calls gfx_object_get_or_create then stores operand 2 at returned object+0x2d0."
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
role = ""
|
role = "handle_id"
|
||||||
observed_types = ["l-int", "l-ptr"]
|
observed_types = ["l-int", "l-ptr"]
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 2
|
i = 2
|
||||||
role = ""
|
role = "field2d0_value"
|
||||||
observed_types = ["imm"]
|
observed_types = ["imm"]
|
||||||
|
|
||||||
[[opcode]]
|
[[opcode]]
|
||||||
|
|||||||
Reference in New Issue
Block a user