Fix ADV chrome one-shot blending
This commit is contained in:
@@ -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`).
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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}) " +
|
||||||
|
|||||||
Reference in New Issue
Block a user