Fix AE001H translation target query

This commit is contained in:
gamer147
2026-07-11 21:40:16 -04:00
parent 5614fc7631
commit aa14ad6b3d
7 changed files with 124 additions and 20 deletions

View File

@@ -843,7 +843,7 @@ annotated in Ghidra, saved.
| `0x239` | `gfx_op_0x239_set_srcrect_cell` → `gfx_worker_set_srcrect_cell` | one-shot **spritesheet-cell** channel: delay/duration `obj+0x48/+0x5c`, total frames/columns `obj+0x238/+0x23c`, target frame `obj+0x234` | | `0x239` | `gfx_op_0x239_set_srcrect_cell` → `gfx_worker_set_srcrect_cell` | one-shot **spritesheet-cell** channel: delay/duration `obj+0x48/+0x5c`, total frames/columns `obj+0x238/+0x23c`, target frame `obj+0x234` |
| `0x231` | `gfx_op_0x231_anim_srcrect` → `gfx_worker_anim_srcrect` | looping **spritesheet-cell** channel: milliseconds per frame `obj+0x230`, total frames `obj+0x238`, columns `obj+0x23c`; row-major and wraps, not ping-pong | | `0x231` | `gfx_op_0x231_anim_srcrect` → `gfx_worker_anim_srcrect` | looping **spritesheet-cell** channel: milliseconds per frame `obj+0x230`, total frames `obj+0x238`, columns `obj+0x23c`; row-major and wraps, not ping-pong |
| `0x232` | `gfx_op_0x232_anim_color` → `gfx_worker_anim_color` | **animate color** (pulsing GLOW): bit2 active, period `obj+0x220`, target `obj+0x240` → interpolator COLOR channel (ping-pong). Distinct from static `0x202`/`0x203` (`obj+0x60/0x64`) | | `0x232` | `gfx_op_0x232_anim_color` → `gfx_worker_anim_color` | **animate color** (pulsing GLOW): bit2 active, period `obj+0x220`, target `obj+0x240` → interpolator COLOR channel (ping-pong). Distinct from static `0x202`/`0x203` (`obj+0x60/0x64`) |
| `0x228` | `gfx_op_0x228_query_position` (`FUN_0047cdd0`) | **query** current computed (x,y,z) → operand slots 3/4/5 (script logic, not render) | | `0x228` | `gfx_op_0x228_query_position` → `gfx_object_query_translation_target` (`0x47cdd0`) | **query** the decomposed target-translation matrix (x,y,z), `obj+0x1ac/+0x1b0/+0x1b4`, → operand slots 3/4/5; success is 0 and missing is 1 |
| `0x23f` | `gfx_op_0x23f_query_object` (`FUN_0042a520`) | **query** an object status/value → operand slot 1 | | `0x23f` | `gfx_op_0x23f_query_object` (`FUN_0042a520`) | **query** an object status/value → operand slot 1 |
**Follow-up resolution (2026-07-10):** `0x21f` is the one-shot axis-angle channel and is implemented with **Follow-up resolution (2026-07-10):** `0x21f` is the one-shot axis-angle channel and is implemented with
@@ -886,6 +886,19 @@ set by `draw-texture`; `0x231` does not divide that rectangle by its operands. I
`(100, 8, 4)` for AE001H: eight 200x200 cells in an 800x400, 4x2 sheet. Treating 8 and 4 as grid width `(100, 8, 4)` for AE001H: eight 200x200 cells in an 800x400, 4x2 sheet. Treating 8 and 4 as grid width
and height shrank the crop to 25x50 and made the intended cave spirit effectively disappear. and height shrank the crop to 25x50 and made the intended cave spirit effectively disappear.
**AE001H travel-path correction (2026-07-11):** the three post-movie movement legs exposed a separate
port bug from the source-rectangle correction. Native `0x228` copies the full retained-object record and
decomposes the target matrix beginning at `obj+0x17c`; the returned translation comes from
`obj+0x1ac/+0x1b0/+0x1b4`. It does **not** return the draw/base position at `obj+0x24` (`V24`). SC0000 uses
that query before each `0x220` to derive the next translation target. AE001H's draw base is `(360,20)` and
its initial translation target is `(0,0)`, so the first native leg targets `(40,-20)`. The current C#
`0x228` implementation instead returns `(360,20)`, producing `(400,0)`; the compositor then adds that
translation to the unchanged draw base, placing the object near `(760,20)` before its cyclic rotation.
That explained the observed immediate off-screen movement and why later legs never visibly returned. The
port now queries `TranslationTarget` independently of `V24`, returns the native status convention, and
leaves output operands untouched when the object is absent. A bytecode-level regression reproduces the
three SC0000 targets `(40,-20)`, `(50,-80)`, and `(130,-100)` while retaining base `V24=(360,20)`.
**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. The unclassified
`0x242/0x23d/0x20a/0x20e` tail (2-arg flags / inline) remains GAP and outside this slice. `0x242/0x23d/0x20a/0x20e` tail (2-arg flags / inline) remains GAP and outside this slice.

