Fix ADV and BUNKI animation polish
This commit is contained in:
@@ -2330,9 +2330,11 @@ non-overlapping name/dialogue bands at y=447–468, 478–500, and 507–530. A
|
|||||||
#### ADV wait indicator -- ops `0x73` / `0x72` (2026-07-11)
|
#### ADV wait indicator -- ops `0x73` / `0x72` (2026-07-11)
|
||||||
|
|
||||||
The small bat marker is a configured ADV-layout sprite, not a glyph or part of SO001. `SYSTEM4.BIN`
|
The small bat marker is a configured ADV-layout sprite, not a glyph or part of SO001. `SYSTEM4.BIN`
|
||||||
loads universal raw asset `0x337c` (`SO000.AGF`) into surface slot 12. SO000 is a 390x27 strip of thirteen
|
loads universal raw asset `0x337c` (`SO000.AGF`) into surface slot 12. SO000 decodes to a 360x27 strip of
|
||||||
30x27 bat frames. It then executes op `0x73` with
|
twelve 30x27 bat frames. It then executes op `0x73` with
|
||||||
`(layout=1, x=385, y=140, surface=12, src=0,0, cell=30x27, terminalFrame=12, period=48ms)`.
|
`(layout=1, x=385, y=140, surface=12, src=0,0, cell=30x27, terminalFrame=12, period=48ms)`.
|
||||||
|
The terminal value is exclusive/a frame count: native publishes frames `0..11` and wraps before frame
|
||||||
|
`12`.
|
||||||
Layout 1 begins at screen y=430, so the marker lands at `(385,570)`, matching the original screenshot.
|
Layout 1 begins at screen y=430, so the marker lands at `(385,570)`, matching the original screenshot.
|
||||||
|
|
||||||
Native `op_0x73_configure_wait_indicator@0x41e900` writes this descriptor through
|
Native `op_0x73_configure_wait_indicator@0x41e900` writes this descriptor through
|
||||||
@@ -2345,10 +2347,12 @@ the following click releases the wait.
|
|||||||
**Port implementation:** the single-scene harness now injects SYSTEM4's exact SO000 surface-12 and op-`0x73`
|
**Port implementation:** the single-scene harness now injects SYSTEM4's exact SO000 surface-12 and op-`0x73`
|
||||||
configuration alongside its existing SO001 shortcut. The VM forwards all ten operands through the host ABI;
|
configuration alongside its existing SO001 shortcut. The VM forwards all ten operands through the host ABI;
|
||||||
`GodotAdvHost` retains configurations by layout and resolves slot 0 to the active Phase-A layout. A separate
|
`GodotAdvHost` retains configurations by layout and resolves slot 0 to the active Phase-A layout. A separate
|
||||||
Godot atlas overlay advances the inclusive frames `0..12` on the 48 ms host clock only while `IsWaiting`,
|
Godot atlas overlay advances on the 48 ms host clock only while `IsWaiting`, then hides immediately when
|
||||||
then hides immediately when input releases the wait. Keeping this 30x27 overlay outside the software
|
input releases the wait. A 2026-07-27 polish fix removed the port's erroneous `TerminalFrame + 1`
|
||||||
backbuffer preserves the static-wait compositor optimization. Full SYSTEM4 replay remains Phase-B work;
|
interpretation, which had selected nonexistent frame `12` once per cycle and made Godot render one empty
|
||||||
the shortcut carries the same state in the meantime.
|
atlas cell. Frame selection now treats the terminal value as the exclusive frame count and loops `0..11`.
|
||||||
|
Keeping this 30x27 overlay outside the software backbuffer preserves the static-wait compositor optimization.
|
||||||
|
Full SYSTEM4 replay remains Phase-B work; the shortcut carries the same state in the meantime.
|
||||||
|
|
||||||
### SC0000 native SFX / BGM-fade family — `0xb4`/`0xb5`/`0xb6`/`0xc2`/`0xd9` (2026-07-11)
|
### SC0000 native SFX / BGM-fade family — `0xb4`/`0xb5`/`0xb6`/`0xc2`/`0xd9` (2026-07-11)
|
||||||
|
|
||||||
@@ -2997,6 +3001,28 @@ embedded-NUL termination, all observed comparison operand classes, and the exact
|
|||||||
case. The full 281-test engine suite, zero-warning Godot build, and threaded frontend selftest pass; manual
|
case. The full 281-test engine suite, zero-warning Godot build, and threaded frontend selftest pass; manual
|
||||||
DEBUGMAP and developer-menu visual rechecks remain.
|
DEBUGMAP and developer-menu visual rechecks remain.
|
||||||
|
|
||||||
|
#### BUNKI popup publication and BUNKIMOVE lifecycle (2026-07-27)
|
||||||
|
|
||||||
|
BUNKI's opening/closing effect is an offscreen capture, not an independent decoration behind a stable
|
||||||
|
popup. BUNKI builds the complete panel under handles `60000..60099`, including surface-200 text, and calls
|
||||||
|
`BUNKIMOVE.BIN`. BUNKIMOVE selects 800x600 surface 2, publishes that retained range into it, restores the
|
||||||
|
backbuffer, binds the capture at handle `59999`, and scales it horizontally and then vertically through
|
||||||
|
timed callbacks. Each transition frame publishes `[0,60000)`, deliberately including the animated capture
|
||||||
|
while excluding BUNKI's stable objects. On open, BUNKI publishes the stable menu only after BUNKIMOVE
|
||||||
|
returns; on close, it removes the stable range after the reverse animation.
|
||||||
|
|
||||||
|
The port previously treated a backbuffer `0x222` as a generic repaint request and reconstructed every
|
||||||
|
visible retained object. It therefore drew handles `60000+` at full size above handle `59999`, making the
|
||||||
|
real popup appear immediately while its opening animation ran behind it; closing had the symmetric error.
|
||||||
|
Godot now retains zero-based backbuffer publication ranges and filters reconstruction to the active range.
|
||||||
|
Nonzero ranges remain incremental overlays over a full reconstruction because the port does not preserve
|
||||||
|
native D3D backbuffer pixels between publications.
|
||||||
|
|
||||||
|
Surface text also remains modeled metadata rather than pixels. Selected-target publication now projects
|
||||||
|
that metadata into the destination surface, and the final Godot labels inherit the retained object's
|
||||||
|
projected scale and rotation. Consequently BUNKIMOVE's captured labels follow the same transform as its
|
||||||
|
panel instead of escaping as full-size Control overlays.
|
||||||
|
|
||||||
### Formatted integers on text surfaces — opcode `0x205` (2026-07-21)
|
### Formatted integers on text surfaces — opcode `0x205` (2026-07-21)
|
||||||
|
|
||||||
The deployment picker opened from an empty DEBUGMAP deployment slot uses a third numeric path. Its red
|
The deployment picker opened from an empty DEBUGMAP deployment slot uses a third numeric path. Its red
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ Port status (2026-07-24): after the blocking host releases this wait, the VM que
|
|||||||
### 0x73 `configure-adv-wait-indicator` (configure-adv-wait-indicator, argc 10)
|
### 0x73 `configure-adv-wait-indicator` (configure-adv-wait-indicator, argc 10)
|
||||||
- **summary:** (layout_slot)(dst_x)(dst_y)(surface_slot)(src_x)(src_y)(cell_w)(cell_h)(terminal_frame)(frame_period_ms) - configure the animated marker shown while the selected ADV layout waits for input.
|
- **summary:** (layout_slot)(dst_x)(dst_y)(surface_slot)(src_x)(src_y)(cell_w)(cell_h)(terminal_frame)(frame_period_ms) - configure the animated marker shown while the selected ADV layout waits for input.
|
||||||
- **grounding:** source=investigation, confidence=high
|
- **grounding:** source=investigation, confidence=high
|
||||||
- **evidence:** Ghidra /v2: op_0x73_configure_wait_indicator@0x41e900 passes operands 1..9 to adv_text_configure_wait_indicator@0x44ff60 and operand 10 to adv_indicator_set_frame_period@0x44d060. The worker writes surface/source rect, layout-relative destination, enabled=1, and terminal/column values to the selected layout at +0x3c..+0x60. SYSTEM4 executes `set-texture 0x337c 0xc 0xff00` (raw id 0x337c = SO000.AGF, a 390x27 strip of thirteen 30x27 bat frames), then `0x73 1 385 140 12 0 0 30 27 12 48`; layout 1 begins at y=430, placing the 30x27 marker at screen (385,570), matching the original. Op 0x72 activates this descriptor only after reveal completion.
|
- **evidence:** Ghidra /v2: op_0x73_configure_wait_indicator@0x41e900 passes operands 1..9 to adv_text_configure_wait_indicator@0x44ff60 and operand 10 to adv_indicator_set_frame_period@0x44d060. The worker writes surface/source rect, layout-relative destination, enabled=1, and terminal/column values to the selected layout at +0x3c..+0x60. SYSTEM4 executes `set-texture 0x337c 0xc 0xff00` (raw id 0x337c = SO000.AGF, a decoded 360x27 strip of twelve 30x27 bat frames), then `0x73 1 385 140 12 0 0 30 27 12 48`; the value 12 is the exclusive terminal/frame count, so animation publishes frames 0..11 and wraps before 12. Layout 1 begins at y=430, placing the 30x27 marker at screen (385,570), matching the original. Op 0x72 activates this descriptor only after reveal completion.
|
||||||
|
|
||||||
### 0x75 `set-font-size` (set-font-size, argc 1)
|
### 0x75 `set-font-size` (set-font-size, argc 1)
|
||||||
- **summary:** (pixels) - set the primary text font height and rebuild its native rasterization state.
|
- **summary:** (pixels) - set the primary text font height and rebuild its native rasterization state.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Age.Engine.Hosting;
|
||||||
using Age.Engine.Model;
|
using Age.Engine.Model;
|
||||||
using Age.Engine.Sys4;
|
using Age.Engine.Sys4;
|
||||||
using Age.Engine.Vm;
|
using Age.Engine.Vm;
|
||||||
@@ -106,6 +108,22 @@ public class AdvTextOpsTests
|
|||||||
Assert.Single(host.WaitIndicators));
|
Assert.Single(host.WaitIndicators));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WaitIndicatorTerminalFrameIsExclusive()
|
||||||
|
{
|
||||||
|
var config = new AdvWaitIndicatorConfig(
|
||||||
|
1, 385, 140, 12, 0, 0, 30, 27, 12, 48);
|
||||||
|
|
||||||
|
int[] frames = Enumerable.Range(0, 24)
|
||||||
|
.Select(tick => config.FrameAt(tick * 48L))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
Enumerable.Range(0, 12).Concat(Enumerable.Range(0, 12)),
|
||||||
|
frames);
|
||||||
|
Assert.DoesNotContain(12, frames);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void WaitIndicatorToggleAndTextLayoutPublicationReachHost()
|
public void WaitIndicatorToggleAndTextLayoutPublicationReachHost()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -72,5 +72,16 @@ public class GfxRangeTransformTests
|
|||||||
Assert.NotNull(vm.Gfx.SnapshotVisibleObjects().Single().RangeTransform);
|
Assert.NotNull(vm.Gfx.SnapshotVisibleObjects().Single().RangeTransform);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BunkiAnimationPublicationExcludesStablePopupHandles()
|
||||||
|
{
|
||||||
|
var animationRange = new GfxHandleRange(0, 60000);
|
||||||
|
|
||||||
|
Assert.True(animationRange.Contains(59999));
|
||||||
|
Assert.False(animationRange.Contains(60000));
|
||||||
|
Assert.False(animationRange.Contains(60050));
|
||||||
|
Assert.True(GfxHandleRange.All.Contains(60050));
|
||||||
|
}
|
||||||
|
|
||||||
private static Operand I(long v) => new(0, v);
|
private static Operand I(long v) => new(0, v);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,19 @@ public class Transform2DMathTests
|
|||||||
Assert.Equal(0, allocated);
|
Assert.Equal(0, allocated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanvasAxisDecompositionPreservesBunkiPopupScale()
|
||||||
|
{
|
||||||
|
var affine = Transform2DMath.Build(new TransformState(
|
||||||
|
0.05, 0.65, 1, 0, 0, 0, 400, 300, 0));
|
||||||
|
|
||||||
|
var (rotation, scaleX, scaleY) = affine.DecomposeCanvasAxes();
|
||||||
|
|
||||||
|
Assert.Equal(0, rotation, 12);
|
||||||
|
Assert.Equal(0.05, scaleX, 12);
|
||||||
|
Assert.Equal(0.65, scaleY, 12);
|
||||||
|
}
|
||||||
|
|
||||||
private static double Next(Random random, double minimum, double maximum)
|
private static double Next(Random random, double minimum, double maximum)
|
||||||
=> minimum + random.NextDouble() * (maximum - minimum);
|
=> minimum + random.NextDouble() * (maximum - minimum);
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,17 @@ namespace Age.Engine.Hosting;
|
|||||||
public readonly record struct AdvWaitIndicatorConfig(
|
public readonly record struct AdvWaitIndicatorConfig(
|
||||||
int LayoutSlot, int X, int Y, int SurfaceSlot,
|
int LayoutSlot, int X, int Y, int SurfaceSlot,
|
||||||
int SourceX, int SourceY, int CellWidth, int CellHeight,
|
int SourceX, int SourceY, int CellWidth, int CellHeight,
|
||||||
int TerminalFrame, long FramePeriodMs);
|
int TerminalFrame, long FramePeriodMs)
|
||||||
|
{
|
||||||
|
/// <summary>Select the current atlas frame. TerminalFrame is the exclusive native upper bound,
|
||||||
|
/// so SYSTEM4's value 12 addresses the twelve cells 0 through 11.</summary>
|
||||||
|
public int FrameAt(long elapsedMs)
|
||||||
|
{
|
||||||
|
int frameCount = System.Math.Max(1, TerminalFrame);
|
||||||
|
long period = System.Math.Max(1, FramePeriodMs);
|
||||||
|
return (int)(System.Math.Max(0, elapsedMs) / period % frameCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public readonly record struct AdvAutoWaitState(
|
public readonly record struct AdvAutoWaitState(
|
||||||
bool Enabled, bool VoicePending, long PostVoiceDelayMs, long UnvoicedDelayMs);
|
bool Enabled, bool VoicePending, long PostVoiceDelayMs, long UnvoicedDelayMs);
|
||||||
|
|||||||
@@ -84,6 +84,15 @@ public readonly record struct RenderObject(long Handle, long SurfaceResId, long
|
|||||||
Affine2D? RangeTransform = null,
|
Affine2D? RangeTransform = null,
|
||||||
bool TimeVarying = false);
|
bool TimeVarying = false);
|
||||||
|
|
||||||
|
/// <summary>The retained handle interval selected by an op-0x222 backbuffer publication.</summary>
|
||||||
|
public readonly record struct GfxHandleRange(long First, long Count)
|
||||||
|
{
|
||||||
|
public static GfxHandleRange All => new(0, long.MaxValue);
|
||||||
|
|
||||||
|
public bool Contains(long handle)
|
||||||
|
=> Count > 0 && handle >= First && (ulong)(handle - First) < (ulong)Count;
|
||||||
|
}
|
||||||
|
|
||||||
/// <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
|
||||||
/// native retained-gfx owner+0x408 map (EngineCtx+0x46a1c) that op 0x215 queries and the geometry get/set ops share. Each object carries a
|
/// native retained-gfx owner+0x408 map (EngineCtx+0x46a1c) that op 0x215 queries and the geometry get/set ops share. Each object carries a
|
||||||
|
|||||||
@@ -31,6 +31,20 @@ public readonly record struct Affine2D(double XX, double XY, double YX, double Y
|
|||||||
inverse = new(xx, xy, yx, yy, -(TX * xx + TY * yx), -(TX * xy + TY * yy));
|
inverse = new(xx, xy, yx, yy, -(TX * xx + TY * yx), -(TX * xy + TY * yy));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Decompose the projected axes for a Godot-style canvas item. AGE's ordinary retained
|
||||||
|
/// transforms contain scale/rotation but no shear; the determinant preserves reflected Y axes.</summary>
|
||||||
|
public (double RotationRadians, double ScaleX, double ScaleY) DecomposeCanvasAxes()
|
||||||
|
{
|
||||||
|
double scaleX = System.Math.Sqrt(XX * XX + XY * XY);
|
||||||
|
double scaleY = System.Math.Sqrt(YX * YX + YY * YY);
|
||||||
|
double determinant = XX * YY - XY * YX;
|
||||||
|
if (determinant < 0) scaleY = -scaleY;
|
||||||
|
double rotation = scaleX > 1e-12
|
||||||
|
? System.Math.Atan2(XY, XX)
|
||||||
|
: System.Math.Atan2(-YX, YY);
|
||||||
|
return (rotation, scaleX, scaleY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Exact 2D projection of AGE's row-vector retained-object matrix. Native call order is anchored
|
/// <summary>Exact 2D projection of AGE's row-vector retained-object matrix. Native call order is anchored
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ public sealed class GodotAdvHost : IHost
|
|||||||
private GfxState? _foregroundGfx;
|
private GfxState? _foregroundGfx;
|
||||||
private readonly object _screenTransitionLock = new();
|
private readonly object _screenTransitionLock = new();
|
||||||
private readonly Dictionary<int, IReadOnlyList<RenderObject>> _renderTargetSnapshots = new();
|
private readonly Dictionary<int, IReadOnlyList<RenderObject>> _renderTargetSnapshots = new();
|
||||||
|
private readonly object _backbufferRangeLock = new();
|
||||||
|
private GfxHandleRange _backbufferRange = GfxHandleRange.All;
|
||||||
private LegacyScreenTransition? _screenTransition;
|
private LegacyScreenTransition? _screenTransition;
|
||||||
public volatile bool IsWaiting;
|
public volatile bool IsWaiting;
|
||||||
public volatile bool IsTransitionWaiting;
|
public volatile bool IsTransitionWaiting;
|
||||||
@@ -277,10 +279,28 @@ public sealed class GodotAdvHost : IHost
|
|||||||
PublishObjectRangeToSurface(gfx, firstHandle, count);
|
PublishObjectRangeToSurface(gfx, firstHandle, count);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// The software/GPU port reconstructs the backbuffer instead of preserving native D3D pixels.
|
||||||
|
// Zero-based ranges therefore define the complete published scene. A nonzero range is an
|
||||||
|
// incremental overlay in native code, so retain the full reconstruction for those call sites.
|
||||||
|
lock (_backbufferRangeLock)
|
||||||
|
_backbufferRange = firstHandle == 0
|
||||||
|
? new GfxHandleRange(firstHandle, count)
|
||||||
|
: GfxHandleRange.All;
|
||||||
System.Threading.Interlocked.Exchange(ref _presentRequested, 1);
|
System.Threading.Interlocked.Exchange(ref _presentRequested, 1);
|
||||||
_timeline?.Event("present-object-range", new() { ["first"] = firstHandle, ["count"] = count });
|
_timeline?.Event("present-object-range", new() { ["first"] = firstHandle, ["count"] = count });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SnapshotBackbufferObjects(GfxState gfx, long nowMs, List<RenderObject> snapshot)
|
||||||
|
{
|
||||||
|
gfx.SnapshotVisibleObjects(nowMs, snapshot);
|
||||||
|
GfxHandleRange range;
|
||||||
|
lock (_backbufferRangeLock) range = _backbufferRange;
|
||||||
|
int write = 0;
|
||||||
|
for (int read = 0; read < snapshot.Count; read++)
|
||||||
|
if (range.Contains(snapshot[read].Handle)) snapshot[write++] = snapshot[read];
|
||||||
|
if (write < snapshot.Count) snapshot.RemoveRange(write, snapshot.Count - write);
|
||||||
|
}
|
||||||
|
|
||||||
public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config)
|
public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config)
|
||||||
{
|
{
|
||||||
lock (_textLock) _waitIndicators[config.LayoutSlot] = config;
|
lock (_textLock) _waitIndicators[config.LayoutSlot] = config;
|
||||||
@@ -323,9 +343,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
var asset = _res.ResolveTexture(resourceId);
|
var asset = _res.ResolveTexture(resourceId);
|
||||||
var image = asset != null ? Decode(asset) : null;
|
var image = asset != null ? Decode(asset) : null;
|
||||||
if (asset == null || image == null || config.CellWidth <= 0 || config.CellHeight <= 0) return null;
|
if (asset == null || image == null || config.CellWidth <= 0 || config.CellHeight <= 0) return null;
|
||||||
int frames = System.Math.Max(1, config.TerminalFrame + 1);
|
int frame = config.FrameAt(_clock.NowMs - _waitIndicatorStartedMs);
|
||||||
long period = System.Math.Max(1, config.FramePeriodMs);
|
|
||||||
int frame = (int)((_clock.NowMs - _waitIndicatorStartedMs) / period % frames);
|
|
||||||
return new AdvWaitIndicatorSnapshot(image, asset.Name, asset.PackedId, config, frame);
|
return new AdvWaitIndicatorSnapshot(image, asset.Name, asset.PackedId, config, frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,6 +602,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
lock (_backbufferRangeLock) _backbufferRange = GfxHandleRange.All;
|
||||||
int started = gfx.StartForegroundTransitions(_clock.NowMs);
|
int started = gfx.StartForegroundTransitions(_clock.NowMs);
|
||||||
int completed = gfx.CompleteForegroundTransitions(_clock.NowMs);
|
int completed = gfx.CompleteForegroundTransitions(_clock.NowMs);
|
||||||
if (started > 0 || completed > 0)
|
if (started > 0 || completed > 0)
|
||||||
@@ -702,6 +721,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
// scene_context_init_reset releases ordinary surface/movie bindings but keeps decoded asset
|
// scene_context_init_reset releases ordinary surface/movie bindings but keeps decoded asset
|
||||||
// caches and process-owned audio/configuration available to the reloaded SYSTEM4 root.
|
// caches and process-owned audio/configuration available to the reloaded SYSTEM4 root.
|
||||||
ReleaseSurfaceRange(0, 1000);
|
ReleaseSurfaceRange(0, 1000);
|
||||||
|
lock (_backbufferRangeLock) _backbufferRange = GfxHandleRange.All;
|
||||||
lock (_textLock)
|
lock (_textLock)
|
||||||
{
|
{
|
||||||
_surfaceText.Clear();
|
_surfaceText.Clear();
|
||||||
@@ -1162,6 +1182,8 @@ public sealed class GodotAdvHost : IHost
|
|||||||
});
|
});
|
||||||
|
|
||||||
lock (_imageLock) _surfaceImages[targetSlot] = destination;
|
lock (_imageLock) _surfaceImages[targetSlot] = destination;
|
||||||
|
PublishSurfaceTextRangeToSurface(
|
||||||
|
gfx, visible, firstHandle, count, targetSlot, dimensions.W, dimensions.H);
|
||||||
IReadOnlyList<RenderObject> retained = visible
|
IReadOnlyList<RenderObject> retained = visible
|
||||||
.Where(item => item.Handle >= firstHandle && item.Handle - firstHandle < count)
|
.Where(item => item.Handle >= firstHandle && item.Handle - firstHandle < count)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
@@ -1176,6 +1198,51 @@ public sealed class GodotAdvHost : IHost
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void PublishSurfaceTextRangeToSurface(
|
||||||
|
GfxState gfx, IReadOnlyList<RenderObject> visible, long firstHandle, long count,
|
||||||
|
int targetSlot, int targetWidth, int targetHeight)
|
||||||
|
{
|
||||||
|
List<SurfaceTextDraw> projected;
|
||||||
|
lock (_textLock)
|
||||||
|
projected = _surfaceText.TryGetValue(targetSlot, out var retained)
|
||||||
|
? new List<SurfaceTextDraw>(retained)
|
||||||
|
: new List<SurfaceTextDraw>();
|
||||||
|
|
||||||
|
foreach (RenderObject item in visible)
|
||||||
|
{
|
||||||
|
if (item.Handle < firstHandle || item.Handle - firstHandle >= count) continue;
|
||||||
|
var raw = gfx.TryGet(item.Handle);
|
||||||
|
if (raw == null) continue;
|
||||||
|
List<SurfaceTextDraw>? source;
|
||||||
|
lock (_textLock)
|
||||||
|
source = _surfaceText.TryGetValue(raw.SourceSlot, out var draws)
|
||||||
|
? new List<SurfaceTextDraw>(draws)
|
||||||
|
: null;
|
||||||
|
if (source == null) continue;
|
||||||
|
|
||||||
|
Affine2D localToTarget =
|
||||||
|
Transform2DMath.Build(item.Transform, item.Rotation).FromLocalOrigin(item.DstX, item.DstY);
|
||||||
|
if (item.RangeTransform is { } rangeTransform)
|
||||||
|
localToTarget = localToTarget.Then(rangeTransform);
|
||||||
|
foreach (SurfaceTextDraw draw in source)
|
||||||
|
{
|
||||||
|
if (draw.X < item.SrcX || draw.X >= item.SrcX + item.W ||
|
||||||
|
draw.Y < item.SrcY || draw.Y >= item.SrcY + item.H) continue;
|
||||||
|
var position = localToTarget.Apply(draw.X - item.SrcX, draw.Y - item.SrcY);
|
||||||
|
int x = (int)System.Math.Round(position.X);
|
||||||
|
int y = (int)System.Math.Round(position.Y);
|
||||||
|
if (x < 0 || x >= targetWidth || y < 0 || y >= targetHeight) continue;
|
||||||
|
projected.Add(new SurfaceTextDraw(x, y, draw.Text, draw.Style));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_textLock)
|
||||||
|
{
|
||||||
|
if (projected.Count == 0) _surfaceText.Remove(targetSlot);
|
||||||
|
else _surfaceText[targetSlot] = projected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void ReleaseSurfaceRange(int firstSlot, int count)
|
public void ReleaseSurfaceRange(int firstSlot, int count)
|
||||||
{
|
{
|
||||||
IReadOnlyList<MovieSurfaceBinding> stoppedMovies = _movieSurfaces.ReleaseRange(firstSlot, count);
|
IReadOnlyList<MovieSurfaceBinding> stoppedMovies = _movieSurfaces.ReleaseRange(firstSlot, count);
|
||||||
|
|||||||
@@ -864,7 +864,7 @@ public partial class Main : Godot.Control
|
|||||||
long phase = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
long phase = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||||
long allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
long allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
||||||
if (_host.TrySnapshotScreenTransition(out _)) return false; // P4: whole-screen offscreen targets
|
if (_host.TrySnapshotScreenTransition(out _)) return false; // P4: whole-screen offscreen targets
|
||||||
_vm.Gfx.SnapshotVisibleObjects(_clock.NowMs, _visibleSnapshot);
|
_host.SnapshotBackbufferObjects(_vm.Gfx, _clock.NowMs, _visibleSnapshot);
|
||||||
snapshotCaptured = true;
|
snapshotCaptured = true;
|
||||||
_perf?.RecordSnapshotAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
_perf?.RecordSnapshotAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
||||||
_perf?.RecordSnapshot(PerformanceFrameLog.Timestamp() - phase);
|
_perf?.RecordSnapshot(PerformanceFrameLog.Timestamp() - phase);
|
||||||
@@ -952,9 +952,9 @@ public partial class Main : Godot.Control
|
|||||||
{
|
{
|
||||||
if (surfaceText.X < v.SrcX || surfaceText.X >= v.SrcX + v.W ||
|
if (surfaceText.X < v.SrcX || surfaceText.X >= v.SrcX + v.W ||
|
||||||
surfaceText.Y < v.SrcY || surfaceText.Y >= v.SrcY + v.H) continue;
|
surfaceText.Y < v.SrcY || surfaceText.Y >= v.SrcY + v.H) continue;
|
||||||
var textPos = affine.Apply(surfaceText.X - v.SrcX, surfaceText.Y - v.SrcY);
|
|
||||||
var label = GetSurfaceTextLabel(surfaceTextLabelIndex++);
|
var label = GetSurfaceTextLabel(surfaceTextLabelIndex++);
|
||||||
label.Position = new Vector2((float)textPos.X, (float)textPos.Y);
|
ApplySurfaceTextTransform(
|
||||||
|
label, affine, surfaceText.X - v.SrcX, surfaceText.Y - v.SrcY);
|
||||||
label.Size = new Vector2(System.Math.Max(1, v.W - (surfaceText.X - v.SrcX)),
|
label.Size = new Vector2(System.Math.Max(1, v.W - (surfaceText.X - v.SrcX)),
|
||||||
System.Math.Max(1, v.H - (surfaceText.Y - v.SrcY)));
|
System.Math.Max(1, v.H - (surfaceText.Y - v.SrcY)));
|
||||||
label.Text = surfaceText.Text;
|
label.Text = surfaceText.Text;
|
||||||
@@ -1078,7 +1078,8 @@ public partial class Main : Godot.Control
|
|||||||
phase = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
phase = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||||
allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
||||||
if (sampledVisible == null)
|
if (sampledVisible == null)
|
||||||
_vm.Gfx.SnapshotVisibleObjects(_clock.NowMs, _visibleSnapshot); // synchronized objects + ranges
|
_host.SnapshotBackbufferObjects(
|
||||||
|
_vm.Gfx, _clock.NowMs, _visibleSnapshot); // synchronized objects + publication range
|
||||||
_perf?.RecordSnapshotAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
_perf?.RecordSnapshotAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
||||||
_perf?.RecordSnapshot(PerformanceFrameLog.Timestamp() - phase);
|
_perf?.RecordSnapshot(PerformanceFrameLog.Timestamp() - phase);
|
||||||
allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
||||||
@@ -1197,9 +1198,9 @@ public partial class Main : Godot.Control
|
|||||||
{
|
{
|
||||||
if (surfaceText.X < v.SrcX || surfaceText.X >= v.SrcX + v.W ||
|
if (surfaceText.X < v.SrcX || surfaceText.X >= v.SrcX + v.W ||
|
||||||
surfaceText.Y < v.SrcY || surfaceText.Y >= v.SrcY + v.H) continue;
|
surfaceText.Y < v.SrcY || surfaceText.Y >= v.SrcY + v.H) continue;
|
||||||
var textPos = localToDest.Apply(surfaceText.X - v.SrcX, surfaceText.Y - v.SrcY);
|
|
||||||
var label = GetSurfaceTextLabel(surfaceTextLabelIndex++);
|
var label = GetSurfaceTextLabel(surfaceTextLabelIndex++);
|
||||||
label.Position = new Vector2((float)textPos.X, (float)textPos.Y);
|
ApplySurfaceTextTransform(
|
||||||
|
label, localToDest, surfaceText.X - v.SrcX, surfaceText.Y - v.SrcY);
|
||||||
label.Size = new Vector2(System.Math.Max(1, v.W - (surfaceText.X - v.SrcX)),
|
label.Size = new Vector2(System.Math.Max(1, v.W - (surfaceText.X - v.SrcX)),
|
||||||
System.Math.Max(1, v.H - (surfaceText.Y - v.SrcY)));
|
System.Math.Max(1, v.H - (surfaceText.Y - v.SrcY)));
|
||||||
label.Text = surfaceText.Text;
|
label.Text = surfaceText.Text;
|
||||||
@@ -1254,6 +1255,17 @@ public partial class Main : Godot.Control
|
|||||||
return _surfaceTextLabels[index];
|
return _surfaceTextLabels[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ApplySurfaceTextTransform(Label label, Affine2D localToDestination,
|
||||||
|
int localX, int localY)
|
||||||
|
{
|
||||||
|
var position = localToDestination.Apply(localX, localY);
|
||||||
|
var (rotation, scaleX, scaleY) = localToDestination.DecomposeCanvasAxes();
|
||||||
|
label.Position = new Vector2((float)position.X, (float)position.Y);
|
||||||
|
label.PivotOffset = Vector2.Zero;
|
||||||
|
label.Rotation = (float)rotation;
|
||||||
|
label.Scale = new Vector2((float)scaleX, (float)scaleY);
|
||||||
|
}
|
||||||
|
|
||||||
private Label CreateAdvPresentationLabel()
|
private Label CreateAdvPresentationLabel()
|
||||||
{
|
{
|
||||||
var label = new Label
|
var label = new Label
|
||||||
|
|||||||
@@ -1050,7 +1050,7 @@ noop_headless = false
|
|||||||
source = "investigation"
|
source = "investigation"
|
||||||
confidence = "high"
|
confidence = "high"
|
||||||
depends_on = []
|
depends_on = []
|
||||||
evidence = "Ghidra /v2: op_0x73_configure_wait_indicator@0x41e900 passes operands 1..9 to adv_text_configure_wait_indicator@0x44ff60 and operand 10 to adv_indicator_set_frame_period@0x44d060. The worker writes surface/source rect, layout-relative destination, enabled=1, and terminal/column values to the selected layout at +0x3c..+0x60. SYSTEM4 executes `set-texture 0x337c 0xc 0xff00` (raw id 0x337c = SO000.AGF, a 390x27 strip of thirteen 30x27 bat frames), then `0x73 1 385 140 12 0 0 30 27 12 48`; layout 1 begins at y=430, placing the 30x27 marker at screen (385,570), matching the original. Op 0x72 activates this descriptor only after reveal completion."
|
evidence = "Ghidra /v2: op_0x73_configure_wait_indicator@0x41e900 passes operands 1..9 to adv_text_configure_wait_indicator@0x44ff60 and operand 10 to adv_indicator_set_frame_period@0x44d060. The worker writes surface/source rect, layout-relative destination, enabled=1, and terminal/column values to the selected layout at +0x3c..+0x60. SYSTEM4 executes `set-texture 0x337c 0xc 0xff00` (raw id 0x337c = SO000.AGF, a decoded 360x27 strip of twelve 30x27 bat frames), then `0x73 1 385 140 12 0 0 30 27 12 48`; the value 12 is the exclusive terminal/frame count, so animation publishes frames 0..11 and wraps before 12. Layout 1 begins at y=430, placing the 30x27 marker at screen (385,570), matching the original. Op 0x72 activates this descriptor only after reveal completion."
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
@@ -1094,7 +1094,7 @@ observed_types = ["imm"]
|
|||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 9
|
i = 9
|
||||||
role = "terminal frame index / sheet columns"
|
role = "exclusive terminal frame / sheet column count"
|
||||||
observed_types = ["imm"]
|
observed_types = ["imm"]
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
|
|||||||
Reference in New Issue
Block a user