diff --git a/engine/Age.Engine.Tests/RenderObjectBlendTests.cs b/engine/Age.Engine.Tests/RenderObjectBlendTests.cs
new file mode 100644
index 0000000..78f00d1
--- /dev/null
+++ b/engine/Age.Engine.Tests/RenderObjectBlendTests.cs
@@ -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);
+ }
+}
diff --git a/engine/Age.Engine/Model/BlendKind.cs b/engine/Age.Engine/Model/BlendKind.cs
new file mode 100644
index 0000000..9326a1c
--- /dev/null
+++ b/engine/Age.Engine/Model/BlendKind.cs
@@ -0,0 +1,6 @@
+namespace Age.Engine.Model;
+
+/// 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.
+public enum BlendKind { Opaque, Alpha, Additive }
diff --git a/engine/Age.Engine/Model/GfxState.cs b/engine/Age.Engine/Model/GfxState.cs
index 0b4b605..318870a 100644
--- a/engine/Age.Engine/Model/GfxState.cs
+++ b/engine/Age.Engine/Model/GfxState.cs
@@ -16,7 +16,7 @@ public readonly record struct AnimState(bool Enabled, bool Normalized, long TX,
/// "The full gfx render model").
public readonly record struct RenderObject(long Handle, long SurfaceResId, long ColorKey,
int SrcX, int SrcY, int W, int H, int DstX, int DstY,
- AnimState Anim);
+ AnimState Anim, int Alpha, long Tint, BlendKind Blend);
/// 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
@@ -31,6 +31,7 @@ public sealed class GfxState
public (long X, long Y, long Z) V18, V24, V16c;
public long Field64, Field68, Field6c;
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.
public int SourceSlot = -1;
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 ----
private readonly Dictionary _surfaces = new();
public void SetSurface(int slot, long resId, long colorKey) { lock (_lock) { _surfaces[slot] = (resId, colorKey); } }
+
+ /// 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).
+ 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)
/// draw-texture bind (gfx_object_bind_draw): object draws surface
@@ -188,11 +196,20 @@ public sealed class GfxState
var o = kv.Value;
if (!o.Visible) continue;
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,
(int)o.V24.X, (int)o.V24.Y,
new AnimState(o.AnimEnabled, o.AnimNormalized,
o.AnimTarget.X, o.AnimTarget.Y, o.AnimTarget.Z,
- o.AnimDurationTicks, o.AnimGeneration)));
+ o.AnimDurationTicks, o.AnimGeneration),
+ alpha, tint, blend));
}
return list;
}
diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs
index cf85f3d..a003314 100644
--- a/engine/Age.Engine/Vm/VirtualMachine.cs
+++ b/engine/Age.Engine/Vm/VirtualMachine.cs
@@ -270,12 +270,10 @@ public sealed class VirtualMachine
Gfx.EraseRange(Read(a[0]), Read(a[1])); return pc + 1;
case "gfx-elem-release": // 0x1fa (handle)
Gfx.Release(Read(a[0])); return pc + 1;
- case "gfx-blit-color": // 0x202 (handle)(x)(y)(alpha)(color) — blend deferred
- Gfx.GetOrCreate(Read(a[0])).Color = GfxState.PackColor(Read(a[3]), Read(a[4]));
- WarnAlphaDeferredOnce(); return pc + 1;
- case "gfx-draw-color": // 0x203 (handle)(v)(alpha)(color) — blend deferred
- Gfx.GetOrCreate(Read(a[0])).Color = GfxState.PackColor(Read(a[2]), Read(a[3]));
- WarnAlphaDeferredOnce(); return pc + 1;
+ case "gfx-blit-color": // 0x202 (handle)(x)(y)(alpha)(color) — static alpha/tint (anim interp deferred)
+ Gfx.SetObjectColor(Read(a[0]), GfxState.PackColor(Read(a[3]), Read(a[4]))); return pc + 1;
+ case "gfx-draw-color": // 0x203 (handle)(v)(alpha)(color) — static alpha/tint
+ Gfx.SetObjectColor(Read(a[0]), GfxState.PackColor(Read(a[2]), Read(a[3]))); return pc + 1;
// ---- 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
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));
- }
}