View File

@@ -269,8 +269,9 @@ Native handler gfx_op_0x20c_present_frame -> gfx_render_frame @0x4820b0. This is
- **evidence:** Ghidra handler 0x417550 -> gfx_command_queue_clear 0x47cb10, which destroys queued nodes and restores the sentinel links/count. - **evidence:** Ghidra handler 0x417550 -> gfx_command_queue_clear 0x47cb10, which destroys queued nodes and restores the sentinel links/count.
### 0x228 `u00421940` (u00421940, argc 5) ### 0x228 `u00421940` (u00421940, argc 5)
- **summary:** 0x228 query-position (succ)(handle)(outX)(outY)(outZ): read the object's current computed position into vars (worker FUN_0047cdd0). C# VM: writes V24 + success flag. See docs/engine-re.md §SC0000 anim cluster. - **summary:** 0x228 query translation target (succ)(handle)(outX)(outY)(outZ): clone the retained object, decompose its target translation matrix at obj+0x17c, and return matrix translation obj+0x1ac/+0x1b0/+0x1b4. Returns succ=0 when found; when absent, writes succ=1 and leaves outputs untouched. The C# VM queries TranslationTarget independently of V24. See docs/engine-re.md §SC0000 anim cluster.
- **grounding:** source=kelebek, confidence=low - **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2 handler gfx_op_0x228_query_position@0x42a3a0 calls gfx_object_query_translation_target@0x47cdd0. The worker copies the complete 0xb5-dword object record, passes copied obj+0x17c to matrix4_decompose_affine@0x48d7c8, and returns its translation outputs; the decomposition reads matrix elements +0x30/+0x34/+0x38, corresponding to obj+0x1ac/+0x1b0/+0x1b4. SC0000 AE001H queries this before each 0x220 leg. C# regression covers targets (40,-20), (50,-80), (130,-100), plus the missing-object output-preservation path.
### 0x229 `u004219E0` (u004219E0, argc 5) ### 0x229 `u004219E0` (u004219E0, argc 5)
- **summary:** 0x229 set-position2 (handle)(op2)(x)(y)(z): set object position/geometry directly (FUN_00472bb0/be0). C# VM: sets V24. See docs/engine-re.md §SC0000 anim cluster. - **summary:** 0x229 set-position2 (handle)(op2)(x)(y)(z): set object position/geometry directly (FUN_00472bb0/be0). C# VM: sets V24. See docs/engine-re.md §SC0000 anim cluster.

View File

