Fix AE001H cyclic color blending
This commit is contained in:
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user