From 593a24b5109007f7078c53e0e0d73bc63d2cc634 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 11 Jul 2026 20:05:51 -0400 Subject: [PATCH] fix(gfx): restore third CG glow --- docs/engine-re.md | 28 +++++++++++- docs/opcode-reference.md | 13 +++--- docs/phase-a-slice-plan.md | 20 +++++++++ engine/Age.Engine.Tests/GfxAnimationTests.cs | 30 +++++++++++++ .../RenderObjectBlendTests.cs | 43 ++++++++++++++++--- engine/Age.Engine/Model/GfxState.cs | 19 ++++++++ engine/Age.Engine/Vm/VirtualMachine.cs | 3 ++ tools/age_opcodes_himegari.py | 1 + vm-map/opcodes.toml | 24 +++++------ 9 files changed, 154 insertions(+), 27 deletions(-) diff --git a/docs/engine-re.md b/docs/engine-re.md index d4746f7..9316e41 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -695,7 +695,10 @@ per-frame bytecode. Reversed + annotated in Ghidra: `obj+0x214`, period `obj+0x228`, and axis `obj+0x244/248/24c`. - **`gfx_object_set_rotation_cycle`** (`0x47f060`, worker for legacy op **`0x234`**) configures that channel. `gfx_object_anim_interpolate` consumes it from frame-time `ctx+0xb550` - as a repeating 0..360-degree axis rotation. Op **`0x1fd`** is a separate scaled 3-vector setter. + as a repeating 0..360-degree axis rotation. Op **`0x1fd`** + (`gfx_op_0x1fd_set_vec_scaled@0x422650` → `gfx_object_set_scale_current@0x47e6b0`) is an immediate + current-scale setter: it divides integer X/Y/Z percentages by 100, marks scale state at `obj+0x68`, and + writes `matrix4_make_scale` at `obj+0x6c`. - **Op `0x238`** (`gfx_op_0x238_set_anim_clock`) sets a **global animation clock**, **non-blocking**: `ctx+0x51b78 = 0` (elapsed), `ctx+0x51b7c = duration` (the max per-object duration; SC0000 `label_1235a` maxes a table to compute it). It does **not** loop/wait. @@ -763,7 +766,28 @@ Ghidra functions renamed + plate-commented, saved). `SRCALPHA/INVSRCALPHA`, 2/3 additive/special for glow/flash) and passes a modulation color/alpha to the device draw. Slice A ports the **alpha** path (fades); additive (glow) is deferred (its `local_2c` source field is `obj+0x30`, the value written by op `0x203`. Mode 1 uses packed ARGB alpha as opacity and RGB - as multiplicative D3D modulation. Modes 2/3 remain separately scoped beyond the completed mode-1 path. + as multiplicative D3D modulation. For a **textured mode-0** object, preserved `0xffffffff` is opaque + identity, not a request to replace the texture with white; the packed alpha byte is therefore not a + generic tint-strength control. Modes 2/3 remain separately scoped beyond the completed mode-1 path, and + surfaceless mode-0 fills remain a distinct consumer case. + +**SC0000 third-CG white-screen and missing-glow fix (2026-07-11).** The page containing +`大役を担ったのは…` reaches the intended EV052DA image and both animated AE001D layers. A synchronized +Godot capture proved AE001D is correctly alpha-bearing and drawn in mode 1 at only 6-8% object opacity; +it was not the white wall. During the preceding `0x223` EV052CA→EV052DA crossfade, base handle `0xcb2a` +renders EV052DA in mode 2 with identity modulation. When the transition ends, `0x203@0x12478` restores that +same textured object to mode 0 with negative color operands, preserving `0xffffffff`. Native keeps EV052DA +visible; the old port resolved the state as `tintStr=1.00` and lerped every source texel to white. Textured +mode 0 now resolves as opaque multiplicative RGB, making white identity, while surfaceless mode-0 fill +strength remains separate. + +The first corrected capture exposed a second independent gap: AE001D was submitted and rotating but had no +visible pixel contribution. Its 800×800 circle was based at `(0,550)` around anchor `(400,950)` and remained +at the port's default 100% scale, almost wholly below the viewport. SC0000 executes `0x1fd` with +`210/210/100` and `240/240/100`; implementing the native immediate current-scale setter expands the circles +into the viewport (the first top edge reaches y=110). The user positively confirmed both the restored CG and +visible glow on the ordinary interactive path. This glow is a retained scaled/rotating texture, not an +`0x231` spritesheet-cell animation; later missing spritesheet effects remain a separate issue. **Interpolation RE pass (2026-07-08, stalled → both deferrals confirmed).** Attempted to pin how a `0x202` fade animates so smooth ramping could join slice A. Findings (Ghidra `gfx_object_anim_interpolate` diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md index 009d6f8..cd48b79 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -151,6 +151,11 @@ SC0000 label_1235a reaches this when 0x1c7/0x1cc are zero. Native run-state bit - **grounding:** source=investigation, confidence=med - **evidence:** SC0000 CG-load subroutine label_12649: `draw-texture (ptr) (slot) 0 0 (w) (h) (dstx) (dsty)`; full-screen slot-3 draws use 0x320x0x258 (800x600). +### 0x1fd `gfx-set-scale-current` (u00420620, argc 4) +- **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. +- **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. + ### 0x1ff `set-gfx-geom3-c` (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. - **grounding:** source=investigation, confidence=high @@ -162,9 +167,9 @@ SC0000 label_1235a reaches this when 0x1c7/0x1cc are zero. Native run-state bit - **evidence:** Ghidra handler 0x4228d0 packs operands 4/5 and calls worker 0x47ea00(handle,delay,duration,packed). Consumer 0x472f00: shared start +0x34; color delay/duration +0x38/+0x4c; current/target +0x60/+0x64; frame clock ctx+0xb550; bytewise integer LERP; natural or ctx+0xb55c forced completion. /v2 annotated and saved 2026-07-10. ### 0x203 `gfx-draw-color` (gfx-draw-color, argc 4) -- **summary:** 0x203 (handle)(mode)(alpha)(color) — gfx cmd-type 9. Worker stores the D3D blend selector at obj+0x30 and STATIC packed color at obj+0x60. Negative alpha/RGB preserve current static bytes. Mode 0 retains established opaque CG/tint/fill behavior; mode 1 is SRCALPHA/INVSRCALPHA with ARGB alpha opacity and multiplicative RGB modulation; mode 2 is the 0x223 transition-source identity path. +- **summary:** 0x203 (handle)(mode)(alpha)(color) — gfx cmd-type 9. Worker stores the D3D blend selector at obj+0x30 and STATIC packed color at obj+0x60. Negative alpha/RGB preserve current static bytes. Mode 0 is the opaque textured path: preserved 0xffffffff is identity (the alpha byte is not tint strength); mode 1 is SRCALPHA/INVSRCALPHA with ARGB alpha opacity and multiplicative RGB modulation; mode 2 is the 0x223 transition-source identity path. Surfaceless mode-0 fill consumption remains a distinct case. - **grounding:** source=investigation, confidence=high -- **evidence:** Ghidra handler 0x4229a0; negative operands read current obj+0x60, then worker 0x47e9b0 stores op2 at obj+0x30 and ARGB at +0x60. gfx_object_composite call-site 0x47f78f passes +0x30/+0x60 directly to gfx_object_blit_d3d9; mode 1 sets D3DRS SRCALPHA/INVSRCALPHA and the packed color is the device draw modulation. Mode 2 transition setup and synchronized pixels prove 0xffffffff is identity, not solid white. +- **evidence:** Ghidra handler 0x4229a0; negative operands read current obj+0x60, then worker 0x47e9b0 stores op2 at obj+0x30 and ARGB at +0x60. gfx_object_composite call-site 0x47f78f passes +0x30/+0x60 directly to gfx_object_blit_d3d9; mode 1 sets D3DRS SRCALPHA/INVSRCALPHA and the packed color is the device draw modulation. Mode 2 transition setup and synchronized pixels prove 0xffffffff is identity, not solid white. SC0000 page 14 adds the mode-0 endpoint proof: after the EV052CA->EV052DA 0x223 crossfade, 0x203@0x12478 restores the base CG to mode 0 with preserved 0xffffffff; native keeps EV052DA visible while the port's tint-strength interpretation turns every texel white. ### 0x208 `get-texture-size` (get-texture-size, argc 3) - **summary:** 0x208 (slot)(out_w)(out_h) — writes the loaded texture's width/height into two output globals; keystone for bytecode-computed sprite/bg geometry (SC0000 label_12649) @@ -989,10 +994,6 @@ 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 -### 0x1fd `u00420620` (u00420620, argc 4) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - ### 0x1fe `u004206C0` (u004206C0, argc 5) - **summary:** — - **grounding:** source=kelebek, confidence=low diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index 31e4d78..4564754 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -1362,3 +1362,23 @@ may add a fully opaque/unmodulated row-copy path as optional incremental work. **Manual validation:** the user tested the ordinary interactive path—the path whose CG state and fade cadence differ from the console capture—and reported a **massive improvement**. This confirms the identity blit/fill fast path materially improves real fade responsiveness, not merely the isolated kernel benchmark. + +### SC0000 third-CG white screen and AE001D glow fixed (2026-07-11) + +The page-14 screenshot path reproduced the user's all-white EV052DA page exactly. Synchronized compositor +and opcode evidence excluded the animated glow assets as the white owner: both AE001D layers load +successfully, retain their AGF alpha, rotate, and render in mode 1 at 6-8% opacity. Base handle `0xcb2a` +was correct through the +`0x223` EV052CA→EV052DA transition (`mode=2`, identity), then `0x203@0x12478` restores it to textured mode 0 +with preserved `0xffffffff`. The old port resolved that endpoint as `tintStr=1.00`, whitening every EV052DA +pixel. Textured mode 0 now uses opaque multiplicative RGB modulation, while surfaceless mode-0 fills retain +their existing strength behavior. The transition cleanup regression covers mode 2 → mode 0 `(-1,-1)`. + +The resulting non-white capture then proved the glow itself still contributed no visible pixels. Native RE +closed the causal gap: `0x1fd(handle,x%,y%,z%)` immediately writes the current scale matrix at `obj+0x6c`, +distinct from `0x21e`'s one-shot target. SC0000 scales the two AE001D circles to 210% and 240%; the stub left +them at 100%, almost entirely below the viewport. `GfxState.SetCurrentScale` and VM dispatch now reproduce +the native matrices, with focused geometry/dispatch tests. Validation: engine **145/145**, opcode build/lint, +Godot build/capture, Ghidra annotations saved, and user confirmation that the CG and animated glow are both +visible. This AE001D path is a retained scaled/rotating texture; later missing `0x231` spritesheet-cell +effects remain separately scoped. diff --git a/engine/Age.Engine.Tests/GfxAnimationTests.cs b/engine/Age.Engine.Tests/GfxAnimationTests.cs index ff252e5..266311e 100644 --- a/engine/Age.Engine.Tests/GfxAnimationTests.cs +++ b/engine/Age.Engine.Tests/GfxAnimationTests.cs @@ -24,6 +24,22 @@ public class GfxAnimationTests Assert.True(o.TranslationEnabled); } + [Fact] + public void CurrentScaleSetter_ExpandsGlowAroundItsAnchor() + { + var g = new GfxState(); + g.SetSurface(6, 0x29, -1); + g.BindDraw(0xcb8e, 6, 0, 0, 800, 800, 0, 550); + g.GetOrCreate(0xcb8e).V18 = (400, 950, 0); + g.SetCurrentScale(0xcb8e, (210, 210, 100)); + + var v = g.SnapshotVisibleObjects().Single(); + var top = Transform2DMath.Build(v.Transform).FromLocalOrigin(v.DstX, v.DstY).Apply(400, 0); + Assert.Equal(2.1, v.Transform.ScaleX, 3); + Assert.Equal(2.1, v.Transform.ScaleY, 3); + Assert.Equal(110, top.Y, 3); + } + [Fact] public void RotationCycle_DoesNotOverwriteMatrixChannels() { @@ -140,6 +156,20 @@ public class GfxAnimationTests Assert.True(o.RotationChannelEnabled); } + [Fact] + public void Op0x1fd_SetsCurrentScalePercentages() + { + var t = T(); + var scene = ScriptAssembler.Assemble(t, "STATIC_SCALE", new List<(int, Operand[])> + { + MovGI(1, 0xcb8e), MovGI(2, 210), MovGI(3, 240), MovGI(4, 100), + (0x1fd, new[] { G(1), G(2), G(3), G(4) }), Exit(), + }, System.Array.Empty()); + var vm = new VirtualMachine(scene, t, new RecordingHost()); + vm.Run(); + Assert.Equal((2.1, 2.4, 1.0), vm.Gfx.TryGet(0xcb8e)!.ScaleCurrent); + } + [Fact] public void RotationCycleAndClock_DispatchRemainSeparate() { diff --git a/engine/Age.Engine.Tests/RenderObjectBlendTests.cs b/engine/Age.Engine.Tests/RenderObjectBlendTests.cs index 62bab7c..20ae9ce 100644 --- a/engine/Age.Engine.Tests/RenderObjectBlendTests.cs +++ b/engine/Age.Engine.Tests/RenderObjectBlendTests.cs @@ -46,24 +46,53 @@ public class RenderObjectBlendTests } [Fact] - public void DrawColor_0x203_IsTintStrength_ObjectStaysOpaque() + public void TexturedMode0_UsesOpaqueMultiplicativeRgbAndIgnoresPackedAlpha() { - // Op 0x203/0x202 alpha is TINT-BLEND STRENGTH, not object opacity (evidence: a CG drawn with - // (alpha=0, color=white) must stay fully OPAQUE + untinted, not vanish). Root cause of the grey-BG. + // Mode 0 is the native opaque textured path. Its packed alpha byte is neither object opacity nor + // tint strength; RGB multiplicatively modulates the source, so white is identity. var g = WithVisibleObject(0x100, resId: 5, colorKey: -1); g.SetObjectColor(0x100, GfxState.PackColor(0x00, 0xFFFFFF)); // "no tint" — the grey-BG case var ro = g.SnapshotVisibleObjects().Single(); - Assert.Equal(255, ro.Alpha); // OBJECT STAYS OPAQUE (was wrongly 0 -> invisible) + Assert.Equal(255, ro.Alpha); // opaque Assert.Equal(0, ro.TintStrength); // zero tint strength + Assert.True(ro.MultiplyTint); - g.SetObjectColor(0x100, GfxState.PackColor(0x80, 0x102030)); // half-strength tint toward 0x102030 + g.SetObjectColor(0x100, GfxState.PackColor(0x80, 0x102030)); var ro2 = g.SnapshotVisibleObjects().Single(); - Assert.Equal(255, ro2.Alpha); // still opaque - Assert.Equal(0x80, ro2.TintStrength); // strength from the alpha byte + Assert.Equal(255, ro2.Alpha); // packed alpha remains ignored + Assert.Equal(0, ro2.TintStrength); Assert.Equal(0x102030, ro2.Tint); + Assert.True(ro2.MultiplyTint); Assert.Equal(BlendKind.Alpha, ro2.Blend); } + [Fact] + public void TransitionCleanup_Mode2ToMode0PreservedWhite_RemainsIdentity() + { + var g = WithVisibleObject(0x100, resId: 5, colorKey: -1); + g.SetStaticObjectColorResolved(0x100, 2, -1, -1); // 0x128de: transition source + Assert.Equal(0, g.SnapshotVisibleObjects().Single().TintStrength); + + g.SetStaticObjectColorResolved(0x100, 0, -1, -1); // 0x12478: transition cleanup + var rendered = g.SnapshotVisibleObjects().Single(); + Assert.Equal(255, rendered.Alpha); + Assert.Equal(0xffffff, rendered.Tint); + Assert.Equal(0, rendered.TintStrength); + Assert.True(rendered.MultiplyTint); + } + + [Fact] + public void SurfacelessMode0_RetainsFillStrength() + { + var g = new GfxState(); + g.BindDraw(0x100, 1, 0, 0, 10, 10, 0, 0); + g.SetObjectColor(0x100, GfxState.PackColor(0x80, 0x102030)); + var rendered = g.SnapshotVisibleObjects().Single(); + Assert.Equal(255, rendered.Alpha); + Assert.Equal(0x80, rendered.TintStrength); + Assert.False(rendered.MultiplyTint); + } + [Fact] public void Mode1_UsesArgbAlphaAsOpacityAndRgbAsMultiplicativeModulation() { diff --git a/engine/Age.Engine/Model/GfxState.cs b/engine/Age.Engine/Model/GfxState.cs index d7a6678..c382d72 100644 --- a/engine/Age.Engine/Model/GfxState.cs +++ b/engine/Age.Engine/Model/GfxState.cs @@ -410,6 +410,17 @@ public sealed class GfxState } } + /// Op 0x1fd: immediately replace the object's current scale matrix. Script operands are + /// integer percentages; native divides them by 100 before matrix4_make_scale at obj+0x6c. + public void SetCurrentScale(long handle, (long X, long Y, long Z) percent) + { + lock (_lock) + { + var o = GetOrCreate(handle); + o.ScaleCurrent = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0); + } + } + /// Op 0x21e: normalized scale target (100 = identity), with independent delay/duration. public void SetScaleChannel(long handle, long delayMs, long durationMs, (long X, long Y, long Z) percent) { @@ -520,8 +531,16 @@ public sealed class GfxState // not a request to replace every texel with white. alpha = a; strength = 0; blend = BlendKind.Alpha; } + else if (resId != 0) + { + // Native mode 0 is the opaque textured path. RGB modulates the source and the + // packed alpha byte does not become tint strength; in particular 0xffffffff is + // identity after the foreground-transition cleanup write. + alpha = 255; strength = 0; blend = BlendKind.Alpha; multiplyTint = true; + } else { + // A surfaceless mode-0 object is a solid fill, whose high byte remains fill strength. strength = a; blend = BlendKind.Alpha; } } diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index 4a43b89..cd4e6c9 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -404,6 +404,9 @@ public sealed class VirtualMachine 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 Gfx.GetOrCreate(Read(a[0])).V16c = (Read(a[1]), Read(a[2]), Read(a[3])); return pc + 1; + case "u00420620": // upstream ABI label + 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; case "set-gfx-field64": // 0x212 (idx)(val) Gfx.GetOrCreate(Read(a[0])).Field64 = Read(a[1]); return pc + 1; case "set-gfx-xy": // 0x213 (idx)(x)(y) diff --git a/tools/age_opcodes_himegari.py b/tools/age_opcodes_himegari.py index bde70b2..ffe842c 100644 --- a/tools/age_opcodes_himegari.py +++ b/tools/age_opcodes_himegari.py @@ -21,6 +21,7 @@ INFERRED: dict[int, dict] = { 0x1d5: dict(name='cond-block', category='marker', noop=True, confidence='high', source='inference', summary='zero-arg; ALWAYS follows jcc — marks conditional body entry'), 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'), + 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."), 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?'), 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'), diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index a16efea..9ce95b3 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -4676,33 +4676,33 @@ argc = 4 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u00420620" -category = "unknown" -summary = "" +name = "gfx-set-scale-current" +category = "draw" +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." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +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." [[opcode.semantics.args]] i = 1 -role = "" +role = "retained object handle" observed_types = ["imm", "g-int", "l-int"] [[opcode.semantics.args]] i = 2 -role = "" +role = "current X scale percent" observed_types = ["imm", "g-int", "l-int", "l-ptr"] [[opcode.semantics.args]] i = 3 -role = "" +role = "current Y scale percent" observed_types = ["imm", "g-int", "l-int"] [[opcode.semantics.args]] i = 4 -role = "" +role = "current Z scale percent" observed_types = ["imm"] [[opcode]] @@ -4832,12 +4832,12 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "gfx-draw-color" category = "draw" -summary = "0x203 (handle)(mode)(alpha)(color) — gfx cmd-type 9. Worker stores the D3D blend selector at obj+0x30 and STATIC packed color at obj+0x60. Negative alpha/RGB preserve current static bytes. Mode 0 retains established opaque CG/tint/fill behavior; mode 1 is SRCALPHA/INVSRCALPHA with ARGB alpha opacity and multiplicative RGB modulation; mode 2 is the 0x223 transition-source identity path." +summary = "0x203 (handle)(mode)(alpha)(color) — gfx cmd-type 9. Worker stores the D3D blend selector at obj+0x30 and STATIC packed color at obj+0x60. Negative alpha/RGB preserve current static bytes. Mode 0 is the opaque textured path: preserved 0xffffffff is identity (the alpha byte is not tint strength); mode 1 is SRCALPHA/INVSRCALPHA with ARGB alpha opacity and multiplicative RGB modulation; mode 2 is the 0x223 transition-source identity path. Surfaceless mode-0 fill consumption remains a distinct case." noop_headless = false source = "investigation" confidence = "high" depends_on = [] -evidence = "Ghidra handler 0x4229a0; negative operands read current obj+0x60, then worker 0x47e9b0 stores op2 at obj+0x30 and ARGB at +0x60. gfx_object_composite call-site 0x47f78f passes +0x30/+0x60 directly to gfx_object_blit_d3d9; mode 1 sets D3DRS SRCALPHA/INVSRCALPHA and the packed color is the device draw modulation. Mode 2 transition setup and synchronized pixels prove 0xffffffff is identity, not solid white." +evidence = "Ghidra handler 0x4229a0; negative operands read current obj+0x60, then worker 0x47e9b0 stores op2 at obj+0x30 and ARGB at +0x60. gfx_object_composite call-site 0x47f78f passes +0x30/+0x60 directly to gfx_object_blit_d3d9; mode 1 sets D3DRS SRCALPHA/INVSRCALPHA and the packed color is the device draw modulation. Mode 2 transition setup and synchronized pixels prove 0xffffffff is identity, not solid white. SC0000 page 14 adds the mode-0 endpoint proof: after the EV052CA->EV052DA 0x223 crossfade, 0x203@0x12478 restores the base CG to mode 0 with preserved 0xffffffff; native keeps EV052DA visible while the port's tint-strength interpretation turns every texel white." [[opcode.semantics.args]] i = 1