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). - **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) ### 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 - **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). - **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) ### 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 - **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra handler 0x4229a0 (dispatch ctx[0x26c93+0x203]); packs color from operands 3/4, FUN_0047e9b0(op1,op2,packed). - **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`) 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 for interactive multi-object scenes. (d) Model `0xcd get-input-type` (name-entry interactivity, the separate
input gap noted above). 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> /// "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));
}
} }

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 // 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 // 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". // 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, // 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 // 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; _lastClockGen = clockGen;
foreach (var v in _vm.Gfx.SnapshotVisibleObjects()) // already ascending-handle = z-order 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); var bmp = _host.ResolveResIdTexture(v.SurfaceResId);
if (bmp == null) continue; 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); _screenTex.Update(_screen);
} }
@@ -287,44 +299,93 @@ public partial class Main : Godot.Control
return (float)tw.CurrentA; 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(); src = new Image();
if (src.LoadBmpFromBuffer(System.IO.File.ReadAllBytes(bmpPath)) != Error.Ok) if (src.LoadBmpFromBuffer(System.IO.File.ReadAllBytes(bmpPath)) != Error.Ok)
{ GD.Print($"BMP load failed {bmpPath}"); src = null; } { GD.Print($"BMP load failed {bmpPath}"); src = null; }
else if (src.GetFormat() != Image.Format.Rgba8) src.Convert(Image.Format.Rgba8); else
_imgCache[bmpPath] = src; {
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; 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 sw = w > 0 ? w : src.GetWidth();
int sh = h > 0 ? h : src.GetHeight(); int sh = h > 0 ? h : src.GetHeight();
sw = System.Math.Min(sw, src.GetWidth() - srcX); sw = System.Math.Min(sw, src.GetWidth() - srcX);
sh = System.Math.Min(sh, src.GetHeight() - srcY); sh = System.Math.Min(sh, src.GetHeight() - srcY);
if (sw <= 0 || sh <= 0) return; 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)); _screen.BlitRect(src, new Rect2I(srcX, srcY, sw, sh), new Vector2I(dstX, dstY));
return; 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(); 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); int ia = (int)(System.Math.Clamp(alpha, 0f, 1f) * 255);
for (int y = 0; y < sh; y++) for (int y = 0; y < sh; y++)
for (int x = 0; x < sw; x++) for (int x = 0; x < sw; x++)
{ {
int dxp = dstX + x, dyp = dstY + y; 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 di = (dyp * dw + dxp) * 4;
int si = ((srcY + y) * sfw + (srcX + x)) * 4; int si = ((srcY + y) * sfw + (srcX + x)) * 4;
int sa = ss[si + 3] * ia / 255; int sa = ss[si + 3] * ia / 255; // texel alpha (colorkey already 0) × object alpha
for (int c = 0; c < 3; c++) dst[di + c] = (byte)((ss[si + c] * sa + dst[di + c] * (255 - sa)) / 255); 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); 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. // 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] [opcode.semantics]
name = "gfx-blit-color" name = "gfx-blit-color"
category = "draw" 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 noop_headless = false
source = "investigation" source = "investigation"
confidence = "high" confidence = "high"
@@ -4832,7 +4832,7 @@ abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "gfx-draw-color" name = "gfx-draw-color"
category = "draw" 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 noop_headless = false
source = "investigation" source = "investigation"
confidence = "high" confidence = "high"