feat: resolve per-object alpha/tint/blend into RenderObject (colorkey retained)

0x202/0x203 now route through GfxState.SetObjectColor (sets HasColor);
SnapshotVisibleObjects resolves Alpha/Tint/BlendKind. Drops the stale
'alpha deferred' trace stub — alpha/tint is now consumed by the compositor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-07-08 19:06:34 -04:00
parent ee1ee0e829
commit 488e86b2b8
4 changed files with 74 additions and 17 deletions

View File

@@ -0,0 +1,45 @@
using System.Linq;
using Age.Engine.Model;
using Xunit;
public class RenderObjectBlendTests
{
// Make a visible textured object bound to a slot that has a surface, so it appears in the snapshot.
private static GfxState WithVisibleObject(long handle, long resId, long colorKey)
{
var g = new GfxState();
g.SetSurface(1, resId, colorKey);
g.BindDraw(handle, 1, 0, 0, 10, 10, 0, 0); // draw-texture: slot 1, 10x10 at (0,0), visible
return g;
}
[Fact]
public void ObjectWithoutColor_ResolvesOpaqueWhiteTint()
{
var g = WithVisibleObject(0x100, resId: 5, colorKey: -1);
var ro = g.SnapshotVisibleObjects().Single();
Assert.Equal(255, ro.Alpha);
Assert.Equal(0xFFFFFF, ro.Tint);
Assert.Equal(BlendKind.Opaque, ro.Blend);
}
[Fact]
public void DrawColor_0x203_SetsAlphaTintAndBlend()
{
var g = WithVisibleObject(0x100, resId: 5, colorKey: -1);
// emulate op 0x203: pack (alpha=0x80, color=0x102030) and mark HasColor
g.SetObjectColor(0x100, GfxState.PackColor(0x80, 0x102030));
var ro = g.SnapshotVisibleObjects().Single();
Assert.Equal(0x80, ro.Alpha);
Assert.Equal(0x102030, ro.Tint);
Assert.Equal(BlendKind.Alpha, ro.Blend);
}
[Fact]
public void ColorKey_IsCarriedThrough()
{
var g = WithVisibleObject(0x100, resId: 5, colorKey: 0x000000); // key black
var ro = g.SnapshotVisibleObjects().Single();
Assert.Equal(0x000000, ro.ColorKey);
}
}

View File

@@ -0,0 +1,6 @@
namespace Age.Engine.Model;
/// <summary>How an object's surface composites onto the canvas. Opaque = straight copy; Alpha = source-alpha
/// blend (fades). Additive (glow/flash, native blit mode 2/3) is a documented seam — NOT implemented in the
/// blend/transparency slice; see docs/superpowers/specs/2026-07-08-blend-transparency-design.md.</summary>
public enum BlendKind { Opaque, Alpha, Additive }

View File

@@ -16,7 +16,7 @@ public readonly record struct AnimState(bool Enabled, bool Normalized, long TX,
/// "The full gfx render model").</summary> /// "The full gfx render model").</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); AnimState Anim, int Alpha, long Tint, 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
@@ -31,6 +31,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;
public bool HasColor; // true once op 0x202/0x203 set a color/alpha modulation on this object
// draw-texture bind (gfx_object_bind_draw): the surface to draw + its source rect + the visible flag. // draw-texture bind (gfx_object_bind_draw): the surface to draw + its source rect + the visible flag.
public int SourceSlot = -1; public int SourceSlot = -1;
public (int X, int Y, int W, int H) SrcRect; public (int X, int Y, int W, int H) SrcRect;
@@ -129,6 +130,13 @@ public sealed class GfxState
// ---- surfaces (image buffers per slot): ctx+0x52bd4[slot], from create/set-texture ---- // ---- surfaces (image buffers per slot): ctx+0x52bd4[slot], from create/set-texture ----
private readonly Dictionary<int, (long ResId, long ColorKey)> _surfaces = new(); private readonly Dictionary<int, (long ResId, long ColorKey)> _surfaces = new();
public void SetSurface(int slot, long resId, long colorKey) { lock (_lock) { _surfaces[slot] = (resId, colorKey); } } public void SetSurface(int slot, long resId, long colorKey) { lock (_lock) { _surfaces[slot] = (resId, colorKey); } }
/// <summary>Ops 0x202/0x203: record a packed 0xAARRGGBB color/alpha modulation on the object and mark it
/// HasColor so the compositor applies alpha+tint (vs the opaque default).</summary>
public void SetObjectColor(long handle, long packed)
{
lock (_lock) { var o = GetOrCreate(handle); o.Color = packed; o.HasColor = 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>draw-texture bind (gfx_object_bind_draw): object <paramref name="handle"/> draws surface /// <summary>draw-texture bind (gfx_object_bind_draw): object <paramref name="handle"/> draws surface
@@ -188,11 +196,20 @@ public sealed class GfxState
var o = kv.Value; var o = kv.Value;
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);
int alpha = 255; long tint = 0xFFFFFF; var blend = BlendKind.Opaque;
if (o.HasColor)
{
var (a, r, g, b) = BlendMath.UnpackArgb(o.Color);
alpha = a; tint = ((long)r << 16) | ((long)g << 8) | (long)b; blend = BlendKind.Alpha;
}
list.Add(new RenderObject(kv.Key, resId, ck, o.SrcRect.X, o.SrcRect.Y, o.SrcRect.W, o.SrcRect.H, list.Add(new RenderObject(kv.Key, resId, ck, o.SrcRect.X, o.SrcRect.Y, o.SrcRect.W, o.SrcRect.H,
(int)o.V24.X, (int)o.V24.Y, (int)o.V24.X, (int)o.V24.Y,
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));
} }
return list; return list;
} }

