fix: color-op alpha (0x202/0x203/0x232) is TINT STRENGTH, not object opacity

Root cause (evidence: gfx-log) of the opening-CG grey background: a CG drawn with
(alpha=0, color=white) means 'no tint' = fully opaque, but slice-A conflated the
color alpha with object opacity -> the CG rendered transparent. RenderObject now
carries TintStrength separately from Alpha (opacity); resolution keeps textured
objects opaque. Tests updated to the evidence-based semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-07-08 23:41:03 -04:00
parent 620ffb4fb7
commit c0b6199dbc
3 changed files with 36 additions and 21 deletions

View File

@@ -43,15 +43,17 @@ public class AnimInterpolatorTests
} }
[Fact] [Fact]
public void ColorAnim_PingPongsAlphaTowardTarget() public void ColorAnim_PingPongsTintStrength_ObjectStaysOpaque()
{ {
var g = VisibleObj(0x100); var g = VisibleObj(0x100);
// base = opaque white (no static color); target = alpha 0 (fade out) => pulses opaque<->transparent // base = no tint (strength 0); target = full-strength (alpha 0xff) red glow => strength pulses 0<->255
g.SetColorAnim(0x100, period: 1000, target: GfxState.PackColor(0x00, 0x000000)); g.SetColorAnim(0x100, period: 1000, target: GfxState.PackColor(0xFF, 0xFF0000));
g.SnapshotVisibleObjects(0); // seeds start=0 g.SnapshotVisibleObjects(0); // seeds start=0
var opaque = g.SnapshotVisibleObjects(0).Single(); var baseFrame = g.SnapshotVisibleObjects(0).Single();
Assert.Equal(255, opaque.Alpha); // t=0 -> base opaque 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(0, peak.Alpha); // t=1 -> target alpha 0 Assert.Equal(255, peak.Alpha); // still opaque
Assert.Equal(255, peak.TintStrength); // t=1 -> full tint strength (the glow peak)
} }
} }

View File

@@ -14,25 +14,33 @@ public class RenderObjectBlendTests
} }
[Fact] [Fact]
public void ObjectWithoutColor_ResolvesOpaqueWhiteTint() public void ObjectWithoutColor_ResolvesOpaqueNoTint()
{ {
var g = WithVisibleObject(0x100, resId: 5, colorKey: -1); var g = WithVisibleObject(0x100, resId: 5, colorKey: -1);
var ro = g.SnapshotVisibleObjects().Single(); var ro = g.SnapshotVisibleObjects().Single();
Assert.Equal(255, ro.Alpha); Assert.Equal(255, ro.Alpha); // opaque
Assert.Equal(0, ro.TintStrength); // no tint
Assert.Equal(0xFFFFFF, ro.Tint); Assert.Equal(0xFFFFFF, ro.Tint);
Assert.Equal(BlendKind.Opaque, ro.Blend); Assert.Equal(BlendKind.Opaque, ro.Blend);
} }
[Fact] [Fact]
public void DrawColor_0x203_SetsAlphaTintAndBlend() public void DrawColor_0x203_IsTintStrength_ObjectStaysOpaque()
{ {
// Op 0x203/0x202 alpha is TINT-BLEND STRENGTH, not object opacity (evidence: a CG drawn with
// (alpha=0, color=white) must stay fully OPAQUE + untinted, not vanish). Root cause of the grey-BG.
var g = WithVisibleObject(0x100, resId: 5, colorKey: -1); var g = WithVisibleObject(0x100, resId: 5, colorKey: -1);
// emulate op 0x203: pack (alpha=0x80, color=0x102030) and mark HasColor g.SetObjectColor(0x100, GfxState.PackColor(0x00, 0xFFFFFF)); // "no tint" — the grey-BG case
g.SetObjectColor(0x100, GfxState.PackColor(0x80, 0x102030));
var ro = g.SnapshotVisibleObjects().Single(); var ro = g.SnapshotVisibleObjects().Single();
Assert.Equal(0x80, ro.Alpha); Assert.Equal(255, ro.Alpha); // OBJECT STAYS OPAQUE (was wrongly 0 -> invisible)
Assert.Equal(0x102030, ro.Tint); Assert.Equal(0, ro.TintStrength); // zero tint strength
Assert.Equal(BlendKind.Alpha, ro.Blend);
g.SetObjectColor(0x100, GfxState.PackColor(0x80, 0x102030)); // half-strength tint toward 0x102030
var ro2 = g.SnapshotVisibleObjects().Single();
Assert.Equal(255, ro2.Alpha); // still opaque
Assert.Equal(0x80, ro2.TintStrength); // strength from the alpha byte
Assert.Equal(0x102030, ro2.Tint);
Assert.Equal(BlendKind.Alpha, ro2.Blend);
} }
[Fact] [Fact]

