From b41ae361682cc7991f6e3b0ab50a60663aa9d0a0 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 11 Jul 2026 22:21:10 -0400 Subject: [PATCH] Fix AE001H cyclic color blending --- docs/engine-re.md | 21 +++++- docs/opcode-reference.md | 5 +- docs/phase-a-slice-plan.md | 14 ++++ engine/Age.Engine.Tests/AnimChannelTests.cs | 19 ++++++ .../Age.Engine.Tests/AnimInterpolatorTests.cs | 40 ++++++++++-- engine/Age.Engine/Model/GfxState.cs | 65 +++++++++++++------ engine/Age.Engine/Vm/VirtualMachine.cs | 4 +- vm-map/opcodes.toml | 8 +-- 8 files changed, 140 insertions(+), 36 deletions(-) diff --git a/docs/engine-re.md b/docs/engine-re.md index ee766e1..d78036e 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -842,7 +842,7 @@ annotated in Ghidra, saved. | `0x229` | `gfx_op_0x229_set_position` (`FUN_00472bb0`+`FUN_00472be0`) | set object **position/geometry** immediately (`obj+0x420/0x424` + vec `obj+0x440..0x448`) | | `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 | -| `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**: bit2 active, period `obj+0x220`, target `obj+0x240` → interpolator COLOR channel (ping-pong). Negative alpha/RGB preserve corresponding bytes from static color `obj+0x60`; alpha >255 clamps. Distinct from one-shot `0x202`/static `0x203` | | `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 | @@ -899,6 +899,25 @@ port now queries `TranslationTarget` independently of `V24`, returns the native 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)`. +**AE001H white-pulse verification (2026-07-11):** the eight source frames contain only the expected +purple artwork; the white wash is introduced by the port's `0x232` path. SC0000 `0x1a0e` explicitly arms +`0x232(handle,1200,224,-1)`. Native `gfx_op_0x232_anim_color` treats negative RGB as a sentinel and fetches +the object's current static packed color before constructing the target. The C# dispatch instead calls +`PackColor(224,-1)`, whose RGB mask becomes `0xffffff`, and `SnapshotVisibleObjects` maps animated alpha +to mode-0 tint strength. Existing retained-state evidence shows `tintStr` cycling `0 → 0.82 → 0` on +AE001H, exactly matching the reported white pulse. Follow-up native dataflow closes the remaining question: +fresh objects initialize static color `obj+0x60` to `0xffffffff`; the interpolator samples that temporary +color toward target `obj+0x240`, then `gfx_object_composite` passes the sample and the unchanged blend +selector `obj+0x30` to `gfx_object_blit_d3d9`. Mode 0 enables no alpha blending and uses RGB only as vertex +modulation, while mode 1 enables SRCALPHA/INVSRCALPHA. AE001H therefore cycles +`0xffffffff ↔ 0xe0ffffff`: identity RGB throughout, with alpha intentionally inert in mode 0, so native has +no visible pulse. The faithful fix is now fully bounded: initialize/resolve static color correctly, sample +packed ARGB, and consume it through the existing mode-specific blend path instead of converting animated +alpha into tint strength. The port now initializes static color to native identity `0xffffffff`, resolves +negative operands in `SetColorAnimResolved`, samples packed ARGB before blend selection, and feeds it through +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. + **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. diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md index 8aa993f..6d9f386 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -287,8 +287,9 @@ Native handler gfx_op_0x20c_present_frame -> gfx_render_frame @0x4820b0. This is - **evidence:** Native /v2 decompile: worker stores period at obj+0x230, frame_count at +0x238, columns at +0x23c. Interpolator computes ((now-start)/period)%frame_count, then offsets both source-rect X bounds by rect_width*(frame%columns) and Y bounds by rect_height*(frame/columns). SC0000 uses (100,8,4) with AE001H's eight 200x200 cells in a 4x2 800x400 sheet. ### 0x232 `u00421EF0` (u00421EF0, argc 4) -- **summary:** 0x232 anim-color (handle)(period)(alpha)(color): ping-pong the object color/alpha toward the packed target over period ms (pulsing GLOW). Worker gfx_worker_anim_color @0x47ef50 -> interpolator COLOR channel. C# VM: GfxState.SetColorAnim. See docs/engine-re.md §SC0000 anim cluster. -- **grounding:** source=kelebek, confidence=low +- **summary:** 0x232 anim-color (handle)(period)(alpha)(color): ping-pong the temporary packed ARGB passed to the normal object blit. Handler resolves negative alpha/RGB from static color obj+0x60 and clamps alpha above 255. Blend selector obj+0x30 is unchanged: mode 0 keeps default opaque blending (animated alpha is inert; RGB is vertex modulation), while mode 1 consumes ARGB alpha as opacity. Fresh static color is 0xffffffff. The C# VM resolves sentinels and consumes sampled ARGB through the unchanged mode-specific path. See docs/engine-re.md §SC0000 anim cluster. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra /v2: gfx_op_0x232_anim_color@0x423c30 resolves sentinels then calls gfx_worker_anim_color@0x47ef50; gfx_object_anim_interpolate@0x473ed0 samples static obj+0x60 toward target obj+0x240 into a temporary packed color; gfx_object_composite@0x47f650 passes that color plus unchanged selector obj+0x30 to gfx_object_blit_d3d9@0x4774c0. Blit mode 0 enables no alpha blend and passes RGB as modulation; mode 1 enables SRCALPHA/INVSRCALPHA. gfx_object_init_default@0x472810 initializes obj+0x60=0xffffffff. SC0000 0x1a0e (handle,1200,224,-1) is therefore 0xffffffff<->0xe0ffffff with inert alpha and identity RGB: no visible pulse. C# regressions cover exact AE001H visual invariance, negative-RGB preservation, mode-0 RGB modulation, and mode-1 alpha opacity. ### 0x234 `anim-start` (anim-start, argc 5) - **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. diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index 76c9944..19dfd41 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -1419,3 +1419,17 @@ and preserves output operands on a miss. Bytecode-level tests cover the exact th 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. + +### A2b -- SC0000 AE001H white-pulse correction ✅ DONE (2026-07-11) + +Manual movement confirmation exposed a separate color artifact. The sheet itself has no white frames. +SC0000 later calls `0x232(handle,1200,224,-1)`; native treats RGB `-1` as "preserve current static RGB," +whereas the port masks it to white and drives mode-0 tint strength from the animated alpha. The compositor +log correspondingly cycles `tintStr` from 0 to about 0.82 and back. This identifies the white wash as a port +bug, not intended artwork. Native dataflow confirms fresh static color is `0xffffffff`; `0x232` samples +`0xffffffff ↔ 0xe0ffffff` and passes it through unchanged blend mode 0, whose alpha is inert and whose +white RGB is identity modulation. The port now resolves native default/static color and negative sentinels, +samples packed ARGB, and applies it through normal mode-specific blending rather than a separate tint-strength +convention. Focused tests cover exact AE001H invariance, mode-0 RGB modulation, and mode-1 alpha opacity. +Validation: engine **152/152**, zero-warning Godot build, and threaded `SELFTEST OK`; manual confirmation is +the remaining visual gate. diff --git a/engine/Age.Engine.Tests/AnimChannelTests.cs b/engine/Age.Engine.Tests/AnimChannelTests.cs index b6c30dc..2ace38d 100644 --- a/engine/Age.Engine.Tests/AnimChannelTests.cs +++ b/engine/Age.Engine.Tests/AnimChannelTests.cs @@ -105,6 +105,25 @@ public class AnimChannelTests Assert.Equal(GfxState.PackColor(0x80, 0xFF0000), o.ColorTarget); } + [Fact] + public void Op0x232_NegativeRgbPreservesNativeDefaultStaticColor() + { + var t = T(); + var scene = ScriptAssembler.Assemble(t, "AE001H_COLOR", new List<(int, Operand[])> + { + (0x55, new[]{G(1), I(0xcb8e)}), (0x55, new[]{G(2), I(1200)}), + (0x55, new[]{G(3), I(224)}), (0x55, new[]{G(4), I(1)}), + (0x51, new[]{G(4), I(0), G(4)}), + (0x232, new[]{G(1), G(2), G(3), G(4)}), Exit(), + }, System.Array.Empty()); + var vm = new VirtualMachine(scene, t, new RecordingHost()); + vm.Run(); + var o = vm.Gfx.TryGet(0xcb8e)!; + Assert.Equal(0xffffffff, o.Color); + Assert.Equal(0xe0ffffff, o.ColorTarget); + Assert.Equal(0, o.StaticColorMode); + } + [Fact] public void Op0x239_SetsSpritesheetGridCell() { diff --git a/engine/Age.Engine.Tests/AnimInterpolatorTests.cs b/engine/Age.Engine.Tests/AnimInterpolatorTests.cs index 794ac28..cf203a5 100644 --- a/engine/Age.Engine.Tests/AnimInterpolatorTests.cs +++ b/engine/Age.Engine.Tests/AnimInterpolatorTests.cs @@ -45,17 +45,43 @@ public class AnimInterpolatorTests } [Fact] - public void ColorAnim_PingPongsTintStrength_ObjectStaysOpaque() + public void ColorAnim_Mode0Textured_UsesRgbModulationAndIgnoresAlpha() { var g = VisibleObj(0x100); - // base = no tint (strength 0); target = full-strength (alpha 0xff) red glow => strength pulses 0<->255 - g.SetColorAnim(0x100, period: 1000, target: GfxState.PackColor(0xFF, 0xFF0000)); + g.SetColorAnim(0x100, period: 1000, target: GfxState.PackColor(0x80, 0xFF0000)); g.SnapshotVisibleObjects(0); // seeds start=0 var baseFrame = g.SnapshotVisibleObjects(0).Single(); - Assert.Equal(255, baseFrame.Alpha); // object opacity ALWAYS opaque (never the color alpha) - Assert.Equal(0, baseFrame.TintStrength); // t=0 -> no tint var peak = g.SnapshotVisibleObjects(500).Single(); - Assert.Equal(255, peak.Alpha); // still opaque - Assert.Equal(255, peak.TintStrength); // t=1 -> full tint strength (the glow peak) + Assert.Equal((255, 0xffffffL, 0, true), + (baseFrame.Alpha, baseFrame.Tint, baseFrame.TintStrength, baseFrame.MultiplyTint)); + Assert.Equal((255, 0xff0000L, 0, true), + (peak.Alpha, peak.Tint, peak.TintStrength, peak.MultiplyTint)); + } + + [Fact] + public void ColorAnim_Mode1_UsesSampledAlphaAsOpacity() + { + var g = VisibleObj(0x100); + g.SetStaticObjectColorResolved(0x100, mode: 1, alpha: 255, rgb: 0xffffff); + g.SetColorAnim(0x100, period: 1000, target: GfxState.PackColor(0x80, 0xffffff)); + g.SnapshotVisibleObjects(0); + var peak = g.SnapshotVisibleObjects(500).Single(); + Assert.Equal(0x80, peak.Alpha); + Assert.Equal(0, peak.TintStrength); + Assert.True(peak.MultiplyTint); + } + + [Fact] + public void Ae001hMode0AlphaCycle_IsVisuallyInert() + { + var g = VisibleObj(0x100); + g.SetColorAnimResolved(0x100, period: 1200, alpha: 224, rgb: -1); + var start = g.SnapshotVisibleObjects(1000).Single(); + var peak = g.SnapshotVisibleObjects(1600).Single(); + Assert.Equal(0xe0ffffff, g.TryGet(0x100)!.ColorTarget); + Assert.Equal((255, 0xffffffL, 0, true), + (start.Alpha, start.Tint, start.TintStrength, start.MultiplyTint)); + Assert.Equal((start.Alpha, start.Tint, start.TintStrength, start.MultiplyTint), + (peak.Alpha, peak.Tint, peak.TintStrength, peak.MultiplyTint)); } } diff --git a/engine/Age.Engine/Model/GfxState.cs b/engine/Age.Engine/Model/GfxState.cs index aac1f94..803b781 100644 --- a/engine/Age.Engine/Model/GfxState.cs +++ b/engine/Age.Engine/Model/GfxState.cs @@ -28,9 +28,9 @@ public readonly record struct ColorTransitionState(long Current, long Target, /// (= the engine's z-order) each frame. Built by ; the surface /// resId/colorkey are resolved from the object's live source slot at snapshot time (see docs/engine-re.md, /// "The full gfx render model"). -/// The packed-color channel is mode-dependent. Static mode 0 uses -/// to blend into the texel. A mode-0 color which has passed through op 0x202, and -/// mode 1, use as opacity and multiply the texel by . +/// The packed-color channel is mode-dependent. Textured mode 0 ignores packed alpha and +/// multiplicatively modulates by ; surfaceless mode 0 uses . +/// A mode-0 color which has passed through op 0x202, and mode 1, use as opacity. /// selects the latter compositor path. public readonly record struct RenderObject(long Handle, long SurfaceResId, long ColorKey, int SrcX, int SrcY, int W, int H, int DstX, int DstY, @@ -59,7 +59,7 @@ public sealed class GfxState { public (long X, long Y, long Z) V18, V24, V16c; public long Field64, Field68, Field6c; - public long Color; + public long Color = 0xffffffff; // native gfx_object_init_default obj+0x60: identity packed ARGB public bool HasColor; // true once op 0x202/0x203 set a color/alpha modulation on this object public long StaticColorMode; // op 0x203 operand 2 -> obj+0x30; mode 2 is transition alpha/identity // Op 0x202 one-shot packed-color channel: current +0x60, target +0x64, delay +0x38, @@ -297,8 +297,8 @@ public sealed class GfxState } } - /// Op 0x232 (anim-color): ping-pong the object's color/alpha toward - /// (packed 0xAARRGGBB) over ms — the pulsing glow. Distinct from static 0x202/0x203. + /// Op 0x232 worker contract: ping-pong a temporary copy of static packed ARGB toward + /// . The sampled value is consumed by the object's unchanged blend mode. public void SetColorAnim(long handle, long period, long target) { lock (_lock) @@ -307,6 +307,23 @@ public sealed class GfxState o.ColorPeriod = period; o.ColorTarget = target; o.ColorStart = -1; o.ColorAnim = true; } } + + /// Op 0x232 handler contract: resolve negative alpha/RGB sentinels from static obj+0x60, + /// clamp alpha above 255, then arm the cyclic packed-color channel without changing blend mode. + public void SetColorAnimResolved(long handle, long period, long alpha, long rgb) + { + lock (_lock) + { + var o = GetOrCreate(handle); + long current = o.Color & 0xffffffff; + long resolvedAlpha = alpha < 0 ? (current >> 24) & 0xff : System.Math.Min(alpha, 0xff); + long resolvedRgb = rgb < 0 ? current & 0xffffff : rgb & 0xffffff; + o.ColorPeriod = period; + o.ColorTarget = PackColor(resolvedAlpha, resolvedRgb); + o.ColorStart = -1; + o.ColorAnim = true; + } + } public void ClearSurface(int slot) { lock (_lock) { _surfaces[slot] = (0, 0); } } // create-texture (blank) /// Op 0x223: queue a type-0 timed alpha transition into a target surface slot. @@ -524,17 +541,23 @@ public sealed class GfxState // ---- packed color: a static mode 0 treats alpha as tint/fill strength. Once op 0x202 has // armed the one-shot channel, its current/target ARGB instead supplies opacity and D3D-style // multiplicative RGB modulation (including after target commit). Mode 1 uses the same blend. - // 0x232 remains the separate ping-pong mode-0 strength/tint channel. ---- + // Op 0x232 modifies this temporary packed color before the unchanged mode consumes it. ---- int alpha = 255; long tint = 0xFFFFFF; int strength = 0; var blend = BlendKind.Opaque; bool multiplyTint = false; - long sampledColor = o.Color; + long sampledColor = o.HasColor ? o.Color : 0xffffffff; ColorTransitionState? colorTransition = null; if (o.OneShotColorEnabled) { if (o.OneShotStartMs < 0) o.OneShotStartMs = nowMs; (sampledColor, colorTransition) = SampleOneShotColor(o, nowMs); } - if (o.HasColor) + if (o.ColorAnim) + { + if (o.ColorStart < 0) o.ColorStart = nowMs; + sampledColor = InterpolatePackedColor(sampledColor, o.ColorTarget, + PingPongWeight(nowMs, o.ColorStart, o.ColorPeriod)); + } + if (o.HasColor || o.ColorAnim) { var (a, r, g, b) = BlendMath.UnpackArgb(sampledColor); tint = ((long)r << 16) | ((long)g << 8) | (long)b; @@ -563,17 +586,6 @@ public sealed class GfxState strength = a; blend = BlendKind.Alpha; } } - if (o.ColorAnim) - { - if (o.ColorStart < 0) o.ColorStart = nowMs; - double t = PingPongWeight(nowMs, o.ColorStart, o.ColorPeriod); - var (ta, tr, tg, tb) = BlendMath.UnpackArgb(o.ColorTarget); - var (br, bg, bb) = ((int)((tint >> 16) & 0xff), (int)((tint >> 8) & 0xff), (int)(tint & 0xff)); - strength = (int)(strength + (ta - strength) * t); - tint = ((long)(br + (tr - br) * t) << 16) | ((long)(bg + (tg - bg) * t) << 8) | (long)(bb + (tb - bb) * t); - blend = BlendKind.Alpha; - } - // ---- src-rect: preserve cell dimensions and offset it row-major through the sheet ---- int srcX = o.SrcRect.X, srcY = o.SrcRect.Y, w = o.SrcRect.W, h = o.SrcRect.H; if (o.SrcAnim && o.SrcFrameCount >= 1) @@ -696,6 +708,19 @@ public sealed class GfxState current.Angle + (target.Angle - current.Angle) * t); } + private static long InterpolatePackedColor(long current, long target, double t) + { + t = System.Math.Clamp(t, 0.0, 1.0); + long packed = 0; + for (int shift = 0; shift <= 24; shift += 8) + { + long c = (current >> shift) & 0xff; + long v = (target >> shift) & 0xff; + packed |= ((long)(c + (v - c) * t) & 0xff) << shift; + } + return packed; + } + /// Ping-pong interpolation weight in [0,1] toward the target: 0 at cycle start, 1 at half-period. private static double PingPongWeight(long now, long start, long period) { diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index 3c13d5d..960d3a4 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -397,8 +397,8 @@ public sealed class VirtualMachine Gfx.SetSrcRect(Read(a[0]), Read(a[3]), Read(a[4]), Read(a[5]), 0); return pc + 1; case "u00421EA0": // 0x231 looping spritesheet: (handle)(ms per frame)(frame count)(columns) 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 - Gfx.SetColorAnim(Read(a[0]), Read(a[1]), GfxState.PackColor(Read(a[2]), Read(a[3]))); return pc + 1; + case "u00421EF0": // 0x232 cyclic packed ARGB; negative alpha/RGB preserve static obj color + Gfx.SetColorAnimResolved(Read(a[0]), Read(a[1]), Read(a[2]), Read(a[3])); return pc + 1; case "u00421940": // 0x228: (succ)(handle)(outX)(outY)(outZ) <- target translation matrix { if (Gfx.TryQueryTranslationTarget(Read(a[1]), out var v)) diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index e8da3a0..650b197 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -6035,12 +6035,12 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "u00421EF0" category = "draw" -summary = "0x232 anim-color (handle)(period)(alpha)(color): ping-pong the object color/alpha toward the packed target over period ms (pulsing GLOW). Worker gfx_worker_anim_color @0x47ef50 -> interpolator COLOR channel. C# VM: GfxState.SetColorAnim. See docs/engine-re.md §SC0000 anim cluster." +summary = "0x232 anim-color (handle)(period)(alpha)(color): ping-pong the temporary packed ARGB passed to the normal object blit. Handler resolves negative alpha/RGB from static color obj+0x60 and clamps alpha above 255. Blend selector obj+0x30 is unchanged: mode 0 keeps default opaque blending (animated alpha is inert; RGB is vertex modulation), while mode 1 consumes ARGB alpha as opacity. Fresh static color is 0xffffffff. The C# VM resolves sentinels and consumes sampled ARGB through the unchanged mode-specific path. See docs/engine-re.md §SC0000 anim cluster." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: gfx_op_0x232_anim_color@0x423c30 resolves sentinels then calls gfx_worker_anim_color@0x47ef50; gfx_object_anim_interpolate@0x473ed0 samples static obj+0x60 toward target obj+0x240 into a temporary packed color; gfx_object_composite@0x47f650 passes that color plus unchanged selector obj+0x30 to gfx_object_blit_d3d9@0x4774c0. Blit mode 0 enables no alpha blend and passes RGB as modulation; mode 1 enables SRCALPHA/INVSRCALPHA. gfx_object_init_default@0x472810 initializes obj+0x60=0xffffffff. SC0000 0x1a0e (handle,1200,224,-1) is therefore 0xffffffff<->0xe0ffffff with inert alpha and identity RGB: no visible pulse. C# regressions cover exact AE001H visual invariance, negative-RGB preservation, mode-0 RGB modulation, and mode-1 alpha opacity." [[opcode.semantics.args]] i = 1