From ee1ee0e829748a7fd07ffb2c48767df754ae3f04 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Wed, 8 Jul 2026 19:04:12 -0400 Subject: [PATCH 1/5] feat: add BlendMath (colorkey match + ARGB unpack) Co-Authored-By: Claude Opus 4.8 --- engine/Age.Engine.Tests/BlendMathTests.cs | 34 +++++++++++++++++++++++ engine/Age.Engine/Model/BlendMath.cs | 22 +++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 engine/Age.Engine.Tests/BlendMathTests.cs create mode 100644 engine/Age.Engine/Model/BlendMath.cs 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/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)); +} From 488e86b2b81cb11dc810148a4d8334ac15177a28 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Wed, 8 Jul 2026 19:06:34 -0400 Subject: [PATCH 2/5] feat: resolve per-object alpha/tint/blend into RenderObject (colorkey retained) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../RenderObjectBlendTests.cs | 45 +++++++++++++++++++ engine/Age.Engine/Model/BlendKind.cs | 6 +++ engine/Age.Engine/Model/GfxState.cs | 21 ++++++++- engine/Age.Engine/Vm/VirtualMachine.cs | 19 ++------ 4 files changed, 74 insertions(+), 17 deletions(-) create mode 100644 engine/Age.Engine.Tests/RenderObjectBlendTests.cs create mode 100644 engine/Age.Engine/Model/BlendKind.cs 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)); - } } From 560be51011d3bc8587684dc7bdc46f30353555c5 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Wed, 8 Jul 2026 19:07:48 -0400 Subject: [PATCH 3/5] feat(godot): colorkey bake + per-object alpha/tint blit Co-Authored-By: Claude Opus 4.8 --- godot/Main.cs | 56 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/godot/Main.cs b/godot/Main.cs index df495dc..c4da62b 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 @@ -260,7 +260,8 @@ public partial class Main : Godot.Control if (v.SurfaceResId == 0) continue; // render-target/blank surface (no file) — later phase 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)); + float a = AlphaFor(v, clockReset, clockDur) * (v.Alpha / 255f); + BlitLayer(bmp, v.ColorKey, v.Tint, v.SrcX, v.SrcY, v.W, v.H, v.DstX, v.DstY, a); } _screenTex.Update(_screen); } @@ -287,44 +288,71 @@ 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); + } + + // 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. From bea5a991397d1507b3d47745bb032562ab76b892 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Wed, 8 Jul 2026 19:09:18 -0400 Subject: [PATCH 4/5] feat(godot): surfaceless color-fill quads for AE* fades (opening no longer opaque-grey) Co-Authored-By: Claude Opus 4.8 --- godot/Main.cs | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/godot/Main.cs b/godot/Main.cs index c4da62b..a43ab30 100644 --- a/godot/Main.cs +++ b/godot/Main.cs @@ -257,10 +257,21 @@ 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; - float a = AlphaFor(v, clockReset, clockDur) * (v.Alpha / 255f); BlitLayer(bmp, v.ColorKey, v.Tint, v.SrcX, v.SrcY, v.W, v.H, v.DstX, v.DstY, a); } _screenTex.Update(_screen); @@ -344,6 +355,28 @@ public partial class Main : Godot.Control _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) { From 21788a6264ae7e7f13b188a10a1bd3ae6648faab Mon Sep 17 00:00:00 2001 From: gamer147 Date: Wed, 8 Jul 2026 19:10:31 -0400 Subject: [PATCH 5/5] docs: record blend/transparency slice A (colorkey + alpha/tint + fade fill) Co-Authored-By: Claude Opus 4.8 --- docs/opcode-reference.md | 4 ++-- docs/phase-a-slice-plan.md | 29 +++++++++++++++++++++++++++++ vm-map/opcodes.toml | 4 ++-- 3 files changed, 33 insertions(+), 4 deletions(-) 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/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"