View File

@@ -14,9 +14,13 @@ public readonly record struct AnimState(bool Enabled, bool Normalized, long TX,
/// (= 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><paramref name="Alpha"/> is the object's OPACITY (0-255). <paramref name="TintStrength"/> is how
/// strongly <paramref name="Tint"/> (RGB) is blended into the texel (0=keep texel, 255=full tint) — this is the
/// op 0x202/0x203/0x232 "alpha" byte, which is a tint strength, NOT opacity (conflating them made opaque CGs
/// vanish — the grey-background bug).</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,
AnimState Anim, int Alpha, long Tint, BlendKind Blend); AnimState Anim, int Alpha, long Tint, int TintStrength, BlendKind Blend);
/// <summary>Host-agnostic model of the AGE native gfx command-buffer (reversed in /// <summary>Host-agnostic model of the AGE native gfx command-buffer (reversed in
/// docs/engine-re.md, gfx op-contract table). One registry maps an object handle to a GfxObject — the /// docs/engine-re.md, gfx op-contract table). One registry maps an object handle to a GfxObject — the
@@ -236,20 +240,21 @@ 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);
// ---- color: slice-A static base, then op-0x232 ping-pong toward the target ---- // ---- color: object stays OPAQUE; the op-0x202/0x203 alpha is a TINT STRENGTH (0=keep texel,
int alpha = 255; long tint = 0xFFFFFF; var blend = BlendKind.Opaque; // 255=full tint), NOT opacity. 0x232 ping-pongs the strength (+tint) toward the target (glow). ----
int alpha = 255; long tint = 0xFFFFFF; int strength = 0; var blend = BlendKind.Opaque;
if (o.HasColor) if (o.HasColor)
{ {
var (a, r, g, b) = BlendMath.UnpackArgb(o.Color); var (a, r, g, b) = BlendMath.UnpackArgb(o.Color);
alpha = a; tint = ((long)r << 16) | ((long)g << 8) | (long)b; blend = BlendKind.Alpha; strength = a; tint = ((long)r << 16) | ((long)g << 8) | (long)b; blend = BlendKind.Alpha;
} }
if (o.ColorAnim) if (o.ColorAnim)
{ {
if (o.ColorStart < 0) o.ColorStart = nowMs; if (o.ColorStart < 0) o.ColorStart = nowMs;
double t = PingPongWeight(nowMs, o.ColorStart, o.ColorPeriod); double t = PingPongWeight(nowMs, o.ColorStart, o.ColorPeriod);
var (ta, tr, tg, tb) = BlendMath.UnpackArgb(o.ColorTarget); var (ta, tr, tg, tb) = BlendMath.UnpackArgb(o.ColorTarget);
var (ba, br, bg, bb) = (alpha, (int)((tint >> 16) & 0xff), (int)((tint >> 8) & 0xff), (int)(tint & 0xff)); var (br, bg, bb) = ((int)((tint >> 16) & 0xff), (int)((tint >> 8) & 0xff), (int)(tint & 0xff));
alpha = (int)(ba + (ta - ba) * t); strength = (int)(strength + (ta - strength) * t);
tint = ((long)(br + (tr - br) * t) << 16) | ((long)(bg + (tg - bg) * t) << 8) | (long)(bb + (tb - bb) * t); tint = ((long)(br + (tr - br) * t) << 16) | ((long)(bg + (tg - bg) * t) << 8) | (long)(bb + (tb - bb) * t);
blend = BlendKind.Alpha; blend = BlendKind.Alpha;
} }
@@ -278,7 +283,7 @@ public sealed class GfxState
new AnimState(o.AnimEnabled, o.AnimNormalized, new AnimState(o.AnimEnabled, o.AnimNormalized,
o.AnimTarget.X, o.AnimTarget.Y, o.AnimTarget.Z, o.AnimTarget.X, o.AnimTarget.Y, o.AnimTarget.Z,
o.AnimDurationTicks, o.AnimGeneration), o.AnimDurationTicks, o.AnimGeneration),
alpha, tint, blend)); alpha, tint, strength, blend));
} }
return list; return list;
} }