@@ -1405,3 +1405,17 @@ now cycles the eight AE001H cells row-major and wraps. Focused tests cover the e
Validation: engine **146/146**, opcode sources regenerated, Godot zero-warning build and threaded selftest, Validation: engine **146/146**, opcode sources regenerated, Godot zero-warning build and threaded selftest,
real-scene compositor trace across all eight cells, Ghidra `/v2` annotations saved, and user confirmation real-scene compositor trace across all eight cells, Ghidra `/v2` annotations saved, and user confirmation
on the normal client path. The automated-shot wrong-CG state remains the pre-existing separate gotcha. on the normal client path. The automated-shot wrong-CG state remains the pre-existing separate gotcha.
### A2b -- SC0000 post-movie AE001H travel-path correction ✅ DONE (2026-07-11)
The remaining off-screen motion is a separate `0x228` query-model bug, not part of the spritesheet crop.
Native `0x228` decomposes and returns the retained object's target translation (`obj+0x1ac..+0x1b4`), while
the C# VM currently returns its static draw/base position `V24`. For AE001H this changes the first `0x220`
target from `(40,-20)` to `(400,0)`; because the compositor also retains the `(360,20)` draw base, the
sprite was displaced to approximately x=760 before cyclic rotation. `GfxState.TryQueryTranslationTarget`
now exposes the retained target independently of `V24`; VM dispatch matches the native 0/1 status convention
and preserves output operands on a miss. Bytecode-level tests cover the exact three targets `(40,-20)`,
`(50,-80)`, `(130,-100)` plus missing-object behavior. Validation: engine **149/149**, zero-warning Godot
build, and threaded `SELFTEST OK`. Current static SC0000 coverage is **94/129 distinct ops handled
(72.9%)**: 82 implemented + 12 safe no-ops, leaving 35 GAP ops / 88 GAP instructions. By instruction
frequency, 16,169/16,257 are handled (**99.5%**); this gauge does not replace manual end-to-end fidelity.

View File

