Fix ADV chrome one-shot blending

This commit is contained in:
gamer147
2026-07-11 13:28:31 -04:00
parent 71ceab5458
commit 54d11f513d
7 changed files with 85 additions and 14 deletions

View File

@@ -792,6 +792,16 @@ The port now carries current and target separately and samples them from the uni
target. Mode 0 retains the established CG/tint/fill behavior; mode 1 now uses native alpha opacity plus RGB target. Mode 0 retains the established CG/tint/fill behavior; mode 1 now uses native alpha opacity plus RGB
modulation. `draw-string 0x204`/`0x7a` remains a separate dependency. modulation. `draw-string 0x204`/`0x7a` remains a separate dependency.
**ADV chrome correction (2026-07-11).** Mode 0 cannot be classified from the final packed color alone.
Static `0x203(mode=0, alpha=0, rgb=white)` remains the established opaque/no-tint CG initializer, but a
mode-0 object whose current/target channel was armed by `0x202` consumes that sampled ARGB as opacity plus
multiplicative RGB modulation, including after target commit. SC0000 proves the distinction with SYSTEM4's
SO001 surface: backing object `0xd2f0` ramps `0x00000000 <-> 0xff000000`, while control-strip object
`0xd2f1` ramps `0x00ffffff <-> 0xffffffff`. Treating alpha as tint strength made the visible controls solid
white and the hidden backing expose SO001's raw white crop. Preserving one-shot provenance makes white an
identity modulation for the yellow controls and alpha zero fully transparent. A matching windowed capture
and user manual check confirmed both endpoints.
### SC0000 anim/transform/spritesheet cluster — op→field map (2026-07-08) ### SC0000 anim/transform/spritesheet cluster — op→field map (2026-07-08)
Reversed for the animation cluster slice (spec `docs/superpowers/specs/2026-07-08-sc0000-anim-transform-cluster-design.md`). Reversed for the animation cluster slice (spec `docs/superpowers/specs/2026-07-08-sc0000-anim-transform-cluster-design.md`).

View File

@@ -1239,3 +1239,20 @@ also pass with all of `extracted/` physically moved aside. The real-scene trace
the existing test bootstrap still finds root script fixtures through `Paths.Scripts()` under `extracted/`, the existing test bootstrap still finds root script fixtures through `Paths.Scripts()` under `extracted/`,
and changing that unrelated bootstrap was outside this movie slice. Final validation: engine **132/132**, and changing that unrelated bootstrap was outside this movie slice. Final validation: engine **132/132**,
Godot build with zero warnings, threaded `SELFTEST OK`, and opcode-map lint clean. Godot build with zero warnings, threaded `SELFTEST OK`, and opcode-map lint clean.
### Phase A — SC0000 textbox/control-strip one-shot blend correction DONE (2026-07-11)
The lower white panel noted after movie publication and the white-outline controls had one shared cause,
not a movie or AGF-alpha fault. SYSTEM4's inherited SO001 surface is cropped by ADV backing object `0xd2f0`
and control-strip object `0xd2f1`; both use a mode-0 `0x202` one-shot color transition. The port discarded
that one-shot provenance after sampling and interpreted the packed alpha as static tint strength. As a
result, full-strength white replaced the controls' yellow pixels, while zero strength exposed the raw
mostly-white backing crop instead of hiding it.
`GfxState` now distinguishes static mode-0 `0x203` state from mode-0 color state which has passed through
`0x202`. The latter retains ARGB-opacity plus multiplicative-RGB semantics after target commit. This keeps
the established `0x00ffffff` static CG initializer opaque, preserves yellow under white identity modulation,
and makes a committed alpha-zero ADV crop transparent. Focused model/raster tests cover the committed
endpoint, identity modulation, and zero-opacity output. Validation: engine **134/134**, Godot build with
zero warnings, matching windowed capture, and user manual confirmation that the box disappears fully and
the controls retain their normal color while visible.

View File

