feat: add affine rotation rendering and timeline diagnostics
This commit is contained in:
@@ -445,26 +445,45 @@ independent matrix channels, not two encodings of one vec3 property.
|
||||
- `0x220` passes raw operands 4–6 to `gfx_object_set_translation_channel` (`0x47ecc0`), stores timing at
|
||||
`obj+0x44/+0x58`, and calls `0x48afb1`, which writes them into matrix entries 12–14 at
|
||||
`obj+0x1ac`: a **translation matrix**.
|
||||
- `gfx_object_apply_transform_channels` (`0x472f00`) supplies the timing contract. Both channels use
|
||||
- `0x21f` converts operands 4–7 to floats and calls `gfx_object_set_rotation_channel` (`0x47eb70`). It
|
||||
stores delay/duration at `obj+0x40/+0x54`, target axis at `obj+0x1f8..0x200`, target angle (degrees)
|
||||
at `obj+0x208`, and the target axis-angle matrix at `obj+0x12c`. Current axis/angle are
|
||||
`obj+0x1ec..0x1f4/+0x204`, with current matrix `obj+0xec`.
|
||||
- `gfx_object_apply_transform_channels` (`0x472f00`) supplies the timing contract. All three channels use
|
||||
shared start timestamp `obj+0x34` and global frame-time `ctx+0xb550`, but have independent delay/duration:
|
||||
scale `obj+0x3c/+0x50`, translation `obj+0x44/+0x58`. Each holds its current matrix through the
|
||||
scale `obj+0x3c/+0x50`, rotation `obj+0x40/+0x54`, translation `obj+0x44/+0x58`. Each holds current through the
|
||||
delay, linearly interpolates current→target for its duration, then commits the target and clears its own timing.
|
||||
Neither third component is opacity.
|
||||
|
||||
**Exact composition and 2D reduction (live-validated 2026-07-10).** The consumer starts from identity and
|
||||
right-multiplies `T(-V18) → scale-current → middle/rotation → translation-current → T(+V18)`;
|
||||
**Exact composition and 2D reduction (live-validated 2026-07-10).** The one-shot consumer starts from identity and
|
||||
right-multiplies `T(-V18) → scale-current → rotation-current → translation-current → T(+V18)`;
|
||||
`matrix4_multiply` at `0x4ee2a4` computes `out = left * right`. AGE uses row vectors. With no
|
||||
rotation/perspective, the screen projection is therefore exactly
|
||||
`V18.xy + (point.xy - V18.xy) * scale.xy + translation.xy`. The captured SC0000 handle `0xcbc0`
|
||||
has base `(0,600)`, anchor `(400,1000)`, and final scale `(5,5)`; native matrix translation
|
||||
terms are `(-1600,-4000)`, projecting the base point to `(-1600,-1000)`. The port's focused
|
||||
projection test and transform-aware gfx log reproduce those values. Rotation projection and final D3D
|
||||
raster/rounding details remain deferred; the axis-aligned anchor/order/projection no longer are provisional.
|
||||
projection test and transform-aware gfx log reproduce those values.
|
||||
|
||||
**Port result (2026-07-10):** `GfxState` now retains separate current/target scale and translation
|
||||
channels with the native shared-start/independent-timing model. Godot scales around `V18` (anchor),
|
||||
applies translation independently, and never derives opacity from transform Z. The shared `AnimTarget`
|
||||
and `TZ/100` alpha tween are gone.
|
||||
`gfx_object_composite` then right-multiplies `gfx_object_anim_interpolate`'s separately anchored product,
|
||||
which contains op `0x234`'s cyclic rotation. With the other oscillating matrices at identity, adjacent anchors
|
||||
cancel and the full order is
|
||||
`T(-V18) * scale * one-shot-rotation * translation * cyclic-rotation * T(+V18)`. Thus cyclic rotation
|
||||
also rotates the translation vector. The cyclic angle is integer degrees
|
||||
`floor(((frameTime-start) % period) * 360 / period)`; it wraps to zero without ping-pong. Positive Z produces
|
||||
`m01=+sin, m10=-sin`, clockwise on the Y-down screen.
|
||||
|
||||
Native matrix oracle: handle `0xcb8e`, anchor `(700,600)`, scale current `0.9`, op `0x21f` target axis
|
||||
`(0,0,1)`/30° after 500 ms for 390 ms, sampled 11 ms into the ramp as
|
||||
`[0.9055,0.0134;-0.0134,0.9055]` with translation `(74.1449,47.3127)`. The port focused test matches
|
||||
those terms. In the windowed port capture, the two SC0000 `0x234` sites (periods 9000/13000 ms, Z axes
|
||||
`+1/-1`) advanced after 563 ms to integer angles `22/15`, exactly the native formula, and produced distinct
|
||||
affine PNG frames. Nearest-neighbour inverse mapping is the deliberate software raster sampling policy;
|
||||
native D3D9 subpixel filtering remains a possible pixel-level difference, not an uncertain matrix approximation.
|
||||
|
||||
**Port result (2026-07-10):** `GfxState` retains scale, one-shot rotation, translation, and cyclic rotation
|
||||
with their native clocks/order. `Transform2DMath` composes the full row-vector 4×4 transform before 2D
|
||||
projection. Godot uses an inverse-mapped affine RGBA8 rasterizer for textured objects and solid fills,
|
||||
preserving colorkey/tint/opacity behavior and never deriving opacity from transform Z.
|
||||
|
||||
##### `anim_start`/`set_anim_clock` decoded + opening confirmed (2026-07-07, animation-slice Task 1)
|
||||
|
||||
@@ -687,8 +706,8 @@ per-frame bytecode. Reversed + annotated in Ghidra:
|
||||
VM is parked at wait-for-input — no blocking present op, no VM/host frame-lockstep** (the answer to the
|
||||
"frame loop" question).
|
||||
|
||||
Consequence: animation needs a retained per-frame compositor. That architecture is live; the 2026-07-10
|
||||
matrix slice adds native one-shot scale/translation, while cyclic rotation remains a later affine step.
|
||||
Consequence: animation needs a retained per-frame compositor. That architecture is live; scale,
|
||||
one-shot rotation/translation, and cyclic rotation now rasterize through the affine software path.
|
||||
|
||||
### The full gfx render model — surfaces + objects + composite (2026-07-07)
|
||||
|
||||
@@ -720,9 +739,9 @@ buffers (present). **Slot 0 is NOT special** — a normal slot; several objects
|
||||
(`handle → {slot, srcRect, position, anchor, scale, anim, alpha, visible}`, from draw-texture + the gfx ops) +
|
||||
a host per-frame compositor that draws visible objects **in ascending-handle order** from their live surface,
|
||||
interpolating animations by elapsed time. No VM/host lockstep: animations play during the wait-for-input park.
|
||||
Separate scale/translation state and timing are implemented. The current anchored 2D composition is provisional:
|
||||
exact anchor semantics, multiplication order, and projection still need slow native-versus-port frame comparison.
|
||||
Full affine rotation remains deferred.
|
||||
Separate scale/rotation/translation state and timing are implemented. Anchor semantics, multiplication order,
|
||||
cyclic wrapping, 2D projection, and affine raster coverage have focused native-oracle tests. Native D3D9 filtering
|
||||
and render-target command execution remain separate fidelity work.
|
||||
|
||||
### Blend & transparency — colorkey + `0x202`/`0x203` color/alpha (2026-07-08)
|
||||
|
||||
@@ -778,8 +797,15 @@ annotated in Ghidra, saved.
|
||||
| `0x228` | `gfx_op_0x228_query_position` (`FUN_0047cdd0`) | **query** current computed (x,y,z) → operand slots 3/4/5 (script logic, not render) |
|
||||
| `0x23f` | `gfx_op_0x23f_query_object` (`FUN_0042a520`) | **query** an object status/value → operand slot 1 |
|
||||
|
||||
**Deferred (own follow-ups, per scope decision):** `0x21f` (`FUN_0047eb70`, 4-float scale/matrix), `0x223`
|
||||
(`FUN_0047f440`, 8-arg matrix row) → need **affine rendering**; `0x236` (`gfx_op_0x236` @`0x423ee0`) a
|
||||
**Follow-up resolution (2026-07-10):** `0x21f` is the one-shot axis-angle channel and is implemented with
|
||||
affine rasterization. `0x223` is **not affine**: `gfx_queue_surface_alpha_transition` (`0x47f440`) inserts
|
||||
a type-0 command-map record keyed by arg 1: start `+4`, delay/duration `+8/+0xc`, target surface slot `+0x10`,
|
||||
and two object handle ranges at `+0x14/+0x1c` and `+0x18/+0x20`. `gfx_render_frame` composites those ranges
|
||||
into the target and ramps alpha 0→1. Its SC0000 site `0x129e7` passes `(handle+2, transition slot,
|
||||
handle+1,1,handle,1,G[0x6249f],G[0x624a0])`. It remains a render-target/transition slice dependency rather
|
||||
than being approximated in the affine object compositor.
|
||||
|
||||
**Still deferred:** `0x236` (`gfx_op_0x236` @`0x423ee0`) a
|
||||
**timed/animated-surface (movie-like) op**; plus the unclassified `0x21c/0x21d/0x224/0x242/0x23d/0x20a/0x20e/0x243`
|
||||
tail (2-arg flags / inline). These stay GAP until a follow-up slice or are safe-noop'd if the opening tolerates it.
|
||||
|
||||
|
||||
@@ -193,11 +193,21 @@ Native handler gfx_op_0x20c_present_frame (dispatch ctx[0x26c93+0x20c]) -> gfx_r
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra 0x47eaa0 calls matrix builder 0x48af1d for target obj+0xac. Consumer 0x472f00 uses delay obj+0x3c, duration obj+0x50, current obj+0x6c, target obj+0xac, shared start obj+0x34, and frame-time ctx+0xb550.
|
||||
|
||||
### 0x21f `set-anim-rotation-axis-angle` (set-anim-rotation-axis-angle, argc 7)
|
||||
- **summary:** (handle)(delay_ms)(duration_ms)(axis_x)(axis_y)(axis_z)(angle_deg) — set the delayed one-shot axis-angle rotation channel. Handler converts axis/angle integers to floats; worker stores target axis obj+0x1f8 and angle obj+0x208 and builds target matrix obj+0x12c. gfx_object_apply_transform_channels samples current axis/angle linearly on shared start obj+0x34 and composes T(-anchor)*scale*rotation*translation*T(anchor).
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra handler 0x423410 -> gfx_object_set_rotation_channel@0x47eb70; consumer gfx_object_apply_transform_channels@0x472f00 uses delay +0x40, duration +0x54, current axis +0x1ec/angle +0x204, target axis +0x1f8/angle +0x208, current matrix +0xec and target +0x12c. Native SC0000 handle 0xcb8e sample at 11/390 of axis (0,0,1), 30deg matches matrix [0.9055,0.0134;-0.0134,0.9055] and translation (74.1449,47.3127).
|
||||
|
||||
### 0x220 `set-anim-transform-abs` (set-anim-transform-abs, argc 6)
|
||||
- **summary:** (handle)(delay_ms)(duration_ms)(tx)(ty)(tz) — set the absolute TRANSLATION-matrix channel. Target obj+0x1ac is linearly sampled from current obj+0x16c by gfx_object_apply_transform_channels@0x472f00 on frame-time ctx+0xb550, after delay and for duration, then committed. Independent of op 0x21e scale; neither Z is opacity.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **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 frame-time ctx+0xb550.
|
||||
|
||||
### 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.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra handler 0x423620 -> gfx_queue_surface_alpha_transition@0x47f440. Record fields: type +0=0, start +4=0, delay +8=arg7, duration +0xc=arg8, slot +0x10=arg2, range A +0x14/+0x1c=args3/4, range B +0x18/+0x20=args5/6. gfx_render_frame@0x47fbc0 initializes start from ctx+0xb550 and consumes type 0 as an alpha ramp. SC0000 executes one shared-helper site at 0x129e7.
|
||||
|
||||
### 0x224 `clear-gfx-command-queue` (clear-gfx-command-queue, argc 0)
|
||||
- **summary:** Clear the native gfx command queue rooted at ctx+0x418. Host-implicit because the port composites retained state directly.
|
||||
- **grounding:** source=investigation, confidence=high, noop_headless=True
|
||||
@@ -224,9 +234,9 @@ Native handler gfx_op_0x20c_present_frame (dispatch ctx[0x26c93+0x20c]) -> gfx_r
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x234 `anim-start` (anim-start, argc 5)
|
||||
- **summary:** (handle)(period_ms)(axis_x)(axis_y)(axis_z) — configure a cyclic ROTATION channel. Worker gfx_object_set_rotation_cycle@0x47f060 stores period obj+0x228 and axis obj+0x244..0x24c; interpolator 0x473ed0 applies 0..360 degrees from frame-time ctx+0xb550. Separate from scale, translation, opacity, and op 0x238's clock.
|
||||
- **summary:** (handle)(period_ms)(axis_x)(axis_y)(axis_z) — configure cyclic rotation. Worker stores period obj+0x228, start obj+0x214=0, and float axis obj+0x244; each frame uses integer degrees floor(((now-start)%period)*360/period). gfx_object_composite right-multiplies this separately anchored transform after the one-shot scale/rotation/translation product, so cyclic rotation also rotates the translation vector.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra handler 0x423da0 -> worker 0x47f060. gfx_object_anim_interpolate@0x473ed0 consumes obj+0x228/0x244 on ctx+0xb550 and builds an axis-angle rotation matrix with angle 360*((now-start)%period)/period.
|
||||
- **evidence:** Ghidra handler 0x423da0 converts axis ints to floats -> worker 0x47f060. gfx_object_anim_interpolate@0x473ed0 consumes obj+0x228/+0x214/+0x244 on ctx+0xb550 and matrix4_make_axis_angle@0x48b215. gfx_object_composite@0x47f650 calls one-shot transform first, cyclic animation second.
|
||||
|
||||
### 0x238 `set-anim-clock` (set-anim-clock, argc 1)
|
||||
- **summary:** (duration) — set the GLOBAL animation clock: native ctx+0x51b78=0 (elapsed), +0x51b7c=duration. cmd-type 3. NON-BLOCKING: only configures; the render loop advances it and interpolates all animating objects. SC0000 opening @0x123bd/@0x13858. Handler 0x4240e0; Kelebek VA 0x422390 is drift.
|
||||
@@ -991,18 +1001,10 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x21f `u00421510` (u00421510, argc 7)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x222 `u004216C0` (u004216C0, argc 2)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x223 `u00421700` (u00421700, argc 8)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x22a `u00421A90` (u00421A90, argc 3)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
@@ -737,3 +737,161 @@ cyclic-rotation rasterization remains the next affine-rendering slice.
|
||||
Verification: engine **92/92** after the opcode-clock reset test, corpus sweep unchanged at
|
||||
**284 exit / 13 STEP-LIMIT**, Godot build and threaded `SELFTEST OK`. The transform capture tool and
|
||||
transform-aware `--gfx-log` are documented in `docs/tools-reference.md`.
|
||||
|
||||
### A2b — ADV transition/lifecycle diagnosis plan ⏳ OPEN (2026-07-10)
|
||||
|
||||
This is the next SC0000 correctness slice. It is driven by live A/B observations, not by static opcode
|
||||
coverage alone. The direct SC0000 histogram is currently **80/129 distinct ops handled (62.0%)** and
|
||||
**96.2% instruction-weighted**, but a rare query, scheduler op, or render worker can still control an
|
||||
entire visible section. Coverage also excludes called scripts; this slice follows only callees actually
|
||||
entered on the failing path rather than expanding into blanket subscript completion.
|
||||
|
||||
**Observed native ADV contract (client behavior):** foreground presentation changes are transitions even
|
||||
when they are only in-place fades. SC0000 begins black and fades into the first CG; the message window then
|
||||
fades in before text reveals. During a CG swap, the window transitions out, one or more CG transitions run,
|
||||
then the window transitions back in. A click during an active foreground transition completes it immediately
|
||||
and lets the next presentation step start; a click at a stable `wait-for-input` advances the script. Ambient
|
||||
retained animation is a separate class and must not all be completed by that click.
|
||||
|
||||
**Current port failures (2026-07-10):**
|
||||
|
||||
- The initial black state holds and then the first CG pops in instead of fading. This is consistent with the
|
||||
port applying op `0x202`'s endpoint as a static tint while its native animated-color consumer remains
|
||||
unmodeled.
|
||||
- The retained textbox artwork appears to begin fading in, then disappears. The Godot `Label` shortcut stays
|
||||
visible; it is deliberately out of scope for this slice because native `draw-string`/text presentation will
|
||||
replace it before SC0000 is called complete. Diagnose the **box object's** lifetime, not the shortcut text.
|
||||
- The late animation burst before the first music change is badly wrong, then the screen becomes white and
|
||||
interactive play does not proceed to the music change.
|
||||
|
||||
**Concrete failing boundary.** `BGM005` begins at SC0000 offset `0x7fa`; the expected first change to
|
||||
`BGM008` is `play-bgm 0x8` at **`0x1728`**. The immediately preceding block `0x133f..0x1725` exercises
|
||||
repeated `0x202/0x203` color operations and the implemented geometry/scale family, but also directly executes
|
||||
three still-stubbed gfx ops: **`0x236`** at `0x13c8`, **`0x1fd`** at `0x14f3`, and **`0x21f`** at `0x159a`.
|
||||
It calls the shared animation finalizer `label_1235a` several times, including at `0x1725`; that finalizer
|
||||
sets op `0x238`'s duration, then reads still-unimplemented **`0x1c7 get-message-skip`** and
|
||||
**`0x1cc get-adv-service-state`** to choose its present/yield path. Therefore the white stall could be an
|
||||
object/compositor error, an unmodeled foreground-transition gate, or wrong control flow caused by a stubbed
|
||||
output—not safely assumed to be “just interpolation.”
|
||||
|
||||
#### Investigation order
|
||||
|
||||
1. **Make the failure boundary deterministic before changing semantics.** Reproduce from `--boot` with
|
||||
auto-input and a long enough `--shot-sequence`, plus `--gfx-log`. Add a single synchronized diagnostic
|
||||
timeline if the existing logs cannot answer the boundary: frame/virtual time; active script + PC/opcode;
|
||||
VM state (`running`, sleep, transition, input wait, halt); BGM event; and every changed visible object's
|
||||
handle, surface slot/resId, tint/alpha, transform, and lifecycle event. Use `play-bgm 0x8 @ 0x1728` as the
|
||||
reachability sentinel. Do not judge progress from the white pixels alone.
|
||||
|
||||
2. **Classify before fixing.** If the VM reaches/passes `0x1728` while the frame stays white, identify the
|
||||
topmost white/fill object and whether its handle remains visible, loses/rebinds its live surface, or has a
|
||||
stuck color endpoint. If the VM never reaches `0x1728`, record the last PC and whether it is sleeping,
|
||||
input-waiting, transition-waiting, polling, halted, or still executing. If the executed path itself is
|
||||
suspect, capture the same native passage with `trace_engine_ops.py` and use `diff_optrace.py` to find the
|
||||
first engine/port offset divergence.
|
||||
|
||||
3. **Track the textbox artwork as an AGE object.** From its first visible frame, identify its retained handle
|
||||
and follow bind, color/animation, present, erase, release, and surface-rebind events through the first CG
|
||||
swap. The key result is one of: `GONE` (premature erase), still present but covered (z/lifecycle input),
|
||||
still present but transparent/tinted (color channel), or bound to a replaced slot (surface lifetime).
|
||||
Compare only those corresponding native object events; do not spend this slice synchronizing the Godot
|
||||
text overlay.
|
||||
|
||||
4. **Recover the foreground ADV transition contract.** Reverse/capture the producer behind
|
||||
`get-adv-service-state` (`0x1cc`), implement the already-known `get-message-skip` output (`0x1c7`), and
|
||||
observe what a click changes during the initial fade and the pre-`0x1728` burst. Establish an explicit
|
||||
host-level foreground transition with `start → per-frame progress → natural/forced completion → resume`.
|
||||
Click completes and consumes the active foreground transition; only a stable input wait advances content.
|
||||
The wall clock remains the progress source and opcode pacing remains a guard within runnable bursts, not
|
||||
the mechanism that decides how long a presentation state lives.
|
||||
|
||||
5. **Reverse only the executed missing gfx dependency that remains causal.** Triage the three direct gaps in
|
||||
failing-order: `0x236` (timed/animated-surface worker), `0x1fd` (scaled vector/animation setter), and
|
||||
`0x21f` (affine/matrix channel). For each, capture native inputs, retained fields, and sampled output at the
|
||||
exact SC0000 site; implement it with a focused VM/state/compositor test. Do not declare a stub harmless
|
||||
merely because it is rare, and do not implement the whole remaining opcode list without evidence.
|
||||
|
||||
6. **Validate as presentation checkpoints.** Native/manual observation remains the final visual oracle, but
|
||||
each check should first have machine evidence (PC reached, object identity/lifetime, transition progress,
|
||||
and final state). Required checkpoints: black visibly ramps into the first CG; the textbox artwork survives
|
||||
until its intended transition-out; a CG swap orders window-out → CG transition(s) → window-in; clicking an
|
||||
active transition snaps to its endpoint without also advancing a stable page; the late burst has no stuck
|
||||
white owner; and execution reaches `BGM008 @ 0x1728`. Re-run engine tests, corpus sweep, Godot threaded
|
||||
self-test, and the SC0000 coverage report after each landed opcode or scheduler change.
|
||||
|
||||
**Stop conditions / scope guard:** this slice is complete when the port reaches `0x1728` interactively and
|
||||
the above foreground transitions have correct lifecycle/click behavior. Native glyph rendering, configurable
|
||||
text reveal speed, and unrelated subscript opcode completeness remain separate work. Any called script proven
|
||||
to own the first divergence becomes an explicit dependency of this slice; otherwise it stays out of scope.
|
||||
|
||||
#### Investigation 1 result — deterministic boundary classification (2026-07-10)
|
||||
|
||||
Added the observe-only Godot `--timeline-log <jsonl>` diagnostic so VM steps (real byte offsets), virtual
|
||||
time/frame, host state, BGM events, and changed visible-object outcomes share one ordered stream. The
|
||||
reproduction was `SC0000 --boot`, stable-wait auto-input via a long `--shot-sequence`, `--gfx-log`, and a
|
||||
uniform diagnostic `--speed 8`; speed scales VM, sleeps, and animation clocks together and does not inject
|
||||
input outside `wait-for-input`.
|
||||
|
||||
**Classification: not a VM/control-flow stall on the deterministic path.** The run executed all three direct
|
||||
gaps (`0x236 @ 0x13c8`, `0x1fd @ 0x14f3`, `0x21f @ 0x159a`), called the finalizer at `0x1725`, then executed
|
||||
`play-bgm 0x8 @ 0x1728` in `running` state at frame 818 / virtual `70,262 ms`. The BGM event resolved to
|
||||
`BGM008.OGG` in the same synchronized event and execution continued through `0x172b` and beyond; the full
|
||||
1,800-frame run reached page 80 and five BGM events. Therefore a native offset-path diff is not warranted for
|
||||
this boundary unless a separately reproducible manual-input path fails to reach the sentinel.
|
||||
|
||||
The full-screen fill owner is retained handle **`0xcf08`**, but it is not stuck at this boundary. It was a
|
||||
transparent white `800x600` fill (`a=0.00`) when `0x1728` executed. Later, `0x203 @ 0x1337f` made it solid
|
||||
white for one sampled diagnostic frame (frame 911); the following `label_1235a` path executed `0x1c7`,
|
||||
`0x1cc`, and `0x21c`, and the compositor sampled the same handle back at `a=0.00` on frame 912. This is
|
||||
evidence of a likely incorrect flash/color presentation contract, not evidence for the reported pre-BGM
|
||||
infinite stall. No opcode, scheduler, or compositor semantic fix was made in this investigation step.
|
||||
|
||||
Validation after adding the diagnostic: engine **92/92**, Godot build clean, threaded `SELFTEST OK`, and
|
||||
`git diff --check` clean. The headless `--shot-sequence` PNG capture path emits dummy-renderer `GetImage`
|
||||
errors, but the CPU compositor/timeline completed and the same path already had this limitation; use a
|
||||
windowed sequence when pixel files rather than object-state evidence are required.
|
||||
|
||||
### A2b — cyclic rotation and affine rasterization ✅ DONE (2026-07-10)
|
||||
|
||||
This bounded slice followed the non-reproduced white-stall classification above; it did not resume that
|
||||
investigation and does not claim the interactive symptom is fixed.
|
||||
|
||||
**Native contracts.** Op `0x21f` is a delayed one-shot axis-angle rotation, not a generic matrix row:
|
||||
`(handle,delay,duration,axisX,axisY,axisZ,angleDegrees)`. It shares `obj+0x34`'s start with scale/translation,
|
||||
uses delay/duration `+0x40/+0x54`, and linearly samples current axis/angle `+0x1ec/+0x204` to target
|
||||
`+0x1f8/+0x208`. Op `0x234` is separately anchored cyclic rotation with integer-degree phase
|
||||
`floor(((now-start)%period)*360/period)`. The native call order reduces to
|
||||
`T(-anchor)*scale*oneShotRotation*translation*cyclicRotation*T(anchor)`, so the cycle rotates translation.
|
||||
|
||||
Op `0x223` was also closed out but deliberately not implemented here: it inserts a type-0 timed-alpha record
|
||||
in the surface command map, containing a target surface slot and two object ranges. SC0000's shared site
|
||||
`0x129e7` passes `(handle+2, slot, handle+1,1,handle,1,delay,duration)`. This belongs to render-target/
|
||||
foreground-transition presentation, not affine object state, and remains a visible GAP rather than receiving
|
||||
an uncertain approximation.
|
||||
|
||||
**Native matrix oracle.** The retained trace's handle `0xcb8e` sample (anchor `(700,600)`, scale from 0.9,
|
||||
axis `(0,0,1)`, 30° target, sampled 11 ms into a 390 ms ramp after 500 ms delay) is
|
||||
`[0.9055,0.0134;-0.0134,0.9055]`, translation `(74.1449,47.3127)`; the focused port test matches it.
|
||||
The two executed SC0000 cycle sites use periods 9000/13000 ms and axes `+Z/-Z`. A windowed port capture
|
||||
advanced 563 ms from their first sample to phase angles `22°/15°`, exactly the native integer formula.
|
||||
|
||||
**Port result.** `GfxState` now retains/samples the one-shot rotation and cyclic start/phase. `Transform2DMath`
|
||||
composes a row-vector 4×4 matrix and projects it to an invertible 2D affine transform. The Godot compositor
|
||||
uses a pure inverse-mapped nearest-neighbour RGBA8 rasterizer for both textures and solid fills, preserving
|
||||
the existing colorkey, tint-strength, opacity, clipping, flipping, and z-order paths. Native D3D9 filtering
|
||||
can still differ at subpixels; the matrix/order is oracle-backed rather than approximated.
|
||||
|
||||
The windowed `--shot-sequence` run wrote 454 PNGs with 102 pixel-state transitions; the first cyclic passage
|
||||
produced distinct affine frames as `0xcb8e/0xcb98` advanced. There is no corresponding native PNG sequence in
|
||||
the workspace, so validation is matrix/phase exact plus port-pixel coverage—not a false claim of pixel-perfect
|
||||
native frame equality.
|
||||
|
||||
**Ghidra.** Renamed/commented `gfx_object_set_rotation_channel` (`0x47eb70`),
|
||||
`gfx_queue_surface_alpha_transition` (`0x47f440`), the surface-command map helpers, and
|
||||
`matrix4_make_axis_angle` (`0x48b215`); corrected comments on the one-shot consumer, cyclic interpolator,
|
||||
and composite call order; named useful parameters; saved `/v2`.
|
||||
|
||||
**Validation:** engine **97/97**; full sweep unchanged at **284 exit / 13 STEP-LIMIT**; Godot build clean
|
||||
apart from the pre-existing nullable warning and threaded `SELFTEST OK`; opcode tooling and focused Python
|
||||
tests clean; transform tool compiles; SC0000 coverage **81/129 handled (62.8%)**, 48 GAP ops / 602 GAP
|
||||
instructions; windowed affine capture clean. Final whitespace/diff validation is recorded with the handoff.
|
||||
|
||||
@@ -137,13 +137,19 @@ texture ops (no GPU context) — run windowed for real scenes. User args (after
|
||||
- `--sleep-scale <f>` — multiply every `sleep` (op 0xc8) duration by `f` (default 1.0). The authentic opening burst is only ~2 s, too fast to eyeball live; `--sleep-scale 5` stretches it to ~10 s so the paced sequence (arcane `AE*` → character CGs → settled BG) is watchable. Debug-only; leave at 1.0 for real playback.
|
||||
- `--speed <f>` — scale the unified runtime clock (VM cadence, sleeps, and retained animation) without auto-advancing input waits. Values 0.05–8 are accepted; `--speed 0.25` is useful for transform inspection, while 1.0 is normal playback.
|
||||
- `--gfx-log <file>` — **compositor + op diagnostic** (the tool that root-caused the grey background). Logs, per rendered frame, only the objects whose draw outcome **CHANGED** (drawn↔skip↔gone, resId, resolved file, `slot`, `src`/`dst`, `op`acity, `tintStr`ength) — quiet until something actually changes, so the exact frame a layer drops out (and why) stands out. Also traces every `set-texture`/`create-texture` **slot assignment** (via `GodotAdvHost.TraceOps`). Works live or with `--shot-sequence`. Use it before theorising about layering/blend/geometry: it showed the grey BG = the slot-selecting globals resolving to 0 → every texture collapsing into slot 0 (see engine-re.md §"Grey-background root cause"). e.g. `godot --path godot -- --boot --gfx-log out/gfx.log` then click to the bad page.
|
||||
Matrix-channel outcomes also include `base`, `anchor`, projected `dst`, and sampled `scale`/`trans` values. Parent directories are created automatically.
|
||||
Matrix-channel outcomes also include `base`, `anchor`, projected `dst`, sampled `scale`/`trans`, and
|
||||
one-shot-plus-cyclic `rot`ation angles. Parent directories are created automatically.
|
||||
- `--timeline-log <jsonl>` — diagnostic-only synchronized event stream for a real Godot run. Records every
|
||||
executed script byte offset/opcode, virtual time/frame, VM state changes (`running`, `sleep`, `input-wait`,
|
||||
`halted`), BGM events, and changed visible-object compositor outcomes in one ordered JSONL file. Combine with
|
||||
`--boot --shot-sequence ... --gfx-log ...` to distinguish control-flow stalls from retained-object/compositor
|
||||
failures at an exact bytecode boundary. Relative output paths are project-relative (`godot/`).
|
||||
|
||||
## Asset resolution / graphics
|
||||
|
||||
| Tool | Purpose | Run | Reads → Writes |
|
||||
|---|---|---|---|
|
||||
| `tools/frida/capture_native_transforms.py` | Capture the native retained-object transform input fields and the exact composed 4×4 matrix at `gfx_object_apply_transform_channels`. Optional handle filter; read-only. | `py -3.11 -u -X utf8 tools/frida/capture_native_transforms.py [secs] [pid|AGE.EXE] [--handle 0xHANDLE]` | running game → `build/native-transform-trace.jsonl` |
|
||||
| `tools/frida/capture_native_transforms.py` | Capture native `0x21f`/`0x223`/`0x234` worker operands, corrected integer base/anchor coordinates, all one-shot/cyclic retained fields, the one-shot 4×4 matrix, and the final post-cyclic 4×4 matrix. Optional handle filter; read-only. | `py -3.11 -u -X utf8 tools/frida/capture_native_transforms.py [secs] [pid|AGE.EXE] [--handle 0xHANDLE]` | running game → `build/native-transform-trace.jsonl` |
|
||||
| `parse_sys4ini.py` | Parse `SYS4INI.BIN` (S4IC422, LZSS-compressed) into the authoritative asset index — name ↔ archive ↔ offset ↔ size for all DATA*.ALF (the `resId→file` answer key). Each entry carries `raw_index` (its 0-based position in the SYS4INI record table incl. `@` placeholders) = the engine's universal file id. Also emits the **`call-script <id> → name`** map (id = `raw_index`; see `engine-re.md`). | `parse_sys4ini.py [--check]` (`--check` validates vs `extracted/` + `.ALF` sizes) | `姫狩り…/SYS4INI.BIN` → `build/asset-index.json` + `build/callscript-names.json` |
|
||||
| `resolve_asset.py` | ★ **The static asset resolver.** SYS4INI is sectioned (one per scene: `SCxxxx.BIN` + its cross-archive manifest; `file_number` = index within section). Resolves `resId → files[section_base(scene) + resId]` for graphics AND audio, no capture. | `resolve_asset.py --build` · `resolve_asset.py <SCENE> [resId]` | `build/asset-index.json` → `build/asset-sections.json`; resolves any (scene, resId) |
|
||||
| `resolve_frida_reads.py` | Rescue noisy Frida archive-read offsets → asset names via the index (per-archive range search; drops 0x20000 paging reads); recovers the per-scene asset load order. | `resolve_frida_reads.py [reads.log] [-o out.json]` | `build/frida-reads.log` + `build/asset-index.json` → `build/frida-asset-loads.json` |
|
||||
|
||||
Reference in New Issue
Block a user