Fix AE001H cyclic color blending

This commit is contained in:
gamer147
2026-07-11 22:21:10 -04:00
parent f195934324
commit b41ae36168
8 changed files with 140 additions and 36 deletions

View File

@@ -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`) | | `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` | | `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**: 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 | | `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 |
@@ -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 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)`. 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 **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

@@ -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. - **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) ### 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. - **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=kelebek, confidence=low - **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) ### 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. - **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.

View File

@@ -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 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 (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. 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.

View File

@@ -105,6 +105,25 @@ public class AnimChannelTests
Assert.Equal(GfxState.PackColor(0x80, 0xFF0000), o.ColorTarget); 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<string>());
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] [Fact]
public void Op0x239_SetsSpritesheetGridCell() public void Op0x239_SetsSpritesheetGridCell()
{ {

View File

@@ -45,17 +45,43 @@ public class AnimInterpolatorTests
} }
[Fact] [Fact]
public void ColorAnim_PingPongsTintStrength_ObjectStaysOpaque() public void ColorAnim_Mode0Textured_UsesRgbModulationAndIgnoresAlpha()
{ {
var g = VisibleObj(0x100); 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(0x80, 0xFF0000));
g.SetColorAnim(0x100, period: 1000, target: GfxState.PackColor(0xFF, 0xFF0000));
g.SnapshotVisibleObjects(0); // seeds start=0 g.SnapshotVisibleObjects(0); // seeds start=0
var baseFrame = g.SnapshotVisibleObjects(0).Single(); 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(); var peak = g.SnapshotVisibleObjects(500).Single();
Assert.Equal(255, peak.Alpha); // still opaque Assert.Equal((255, 0xffffffL, 0, true),
Assert.Equal(255, peak.TintStrength); // t=1 -> full tint strength (the glow peak) (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));
} }
} }

View File