@@ -35,17 +35,19 @@ public class OneShotColorTests
gfx.SetAnimatedObjectColorResolved(0x100, 100, 400, 0xff, 0x90a0b0); gfx.SetAnimatedObjectColorResolved(0x100, 100, 400, 0xff, 0x90a0b0);
var start = gfx.SnapshotVisibleObjects(1000).Single(); var start = gfx.SnapshotVisibleObjects(1000).Single();
Assert.Equal(0, start.TintStrength); Assert.Equal(0, start.Alpha);
Assert.True(start.MultiplyTint);
Assert.Equal(0.0, start.ColorTransition!.Value.Progress); Assert.Equal(0.0, start.ColorTransition!.Value.Progress);
Assert.Equal(0, gfx.SnapshotVisibleObjects(1100).Single().TintStrength); Assert.Equal(0, gfx.SnapshotVisibleObjects(1100).Single().Alpha);
var half = gfx.SnapshotVisibleObjects(1300).Single(); var half = gfx.SnapshotVisibleObjects(1300).Single();
Assert.Equal(0x7f, half.TintStrength); Assert.Equal(0x7f, half.Alpha);
Assert.Equal(0x506070, half.Tint); Assert.Equal(0x506070, half.Tint);
Assert.Equal(0.5, half.ColorTransition!.Value.Progress, 3); Assert.Equal(0.5, half.ColorTransition!.Value.Progress, 3);
var end = gfx.SnapshotVisibleObjects(1500).Single(); var end = gfx.SnapshotVisibleObjects(1500).Single();
Assert.Equal(0xff, end.TintStrength); Assert.Equal(0xff, end.Alpha);
Assert.True(end.MultiplyTint); // committed endpoints keep the op-0x202 blend contract
Assert.Equal(0x90a0b0, end.Tint); Assert.Equal(0x90a0b0, end.Tint);
Assert.False(gfx.TryGet(0x100)!.OneShotColorEnabled); Assert.False(gfx.TryGet(0x100)!.OneShotColorEnabled);
Assert.Equal(-1, gfx.TryGet(0x100)!.OneShotColorTarget); Assert.Equal(-1, gfx.TryGet(0x100)!.OneShotColorTarget);
@@ -59,10 +61,10 @@ public class OneShotColorTests
gfx.SetStaticObjectColorResolved(0x100, 0, 0xff, 0xffffff); gfx.SetStaticObjectColorResolved(0x100, 0, 0xff, 0xffffff);
var start = gfx.SnapshotVisibleObjects(2000).Single(); var start = gfx.SnapshotVisibleObjects(2000).Single();
Assert.Equal(0xff, start.TintStrength); Assert.Equal(0xff, start.Alpha);
Assert.Equal(0xffffff, start.Tint); Assert.Equal(0xffffff, start.Tint);
var half = gfx.SnapshotVisibleObjects(2150).Single(); var half = gfx.SnapshotVisibleObjects(2150).Single();
Assert.Equal(0x7f, half.TintStrength); Assert.Equal(0x7f, half.Alpha);
Assert.Equal(0xffffff, half.Tint); Assert.Equal(0xffffff, half.Tint);
} }
@@ -75,7 +77,7 @@ public class OneShotColorTests
gfx.SnapshotVisibleObjects(5000); gfx.SnapshotVisibleObjects(5000);
var half = gfx.SnapshotVisibleObjects(5500).Single(); var half = gfx.SnapshotVisibleObjects(5500).Single();
Assert.Equal(0x7f, half.TintStrength); Assert.Equal(0x7f, half.Alpha);
Assert.Equal(1.5, half.Transform.ScaleX, 3); Assert.Equal(1.5, half.Transform.ScaleX, 3);
Assert.Equal(5000, half.ColorTransition!.Value.StartMs); Assert.Equal(5000, half.ColorTransition!.Value.StartMs);
} }

View File