@@ -8,26 +8,83 @@ public class AnimChannelTests
{ {
private static OpcodeTable T() => OpcodeTableJson.Load(Paths.OpcodesJson); private static OpcodeTable T() => OpcodeTableJson.Load(Paths.OpcodesJson);
private static Operand G(int addr) => new(3, addr); private static Operand G(int addr) => new(3, addr);
private static Operand I(long value) => new(0, value);
private static (int, Operand[]) Exit() => (0x2, System.Array.Empty<Operand>()); private static (int, Operand[]) Exit() => (0x2, System.Array.Empty<Operand>());
[Fact] [Fact]
public void Op0x22f_SetsPosition_Op0x228_QueriesItBack() public void Op0x228_QueriesTranslationTarget_NotBasePosition()
{ {
var t = T(); var t = T();
// g1=handle, g3=x=50, g4=y=60, g5=z=0; op 0x22f sets V24; op 0x228 reads it into g11/g12/g13. // AE001H has draw/base V24=(360,20), independent of its op-0x220 translation target (40,-20).
var scene = ScriptAssembler.Assemble(t, "GFX", new List<(int, Operand[])> var scene = ScriptAssembler.Assemble(t, "GFX", new List<(int, Operand[])>
{ {
(0x55, new[]{G(1), new Operand(0,0x1000)}), (0x55, new[]{G(2), new Operand(0,0)}), (0x55, new[]{G(1), I(0xcf3a)}), (0x55, new[]{G(2), I(0)}),
(0x55, new[]{G(3), new Operand(0,50)}), (0x55, new[]{G(4), new Operand(0,60)}), (0x55, new[]{G(5), new Operand(0,0)}), (0x55, new[]{G(3), I(360)}), (0x55, new[]{G(4), I(20)}), (0x55, new[]{G(5), I(0)}),
(0x22f, new[]{G(1), G(2), G(3), G(4), G(5)}), // set position (0x22f, new[]{G(1), G(2), G(3), G(4), G(5)}),
(0x228, new[]{G(10), G(1), G(11), G(12), G(13)}), // query position -> g11,g12,g13 (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)}),
(0x220, new[]{G(1), G(2), G(6), G(7), G(8), G(5)}),
(0x228, new[]{G(10), G(1), G(11), G(12), G(13)}),
Exit(), Exit(),
}, 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(50, vm.Globals[11]); Assert.Equal(40, vm.Globals[11]);
Assert.Equal(60, vm.Globals[12]); Assert.Equal(-20, vm.Globals[12]);
Assert.Equal(0, vm.Globals[13]);
Assert.Equal(0, vm.Globals[10]); // success flag Assert.Equal(0, vm.Globals[10]); // success flag
Assert.Equal((360L, 20L, 0L), vm.Gfx.TryGet(0xcf3a)!.V24);
}
[Fact]
public void Op0x228_MissingObject_SetsFailureAndPreservesOutputs()
{
var t = T();
var scene = ScriptAssembler.Assemble(t, "GFX_MISSING", new List<(int, Operand[])>
{
(0x55, new[]{G(1), I(0xdead)}), (0x55, new[]{G(11), I(7)}),
(0x55, new[]{G(12), I(8)}), (0x55, new[]{G(13), I(9)}),
(0x228, new[]{G(10), G(1), G(11), G(12), G(13)}), Exit(),
}, System.Array.Empty<string>());
var vm = new VirtualMachine(scene, t, new RecordingHost());
vm.Run();
Assert.Equal(1, vm.Globals[10]);
Assert.Equal((7L, 8L, 9L), (vm.Globals[11], vm.Globals[12], vm.Globals[13]));
}
[Fact]
public void Ae001hThreeLegSequence_BuildsEachTargetFromPreviousTranslationTarget()
{
var t = T();
var ops = new List<(int, Operand[])>
{
(0x55, new[]{G(1), I(0xcf3a)}), (0x55, new[]{G(2), I(0)}), (0x55, new[]{G(3), I(300)}),
(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(41), I(60)}), (0x51, new[]{G(41), I(0), G(41)}),
(0x22f, new[]{G(1), G(2), G(4), G(5), G(6)}),
(0x228, new[]{G(10), G(1), G(11), G(12), G(13)}),
(0x50, new[]{G(11), G(11), I(40)}), (0x50, new[]{G(12), G(12), G(40)}),
(0x220, new[]{G(1), G(2), G(3), G(11), G(12), G(13)}),
(0x228, new[]{G(20), G(1), G(21), G(22), G(23)}),
(0x50, new[]{G(21), G(21), I(10)}), (0x50, new[]{G(22), G(22), G(41)}),
(0x220, new[]{G(1), G(2), G(3), G(21), G(22), G(23)}),
(0x228, new[]{G(30), G(1), G(31), G(32), G(33)}),
(0x50, new[]{G(31), G(31), I(80)}), (0x50, new[]{G(32), G(32), G(40)}),
(0x220, new[]{G(1), G(2), G(3), G(31), G(32), G(33)}),
Exit(),
};
var vm = new VirtualMachine(ScriptAssembler.Assemble(t, "AE001H_TRAVEL", ops,
System.Array.Empty<string>()), t, new RecordingHost());
vm.Run();
Assert.Equal((40L, -20L, 0L), (vm.Globals[11], vm.Globals[12], vm.Globals[13]));
Assert.Equal((50L, -80L, 0L), (vm.Globals[21], vm.Globals[22], vm.Globals[23]));
Assert.Equal((130L, -100L, 0L), (vm.Globals[31], vm.Globals[32], vm.Globals[33]));
Assert.Equal((130.0, -100.0, 0.0), vm.Gfx.TryGet(0xcf3a)!.TranslationTarget);
} }
[Fact] [Fact]

View File

@@ -448,6 +448,22 @@ public sealed class GfxState
} }
} }
/// <summary>Op 0x228: query the translation target decomposed from the native target matrix at
/// obj+0x17c (translation obj+0x1ac/+0x1b0/+0x1b4). This is independent of draw/base position V24.</summary>
public bool TryQueryTranslationTarget(long handle, out (double X, double Y, double Z) target)
{
lock (_lock)
{
if (_objects.TryGetValue(handle, out var o))
{
target = o.TranslationTarget;
return true;
}
target = default;
return false;
}
}
/// <summary>Op 0x21f: delayed one-shot axis-angle rotation target, sharing obj+0x34's start timestamp.</summary> /// <summary>Op 0x21f: delayed one-shot axis-angle rotation target, sharing obj+0x34's start timestamp.</summary>
public void SetRotationChannel(long handle, long delayMs, long durationMs, public void SetRotationChannel(long handle, long delayMs, long durationMs,
(long X, long Y, long Z) axis, long angleDegrees) (long X, long Y, long Z) axis, long angleDegrees)

