Fix FIELD movement sprite ghosting

This commit is contained in:
gamer147
2026-07-27 11:34:40 -04:00
parent 9c99254a9a
commit da34e6ad15
5 changed files with 67 additions and 6 deletions

View File

@@ -770,6 +770,16 @@ and those clones, so every copied start remains zero and their first render seed
Those unit sprites are therefore phase-locked at one cell change every 200 ms (5 Hz), even though the Those unit sprites are therefore phase-locked at one cell change every 200 ms (5 Hz), even though the
engine generally permits different object-local starts and periods. engine generally permits different object-local starts and periods.
FIELD's per-tile movement animation uses two retained objects. At `0x45c9`/`0x4686` and again at
`0x51fd`, it clones the bound idle handle `0x9c40+entity_index` into temporary moving handle
`0x9c40+0x76c`. The following mode-0 `0x203` write sets the idle handle's alpha to zero, while translation
`0x220` and source-cell endpoint `0x239` animate only the temporary handle. After the presentation boundary,
`DRAWCH@0x51ec` rebinds the idle handle at the new tile; a multi-tile route immediately repeats the
clone/suppress cycle. Treating every static mode-0 alpha byte on a loaded texture as inert therefore leaves
the old idle sprite composited beneath the moving clone until each segment ends. The port records the
post-bind static-alpha consumption for looping objects as a transient opacity latch, cleared by the next
`draw-texture` bind. This preserves ADV's distinct pre-animation/static alpha-zero CG initialization.
**Port implication:** replace the current “any spritesheet is active” redraw predicate with native-style **Port implication:** replace the current “any spritesheet is active” redraw predicate with native-style
shared current/previous sampling and request a composition only when at least one visible sampled cell shared current/previous sampling and request a composition only when at least one visible sampled cell
changes. The FIELD `sleep(1)` input-poll loop must also stop requesting a redraw when no retained mutation changes. The FIELD `sleep(1)` input-poll loop must also stop requesting a redraw when no retained mutation

View File

@@ -667,9 +667,9 @@ The handler clears the map embedded at retained-gfx owner+0x408, resets its coun
- **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 retained-gfx owner+0xb550 (EngineCtx+0x51b64); bytewise integer LERP; natural or owner+0xb55c (EngineCtx+0x51b70) forced completion. /v2 annotated and saved 2026-07-10. - **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 retained-gfx owner+0xb550 (EngineCtx+0x51b64); bytewise integer LERP; natural or owner+0xb55c (EngineCtx+0x51b70) forced completion. /v2 annotated and saved 2026-07-10.
### 0x203 `gfx-draw-color` (gfx-draw-color, argc 4) ### 0x203 `gfx-draw-color` (gfx-draw-color, argc 4)
- **summary:** 0x203 (handle)(mode)(alpha)(color) — worker stores the D3D blend selector at obj+0x30 and STATIC packed color at obj+0x60; the handler's ctx+0x53d88 write is the generic 9-dword instruction length. Negative alpha/RGB preserve current static bytes. Mode 0 is the default textured path: preserved 0xffffffff is identity (the alpha byte is not tint strength). Mode 1 is SRCALPHA/ONE additive glow with ARGB alpha scaling the source contribution and RGB providing multiplicative modulation. Mode 2 conditionally forces ONE/ZERO for the selected render target and is used by 0x223 transition sources; mode 3 selects a subtractive special path. Surfaceless mode-0 fill consumption remains distinct. - **summary:** 0x203 (handle)(mode)(alpha)(color) — worker stores the D3D blend selector at obj+0x30 and STATIC packed color at obj+0x60; the handler's ctx+0x53d88 write is the generic 9-dword instruction length. Negative alpha/RGB preserve current static bytes. Mode 0 is the default textured path: preserved 0xffffffff is identity (the alpha byte is not tint strength). Its consumption is order-sensitive for FIELD's looping unit objects: a post-bind write supplies opacity to suppress/restore the idle object after its moving clone is made, while the prototype's pre-loop alpha-zero initialization remains opaque after DRAWCH binds the clone. Mode 1 is SRCALPHA/ONE additive glow with ARGB alpha scaling the source contribution and RGB providing multiplicative modulation. Mode 2 conditionally forces ONE/ZERO for the selected render target and is used by 0x223 transition sources; mode 3 selects a subtractive special path. Surfaceless mode-0 fill consumption remains distinct.
- **grounding:** source=investigation, confidence=high - **grounding:** source=investigation, confidence=high
- **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. Correct D3D9 constants at 0x4774c0 prove mode 1 writes SRCBLEND=5/SRCALPHA and DESTBLEND=2/ONE, not INVSRCALPHA. TITLE draws two opaque-black, no-colorkey SO022 flame sprites with mode 1; additive composition removes black and preserves the blue glow, verified in a windowed capture. Mode 2 transition setup and synchronized pixels prove 0xffffffff is identity, not solid white. SC0000 page 14 adds the mode-0 endpoint proof: after the EV052CA->EV052DA 0x223 crossfade, 0x203@0x12478 restores the base CG to mode 0 with preserved 0xffffffff; native keeps EV052DA visible while the port's tint-strength interpretation turns every texel white. - **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. Correct D3D9 constants at 0x4774c0 prove mode 1 writes SRCBLEND=5/SRCALPHA and DESTBLEND=2/ONE, not INVSRCALPHA. TITLE draws two opaque-black, no-colorkey SO022 flame sprites with mode 1; additive composition removes black and preserves the blue glow, verified in a windowed capture. Mode 2 transition setup and synchronized pixels prove 0xffffffff is identity, not solid white. SC0000 page 14 adds the mode-0 endpoint proof: after the EV052CA->EV052DA 0x223 crossfade, 0x203@0x12478 restores the base CG to mode 0 with preserved 0xffffffff; native keeps EV052DA visible while the port's tint-strength interpretation turns every texel white. FIELD's movement lifecycle clones idle handle 0x9c40+entity to temporary handle 0x9c40+0x76c, writes mode-0 alpha zero to the bound looping idle handle at 0x45dc/0x4699/0x5210, animates only the temporary handle, then DRAWCH rebinds the idle handle at the next tile. Rendering static mode-0 alpha as universally inert leaves the reported stationary ghost at every route segment.
### 0x205 `draw-formatted-integer` (u00420A60, argc 6) ### 0x205 `draw-formatted-integer` (u00420A60, argc 6)
- **summary:** Format an integer into a fixed-width decimal field and rasterize it immediately into a numbered graphics surface using the current font/color/effect state. - **summary:** Format an integer into a fixed-width decimal field and rasterize it immediately into a numbered graphics surface using the current font/color/effect state.