@@ -28,9 +28,9 @@ public readonly record struct ColorTransitionState(long Current, long Target,
/// (= the engine's z-order) each frame. Built by <see cref="GfxState.SnapshotVisibleObjects"/>; the surface /// (= the engine's z-order) each frame. Built by <see cref="GfxState.SnapshotVisibleObjects"/>; the surface
/// resId/colorkey are resolved from the object's live source slot at snapshot time (see docs/engine-re.md, /// resId/colorkey are resolved from the object's live source slot at snapshot time (see docs/engine-re.md,
/// "The full gfx render model").</summary> /// "The full gfx render model").</summary>
/// <summary>The packed-color channel is mode-dependent. Static mode 0 uses <paramref name="TintStrength"/> /// <summary>The packed-color channel is mode-dependent. Textured mode 0 ignores packed alpha and
/// to blend <paramref name="Tint"/> into the texel. A mode-0 color which has passed through op 0x202, and /// multiplicatively modulates by <paramref name="Tint"/>; surfaceless mode 0 uses <paramref name="TintStrength"/>.
/// mode 1, use <paramref name="Alpha"/> as opacity and multiply the texel by <paramref name="Tint"/>. /// A mode-0 color which has passed through op 0x202, and mode 1, use <paramref name="Alpha"/> as opacity.
/// <paramref name="MultiplyTint"/> selects the latter compositor path.</summary> /// <paramref name="MultiplyTint"/> selects the latter compositor path.</summary>
public readonly record struct RenderObject(long Handle, long SurfaceResId, long ColorKey, public readonly record struct RenderObject(long Handle, long SurfaceResId, long ColorKey,
int SrcX, int SrcY, int W, int H, int DstX, int DstY, 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 X, long Y, long Z) V18, V24, V16c;
public long Field64, Field68, Field6c; 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 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 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, // Op 0x202 one-shot packed-color channel: current +0x60, target +0x64, delay +0x38,
@@ -297,8 +297,8 @@ public sealed class GfxState
} }
} }
/// <summary>Op 0x232 (anim-color): ping-pong the object's color/alpha toward <paramref name="target"/> /// <summary>Op 0x232 worker contract: ping-pong a temporary copy of static packed ARGB toward
/// (packed 0xAARRGGBB) over <paramref name="period"/> ms — the pulsing glow. Distinct from static 0x202/0x203.</summary> /// <paramref name="target"/>. The sampled value is consumed by the object's unchanged blend mode.</summary>
public void SetColorAnim(long handle, long period, long target) public void SetColorAnim(long handle, long period, long target)
{ {
lock (_lock) lock (_lock)
@@ -307,6 +307,23 @@ public sealed class GfxState
o.ColorPeriod = period; o.ColorTarget = target; o.ColorStart = -1; o.ColorAnim = true; o.ColorPeriod = period; o.ColorTarget = target; o.ColorStart = -1; o.ColorAnim = true;
} }
} }
/// <summary>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.</summary>
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) public void ClearSurface(int slot) { lock (_lock) { _surfaces[slot] = (0, 0); } } // create-texture (blank)
/// <summary>Op 0x223: queue a type-0 timed alpha transition into a target surface slot.</summary> /// <summary>Op 0x223: queue a type-0 timed alpha transition into a target surface slot.</summary>
@@ -524,17 +541,23 @@ public sealed class GfxState
// ---- packed color: a static mode 0 treats alpha as tint/fill strength. Once op 0x202 has // ---- 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 // 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. // 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; int alpha = 255; long tint = 0xFFFFFF; int strength = 0; var blend = BlendKind.Opaque;
bool multiplyTint = false; bool multiplyTint = false;
long sampledColor = o.Color; long sampledColor = o.HasColor ? o.Color : 0xffffffff;
ColorTransitionState? colorTransition = null; ColorTransitionState? colorTransition = null;
if (o.OneShotColorEnabled) if (o.OneShotColorEnabled)
{ {
if (o.OneShotStartMs < 0) o.OneShotStartMs = nowMs; if (o.OneShotStartMs < 0) o.OneShotStartMs = nowMs;
(sampledColor, colorTransition) = SampleOneShotColor(o, 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); var (a, r, g, b) = BlendMath.UnpackArgb(sampledColor);
tint = ((long)r << 16) | ((long)g << 8) | (long)b; tint = ((long)r << 16) | ((long)g << 8) | (long)b;
@@ -563,17 +586,6 @@ public sealed class GfxState
strength = a; blend = BlendKind.Alpha; 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 ---- // ---- 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; int srcX = o.SrcRect.X, srcY = o.SrcRect.Y, w = o.SrcRect.W, h = o.SrcRect.H;
if (o.SrcAnim && o.SrcFrameCount >= 1) if (o.SrcAnim && o.SrcFrameCount >= 1)
@@ -696,6 +708,19 @@ public sealed class GfxState
current.Angle + (target.Angle - current.Angle) * t); 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;
}
/// <summary>Ping-pong interpolation weight in [0,1] toward the target: 0 at cycle start, 1 at half-period.</summary> /// <summary>Ping-pong interpolation weight in [0,1] toward the target: 0 at cycle start, 1 at half-period.</summary>
private static double PingPongWeight(long now, long start, long period) private static double PingPongWeight(long now, long start, long period)
{ {

View File

@@ -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; 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) 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; 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 cyclic packed ARGB; negative alpha/RGB preserve static obj color
Gfx.SetColorAnim(Read(a[0]), Read(a[1]), GfxState.PackColor(Read(a[2]), Read(a[3]))); return pc + 1; 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 case "u00421940": // 0x228: (succ)(handle)(outX)(outY)(outZ) <- target translation matrix
{ {
if (Gfx.TryQueryTranslationTarget(Read(a[1]), out var v)) if (Gfx.TryQueryTranslationTarget(Read(a[1]), out var v))

View File

@@ -6035,12 +6035,12 @@ abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u00421EF0" name = "u00421EF0"
category = "draw" 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 noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] 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]] [[opcode.semantics.args]]
i = 1 i = 1