Fix ADV and BUNKI animation polish

This commit is contained in:
gamer147
2026-07-27 11:15:41 -04:00
parent 4b5b451df0
commit 9c99254a9a
11 changed files with 199 additions and 19 deletions

View File

@@ -2330,9 +2330,11 @@ non-overlapping name/dialogue bands at y=447468, 478500, and 507530. 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

View File

@@ -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.

View File

@@ -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()
{ {

View File

@@ -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);
} }

View File

@@ -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);

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -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

View File

@@ -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]]