Merge feat/blend-transparency: colorkey + per-object alpha/tint blend (graphics slice A)

Fixes the SC0000 opening's opaque grey-wall: layers now alpha-blend and honor
per-surface colorkey. Hybrid arch (engine resolves RenderObject blend plan,
Godot host blits). Deferred: smooth color-anim interpolation + additive/glow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-07-08 19:13:18 -04:00
10 changed files with 239 additions and 36 deletions

View File

@@ -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).

View File

@@ -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).

View File

@@ -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));
}
}

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

@@ -0,0 +1,22 @@
namespace Age.Engine.Model;
/// <summary>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 &lt; 0 = no key; otherwise
/// the operand is 0xRRGGBB and matching (R,G,B) texels are transparent (operand 0 = key black).</summary>
public static class BlendMath
{
/// <summary>A colorkey operand &gt;= 0 is an active key; a negative operand means "no colorkey".</summary>
public static bool HasColorKey(long colorKey) => colorKey >= 0;
/// <summary>True when (r,g,b) exactly equals the key's 0xRRGGBB bytes. Always false when there is no key.</summary>
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);
}
/// <summary>Split a 0xAARRGGBB packed color (see GfxState.PackColor) into its byte channels.</summary>
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));
}

View File

@@ -16,7 +16,7 @@ public readonly record struct AnimState(bool Enabled, bool Normalized, long TX,
/// "The full gfx render model").</summary>
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);
/// <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
@@ -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<int, (long ResId, long ColorKey)> _surfaces = new();
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)
/// <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;
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;
}

View File

@@ -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));
}
}

View File

@@ -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<string, Image?> _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.

View File

@@ -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"