View File

@@ -66,6 +66,41 @@ public class RenderObjectBlendTests
Assert.Equal(BlendKind.Alpha, ro2.Blend); Assert.Equal(BlendKind.Alpha, ro2.Blend);
} }
[Fact]
public void FieldMovement_StaticAlphaSuppressesOnlyIdleLoopingClone()
{
const long prototype = 0x9c40;
const long idle = prototype + 7;
const long moving = prototype + 0x76c;
var g = new GfxState();
g.SetSurface(0x8e, 0x1234, 0x00ff00);
// FIELD@0x9230: the shared zero-area prototype receives mode-0 color before its loop is armed.
g.BindDraw(prototype, 0x3e, 0, 0, 0, 0, 0, 0);
g.SetStaticObjectColorResolved(prototype, mode: 0, alpha: 0, rgb: 0);
g.SetSrcRect(prototype, frameCount: 4, columns: 2, cell: 0, period: 200);
// DRAWCH clones the prototype, then draw-texture makes the concrete unit visible.
Assert.True(g.CloneObject(prototype, idle));
g.BindDraw(idle, 0x8e, 0, 0, 32, 48, 100, 100);
Assert.Equal(255, g.SnapshotVisibleObjects().Single(x => x.Handle == idle).Alpha);
// FIELD@0x45c9 clones the idle unit for motion and @0x45dc suppresses the retained original.
Assert.True(g.CloneObject(idle, moving));
g.SetStaticObjectColorResolved(idle, mode: 0, alpha: 0, rgb: -1);
var movingFrame = g.SnapshotVisibleObjects();
Assert.Equal(0, movingFrame.Single(x => x.Handle == idle).Alpha);
Assert.Equal(255, movingFrame.Single(x => x.Handle == moving).Alpha);
// DRAWCH at the next tile republishes the idle object; the following static write can suppress
// it for another route segment or restore it at alpha 255 when movement finishes.
g.BindDraw(idle, 0x8e, 0, 0, 32, 48, 200, 100);
Assert.Equal(255, g.SnapshotVisibleObjects().Single(x => x.Handle == idle).Alpha);
g.SetStaticObjectColorResolved(idle, mode: 0, alpha: 255, rgb: 0xffffff);
Assert.Equal(255, g.SnapshotVisibleObjects().Single(x => x.Handle == idle).Alpha);
}
[Fact] [Fact]
public void TransitionCleanup_Mode2ToMode0PreservedWhite_RemainsIdentity() public void TransitionCleanup_Mode2ToMode0PreservedWhite_RemainsIdentity()
{ {

View File

@@ -127,6 +127,9 @@ public sealed class GfxState
// opacity + multiplicative modulation even after the target commits. This distinguishes ADV chrome // opacity + multiplicative modulation even after the target commits. This distinguishes ADV chrome
// fades from a static mode-0 0x203 such as a CG initialized with 0x00ffffff (opaque identity). // fades from a static mode-0 0x203 such as a CG initialized with 0x00ffffff (opaque identity).
public bool OneShotColorBlend; public bool OneShotColorBlend;
// FIELD applies static mode-0 alpha after binding a looping unit sprite to suppress the retained
// idle object while a separate clone moves between tiles. A later draw-texture rebind clears it.
public bool StaticLoopColorBlend;
// ---- src-rect / spritesheet-cell channel (ops 0x239 one-shot cell, 0x231 looping animation). // ---- src-rect / spritesheet-cell channel (ops 0x239 one-shot cell, 0x231 looping animation).
// Native obj+0x238 is the TOTAL FRAME COUNT and +0x23c is the COLUMN COUNT. Each frame keeps // Native obj+0x238 is the TOTAL FRAME COUNT and +0x23c is the COLUMN COUNT. Each frame keeps
@@ -483,7 +486,7 @@ public sealed class GfxState
Color = s.Color, HasColor = s.HasColor, StaticColorMode = s.StaticColorMode, Color = s.Color, HasColor = s.HasColor, StaticColorMode = s.StaticColorMode,
OneShotColorTarget = s.OneShotColorTarget, ColorDelayMs = s.ColorDelayMs, OneShotColorTarget = s.OneShotColorTarget, ColorDelayMs = s.ColorDelayMs,
ColorDurationMs = s.ColorDurationMs, OneShotColorEnabled = s.OneShotColorEnabled, ColorDurationMs = s.ColorDurationMs, OneShotColorEnabled = s.OneShotColorEnabled,
OneShotColorBlend = s.OneShotColorBlend, OneShotColorBlend = s.OneShotColorBlend, StaticLoopColorBlend = s.StaticLoopColorBlend,
SrcFrameCount = s.SrcFrameCount, SrcColumns = s.SrcColumns, SrcCell = s.SrcCell, SrcFrameCount = s.SrcFrameCount, SrcColumns = s.SrcColumns, SrcCell = s.SrcCell,
SrcPeriod = s.SrcPeriod, SrcStart = s.SrcStart, SrcAnim = s.SrcAnim, SrcPeriod = s.SrcPeriod, SrcStart = s.SrcStart, SrcAnim = s.SrcAnim,
ColorPeriod = s.ColorPeriod, ColorStart = s.ColorStart, ColorTarget = s.ColorTarget, ColorPeriod = s.ColorPeriod, ColorStart = s.ColorStart, ColorTarget = s.ColorTarget,
@@ -630,7 +633,12 @@ public sealed class GfxState
public void SetStaticObjectColorResolved(long handle, long mode, long alpha, long rgb) public void SetStaticObjectColorResolved(long handle, long mode, long alpha, long rgb)
{ {
SetObjectColorResolved(handle, alpha, rgb); SetObjectColorResolved(handle, alpha, rgb);
lock (_lock) GetOrCreate(handle).StaticColorMode = mode; lock (_lock)
{
var o = GetOrCreate(handle);
o.StaticColorMode = mode;
o.StaticLoopColorBlend = mode == 0 && o.Visible && o.SrcAnim;
}
} }
/// <summary>Ops 0x239 (one-shot cell endpoint, period=0 in this retained model) / 0x231 (looping): /// <summary>Ops 0x239 (one-shot cell endpoint, period=0 in this retained model) / 0x231 (looping):
@@ -903,6 +911,7 @@ public sealed class GfxState
{ {
var o = GetOrCreate(handle); var o = GetOrCreate(handle);
o.SourceSlot = slot; o.SrcRect = (sx, sy, w, h); o.V24 = (dstX, dstY, 0); o.Visible = true; o.SourceSlot = slot; o.SrcRect = (sx, sy, w, h); o.V24 = (dstX, dstY, 0); o.Visible = true;
o.StaticLoopColorBlend = false;
} }
} }
@@ -1258,6 +1267,13 @@ public sealed class GfxState
{ {
alpha = a; strength = 0; blend = BlendKind.Alpha; multiplyTint = true; alpha = a; strength = 0; blend = BlendKind.Alpha; multiplyTint = true;
} }
else if (o.StaticColorMode == 0 && o.StaticLoopColorBlend)
{
// FIELD clones the visible idle unit to its moving handle, then writes alpha zero
// to the still-bound looping original. Consume that post-bind write as opacity;
// ADV's ordinary static alpha-zero CG initialization remains the opaque mode-0 path.
alpha = a; strength = 0; blend = BlendKind.Alpha; multiplyTint = true;
}
else if (o.StaticColorMode == 2) else if (o.StaticColorMode == 2)
{ {
// Native transition-source mode: 0xffffffff is opaque identity modulation, // Native transition-source mode: 0xffffffff is opaque identity modulation,

View File

@@ -4915,12 +4915,12 @@ abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "gfx-draw-color" name = "gfx-draw-color"
category = "draw" category = "draw"
summary = "0x203 (handle)(mode)(alpha)(color) — worker stores the D3D blend selector at obj+0x30 and STATIC packed color at obj+0x60; the handler's ctx+0x53d88 write is the generic 9-dword instruction length. Negative alpha/RGB preserve current static bytes. Mode 0 is the default textured path: preserved 0xffffffff is identity (the alpha byte is not tint strength). Mode 1 is SRCALPHA/ONE additive glow with ARGB alpha scaling the source contribution and RGB providing multiplicative modulation. Mode 2 conditionally forces ONE/ZERO for the selected render target and is used by 0x223 transition sources; mode 3 selects a subtractive special path. Surfaceless mode-0 fill consumption remains distinct." summary = "0x203 (handle)(mode)(alpha)(color) — worker stores the D3D blend selector at obj+0x30 and STATIC packed color at obj+0x60; the handler's ctx+0x53d88 write is the generic 9-dword instruction length. Negative alpha/RGB preserve current static bytes. Mode 0 is the default textured path: preserved 0xffffffff is identity (the alpha byte is not tint strength). Its consumption is order-sensitive for FIELD's looping unit objects: a post-bind write supplies opacity to suppress/restore the idle object after its moving clone is made, while the prototype's pre-loop alpha-zero initialization remains opaque after DRAWCH binds the clone. Mode 1 is SRCALPHA/ONE additive glow with ARGB alpha scaling the source contribution and RGB providing multiplicative modulation. Mode 2 conditionally forces ONE/ZERO for the selected render target and is used by 0x223 transition sources; mode 3 selects a subtractive special path. Surfaceless mode-0 fill consumption remains distinct."
noop_headless = false noop_headless = false
source = "investigation" source = "investigation"
confidence = "high" confidence = "high"
depends_on = [] depends_on = []
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. Correct D3D9 constants at 0x4774c0 prove mode 1 writes SRCBLEND=5/SRCALPHA and DESTBLEND=2/ONE, not INVSRCALPHA. TITLE draws two opaque-black, no-colorkey SO022 flame sprites with mode 1; additive composition removes black and preserves the blue glow, verified in a windowed capture. Mode 2 transition setup and synchronized pixels prove 0xffffffff is identity, not solid white. SC0000 page 14 adds the mode-0 endpoint proof: after the EV052CA->EV052DA 0x223 crossfade, 0x203@0x12478 restores the base CG to mode 0 with preserved 0xffffffff; native keeps EV052DA visible while the port's tint-strength interpretation turns every texel white." 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. Correct D3D9 constants at 0x4774c0 prove mode 1 writes SRCBLEND=5/SRCALPHA and DESTBLEND=2/ONE, not INVSRCALPHA. TITLE draws two opaque-black, no-colorkey SO022 flame sprites with mode 1; additive composition removes black and preserves the blue glow, verified in a windowed capture. Mode 2 transition setup and synchronized pixels prove 0xffffffff is identity, not solid white. SC0000 page 14 adds the mode-0 endpoint proof: after the EV052CA->EV052DA 0x223 crossfade, 0x203@0x12478 restores the base CG to mode 0 with preserved 0xffffffff; native keeps EV052DA visible while the port's tint-strength interpretation turns every texel white. FIELD's movement lifecycle clones idle handle 0x9c40+entity to temporary handle 0x9c40+0x76c, writes mode-0 alpha zero to the bound looping idle handle at 0x45dc/0x4699/0x5210, animates only the temporary handle, then DRAWCH rebinds the idle handle at the next tile. Rendering static mode-0 alpha as universally inert leaves the reported stationary ghost at every route segment."
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1