@@ -77,6 +77,21 @@ public class RenderObjectBlendTests
Assert.Equal(BlendKind.Alpha, ro.Blend); Assert.Equal(BlendKind.Alpha, ro.Blend);
} }
[Fact]
public void Mode0_OneShotColor_UsesOpacityAndMultiplicativeRgbAfterCommit()
{
var g = WithVisibleObject(0x100, resId: 5, colorKey: -1);
g.SetAnimatedObjectColorResolved(0x100, 0, 100, 0x00, 0xffffff);
g.SetStaticObjectColorResolved(0x100, 0, 0xff, 0xffffff);
g.SnapshotVisibleObjects(1000); // seed
var hidden = g.SnapshotVisibleObjects(1100).Single();
Assert.Equal(0, hidden.Alpha);
Assert.Equal(0, hidden.TintStrength);
Assert.True(hidden.MultiplyTint);
Assert.False(g.TryGet(0x100)!.OneShotColorEnabled);
}
[Fact] [Fact]
public void ColorKey_IsCarriedThrough() public void ColorKey_IsCarriedThrough()
{ {

View File

@@ -40,4 +40,20 @@ public class SoftwareAffineRasterizerTests
Assert.InRange(dst[2], 5, 7); Assert.InRange(dst[2], 5, 7);
Assert.InRange(dst[3], 126, 127); Assert.InRange(dst[3], 126, 127);
} }
[Fact]
public void BlitRgba_WhiteModulationPreservesYellowAndZeroOpacityHidesIt()
{
byte[] src = { 240, 192, 16, 255 };
byte[] visible = new byte[4];
var identity = new Affine2D(1, 0, 0, 1, 0, 0);
SoftwareAffineRasterizer.BlitRgba(visible, 1, 1, src, 1, 1, 0, 0, 1, 1,
identity, 0xffffff, 0, 1, multiplyTint: true);
Assert.Equal(new byte[] { 240, 192, 16, 255 }, visible);
byte[] hidden = new byte[4];
SoftwareAffineRasterizer.BlitRgba(hidden, 1, 1, src, 1, 1, 0, 0, 1, 1,
identity, 0xffffff, 0, 0, multiplyTint: true);
Assert.Equal(new byte[4], hidden);
}
} }

View File

