Implement native 0x202 color interpolation
This commit is contained in:
@@ -756,14 +756,15 @@ Ghidra functions renamed + plate-commented, saved).
|
||||
RGB888; `<0` = none; else texels whose `(R,G,B)` equal the key become transparent when the surface image
|
||||
is loaded/cached.
|
||||
- **`0x202` (`gfx_op_0x202_worker_set_color_anim` `0x47ea00`)**: sets an **animated** color/alpha target
|
||||
`obj+0x64 = packedARGB`, the color-anim active bit, resets progress `obj+0x34=0`. Animates over the global
|
||||
clock (`0x238`).
|
||||
`obj+0x64 = packedARGB`, the color-anim active bit, and resets shared start `obj+0x34=0`. Operands 2/3
|
||||
are delay/duration at `obj+0x38/+0x4c`; sampling uses frame clock `ctx+0xb550`, not op `0x238`.
|
||||
- **`0x203` (`gfx_op_0x203_worker_set_color` `0x47e9b0`)**: sets a **static** color/alpha `obj+0x60`, no anim
|
||||
bit. Immediate per-object modulation.
|
||||
- **Blit** (`gfx_object_blit_d3d9` `0x4774c0`): selects a **blend mode** (`local_2c`: 0 opaque, 1 alpha
|
||||
`SRCALPHA/INVSRCALPHA`, 2/3 additive/special for glow/flash) and passes a modulation color/alpha to the
|
||||
device draw. Slice A ports the **alpha** path (fades); additive (glow) is deferred (its `local_2c` source
|
||||
field is not yet pinned).
|
||||
field is `obj+0x30`, the value written by op `0x203`. Mode 1 uses packed ARGB alpha as opacity and RGB
|
||||
as multiplicative D3D modulation. Modes 2/3 remain separately scoped beyond the completed mode-1 path.
|
||||
|
||||
**Interpolation RE pass (2026-07-08, stalled → both deferrals confirmed).** Attempted to pin how a `0x202`
|
||||
fade animates so smooth ramping could join slice A. Findings (Ghidra `gfx_object_anim_interpolate`
|
||||
@@ -778,6 +779,20 @@ one-shot fade's exact source→target→easing is still unresolved and would tak
|
||||
stays deferred**; slice A ships the static end-state (which reaches the correct final alpha/tint and fixes
|
||||
the stuck-opaque bug), with interpolation as a scoped follow-up.
|
||||
|
||||
**Resolution (2026-07-10 — supersedes the deferral above).** The missing consumer was the bit-1 branch in
|
||||
`gfx_object_apply_transform_channels` (`0x472f00`), before its matrix channels. It seeds shared start
|
||||
`obj+0x34` from `ctx+0xb550`; holds current packed ARGB `obj+0x60` through delay `+0x38`; then performs an
|
||||
integer, bytewise linear interpolation to target `+0x64` for duration `+0x4c`. At natural completion—or
|
||||
when `ctx+0xb55c == 1` requests forced completion—the target commits to current, delay/duration clear,
|
||||
target becomes `0xffffffff`, and the one-shot active bit clears when no color/matrix/src-rect sibling remains.
|
||||
The object-local override bit at `+0x2d0` suppresses the global force. Negative alpha/RGB target operands
|
||||
independently preserve their bytes from current `+0x60`.
|
||||
|
||||
The port now carries current and target separately and samples them from the unified `FrameClock`; an op
|
||||
`0x203` static write after `0x202` therefore becomes the ramp's current value rather than overwriting its
|
||||
target. Mode 0 retains the established CG/tint/fill behavior; mode 1 now uses native alpha opacity plus RGB
|
||||
modulation. `draw-string 0x204`/`0x7a` remains a separate dependency.
|
||||
|
||||
### SC0000 anim/transform/spritesheet cluster — op→field map (2026-07-08)
|
||||
|
||||
Reversed for the animation cluster slice (spec `docs/superpowers/specs/2026-07-08-sc0000-anim-transform-cluster-design.md`).
|
||||
|
||||
@@ -132,14 +132,14 @@ SC0000 label_1235a reaches this when the OR of 0x1c7 message-skip and 0x1cc read
|
||||
- **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: 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.
|
||||
- **summary:** 0x202 (handle)(delay_ms)(duration_ms)(alpha)(color) — arm the one-shot packed-ARGB channel. Worker gfx_op_0x202_worker_set_color_anim @0x47ea00 resets shared start obj+0x34, writes delay +0x38, duration +0x4c, and target +0x64. gfx_object_apply_transform_channels @0x472f00 linearly interpolates each byte from current +0x60 on frame clock ctx+0xb550, commits the target, clears timing, writes target -1, and clears the one-shot active bit when all sibling channels finish. Negative alpha/RGB independently preserve current bytes. Implemented in GfxState with synchronized current/target timeline evidence; draw-string 0x204/0x7a remains separate.
|
||||
- **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 packs operands 4/5 and calls worker 0x47ea00(handle,delay,duration,packed). Consumer 0x472f00: shared start +0x34; color delay/duration +0x38/+0x4c; current/target +0x60/+0x64; frame clock ctx+0xb550; bytewise integer LERP; natural or ctx+0xb55c forced completion. /v2 annotated and saved 2026-07-10.
|
||||
|
||||
### 0x203 `gfx-draw-color` (gfx-draw-color, argc 4)
|
||||
- **summary:** 0x203 (handle)(mode)(alpha)(color) — gfx cmd-type 9. Worker stores mode at obj+0x30 and STATIC packed color at obj+0x60. Negative alpha/RGB preserve current static bytes through FUN_0047f3e0. Mode 2 is the 0x223 transition-source alpha/identity path (0xffffffff = opaque identity, not solid white); mode 0 retains the established tint-strength behavior.
|
||||
- **summary:** 0x203 (handle)(mode)(alpha)(color) — gfx cmd-type 9. Worker stores the D3D blend selector at obj+0x30 and STATIC packed color at obj+0x60. Negative alpha/RGB preserve current static bytes. Mode 0 retains established opaque CG/tint/fill behavior; mode 1 is SRCALPHA/INVSRCALPHA with ARGB alpha opacity and multiplicative RGB modulation; mode 2 is the 0x223 transition-source identity path.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra handler 0x4229a0 (dispatch ctx[0x26c93+0x203]); negative operands read current obj+0x60 via FUN_0047f3e0, then gfx_op_0x203_worker_set_color stores op2 at obj+0x30 and packed ARGB at obj+0x60. SC0000 transition setup uses mode 2 with -1/-1 before 0x21d/0x223; synchronized window pixels prove treating 0xffffffff as generic white tint is wrong.
|
||||
- **evidence:** Ghidra handler 0x4229a0; negative operands read current obj+0x60, then worker 0x47e9b0 stores op2 at obj+0x30 and ARGB at +0x60. gfx_object_composite call-site 0x47f78f passes +0x30/+0x60 directly to gfx_object_blit_d3d9; mode 1 sets D3DRS SRCALPHA/INVSRCALPHA and the packed color is the device draw modulation. Mode 2 transition setup and synchronized pixels prove 0xffffffff is identity, not solid white.
|
||||
|
||||
### 0x208 `get-texture-size` (get-texture-size, argc 3)
|
||||
- **summary:** 0x208 (slot)(out_w)(out_h) — writes the loaded texture's width/height into two output globals; keystone for bytecode-computed sprite/bg geometry (SC0000 label_12649)
|
||||
|
||||
@@ -939,3 +939,44 @@ apart from the pre-existing nullable warning and threaded `SELFTEST OK`; all six
|
||||
opcode/ctx lint, 481-script decode validation, RECOVER, and Windows CR-aware `git diff --check` clean.
|
||||
The plain check reports only the generated reference's CRLF on its newly added row. SC0000 coverage is
|
||||
**85/129 handled (65.9%)**, 44 GAP ops / 598 GAP instructions. No commit was made.
|
||||
|
||||
### A2b — op 0x202 one-shot color/presentation ✅ DONE (2026-07-10)
|
||||
|
||||
**Native contract.** `gfx_op_0x202_worker_set_color_anim@0x47ea00` writes target packed ARGB at
|
||||
`obj+0x64`, delay/duration at `+0x38/+0x4c`, and resets the one-shot family's shared start `+0x34`.
|
||||
The missing consumer is the bit-1 path in `gfx_object_apply_transform_channels@0x472f00`: it seeds start
|
||||
from frame time `ctx+0xb550`, performs an integer bytewise current-`+0x60` to target-`+0x64` LERP, then
|
||||
commits target, clears timing, writes target `-1`, and clears the active bit once all sibling one-shot
|
||||
channels finish. This is ordinary presentation-clock coupling, not op `0x238`'s separate service clock.
|
||||
`ctx+0xb55c == 1` forces completion unless the object-local `+0x2d0` override bit is set. `/v2` comments
|
||||
were corrected and saved.
|
||||
|
||||
**Port and endpoint diagnosis.** `GfxState` now retains current/target/delay/duration separately, shares
|
||||
the start timestamp with scale/rotation/translation, and exposes synchronized `color=current->target` plus
|
||||
progress in the compositor/timeline log. This fixes the initial black-to-EV049AA cut: windowed frames sample
|
||||
the full-screen `0xcf08` black owner through 11 monotonically decreasing states from `1.00` to `0.00` instead
|
||||
of installing the transparent endpoint immediately. The ADV textbox backing `0xd2f0` is a real 800×227 black
|
||||
fill at `(0,373)`; repeated show/hide passages now produce intermediate samples (for example
|
||||
`0.00, 0.26, 0.54, 0.82, 1.00` and the reverse), synchronized to the executing `0x202` sites around
|
||||
`0x12283..0x12342`.
|
||||
|
||||
The remaining full-white runs are not a stuck color clock or d2f0 endpoint: pixel windows place them after
|
||||
d2f0 is transparent, while object timelines show `AE*` handles first becoming visible in mode 0 with default
|
||||
`0xffffffff` before their later mode-1 initializer. Native call-site disassembly proves op `0x203`'s
|
||||
`obj+0x30` is passed directly as the D3D blend selector and mode 1 uses `SRCALPHA/INVSRCALPHA`; the port now
|
||||
uses ARGB alpha as opacity and RGB as multiplicative modulation for that mode. The long white intervals did
|
||||
not disappear, which localizes the residual to when intermediate retained state is presented/batched, not to
|
||||
0x202 interpolation. Altering that scheduler boundary without a matching native present trace would exceed
|
||||
this bounded slice. `draw-string 0x204`/`0x7a` was not pulled in.
|
||||
|
||||
Windowed evidence: 340 PNGs, 107 distinct pixel states; black-to-first-CG and d2f0 ramps agree with synchronized
|
||||
object progress. Focused tests cover exact integer midpoint math, delay, natural completion field cleanup,
|
||||
negative sentinel resolution, the 0x202-then-0x203 current/target pattern, shared color/matrix clock start,
|
||||
mode-1 opacity/modulation, and the software rasterizer.
|
||||
|
||||
**Validation:** engine **108/108**; full sweep unchanged at **284 exit / 13 STEP-LIMIT**; Godot build clean
|
||||
apart from the pre-existing nullable warning and threaded `SELFTEST OK`; all seven Python test scripts,
|
||||
opcode/ctx lint, 481-script decode validation, and RECOVER clean. SC0000 coverage remains **85/129 handled
|
||||
(65.9%)**, 44 GAP ops / 598 GAP instructions. The differential oracle retains its prior branch-state
|
||||
divergence after `0x12031`; it agrees through the executed `0x202`/`0x203` sequence and does not implicate
|
||||
this slice. Windows CR-aware whitespace validation is clean.
|
||||
|
||||
@@ -138,7 +138,9 @@ texture ops (no GPU context) — run windowed for real scenes. User args (after
|
||||
- `--speed <f>` — scale the unified runtime clock (VM cadence, sleeps, and retained animation) without auto-advancing input waits. Values 0.05–8 are accepted; `--speed 0.25` is useful for transform inspection, while 1.0 is normal playback.
|
||||
- `--gfx-log <file>` — **compositor + op diagnostic** (the tool that root-caused the grey background). Logs, per rendered frame, only the objects whose draw outcome **CHANGED** (drawn↔skip↔gone, resId, resolved file, `slot`, `src`/`dst`, `op`acity, `tintStr`ength) — quiet until something actually changes, so the exact frame a layer drops out (and why) stands out. Also traces every `set-texture`/`create-texture` **slot assignment** (via `GodotAdvHost.TraceOps`). Works live or with `--shot-sequence`. Use it before theorising about layering/blend/geometry: it showed the grey BG = the slot-selecting globals resolving to 0 → every texture collapsing into slot 0 (see engine-re.md §"Grey-background root cause"). e.g. `godot --path godot -- --boot --gfx-log out/gfx.log` then click to the bad page.
|
||||
Matrix-channel outcomes also include `base`, `anchor`, projected `dst`, sampled `scale`/`trans`, and
|
||||
one-shot-plus-cyclic `rot`ation angles. Parent directories are created automatically.
|
||||
one-shot-plus-cyclic `rot`ation angles. Active op-`0x202` outcomes include packed `color=current->target`
|
||||
and `colorProgress`, synchronized with the same frame/clock in `--timeline-log`. Parent directories are
|
||||
created automatically.
|
||||
- `--transition-click-ms <n>` — diagnostic-only input injector: after a foreground transition has been active for `n` virtual milliseconds, send one click through the real input lifecycle. The click completes/consumes the transition and does not advance a stable page. Use with `--timeline-log`, `--gfx-log`, and windowed `--shot-sequence`; omit for normal play.
|
||||
- `--timeline-log <jsonl>` — diagnostic-only synchronized event stream for a real Godot run. Records every
|
||||
executed script byte offset/opcode, virtual time/frame, VM state changes (`running`, `sleep`, `input-wait`,
|
||||
|
||||
@@ -73,7 +73,9 @@ public class GfxCommandBufferTests
|
||||
}, System.Array.Empty<string>());
|
||||
var vm = new VirtualMachine(scene, t, new RecordingHost());
|
||||
vm.Run();
|
||||
Assert.Equal(0x80_112233L, vm.Gfx.TryGet(0x1000)!.Color);
|
||||
var o = vm.Gfx.TryGet(0x1000)!;
|
||||
Assert.Equal(0x80_112233L, o.OneShotColorTarget);
|
||||
Assert.True(o.OneShotColorEnabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
75
engine/Age.Engine.Tests/OneShotColorTests.cs
Normal file
75
engine/Age.Engine.Tests/OneShotColorTests.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using Age.Engine.Model;
|
||||
|
||||
namespace Age.Engine.Tests;
|
||||
|
||||
public class OneShotColorTests
|
||||
{
|
||||
private static GfxState Visible(long current)
|
||||
{
|
||||
var gfx = new GfxState();
|
||||
gfx.SetSurface(1, 0x23, -1);
|
||||
gfx.BindDraw(0x100, 1, 0, 0, 100, 100, 0, 0);
|
||||
gfx.SetObjectColor(0x100, current);
|
||||
return gfx;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DelayedColor_UsesCurrentTargetAndCommitsWithNativeIntegerLerp()
|
||||
{
|
||||
var gfx = Visible(GfxState.PackColor(0x00, 0x102030));
|
||||
gfx.SetAnimatedObjectColorResolved(0x100, 100, 400, 0xff, 0x90a0b0);
|
||||
|
||||
var start = gfx.SnapshotVisibleObjects(1000).Single();
|
||||
Assert.Equal(0, start.TintStrength);
|
||||
Assert.Equal(0.0, start.ColorTransition!.Value.Progress);
|
||||
Assert.Equal(0, gfx.SnapshotVisibleObjects(1100).Single().TintStrength);
|
||||
|
||||
var half = gfx.SnapshotVisibleObjects(1300).Single();
|
||||
Assert.Equal(0x7f, half.TintStrength);
|
||||
Assert.Equal(0x506070, half.Tint);
|
||||
Assert.Equal(0.5, half.ColorTransition!.Value.Progress, 3);
|
||||
|
||||
var end = gfx.SnapshotVisibleObjects(1500).Single();
|
||||
Assert.Equal(0xff, end.TintStrength);
|
||||
Assert.Equal(0x90a0b0, end.Tint);
|
||||
Assert.False(gfx.TryGet(0x100)!.OneShotColorEnabled);
|
||||
Assert.Equal(-1, gfx.TryGet(0x100)!.OneShotColorTarget);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticWriteAfterOp202_BecomesInterpolationCurrent_NotAnEndpointOverwrite()
|
||||
{
|
||||
var gfx = Visible(GfxState.PackColor(0xff, 0x000000));
|
||||
gfx.SetAnimatedObjectColorResolved(0x100, 0, 300, 0x00, 0xffffff);
|
||||
gfx.SetStaticObjectColorResolved(0x100, 0, 0xff, 0xffffff);
|
||||
|
||||
var start = gfx.SnapshotVisibleObjects(2000).Single();
|
||||
Assert.Equal(0xff, start.TintStrength);
|
||||
Assert.Equal(0xffffff, start.Tint);
|
||||
var half = gfx.SnapshotVisibleObjects(2150).Single();
|
||||
Assert.Equal(0x7f, half.TintStrength);
|
||||
Assert.Equal(0xffffff, half.Tint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ColorAndScale_ShareOneShotStartClock()
|
||||
{
|
||||
var gfx = Visible(GfxState.PackColor(0x00, 0xffffff));
|
||||
gfx.SetAnimatedObjectColorResolved(0x100, 0, 1000, 0xff, 0xffffff);
|
||||
gfx.SetScaleChannel(0x100, 0, 1000, (200, 200, 100));
|
||||
|
||||
gfx.SnapshotVisibleObjects(5000);
|
||||
var half = gfx.SnapshotVisibleObjects(5500).Single();
|
||||
Assert.Equal(0x7f, half.TintStrength);
|
||||
Assert.Equal(1.5, half.Transform.ScaleX, 3);
|
||||
Assert.Equal(5000, half.ColorTransition!.Value.StartMs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NegativeTargetComponents_PreserveCurrentStaticBytes()
|
||||
{
|
||||
var gfx = Visible(GfxState.PackColor(0x33, 0x123456));
|
||||
gfx.SetAnimatedObjectColorResolved(0x100, 0, 100, -1, -1);
|
||||
Assert.Equal(GfxState.PackColor(0x33, 0x123456), gfx.TryGet(0x100)!.OneShotColorTarget);
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,19 @@ public class RenderObjectBlendTests
|
||||
Assert.Equal(BlendKind.Alpha, ro2.Blend);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mode1_UsesArgbAlphaAsOpacityAndRgbAsMultiplicativeModulation()
|
||||
{
|
||||
var g = WithVisibleObject(0x100, resId: 5, colorKey: -1);
|
||||
g.SetStaticObjectColorResolved(0x100, 1, 0x40, 0x80ff40);
|
||||
var ro = g.SnapshotVisibleObjects().Single();
|
||||
Assert.Equal(0x40, ro.Alpha);
|
||||
Assert.Equal(0, ro.TintStrength);
|
||||
Assert.Equal(0x80ff40, ro.Tint);
|
||||
Assert.True(ro.MultiplyTint);
|
||||
Assert.Equal(BlendKind.Alpha, ro.Blend);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ColorKey_IsCarriedThrough()
|
||||
{
|
||||
|
||||
@@ -27,4 +27,17 @@ public class SoftwareAffineRasterizerTests
|
||||
Assert.InRange(colored, 3, 5);
|
||||
Assert.Equal(0, dst[(1*5+1)*4+3]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BlitRgba_MultiplyTintUsesD3dStyleRgbModulationAndOpacity()
|
||||
{
|
||||
byte[] src = { 200, 100, 50, 255 };
|
||||
byte[] dst = new byte[4];
|
||||
SoftwareAffineRasterizer.BlitRgba(dst, 1, 1, src, 1, 1, 0, 0, 1, 1,
|
||||
new Affine2D(1, 0, 0, 1, 0, 0), 0x80ff40, 0, 0.5f, multiplyTint: true);
|
||||
Assert.InRange(dst[0], 49, 50);
|
||||
Assert.InRange(dst[1], 49, 50);
|
||||
Assert.InRange(dst[2], 5, 7);
|
||||
Assert.InRange(dst[3], 126, 127);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,19 +20,24 @@ public readonly record struct SurfaceTransitionState(long CommandKey, int Target
|
||||
long RangeAStart, int RangeACount, long RangeBStart, int RangeBCount,
|
||||
long DelayMs, long DurationMs, long StartMs, double Progress, bool Forced);
|
||||
|
||||
/// <summary>One synchronized sample of op 0x202's native one-shot packed-color channel.</summary>
|
||||
public readonly record struct ColorTransitionState(long Current, long Target,
|
||||
long DelayMs, long DurationMs, long StartMs, double Progress, bool Active);
|
||||
|
||||
/// <summary>A renderable view of one visible gfx object — the host composites these in ascending-handle order
|
||||
/// (= the engine's z-order) each frame. Built by <see cref="GfxState.SnapshotVisibleObjects"/>; the surface
|
||||
/// resId/colorkey are resolved from the object's live source slot at snapshot time (see docs/engine-re.md,
|
||||
/// "The full gfx render model").</summary>
|
||||
/// <summary><paramref name="Alpha"/> is the object's OPACITY (0-255). <paramref name="TintStrength"/> is how
|
||||
/// strongly <paramref name="Tint"/> (RGB) is blended into the texel (0=keep texel, 255=full tint) — this is the
|
||||
/// op 0x202/0x203/0x232 "alpha" byte, which is a tint strength, NOT opacity (conflating them made opaque CGs
|
||||
/// vanish — the grey-background bug).</summary>
|
||||
/// <summary>The packed-color channel is mode-dependent. Mode 0 uses <paramref name="TintStrength"/> to blend
|
||||
/// <paramref name="Tint"/> into the texel; mode 1 uses <paramref name="Alpha"/> as opacity and multiplies the
|
||||
/// texel by <paramref name="Tint"/>. <paramref name="MultiplyTint"/> selects the latter compositor path.</summary>
|
||||
public readonly record struct RenderObject(long Handle, long SurfaceResId, long ColorKey,
|
||||
int SrcX, int SrcY, int W, int H, int DstX, int DstY,
|
||||
TransformState Transform, RotationCycleState Rotation,
|
||||
int Alpha, long Tint, int TintStrength, BlendKind Blend,
|
||||
SurfaceTransitionState? SurfaceTransition = null);
|
||||
bool MultiplyTint,
|
||||
SurfaceTransitionState? SurfaceTransition = null,
|
||||
ColorTransitionState? ColorTransition = null);
|
||||
|
||||
/// <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
|
||||
@@ -56,6 +61,10 @@ public sealed class GfxState
|
||||
public long Color;
|
||||
public bool HasColor; // true once op 0x202/0x203 set a color/alpha modulation on this object
|
||||
public long StaticColorMode; // op 0x203 operand 2 -> obj+0x30; mode 2 is transition alpha/identity
|
||||
// Op 0x202 one-shot packed-color channel: current +0x60, target +0x64, delay +0x38,
|
||||
// duration +0x4c. It shares obj+0x34's start timestamp with the one-shot matrix channels.
|
||||
public long OneShotColorTarget = -1, ColorDelayMs, ColorDurationMs;
|
||||
public bool OneShotColorEnabled;
|
||||
|
||||
// ---- src-rect / spritesheet-cell channel (ops 0x239 static cell, 0x231 animate). Interpolator
|
||||
// SRC-RECT SCROLL channel: period obj+0x230, start obj+0x21c, grid obj+0x238/0x23c. ----
|
||||
@@ -84,7 +93,7 @@ public sealed class GfxState
|
||||
public long RotationDelayMs, RotationDurationMs;
|
||||
public bool RotationChannelEnabled;
|
||||
// Shared matrix-channel start timestamp obj+0x34, seeded from frame-time ctx+0xb550.
|
||||
public long MatrixStartMs = -1;
|
||||
public long OneShotStartMs = -1;
|
||||
|
||||
// Op 0x234 is a separate cyclic rotation channel (period obj+0x228, axis obj+0x244..0x24c).
|
||||
public long RotationPeriodMs;
|
||||
@@ -141,6 +150,8 @@ public sealed class GfxState
|
||||
V18 = s.V18, V24 = s.V24, V16c = s.V16c,
|
||||
Field64 = s.Field64, Field68 = s.Field68, Field6c = s.Field6c,
|
||||
Color = s.Color, HasColor = s.HasColor, StaticColorMode = s.StaticColorMode,
|
||||
OneShotColorTarget = s.OneShotColorTarget, ColorDelayMs = s.ColorDelayMs,
|
||||
ColorDurationMs = s.ColorDurationMs, OneShotColorEnabled = s.OneShotColorEnabled,
|
||||
SrcGridW = s.SrcGridW, SrcGridH = s.SrcGridH, SrcCell = s.SrcCell,
|
||||
SrcPeriod = s.SrcPeriod, SrcStart = s.SrcStart, SrcAnim = s.SrcAnim,
|
||||
ColorPeriod = s.ColorPeriod, ColorStart = s.ColorStart, ColorTarget = s.ColorTarget,
|
||||
@@ -152,7 +163,7 @@ public sealed class GfxState
|
||||
TranslationEnabled = s.TranslationEnabled,
|
||||
RotationCurrent = s.RotationCurrent, RotationTarget = s.RotationTarget,
|
||||
RotationDelayMs = s.RotationDelayMs, RotationDurationMs = s.RotationDurationMs,
|
||||
RotationChannelEnabled = s.RotationChannelEnabled, MatrixStartMs = s.MatrixStartMs,
|
||||
RotationChannelEnabled = s.RotationChannelEnabled, OneShotStartMs = s.OneShotStartMs,
|
||||
RotationPeriodMs = s.RotationPeriodMs, RotationAxis = s.RotationAxis,
|
||||
RotationEnabled = s.RotationEnabled, RotationStartMs = s.RotationStartMs,
|
||||
};
|
||||
@@ -229,6 +240,25 @@ public sealed class GfxState
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Op 0x202: arm the delayed one-shot packed-color interpolation without replacing the
|
||||
/// current static color. Negative alpha/RGB operands resolve from current obj+0x60 independently.</summary>
|
||||
public void SetAnimatedObjectColorResolved(long handle, long delayMs, long durationMs, long alpha, long rgb)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var o = GetOrCreate(handle);
|
||||
long current = o.HasColor ? o.Color : 0xffffffff;
|
||||
long resolvedAlpha = alpha < 0 ? (current >> 24) & 0xff : System.Math.Min(alpha, 0xff);
|
||||
long resolvedRgb = rgb < 0 ? current & 0xffffff : rgb & 0xffffff;
|
||||
if (!o.HasColor) { o.Color = current; o.HasColor = true; }
|
||||
o.OneShotColorTarget = PackColor(resolvedAlpha, resolvedRgb);
|
||||
o.ColorDelayMs = delayMs;
|
||||
o.ColorDurationMs = durationMs;
|
||||
o.OneShotColorEnabled = true;
|
||||
o.OneShotStartMs = -1;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetStaticObjectColorResolved(long handle, long mode, long alpha, long rgb)
|
||||
{
|
||||
SetObjectColorResolved(handle, alpha, rgb);
|
||||
@@ -345,7 +375,7 @@ public sealed class GfxState
|
||||
var o = GetOrCreate(handle);
|
||||
o.ScaleDelayMs = delayMs; o.ScaleDurationMs = durationMs;
|
||||
o.ScaleTarget = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0);
|
||||
o.ScaleEnabled = durationMs > 0; o.MatrixStartMs = -1;
|
||||
o.ScaleEnabled = durationMs > 0; o.OneShotStartMs = -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,7 +387,7 @@ public sealed class GfxState
|
||||
var o = GetOrCreate(handle);
|
||||
o.TranslationDelayMs = delayMs; o.TranslationDurationMs = durationMs;
|
||||
o.TranslationTarget = target;
|
||||
o.TranslationEnabled = durationMs > 0; o.MatrixStartMs = -1;
|
||||
o.TranslationEnabled = durationMs > 0; o.OneShotStartMs = -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,7 +400,7 @@ public sealed class GfxState
|
||||
var o = GetOrCreate(handle);
|
||||
o.RotationDelayMs = delayMs; o.RotationDurationMs = durationMs;
|
||||
o.RotationTarget = (axis.X, axis.Y, axis.Z, angleDegrees);
|
||||
o.RotationChannelEnabled = durationMs > 0; o.MatrixStartMs = -1;
|
||||
o.RotationChannelEnabled = durationMs > 0; o.OneShotStartMs = -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,14 +448,28 @@ public sealed class GfxState
|
||||
if (!o.Visible) continue;
|
||||
var (resId, ck) = _surfaces.TryGetValue(o.SourceSlot, out var s) ? s : (0L, 0L);
|
||||
|
||||
// ---- color: object stays OPAQUE; the op-0x202/0x203 alpha is a TINT STRENGTH (0=keep texel,
|
||||
// 255=full tint), NOT opacity. 0x232 ping-pongs the strength (+tint) toward the target (glow). ----
|
||||
// ---- packed color: mode 0 treats alpha as tint/fill strength; mode 1 treats it as opacity and
|
||||
// RGB as multiplicative source modulation. 0x232 ping-pongs the mode-0 strength/tint. ----
|
||||
int alpha = 255; long tint = 0xFFFFFF; int strength = 0; var blend = BlendKind.Opaque;
|
||||
bool multiplyTint = false;
|
||||
long sampledColor = o.Color;
|
||||
ColorTransitionState? colorTransition = null;
|
||||
if (o.OneShotColorEnabled)
|
||||
{
|
||||
if (o.OneShotStartMs < 0) o.OneShotStartMs = nowMs;
|
||||
(sampledColor, colorTransition) = SampleOneShotColor(o, nowMs);
|
||||
}
|
||||
if (o.HasColor)
|
||||
{
|
||||
var (a, r, g, b) = BlendMath.UnpackArgb(o.Color);
|
||||
var (a, r, g, b) = BlendMath.UnpackArgb(sampledColor);
|
||||
tint = ((long)r << 16) | ((long)g << 8) | (long)b;
|
||||
if (o.StaticColorMode == 2)
|
||||
if (o.StaticColorMode == 1)
|
||||
{
|
||||
// Native mode 1 enables SRCALPHA/INVSRCALPHA and passes packed ARGB as D3D
|
||||
// modulation. Its high byte is opacity, not mode-0 tint/fill strength.
|
||||
alpha = a; strength = 0; blend = BlendKind.Alpha; multiplyTint = true;
|
||||
}
|
||||
else if (o.StaticColorMode == 2)
|
||||
{
|
||||
// Native transition-source mode: 0xffffffff is opaque identity modulation,
|
||||
// not a request to replace every texel with white.
|
||||
@@ -467,17 +511,19 @@ public sealed class GfxState
|
||||
}
|
||||
|
||||
// One-shot matrix channels: hold current through delay, then linearly sample current -> target.
|
||||
if ((o.ScaleEnabled || o.RotationChannelEnabled || o.TranslationEnabled) && o.MatrixStartMs < 0)
|
||||
o.MatrixStartMs = nowMs;
|
||||
if ((o.OneShotColorEnabled || o.ScaleEnabled || o.RotationChannelEnabled || o.TranslationEnabled)
|
||||
&& o.OneShotStartMs < 0)
|
||||
o.OneShotStartMs = nowMs;
|
||||
var scale = SampleMatrixChannel(ref o.ScaleCurrent, o.ScaleTarget, o.ScaleDelayMs,
|
||||
o.ScaleDurationMs, o.MatrixStartMs, ref o.ScaleEnabled, nowMs);
|
||||
o.ScaleDurationMs, o.OneShotStartMs, ref o.ScaleEnabled, nowMs);
|
||||
var rotation = SampleRotationChannel(ref o.RotationCurrent, o.RotationTarget,
|
||||
o.RotationDelayMs, o.RotationDurationMs,
|
||||
o.MatrixStartMs, ref o.RotationChannelEnabled, nowMs);
|
||||
o.OneShotStartMs, ref o.RotationChannelEnabled, nowMs);
|
||||
var translation = SampleMatrixChannel(ref o.TranslationCurrent, o.TranslationTarget,
|
||||
o.TranslationDelayMs, o.TranslationDurationMs,
|
||||
o.MatrixStartMs, ref o.TranslationEnabled, nowMs);
|
||||
if (!o.ScaleEnabled && !o.RotationChannelEnabled && !o.TranslationEnabled) o.MatrixStartMs = -1;
|
||||
o.OneShotStartMs, ref o.TranslationEnabled, nowMs);
|
||||
if (!o.OneShotColorEnabled && !o.ScaleEnabled && !o.RotationChannelEnabled && !o.TranslationEnabled)
|
||||
o.OneShotStartMs = -1;
|
||||
|
||||
double cycleAngle = 0;
|
||||
if (o.RotationEnabled && o.RotationPeriodMs > 0)
|
||||
@@ -498,12 +544,44 @@ public sealed class GfxState
|
||||
new RotationCycleState(o.RotationEnabled, o.RotationPeriodMs,
|
||||
o.RotationAxis.X, o.RotationAxis.Y,
|
||||
o.RotationAxis.Z, cycleAngle),
|
||||
alpha, tint, strength, blend, transition));
|
||||
alpha, tint, strength, blend, multiplyTint, transition, colorTransition));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
private static (long Packed, ColorTransitionState State) SampleOneShotColor(GfxObject o, long nowMs)
|
||||
{
|
||||
long current = o.Color & 0xffffffff;
|
||||
long target = o.OneShotColorTarget & 0xffffffff;
|
||||
long start = o.OneShotStartMs;
|
||||
long elapsed = nowMs - start - o.ColorDelayMs;
|
||||
if (o.ColorDurationMs <= 0 || elapsed >= o.ColorDurationMs)
|
||||
{
|
||||
long delay = o.ColorDelayMs, duration = o.ColorDurationMs;
|
||||
o.Color = target;
|
||||
o.ColorDelayMs = 0;
|
||||
o.ColorDurationMs = 0;
|
||||
o.OneShotColorTarget = -1;
|
||||
o.OneShotColorEnabled = false;
|
||||
return (target, new ColorTransitionState(current, target, delay, duration, start, 1.0, false));
|
||||
}
|
||||
if (elapsed <= 0)
|
||||
return (current, new ColorTransitionState(current, target, o.ColorDelayMs,
|
||||
o.ColorDurationMs, start, 0.0, true));
|
||||
|
||||
long packed = 0;
|
||||
long remaining = o.ColorDurationMs - elapsed;
|
||||
for (int shift = 0; shift <= 24; shift += 8)
|
||||
{
|
||||
long c = (current >> shift) & 0xff;
|
||||
long t = (target >> shift) & 0xff;
|
||||
packed |= ((c * remaining + t * elapsed) / o.ColorDurationMs & 0xff) << shift;
|
||||
}
|
||||
return (packed, new ColorTransitionState(current, target, o.ColorDelayMs,
|
||||
o.ColorDurationMs, start, elapsed / (double)o.ColorDurationMs, true));
|
||||
}
|
||||
|
||||
private static (double X, double Y, double Z) SampleMatrixChannel(
|
||||
ref (double X, double Y, double Z) current,
|
||||
(double X, double Y, double Z) target,
|
||||
|
||||
@@ -5,7 +5,7 @@ public static class SoftwareAffineRasterizer
|
||||
{
|
||||
public static void BlitRgba(byte[] dst, int dstW, int dstH, byte[] src, int srcW, int srcH,
|
||||
int srcX, int srcY, int width, int height, Affine2D localToDest,
|
||||
long tint, float tintStrength, float opacity)
|
||||
long tint, float tintStrength, float opacity, bool multiplyTint = false)
|
||||
{
|
||||
if (width <= 0 || height <= 0 || !localToDest.TryInverse(out var inv)) return;
|
||||
Bounds(localToDest, width, height, dstW, dstH, out int x0, out int y0, out int x1, out int y1);
|
||||
@@ -19,9 +19,9 @@ public static class SoftwareAffineRasterizer
|
||||
if ((uint)u >= (uint)width || (uint)v >= (uint)height) continue;
|
||||
int si=((srcY+v)*srcW+(srcX+u))*4, di=(y*dstW+x)*4;
|
||||
int sa=src[si+3]*ia/255; if(sa==0) continue;
|
||||
int sr=(src[si]*(255-istr)+tr*istr)/255;
|
||||
int sg=(src[si+1]*(255-istr)+tg*istr)/255;
|
||||
int sb=(src[si+2]*(255-istr)+tb*istr)/255;
|
||||
int sr=multiplyTint ? src[si]*tr/255 : (src[si]*(255-istr)+tr*istr)/255;
|
||||
int sg=multiplyTint ? src[si+1]*tg/255 : (src[si+1]*(255-istr)+tg*istr)/255;
|
||||
int sb=multiplyTint ? src[si+2]*tb/255 : (src[si+2]*(255-istr)+tb*istr)/255;
|
||||
Blend(dst,di,sr,sg,sb,sa);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,8 +369,9 @@ public sealed class VirtualMachine
|
||||
Gfx.ClearSurface((int)Read(a[0])); return pc + 1;
|
||||
case "clone-gfx-object": // 0x21d (source handle)(destination handle)
|
||||
Gfx.CloneObject(Read(a[0]), Read(a[1])); return pc + 1;
|
||||
case "gfx-blit-color": // 0x202 (handle)(x)(y)(alpha)(color) — static alpha/tint (anim interp deferred)
|
||||
Gfx.SetObjectColorResolved(Read(a[0]), Read(a[3]), Read(a[4])); return pc + 1;
|
||||
case "gfx-blit-color": // 0x202 (handle)(delay)(duration)(alpha)(color) — one-shot color
|
||||
Gfx.SetAnimatedObjectColorResolved(Read(a[0]), Read(a[1]), Read(a[2]), Read(a[3]), Read(a[4]));
|
||||
return pc + 1;
|
||||
case "gfx-draw-color": // 0x203 (handle)(v)(alpha)(color) — static alpha/tint
|
||||
Gfx.SetStaticObjectColorResolved(Read(a[0]), Read(a[1]), Read(a[2]), Read(a[3])); return pc + 1;
|
||||
// ---- sprite transform / animation cluster (docs/engine-re.md "0x21c-0x243 ... ANIMATION") ----
|
||||
|
||||
@@ -297,7 +297,8 @@ public partial class Main : Godot.Control
|
||||
outcome = $"FILL tint=0x{v.Tint:x6} a={fillA:0.00} {baseW}x{baseH}@({dstX},{dstY}) " +
|
||||
$"base=({v.DstX},{v.DstY}) anchor=({t.AnchorX:0.0},{t.AnchorY:0.0}) " +
|
||||
$"scale=({t.ScaleX:0.00},{t.ScaleY:0.00}) " +
|
||||
$"trans=({t.TranslateX:0.0},{t.TranslateY:0.0}) rot={v.Rotation.AngleDegrees:0.0}";
|
||||
$"trans=({t.TranslateX:0.0},{t.TranslateY:0.0}) rot={v.Rotation.AngleDegrees:0.0}" +
|
||||
ColorTimeline(v.ColorTransition);
|
||||
}
|
||||
else outcome = "SKIP(no-resId, opaque render-target)";
|
||||
}
|
||||
@@ -308,14 +309,15 @@ public partial class Main : Godot.Control
|
||||
else
|
||||
{
|
||||
BlitLayer(bmp, v.ColorKey, v.Tint, strength, v.SrcX, v.SrcY, v.W, v.H,
|
||||
localToDest, opacity);
|
||||
localToDest, opacity, v.MultiplyTint);
|
||||
var raw = _vm.Gfx.TryGet(v.Handle);
|
||||
outcome = $"slot={raw?.SourceSlot} DRAWN resId=0x{v.SurfaceResId:x} {System.IO.Path.GetFileName(bmp)} " +
|
||||
$"src=({v.SrcX},{v.SrcY} {v.W}x{v.H}) base=({v.DstX},{v.DstY}) " +
|
||||
$"anchor=({t.AnchorX:0.0},{t.AnchorY:0.0}) dst=({dstX},{dstY}) " +
|
||||
$"scale=({t.ScaleX:0.00},{t.ScaleY:0.00}) trans=({t.TranslateX:0.0},{t.TranslateY:0.0}) " +
|
||||
$"rot=({t.RotationAngleDegrees:0.0}+{v.Rotation.AngleDegrees:0.0}) " +
|
||||
$"op={opacity:0.00} tintStr={strength:0.00}";
|
||||
$"mode={raw?.StaticColorMode} op={opacity:0.00} tintStr={strength:0.00}" +
|
||||
ColorTimeline(v.ColorTransition);
|
||||
}
|
||||
}
|
||||
decisions?.Add(v.Handle, $"z{z} {outcome}");
|
||||
@@ -325,6 +327,11 @@ public partial class Main : Godot.Control
|
||||
if (decisions != null) LogGfxDecisionChanges(decisions);
|
||||
}
|
||||
|
||||
private static string ColorTimeline(Age.Engine.Model.ColorTransitionState? state)
|
||||
=> state is { } c
|
||||
? $" color=0x{c.Current:x8}->0x{c.Target:x8} colorProgress={c.Progress:0.000}"
|
||||
: "";
|
||||
|
||||
// Native type-0 surface commands first leave range A in normal z-order, then alpha-composite range B
|
||||
// into the target surface. SC0000 binds that target to handle+2, above both source handles, so drawing
|
||||
// range B here with progress produces old*(1-progress)+new*progress without disturbing ambient channels.
|
||||
@@ -349,7 +356,7 @@ public partial class Main : Godot.Control
|
||||
var bmp = _host.ResolveResIdTexture(source.SurfaceResId);
|
||||
if (bmp == null) continue;
|
||||
BlitLayer(bmp, source.ColorKey, source.Tint, source.TintStrength / 255f,
|
||||
source.SrcX, source.SrcY, source.W, source.H, affine, opacity);
|
||||
source.SrcX, source.SrcY, source.W, source.H, affine, opacity, source.MultiplyTint);
|
||||
}
|
||||
drawn++;
|
||||
}
|
||||
@@ -388,10 +395,10 @@ public partial class Main : Godot.Control
|
||||
|
||||
// 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).
|
||||
// tintStrength (0..1, the op 0x202/0x203 alpha) LERPs the texel RGB toward tint (0=keep texel, 1=full tint;
|
||||
// fade-to-black uses tint=black, strength=1); alpha is the object's OPACITY (independent of the tint).
|
||||
// Mode 0 uses tintStrength to LERP texel RGB toward tint. Mode 1 sets multiplyTint and uses packed RGB as
|
||||
// multiplicative modulation while alpha is object opacity.
|
||||
private void BlitLayer(string bmpPath, long colorKey, long tint, float tintStrength, int srcX, int srcY, int w, int h,
|
||||
Age.Engine.Model.Affine2D localToDest, float alpha = 1f)
|
||||
Age.Engine.Model.Affine2D localToDest, float alpha = 1f, bool multiplyTint = false)
|
||||
{
|
||||
var cacheKey = (bmpPath, colorKey);
|
||||
if (!_imgCache.TryGetValue(cacheKey, out var src))
|
||||
@@ -416,7 +423,7 @@ public partial class Main : Godot.Control
|
||||
byte[] dst = _screen.GetData(); byte[] ss = src.GetData();
|
||||
Age.Engine.Model.SoftwareAffineRasterizer.BlitRgba(
|
||||
dst, _screen.GetWidth(), _screen.GetHeight(), ss, src.GetWidth(), src.GetHeight(),
|
||||
srcX, srcY, sw, sh, localToDest, tint, tintStrength, alpha);
|
||||
srcX, srcY, sw, sh, localToDest, tint, tintStrength, alpha, multiplyTint);
|
||||
_screen.SetData(_screen.GetWidth(), _screen.GetHeight(), false, _screen.GetFormat(), dst);
|
||||
}
|
||||
|
||||
|
||||
@@ -4791,36 +4791,36 @@ 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: 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."
|
||||
summary = "0x202 (handle)(delay_ms)(duration_ms)(alpha)(color) — arm the one-shot packed-ARGB channel. Worker gfx_op_0x202_worker_set_color_anim @0x47ea00 resets shared start obj+0x34, writes delay +0x38, duration +0x4c, and target +0x64. gfx_object_apply_transform_channels @0x472f00 linearly interpolates each byte from current +0x60 on frame clock ctx+0xb550, commits the target, clears timing, writes target -1, and clears the one-shot active bit when all sibling channels finish. Negative alpha/RGB independently preserve current bytes. Implemented in GfxState with synchronized current/target timeline evidence; draw-string 0x204/0x7a remains separate."
|
||||
noop_headless = false
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
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 packs operands 4/5 and calls worker 0x47ea00(handle,delay,duration,packed). Consumer 0x472f00: shared start +0x34; color delay/duration +0x38/+0x4c; current/target +0x60/+0x64; frame clock ctx+0xb550; bytewise integer LERP; natural or ctx+0xb55c forced completion. /v2 annotated and saved 2026-07-10."
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 1
|
||||
role = ""
|
||||
role = "retained object handle"
|
||||
observed_types = ["imm", "g-int", "l-int", "l-ptr"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 2
|
||||
role = ""
|
||||
role = "delay in frame-clock milliseconds"
|
||||
observed_types = ["imm", "g-int", "l-int"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 3
|
||||
role = ""
|
||||
role = "duration in frame-clock milliseconds"
|
||||
observed_types = ["imm", "g-int", "l-int"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 4
|
||||
role = ""
|
||||
role = "target tint-strength/ARGB high byte; negative preserves current"
|
||||
observed_types = ["imm", "l-int"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 5
|
||||
role = ""
|
||||
role = "target RGB; negative preserves current"
|
||||
observed_types = ["imm", "g-int", "l-int"]
|
||||
|
||||
[[opcode]]
|
||||
@@ -4832,12 +4832,12 @@ abi_source = "kelebek+decode-validated"
|
||||
[opcode.semantics]
|
||||
name = "gfx-draw-color"
|
||||
category = "draw"
|
||||
summary = "0x203 (handle)(mode)(alpha)(color) — gfx cmd-type 9. Worker stores mode at obj+0x30 and STATIC packed color at obj+0x60. Negative alpha/RGB preserve current static bytes through FUN_0047f3e0. Mode 2 is the 0x223 transition-source alpha/identity path (0xffffffff = opaque identity, not solid white); mode 0 retains the established tint-strength behavior."
|
||||
summary = "0x203 (handle)(mode)(alpha)(color) — gfx cmd-type 9. Worker stores the D3D blend selector at obj+0x30 and STATIC packed color at obj+0x60. Negative alpha/RGB preserve current static bytes. Mode 0 retains established opaque CG/tint/fill behavior; mode 1 is SRCALPHA/INVSRCALPHA with ARGB alpha opacity and multiplicative RGB modulation; mode 2 is the 0x223 transition-source identity path."
|
||||
noop_headless = false
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
evidence = "Ghidra handler 0x4229a0 (dispatch ctx[0x26c93+0x203]); negative operands read current obj+0x60 via FUN_0047f3e0, then gfx_op_0x203_worker_set_color stores op2 at obj+0x30 and packed ARGB at obj+0x60. SC0000 transition setup uses mode 2 with -1/-1 before 0x21d/0x223; synchronized window pixels prove treating 0xffffffff as generic white tint is wrong."
|
||||
evidence = "Ghidra handler 0x4229a0; negative operands read current obj+0x60, then worker 0x47e9b0 stores op2 at obj+0x30 and ARGB at +0x60. gfx_object_composite call-site 0x47f78f passes +0x30/+0x60 directly to gfx_object_blit_d3d9; mode 1 sets D3DRS SRCALPHA/INVSRCALPHA and the packed color is the device draw modulation. Mode 2 transition setup and synchronized pixels prove 0xffffffff is identity, not solid white."
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 1
|
||||
|
||||
Reference in New Issue
Block a user