diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md
index 17bbbd2..4b25a21 100644
--- a/docs/opcode-reference.md
+++ b/docs/opcode-reference.md
@@ -102,12 +102,12 @@ Native handler sleep_op_0xc8 @0x420ec0 is NON-BLOCKING: it arms a timer (sleep_t
- **evidence:** Ghidra handler 0x4227b0 (dispatch ctx[0x26c93+0x1ff]); FUN_0047e800(op1,(float)op2,(float)op3,(float)op4).
### 0x202 `gfx-blit-color` (gfx-blit-color, argc 5)
-- **summary:** 0x202 (handle)(x)(y)(alpha)(color) — gfx cmd-type 0xb. Handler gfx_op_0x202_blit_color @0x4228d0: blits object `handle` at (x,y) with a packed ARGB built from alpha(op4, ≥0x100→0xff, <0→FUN_0047f3e0) and color(op5, <0→FUN_0047f3e0) → FUN_0047ea00. See docs/engine-re.md gfx op-contract table.
+- **summary:** 0x202 (handle)(x)(y)(alpha)(color) — gfx cmd-type 0xb. Handler gfx_op_0x202_blit_color @0x4228d0: worker gfx_op_0x202_worker_set_color_anim @0x47ea00 sets an ANIMATED color/alpha target (obj+0x64) + anim bit; packs ARGB from alpha(op4, ≥0x100→0xff, <0→FUN_0047f3e0) and color(op5, <0→FUN_0047f3e0). C# VM (2026-07-08 blend slice): routes through GfxState.SetObjectColor → the compositor applies STATIC alpha/tint (BlendKind.Alpha); smooth color-anim interpolation deferred. See docs/engine-re.md §Blend & transparency.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra handler 0x4228d0 (dispatch ctx[0x26c93+0x202]); packs (alpha<<24|rgb) from operands 4/5, FUN_0047ea00(op1,op2,op3,packed).
### 0x203 `gfx-draw-color` (gfx-draw-color, argc 4)
-- **summary:** 0x203 (handle)(v)(alpha)(color) — gfx cmd-type 9. Handler gfx_op_0x203_draw_color @0x4229a0: draws object `handle` with op2 + a packed ARGB from alpha(op3)/color(op4) → FUN_0047e9b0. Sibling of 0x202 with one fewer positional arg. See docs/engine-re.md gfx op-contract table.
+- **summary:** 0x203 (handle)(v)(alpha)(color) — gfx cmd-type 9. Handler gfx_op_0x203_draw_color @0x4229a0: worker gfx_op_0x203_worker_set_color @0x47e9b0 sets a STATIC color/alpha (obj+0x60), no anim bit; packs ARGB from alpha(op3)/color(op4). Sibling of 0x202 (one fewer positional arg). C# VM (2026-07-08 blend slice): routes through GfxState.SetObjectColor → compositor applies static alpha/tint (BlendKind.Alpha). See docs/engine-re.md §Blend & transparency.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra handler 0x4229a0 (dispatch ctx[0x26c93+0x203]); packs color from operands 3/4, FUN_0047e9b0(op1,op2,packed).
diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md
index 7ab9a2a..3714d4c 100644
--- a/docs/phase-a-slice-plan.md
+++ b/docs/phase-a-slice-plan.md
@@ -600,3 +600,32 @@ compositing + cold-object anchors (the thing that makes the paced opening actual
the Ctrl `Speed` multiplier (ADV-mode-scope RE). (c) Full scene-coroutine framework (`0x7b`/`0x7c`/`0x140`)
for interactive multi-object scenes. (d) Model `0xcd get-input-type` (name-entry interactivity, the separate
input gap noted above).
+
+### A2b — Blend & transparency (slice A) ✅ DONE (2026-07-08)
+
+First of three graphics-fidelity slices (A blend/transparency, B geometry/anchors, C render-targets).
+Spec `docs/superpowers/specs/2026-07-08-blend-transparency-design.md`, plan
+`docs/superpowers/plans/2026-07-08-blend-transparency.md`, branch `feat/blend-transparency` (engine 69/69,
+sweep exit=284/STEP-LIMIT=13 parity, `SELFTEST OK`).
+
+**Landed (hybrid: engine resolves, host blits):**
+- `Age.Engine/Model/BlendMath.cs` — pure colorkey match + ARGB unpack (colorkey format reversed:
+ op arg 3 `<0` = none, else `0xRRGGBB` exact-match, `0` = key black; baked at surface-load, see
+ engine-re.md §Blend).
+- `RenderObject` gains `Alpha`/`Tint`/`Blend` (`BlendKind` Opaque|Alpha|Additive); `GfxObject.HasColor`;
+ `GfxState.SetObjectColor`; `SnapshotVisibleObjects` resolves them. `0x202`/`0x203` route through
+ `SetObjectColor` (the stale "alpha deferred" trace stub is gone — alpha is now consumed).
+- Godot compositor: colorkey-baked image cache (keyed by `(path, colorKey)`), `BlitLayer` applies
+ colorkey + object alpha + RGB-tint modulate, and **surfaceless colored objects fill a tint×alpha quad**
+ (the fades) instead of being skipped.
+
+**Result (pixels):** page-1 event-CG composites cleanly (dialogue + prompt, no opaque boxes); the opening's
+mid-fade frames now alpha-blend (dark bg + light-ray burst, then a blended dark transition) instead of the
+pre-change **full-screen opaque grey wall** that ate the CG. Verified via `--shot`/`--shot-sequence` on
+`SC0000 --boot`.
+
+**Deferred (documented, NOT built — confirmed by a stalled interp RE pass, engine-re.md §Blend):**
+smooth color-animation *interpolation* (the fade ramps snap to the correct end-state rather than gliding —
+the `0x202` color channel's blit consumer + clock coupling is a dedicated dig) and **additive/glow blend**
+(`local_2c` mode 2/3; its object field isn't pinned). `BlendKind.Additive` is an unused seam. Next graphics
+slices unchanged: B (geometry/anchors — sprite *placement*) and C (render-targets).
diff --git a/engine/Age.Engine.Tests/BlendMathTests.cs b/engine/Age.Engine.Tests/BlendMathTests.cs
new file mode 100644
index 0000000..eaffa9a
--- /dev/null
+++ b/engine/Age.Engine.Tests/BlendMathTests.cs
@@ -0,0 +1,34 @@
+using Age.Engine.Model;
+using Xunit;
+
+public class BlendMathTests
+{
+ [Fact]
+ public void NegativeColorKey_IsNoKey()
+ {
+ Assert.False(BlendMath.HasColorKey(-1));
+ Assert.False(BlendMath.ColorKeyMatches(0, 0, 0, -1)); // never matches when no key
+ }
+
+ [Fact]
+ public void ZeroColorKey_KeysBlack()
+ {
+ Assert.True(BlendMath.HasColorKey(0));
+ Assert.True(BlendMath.ColorKeyMatches(0, 0, 0, 0)); // (0,0,0) == key black
+ Assert.False(BlendMath.ColorKeyMatches(1, 0, 0, 0)); // near-black is NOT keyed (exact match)
+ }
+
+ [Fact]
+ public void ColorKey_MatchesRgb888Bytes()
+ {
+ long key = 0xFF00FF; // magenta 0xRRGGBB
+ Assert.True(BlendMath.ColorKeyMatches(0xFF, 0x00, 0xFF, key));
+ Assert.False(BlendMath.ColorKeyMatches(0xFE, 0x00, 0xFF, key));
+ }
+
+ [Fact]
+ public void UnpackArgb_SplitsBytes()
+ {
+ Assert.Equal((0x80, 0x12, 0x34, 0x56), BlendMath.UnpackArgb(0x80123456));
+ }
+}
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/BlendMath.cs b/engine/Age.Engine/Model/BlendMath.cs
new file mode 100644
index 0000000..ff65d74
--- /dev/null
+++ b/engine/Age.Engine/Model/BlendMath.cs
@@ -0,0 +1,22 @@
+namespace Age.Engine.Model;
+
+/// Pure blend helpers shared by the engine resolution and the host blit. Colorkey semantics come
+/// from the native loader (docs/engine-re.md §"Blend & transparency"): op arg 3 < 0 = no key; otherwise
+/// the operand is 0xRRGGBB and matching (R,G,B) texels are transparent (operand 0 = key black).
+public static class BlendMath
+{
+ /// A colorkey operand >= 0 is an active key; a negative operand means "no colorkey".
+ public static bool HasColorKey(long colorKey) => colorKey >= 0;
+
+ /// True when (r,g,b) exactly equals the key's 0xRRGGBB bytes. Always false when there is no key.
+ public static bool ColorKeyMatches(byte r, byte g, byte b, long colorKey)
+ {
+ if (!HasColorKey(colorKey)) return false;
+ return r == ((colorKey >> 16) & 0xff) && g == ((colorKey >> 8) & 0xff) && b == (colorKey & 0xff);
+ }
+
+ /// Split a 0xAARRGGBB packed color (see GfxState.PackColor) into its byte channels.
+ public static (int A, int R, int G, int B) UnpackArgb(long packed)
+ => ((int)((packed >> 24) & 0xff), (int)((packed >> 16) & 0xff),
+ (int)((packed >> 8) & 0xff), (int)(packed & 0xff));
+}
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));
- }
}
diff --git a/godot/Main.cs b/godot/Main.cs
index df495dc..a43ab30 100644
--- a/godot/Main.cs
+++ b/godot/Main.cs
@@ -231,7 +231,7 @@ public partial class Main : Godot.Control
// engine's z-order), each blitting its live surface's rect at its position. Surfaces are cached by BMP
// path (this runs every frame). Animated objects tween over the global anim-clock (0x238); their opacity
// is applied by the alpha-aware BlitLayer. See docs/engine-re.md "sprite transform / ANIMATION cluster".
- private readonly System.Collections.Generic.Dictionary _imgCache = new();
+ private readonly System.Collections.Generic.Dictionary<(string Path, long Key), Image?> _imgCache = new();
// Per-handle wall-clock tween of the animation channel. The engine's clock (op 0x238) is a GLOBAL,
// non-blocking clock; the host advances it here while the VM is parked at wait-for-input. Opacity comes
@@ -257,10 +257,22 @@ public partial class Main : Godot.Control
_lastClockGen = clockGen;
foreach (var v in _vm.Gfx.SnapshotVisibleObjects()) // already ascending-handle = z-order
{
- if (v.SurfaceResId == 0) continue; // render-target/blank surface (no file) — later phase
+ float a = AlphaFor(v, clockReset, clockDur) * (v.Alpha / 255f);
+ if (v.SurfaceResId == 0)
+ {
+ // A colored object with no bound surface = a fade/flash fill (e.g. fade-to-black). Fill its
+ // rect (full-screen when it has no size, the opening's case) with the tint at alpha. Uncolored
+ // surfaceless objects are render targets — still skipped (slice C).
+ if (v.Blend != Age.Engine.Model.BlendKind.Opaque)
+ {
+ int fw = v.W > 0 ? v.W : 800, fh = v.H > 0 ? v.H : 600;
+ FillQuad(v.DstX, v.DstY, fw, fh, v.Tint, a);
+ }
+ continue;
+ }
var bmp = _host.ResolveResIdTexture(v.SurfaceResId);
if (bmp == null) continue;
- BlitLayer(bmp, v.SrcX, v.SrcY, v.W, v.H, v.DstX, v.DstY, AlphaFor(v, clockReset, clockDur));
+ BlitLayer(bmp, v.ColorKey, v.Tint, v.SrcX, v.SrcY, v.W, v.H, v.DstX, v.DstY, a);
}
_screenTex.Update(_screen);
}
@@ -287,44 +299,93 @@ public partial class Main : Godot.Control
return (float)tw.CurrentA;
}
- private void BlitLayer(string bmpPath, int srcX, int srcY, int w, int h, int dstX, int dstY, float alpha = 1f)
+ // Blit one object's surface rect. The source Image is cached per (path, colorKey): on first load, texels
+ // matching the surface colorkey are made transparent (native bakes the key at load — engine-re.md §Blend).
+ // tint (0xRRGGBB) modulates the texel RGB (fade-to-black uses tint=black); alpha is the object's opacity.
+ private void BlitLayer(string bmpPath, long colorKey, long tint, int srcX, int srcY, int w, int h,
+ int dstX, int dstY, float alpha = 1f)
{
- if (!_imgCache.TryGetValue(bmpPath, out var src))
+ var cacheKey = (bmpPath, colorKey);
+ if (!_imgCache.TryGetValue(cacheKey, out var src))
{
src = new Image();
if (src.LoadBmpFromBuffer(System.IO.File.ReadAllBytes(bmpPath)) != Error.Ok)
{ GD.Print($"BMP load failed {bmpPath}"); src = null; }
- else if (src.GetFormat() != Image.Format.Rgba8) src.Convert(Image.Format.Rgba8);
- _imgCache[bmpPath] = src;
+ else
+ {
+ if (src.GetFormat() != Image.Format.Rgba8) src.Convert(Image.Format.Rgba8);
+ if (Age.Engine.Model.BlendMath.HasColorKey(colorKey)) BakeColorKey(src, colorKey);
+ }
+ _imgCache[cacheKey] = src;
}
if (src == null) return;
- // Clamp the source rect to the image; a zero/negative size falls back to the full image.
+
int sw = w > 0 ? w : src.GetWidth();
int sh = h > 0 ? h : src.GetHeight();
sw = System.Math.Min(sw, src.GetWidth() - srcX);
sh = System.Math.Min(sh, src.GetHeight() - srcY);
if (sw <= 0 || sh <= 0) return;
- if (alpha >= 0.999f) // fast opaque path (unchanged behaviour for non-animating objects)
+
+ bool plainOpaque = alpha >= 0.999f && tint == 0xFFFFFF && !Age.Engine.Model.BlendMath.HasColorKey(colorKey);
+ if (plainOpaque) // fast path: unchanged behaviour for opaque, un-keyed, un-tinted layers
{
_screen.BlitRect(src, new Rect2I(srcX, srcY, sw, sh), new Vector2I(dstX, dstY));
return;
}
- // Alpha composite over the raw RGBA byte buffer: out = src*(sa) + dst*(1-sa), sa = srcAlpha * objAlpha.
+
+ int tr = (int)((tint >> 16) & 0xff), tg = (int)((tint >> 8) & 0xff), tb = (int)(tint & 0xff);
byte[] dst = _screen.GetData(); byte[] ss = src.GetData();
- int dw = _screen.GetWidth(), sfw = src.GetWidth();
+ int dw = _screen.GetWidth(), dh = _screen.GetHeight(), sfw = src.GetWidth();
int ia = (int)(System.Math.Clamp(alpha, 0f, 1f) * 255);
for (int y = 0; y < sh; y++)
for (int x = 0; x < sw; x++)
{
int dxp = dstX + x, dyp = dstY + y;
- if (dxp < 0 || dyp < 0 || dxp >= dw || dyp >= _screen.GetHeight()) continue;
+ if (dxp < 0 || dyp < 0 || dxp >= dw || dyp >= dh) continue;
int di = (dyp * dw + dxp) * 4;
int si = ((srcY + y) * sfw + (srcX + x)) * 4;
- int sa = ss[si + 3] * ia / 255;
- for (int c = 0; c < 3; c++) dst[di + c] = (byte)((ss[si + c] * sa + dst[di + c] * (255 - sa)) / 255);
+ int sa = ss[si + 3] * ia / 255; // texel alpha (colorkey already 0) × object alpha
+ if (sa == 0) continue;
+ int sr = ss[si] * tr / 255, sg = ss[si + 1] * tg / 255, sb = ss[si + 2] * tb / 255; // tint modulate
+ dst[di] = (byte)((sr * sa + dst[di] * (255 - sa)) / 255);
+ dst[di + 1] = (byte)((sg * sa + dst[di + 1] * (255 - sa)) / 255);
+ dst[di + 2] = (byte)((sb * sa + dst[di + 2] * (255 - sa)) / 255);
dst[di + 3] = (byte)System.Math.Min(255, dst[di + 3] + sa);
}
- _screen.SetData(dw, _screen.GetHeight(), false, _screen.GetFormat(), dst);
+ _screen.SetData(dw, dh, false, _screen.GetFormat(), dst);
+ }
+
+ // Alpha-blend a solid tint (0xRRGGBB) rectangle over the screen — the surfaceless fade/flash fill.
+ private void FillQuad(int dstX, int dstY, int w, int h, long tint, float alpha)
+ {
+ int ia = (int)(System.Math.Clamp(alpha, 0f, 1f) * 255);
+ if (ia == 0) return;
+ int tr = (int)((tint >> 16) & 0xff), tg = (int)((tint >> 8) & 0xff), tb = (int)(tint & 0xff);
+ byte[] dst = _screen.GetData();
+ int dw = _screen.GetWidth(), dh = _screen.GetHeight();
+ for (int y = 0; y < h; y++)
+ for (int x = 0; x < w; x++)
+ {
+ int dxp = dstX + x, dyp = dstY + y;
+ if (dxp < 0 || dyp < 0 || dxp >= dw || dyp >= dh) continue;
+ int di = (dyp * dw + dxp) * 4;
+ dst[di] = (byte)((tr * ia + dst[di] * (255 - ia)) / 255);
+ dst[di + 1] = (byte)((tg * ia + dst[di + 1] * (255 - ia)) / 255);
+ dst[di + 2] = (byte)((tb * ia + dst[di + 2] * (255 - ia)) / 255);
+ dst[di + 3] = (byte)System.Math.Min(255, dst[di + 3] + ia);
+ }
+ _screen.SetData(dw, dh, false, _screen.GetFormat(), dst);
+ }
+
+ // Make colorkey-matching texels transparent (native colorkey is baked at surface load).
+ private static void BakeColorKey(Image img, long colorKey)
+ {
+ byte[] px = img.GetData();
+ int w = img.GetWidth(), h = img.GetHeight();
+ for (int i = 0; i < px.Length; i += 4)
+ if (Age.Engine.Model.BlendMath.ColorKeyMatches(px[i], px[i + 1], px[i + 2], colorKey))
+ px[i + 3] = 0;
+ img.SetData(w, h, false, img.GetFormat(), px);
}
// Load an OGG off disk and play it. BGM loops; voice plays once, cutting off any prior line.
diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml
index 30654da..186c733 100644
--- a/vm-map/opcodes.toml
+++ b/vm-map/opcodes.toml
@@ -4791,7 +4791,7 @@ abi_source = "kelebek+decode-validated"
[opcode.semantics]
name = "gfx-blit-color"
category = "draw"
-summary = "0x202 (handle)(x)(y)(alpha)(color) — gfx cmd-type 0xb. Handler gfx_op_0x202_blit_color @0x4228d0: blits object `handle` at (x,y) with a packed ARGB built from alpha(op4, ≥0x100→0xff, <0→FUN_0047f3e0) and color(op5, <0→FUN_0047f3e0) → FUN_0047ea00. See docs/engine-re.md gfx op-contract table."
+summary = "0x202 (handle)(x)(y)(alpha)(color) — gfx cmd-type 0xb. Handler gfx_op_0x202_blit_color @0x4228d0: worker gfx_op_0x202_worker_set_color_anim @0x47ea00 sets an ANIMATED color/alpha target (obj+0x64) + anim bit; packs ARGB from alpha(op4, ≥0x100→0xff, <0→FUN_0047f3e0) and color(op5, <0→FUN_0047f3e0). C# VM (2026-07-08 blend slice): routes through GfxState.SetObjectColor → the compositor applies STATIC alpha/tint (BlendKind.Alpha); smooth color-anim interpolation deferred. See docs/engine-re.md §Blend & transparency."
noop_headless = false
source = "investigation"
confidence = "high"
@@ -4832,7 +4832,7 @@ abi_source = "kelebek+decode-validated"
[opcode.semantics]
name = "gfx-draw-color"
category = "draw"
-summary = "0x203 (handle)(v)(alpha)(color) — gfx cmd-type 9. Handler gfx_op_0x203_draw_color @0x4229a0: draws object `handle` with op2 + a packed ARGB from alpha(op3)/color(op4) → FUN_0047e9b0. Sibling of 0x202 with one fewer positional arg. See docs/engine-re.md gfx op-contract table."
+summary = "0x203 (handle)(v)(alpha)(color) — gfx cmd-type 9. Handler gfx_op_0x203_draw_color @0x4229a0: worker gfx_op_0x203_worker_set_color @0x47e9b0 sets a STATIC color/alpha (obj+0x60), no anim bit; packs ARGB from alpha(op3)/color(op4). Sibling of 0x202 (one fewer positional arg). C# VM (2026-07-08 blend slice): routes through GfxState.SetObjectColor → compositor applies static alpha/tint (BlendKind.Alpha). See docs/engine-re.md §Blend & transparency."
noop_headless = false
source = "investigation"
confidence = "high"