@@ -28,9 +28,10 @@ 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. Mode 0 uses <paramref name="TintStrength"/> to blend /// <summary>The packed-color channel is mode-dependent. Static mode 0 uses <paramref name="TintStrength"/>
/// <paramref name="Tint"/> into the texel; mode 1 uses <paramref name="Alpha"/> as opacity and multiplies the /// to blend <paramref name="Tint"/> into the texel. A mode-0 color which has passed through op 0x202, and
/// texel by <paramref name="Tint"/>. <paramref name="MultiplyTint"/> selects the latter compositor path.</summary> /// mode 1, use <paramref name="Alpha"/> as opacity and multiply the texel by <paramref name="Tint"/>.
/// <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,
TransformState Transform, RotationCycleState Rotation, TransformState Transform, RotationCycleState Rotation,
@@ -65,6 +66,10 @@ public sealed class GfxState
// duration +0x4c. It shares obj+0x34's start timestamp with the one-shot matrix channels. // duration +0x4c. It shares obj+0x34's start timestamp with the one-shot matrix channels.
public long OneShotColorTarget = -1, ColorDelayMs, ColorDurationMs; public long OneShotColorTarget = -1, ColorDelayMs, ColorDurationMs;
public bool OneShotColorEnabled; public bool OneShotColorEnabled;
// Once op 0x202 arms this object's color channel, its sampled/current ARGB is consumed as D3D
// opacity + multiplicative modulation even after the target commits. This distinguishes ADV chrome
// fades from a static mode-0 0x203 such as a CG initialized with 0x00ffffff (opaque identity).
public bool OneShotColorBlend;
// ---- src-rect / spritesheet-cell channel (ops 0x239 static cell, 0x231 animate). Interpolator // ---- src-rect / spritesheet-cell channel (ops 0x239 static cell, 0x231 animate). Interpolator
// SRC-RECT SCROLL channel: period obj+0x230, start obj+0x21c, grid obj+0x238/0x23c. ---- // SRC-RECT SCROLL channel: period obj+0x230, start obj+0x21c, grid obj+0x238/0x23c. ----
@@ -152,6 +157,7 @@ public sealed class GfxState
Color = s.Color, HasColor = s.HasColor, StaticColorMode = s.StaticColorMode, Color = s.Color, HasColor = s.HasColor, StaticColorMode = s.StaticColorMode,
OneShotColorTarget = s.OneShotColorTarget, ColorDelayMs = s.ColorDelayMs, OneShotColorTarget = s.OneShotColorTarget, ColorDelayMs = s.ColorDelayMs,
ColorDurationMs = s.ColorDurationMs, OneShotColorEnabled = s.OneShotColorEnabled, ColorDurationMs = s.ColorDurationMs, OneShotColorEnabled = s.OneShotColorEnabled,
OneShotColorBlend = s.OneShotColorBlend,
SrcGridW = s.SrcGridW, SrcGridH = s.SrcGridH, SrcCell = s.SrcCell, SrcGridW = s.SrcGridW, SrcGridH = s.SrcGridH, SrcCell = s.SrcCell,
SrcPeriod = s.SrcPeriod, SrcStart = s.SrcStart, SrcAnim = s.SrcAnim, SrcPeriod = s.SrcPeriod, SrcStart = s.SrcStart, SrcAnim = s.SrcAnim,
ColorPeriod = s.ColorPeriod, ColorStart = s.ColorStart, ColorTarget = s.ColorTarget, ColorPeriod = s.ColorPeriod, ColorStart = s.ColorStart, ColorTarget = s.ColorTarget,
@@ -265,6 +271,7 @@ public sealed class GfxState
o.ColorDelayMs = delayMs; o.ColorDelayMs = delayMs;
o.ColorDurationMs = durationMs; o.ColorDurationMs = durationMs;
o.OneShotColorEnabled = true; o.OneShotColorEnabled = true;
o.OneShotColorBlend = true;
o.OneShotStartMs = -1; o.OneShotStartMs = -1;
} }
} }
@@ -469,8 +476,10 @@ public sealed class GfxState
if (!o.Visible) continue; if (!o.Visible) continue;
var (resId, ck) = _surfaces.TryGetValue(o.SourceSlot, out var s) ? s : (0L, 0L); var (resId, ck) = _surfaces.TryGetValue(o.SourceSlot, out var s) ? s : (0L, 0L);
// ---- packed color: mode 0 treats alpha as tint/fill strength; mode 1 treats it as opacity and // ---- packed color: a static mode 0 treats alpha as tint/fill strength. Once op 0x202 has
// RGB as multiplicative source modulation. 0x232 ping-pongs the mode-0 strength/tint. ---- // 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. ----
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.Color;
@@ -484,7 +493,7 @@ public sealed class GfxState
{ {
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;
if (o.StaticColorMode == 1) if (o.StaticColorMode == 1 || (o.StaticColorMode == 0 && o.OneShotColorBlend))
{ {
// Native mode 1 enables SRCALPHA/INVSRCALPHA and passes packed ARGB as D3D // Native mode 1 enables SRCALPHA/INVSRCALPHA and passes packed ARGB as D3D
// modulation. Its high byte is opacity, not mode-0 tint/fill strength. // modulation. Its high byte is opacity, not mode-0 tint/fill strength.

View File

@@ -335,7 +335,9 @@ public partial class Main : Godot.Control
if (v.Blend != Age.Engine.Model.BlendKind.Opaque) if (v.Blend != Age.Engine.Model.BlendKind.Opaque)
{ {
int baseW = v.W > 0 ? v.W : 800, baseH = v.H > 0 ? v.H : 600; int baseW = v.W > 0 ? v.W : 800, baseH = v.H > 0 ? v.H : 600;
float fillA = opacity * strength; // One-shot/mode-1 packed color supplies opacity directly. Static mode-0 fills retain
// the tint-strength convention used by the existing effect objects.
float fillA = v.MultiplyTint ? opacity : opacity * strength;
FillAffineQuad(baseW, baseH, localToDest, v.Tint, fillA); FillAffineQuad(baseW, baseH, localToDest, v.Tint, fillA);
outcome = $"FILL tint=0x{v.Tint:x6} a={fillA:0.00} {baseW}x{baseH}@({dstX},{dstY}) " + outcome = $"FILL tint=0x{v.Tint:x6} a={fillA:0.00} {baseW}x{baseH}@({dstX},{dstY}) " +
$"base=({v.DstX},{v.DstY}) anchor=({t.AnchorX:0.0},{t.AnchorY:0.0}) " + $"base=({v.DstX},{v.DstY}) anchor=({t.AnchorX:0.0},{t.AnchorY:0.0}) " +