Files
OpenMaidEngine/docs/superpowers/specs/2026-07-08-blend-transparency-design.md
gamer147 d5ea8aa84e docs(spec): blend & transparency slice A design (colorkey + alpha/tint + blend mode)
Hybrid architecture (engine resolves RenderObject blend plan, host blits).
Grounded in fresh Ghidra RE of gfx_object_composite/blit + the 0x202/0x203
color workers (renamed+plate-commented, saved). Includes a bounded RE Task-0
(colorkey format, blend-mode source, color/alpha anim coupling).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 18:02:35 -04:00

132 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Slice A — Blend & Transparency (design)
**Date:** 2026-07-08
**Status:** design, pending implementation plan
**Scope:** the first of three graphics-fidelity slices (A = blend/transparency, B = geometry/anchors,
C = render-targets). This spec covers **A only**.
## Problem
The Godot compositor renders every visible gfx object as an **opaque rectangle**. Two consequences:
1. **Sprites/overlays are opaque boxes** — the per-surface **colorkey** (chroma transparency, op `0x1f9`
arg 3) is captured in the model but never applied, so a character sprite paints its whole rectangle
(including the background color that should be keyed out) over the scene.
2. **Fades/flashes (`AE*`) draw opaque and never clear** — ops `0x202`/`0x203` set a per-object
color/alpha modulation (fade-to-black/white, glow), captured on the object as a packed ARGB but never
read by the compositor. In the SC0000 opening this shows as a full-screen grey layer that covers the
event-CG and stays (verified via windowed `--shot-sequence`, frame ~4560).
Both defects are *blend/transparency*: the model already **captures** the inputs; the render path does not
**consume** them. This slice makes the compositor honor colorkey + per-object alpha/tint + blend mode.
Out of scope (later slices): object placement/anchor math (V18/V24, the "doubling") = slice B;
render-target / off-screen surfaces (`SurfaceResId==0` blanks) = slice C.
## Native model (reversed — this is the source of truth)
Canonical: `docs/engine-re.md` §"The full gfx render model". Confirmed/refined in Ghidra this session
(functions renamed + plate-commented, program saved):
- **Surfaces** (`ctx+0x52bd4[slot]`) carry `{image, colorkey}`. `gfx_op_0x1f9_load_surface` (`0x422360`)
loads the file into the slot **with a colorkey**; colorkey-matching texels become transparent **at load
time** — it is NOT a per-blit compare.
- **Objects** (`ctx+0x408` map, keyed by handle) carry `{slot, srcRect, position V24, anchor V18, anim,
color/alpha, visible, blend-mode}`. Composited in **ascending-handle order = z-order**.
- **`gfx_op_0x202_worker_set_color_anim`** (`0x47ea00`): op `0x202` sets an **animated** color/alpha target
(`obj+0x64 = packedARGB`), sets the color-anim active bit, resets progress `obj+0x34=0`. Animates over the
global clock (op `0x238`), like `0x234` does for position. Fades = animate a full-screen object (e.g.
handle `0xcf08`) toward black/white × alpha.
- **`gfx_op_0x203_worker_set_color`** (`0x47e9b0`): op `0x203` sets a **static** color/alpha (`obj+0x60`),
no anim bit. Sibling of `0x202`.
- **`gfx_object_composite`** (`0x47f650`): transform (`FUN_00472f00`) → anim interpolation
(`FUN_00473ed0`) if the anim bit is set → **`gfx_object_blit_d3d9`** (`0x4774c0`).
- **`gfx_object_blit_d3d9`** (`0x4774c0`): clips the source rect to the surface bounds, selects a **blend
mode** via `local_2c` (0 = opaque; 1 = alpha, `D3DRS_SRCBLEND=SRCALPHA`/`DESTBLEND=INVSRCALPHA`;
2/3 = additive/special for glow/flash), and passes a **modulation color/alpha** (default `1.0f`) to the
device draw. We do **not** reimplement D3D9 — we reproduce its observable effect (alpha blend, additive
blend, tint modulation, colorkey transparency) in Godot.
## Architecture (approach 3: hybrid — engine resolves, host blits)
The engine resolves each visible object into a fully-specified **blend plan** (`RenderObject`); the Godot
host executes a dumb colorkey/alpha/tint/blend blit. The existing `Age.Engine/Model` → host-compositor seam
is unchanged. Rationale: the RE-derived *decision* logic becomes unit-testable in the engine xUnit suite
without building a software rasterizer; the pixel work stays in the host where Godot's `Image` already helps.
### Data flow
```
VM ops (engine thread) Model (GfxState) Host compositor (_Process)
set-texture 0x1f9(resId,slot,colorkey) → _surfaces[slot]={resId,colorKey}
draw-texture 0x1fb(handle,slot,rect,pos)→ obj{slot,srcRect,V24,visible}
0x203 draw-color (handle, packedARGB) → obj.Color (static) + HasColor
0x202 blit-color (handle, packedARGB) → obj.ColorTarget (anim) + anim bit
0x234/0x238 (anim + global clock) → obj.anim, AnimClock
SnapshotVisibleObjects() → RenderObject per visible obj, ascending-handle:
{ Handle, SurfaceResId, ColorKey, srcRect, dstPos,
Alpha(0..255), Tint(RGB24), BlendMode(Opaque|Alpha|Additive), Anim }
├─► load BMP; bake ColorKey→α0 (cache by (path,key))
├─► Alpha = colorAlpha × animTweenAlpha
└─► blit rect@dstPos with BlendMode + Alpha + Tint
```
### Units
**Unit 1 — Engine: `RenderObject` blend fields + resolution** (`Age.Engine/Model/GfxState.cs`; xUnit-tested)
- Extend `RenderObject` with `Alpha` (0255), `Tint` (RGB24), `BlendMode` (`Opaque|Alpha|Additive`).
`ColorKey` already exists.
- `GfxObject` gains a `HasColor` flag so an object that never received `0x202`/`0x203` resolves to
**opaque (255)**, avoiding the "packed-0 ⇒ alpha 0 ⇒ invisible" trap.
- `SnapshotVisibleObjects` resolves `Alpha`/`Tint`/`BlendMode` from the object's static (`0x203`, `obj+0x60`)
and animated (`0x202`, `obj+0x64`) color per the Task-0 formula; unchanged for objects with no color.
- Pure data transform — tested as **ops-in → RenderObject-fields-out** (no pixels).
**Unit 2 — Host: colorkey bake + alpha/tint/blend blit** (`godot/Main.cs` compositor)
- Image cache keyed by `(bmpPath, colorKey)`: on first load, set colorkey-matching pixels to α0.
- `BlitLayer` honors object `Alpha` (folds into the existing alpha path) and `Tint`, and selects the Godot
blend for `BlendMode` (`Alpha` = normal, `Additive` = additive `CanvasItemMaterial`/manual add).
- Surfaceless colored objects (`SurfaceResId==0` **and** `HasColor`) fill a `Tint`×`Alpha` quad over their
rect instead of being skipped.
**Unit 3 — Alpha source unification** (`AlphaFor` in `Main.cs`)
- Final opacity = anim-tween alpha (existing, from the `0x238` clock) combined with the object's static/
animated color alpha, so a fade that ramps its alpha over the clock works and a static alpha holds.
## RE Task-0 (first task in the implementation plan — bounded)
Pin three unknowns in Ghidra (program already loaded: `build/engine-dump/range_00400000.bin`), each with a
known address and a concrete output. Verify each against the SC0000 opening via the `gfx` oracle (model)
and `--shot` (pixels). If a mode proves unused in the opening (e.g. additive), scope it out of this slice
and `log`/note it.
1. **Colorkey format & application** — `gfx_op_0x1f9_load_surface` (`0x422360` → resolver `0x44f390`):
the colorkey value's pixel format (RGB888 / RGB565 / palette index) and confirm load-time transparency.
*Output:* the exact compare the host uses when baking `(path, colorKey)`.
2. **Blend-mode source** — `gfx_object_blit_d3d9` (`0x4774c0`) `local_2c`: which object field selects
opaque/alpha/additive and which op sets it. *Output:* `RenderObject.BlendMode` resolution + the
D3D→Godot mapping.
3. **Color/alpha modulation & anim coupling** — `gfx_object_composite` (`0x47f650`) → `FUN_00473ed0`
(anim interp) + `obj+0x60`/`obj+0x64`: how static (`0x203`) and animated (`0x202`) color resolve to a
final (tint, alpha) at a given clock progress. *Output:* the `Alpha`/`Tint` formula in
`SnapshotVisibleObjects`.
## Testing & acceptance
- **Engine (xUnit):** resolution tests — a small op sequence produces the expected `RenderObject`
{Alpha, Tint, BlendMode, ColorKey}; explicitly cover the opaque-default (no color op) case and the
static-vs-animated color paths.
- **Parity:** the engine suite, `sweep` (exit=284/STEP-LIMIT=13), and the Godot `--selftest` stay green
(this slice adds model fields + host rendering; headless output is unaffected).
- **Host (pixels):** `--shot`/`--shot-sequence` on `SC0000 --boot` — the opening's fade should **fade and
clear** (event-CG visible, no persistent opaque grey), and any keyed sprite composites without its
background box. This is the eyeball acceptance the user validates.
## Non-goals
- Object placement / anchor / scale math (slice B).
- Off-screen render-target surfaces (slice C).
- Reimplementing D3D9; we reproduce its observable blend effects only.
- Ctrl fast-forward `Speed` (separate, already-seamed slice).