View File

@@ -270,12 +270,10 @@ public sealed class VirtualMachine
Gfx.EraseRange(Read(a[0]), Read(a[1])); return pc + 1; Gfx.EraseRange(Read(a[0]), Read(a[1])); return pc + 1;
case "gfx-elem-release": // 0x1fa (handle) case "gfx-elem-release": // 0x1fa (handle)
Gfx.Release(Read(a[0])); return pc + 1; Gfx.Release(Read(a[0])); return pc + 1;
case "gfx-blit-color": // 0x202 (handle)(x)(y)(alpha)(color) — blend deferred case "gfx-blit-color": // 0x202 (handle)(x)(y)(alpha)(color) — static alpha/tint (anim interp deferred)
Gfx.GetOrCreate(Read(a[0])).Color = GfxState.PackColor(Read(a[3]), Read(a[4])); Gfx.SetObjectColor(Read(a[0]), GfxState.PackColor(Read(a[3]), Read(a[4]))); return pc + 1;
WarnAlphaDeferredOnce(); return pc + 1; case "gfx-draw-color": // 0x203 (handle)(v)(alpha)(color) — static alpha/tint
case "gfx-draw-color": // 0x203 (handle)(v)(alpha)(color) — blend deferred Gfx.SetObjectColor(Read(a[0]), GfxState.PackColor(Read(a[2]), Read(a[3]))); return pc + 1;
Gfx.GetOrCreate(Read(a[0])).Color = GfxState.PackColor(Read(a[2]), Read(a[3]));
WarnAlphaDeferredOnce(); return pc + 1;
// ---- sprite transform / animation cluster (docs/engine-re.md "0x21c-0x243 ... ANIMATION") ---- // ---- sprite transform / animation cluster (docs/engine-re.md "0x21c-0x243 ... ANIMATION") ----
case "set-anim-transform-abs": // 0x220 (handle)(p1)(p2)(x)(y)(z) — set transform directly case "set-anim-transform-abs": // 0x220 (handle)(p1)(p2)(x)(y)(z) — set transform directly
Gfx.SetAnimTransform(Read(a[0]), Read(a[1]), Read(a[2]), Gfx.SetAnimTransform(Read(a[0]), Read(a[1]), Read(a[2]),
@@ -294,13 +292,4 @@ public sealed class VirtualMachine
} }
} }
// Colored-draw ops (0x202/0x203) store the packed color on the object now; the actual alpha/additive
// blend in the compositor is deferred. Surface it once (not silently) via the trace sink — observe-only,
// so parity holds. See docs/superpowers/specs/2026-07-07-gfx-command-buffer-design.md (Deferrals).
private bool _warnedAlpha;
private void WarnAlphaDeferredOnce()
{
if (_warnedAlpha) return; _warnedAlpha = true;
_sink.Emit(TraceEvent.Stub(0x202, -1));
}
} }