View File

@@ -399,12 +399,15 @@ public sealed class VirtualMachine
Gfx.SetSrcRect(Read(a[0]), Read(a[2]), Read(a[3]), 0, Read(a[1])); return pc + 1; Gfx.SetSrcRect(Read(a[0]), Read(a[2]), Read(a[3]), 0, Read(a[1])); return pc + 1;
case "u00421EF0": // 0x232 anim color/glow: (handle)(period)(alpha)(color) — ping-pong the color case "u00421EF0": // 0x232 anim color/glow: (handle)(period)(alpha)(color) — ping-pong the color
Gfx.SetColorAnim(Read(a[0]), Read(a[1]), GfxState.PackColor(Read(a[2]), Read(a[3]))); return pc + 1; Gfx.SetColorAnim(Read(a[0]), Read(a[1]), GfxState.PackColor(Read(a[2]), Read(a[3]))); return pc + 1;
case "u00421940": // 0x228 query-position: (succ)(handle)(outX)(outY)(outZ) <- current V24 case "u00421940": // 0x228: (succ)(handle)(outX)(outY)(outZ) <- target translation matrix
{ {
var obj = Gfx.TryGet(Read(a[1])); if (Gfx.TryQueryTranslationTarget(Read(a[1]), out var v))
var v = obj?.V24 ?? default; {
Write(a[2], v.X); Write(a[3], v.Y); Write(a[4], v.Z); Write(a[2], (long)v.X); Write(a[3], (long)v.Y); Write(a[4], (long)v.Z);
Write(a[0], obj != null ? 0 : 1); return pc + 1; Write(a[0], 0);
}
else Write(a[0], 1); // native missing-object path leaves output operands untouched
return pc + 1;
} }
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;

View File

@@ -5752,12 +5752,12 @@ abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u00421940" name = "u00421940"
category = "draw" category = "draw"
summary = "0x228 query-position (succ)(handle)(outX)(outY)(outZ): read the object's current computed position into vars (worker FUN_0047cdd0). C# VM: writes V24 + success flag. See docs/engine-re.md §SC0000 anim cluster." summary = "0x228 query translation target (succ)(handle)(outX)(outY)(outZ): clone the retained object, decompose its target translation matrix at obj+0x17c, and return matrix translation obj+0x1ac/+0x1b0/+0x1b4. Returns succ=0 when found; when absent, writes succ=1 and leaves outputs untouched. The C# VM queries TranslationTarget independently of V24. See docs/engine-re.md §SC0000 anim cluster."
noop_headless = false noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = []
evidence = "" evidence = "Ghidra /v2 handler gfx_op_0x228_query_position@0x42a3a0 calls gfx_object_query_translation_target@0x47cdd0. The worker copies the complete 0xb5-dword object record, passes copied obj+0x17c to matrix4_decompose_affine@0x48d7c8, and returns its translation outputs; the decomposition reads matrix elements +0x30/+0x34/+0x38, corresponding to obj+0x1ac/+0x1b0/+0x1b4. SC0000 AE001H queries this before each 0x220 leg. C# regression covers targets (40,-20), (50,-80), (130,-100), plus the missing-object output-preservation path."
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1