Split Godot host ADV text into partial class

This commit is contained in:
gamer147
2026-08-02 19:11:34 -04:00
parent 4b6d0c9c49
commit 1ffd8a0853
4 changed files with 611 additions and 589 deletions

View File

@@ -145,6 +145,10 @@ surface-transition drawing, raster helpers, and compositor decision logging. `go
Godot input routing, locator/debug hotkeys, debug-scene dispatch, cursor control, native alerts, and Godot input routing, locator/debug hotkeys, debug-scene dispatch, cursor control, native alerts, and
full-width text entry. full-width text entry.
`godot/GodotAdvHost.cs` retains cross-domain host coordination. Its partial-class companion
`godot/GodotAdvHost.AdvText.cs` owns live/retained ADV text, surface glyph rasterization and caching,
history presentation, message-window alpha, and retained wait-indicator configuration/publication.
The disposable `build/page-map-<SCENE>.jsonl` files are produced by editor/development Godot runs and map The disposable `build/page-map-<SCENE>.jsonl` files are produced by editor/development Godot runs and map
runtime ADV page ordinals to their authoritative script offsets for `tools/locate_page.py`. Packaged exports runtime ADV page ordinals to their authoritative script offsets for `tools/locate_page.py`. Packaged exports
have no repository output tree and write their automatic maps below `user://diagnostics/page-maps` instead. have no repository output tree and write their automatic maps below `user://diagnostics/page-maps` instead.

View File

@@ -569,6 +569,12 @@ do not mix mechanical moves with semantic changes.
The partial class retains the same node type, fields, signatures, execution order, and call sites; runtime The partial class retains the same node type, fields, signatures, execution order, and call sites; runtime
validation, including all 590 engine tests and the Godot threaded self-test, remains green after each move. validation, including all 590 engine tests and the Godot threaded self-test, remains green after each move.
The first bounded `GodotAdvHost` split moved live/retained ADV text, surface glyph rasterization and cache
state, history presentation, message-window alpha, and retained wait-indicator publication into
`godot/GodotAdvHost.AdvText.cs`. The host remains a sealed `IHost` implementation, and presentation/input,
reset, and surface-lifecycle consumers retain direct partial-class access to the moved state. Runtime
validation remains green.
**Gate:** no externally visible behavior or command changes; generated artifacts are byte-identical where **Gate:** no externally visible behavior or command changes; generated artifacts are byte-identical where
deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after
each domain move. each domain move.
@@ -939,8 +945,8 @@ layer's rendering diverges from ADV; save layout.
## 8. Immediate next step ## 8. Immediate next step
Continue step 2 of the **codebase consolidation** maintenance slice: behavior-neutral physical splits backed Continue step 2 of the **codebase consolidation** maintenance slice: behavior-neutral physical splits backed
by the tracked launcher and layered validation driver. With the planned `Main` domains isolated, begin by the tracked launcher and layered validation driver. With the planned `Main` domains and the first
`GodotAdvHost` with its ADV-text surface, then continue one existing domain at a time while preserving public `GodotAdvHost` ADV-text surface isolated, move host presentation/input next, then continue one existing domain
types, commands, and generated output. at a time while preserving public types, commands, and generated output.
Concrete playthrough blockers may still preempt this bounded maintenance work; the consolidation effort does Concrete playthrough blockers may still preempt this bounded maintenance work; the consolidation effort does
not replace Phase B gameplay validation or the open cross-platform gates. not replace Phase B gameplay validation or the open cross-platform gates.

View File

@@ -0,0 +1,597 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Age.Engine.Hosting;
using Age.Engine.Model;
using Age.Engine.Sys4;
using Age.Engine.Text;
public sealed partial class GodotAdvHost
{
private readonly object _textLock = new();
private readonly CachedGlyphMaskRasterizer _surfaceTextMaskCache;
private readonly ImmediateSurfaceTextRenderer _surfaceTextPixelRenderer;
private readonly RetainedGlyphLayoutEngine _retainedGlyphLayoutEngine;
private readonly GlyphRasterizerBackendInfo _surfaceTextBackendInfo;
private readonly HashSet<int> _retainedHistoryLayouts = new();
private sealed class LiveTextState
{
public required AdvLiveTextRun Run;
public required long StartedMs;
public required int GlyphDelayMilliseconds;
public int FirstGlyphIndex;
public int GlyphCount;
}
private readonly List<LiveTextState> _liveText = new();
private readonly Dictionary<int, RetainedAdvTextLayoutPresentation>
_retainedTextLayouts = new();
private LiveTextState? _activeLiveText;
private int _suspendedRetainedTextLayoutSlot;
private string _advText = "";
private int _advTextX = 100, _advTextY = 47;
private int _currentAdvLayout = 1; // SYSTEM4's ordinary SC0000 ADV layout
private long _advTextStartedMs;
private int _activeGlyphDelayMilliseconds = 50;
private int _messageGlyphDelayMilliseconds = 50;
private volatile int _messageWindowAlphaSetting;
private bool _advTextForceComplete;
private readonly Dictionary<int, AdvWaitIndicatorConfig> _waitIndicators = new();
private readonly Dictionary<int, (
AdvTextLayoutPresentationBinding Binding,
AdvTextLayoutSnapshot Layout)> _waitIndicatorBindings = new();
private readonly Dictionary<int, RetainedAdvWaitIndicatorPresentation>
_retainedWaitIndicators = new();
private int _activeWaitLayout;
private long _waitIndicatorStartedMs;
private bool _waitIndicatorEnabled;
public volatile bool IsTextRevealing;
public readonly List<(int Offset, string Text)> Captured = new();
public bool UsesSurfaceTextPixels => true;
public GlyphRasterizerBackendInfo SurfaceTextBackendInfo => _surfaceTextBackendInfo;
public (int Count, int Capacity, long Hits, long Misses) SurfaceTextMaskCacheStats
=> (
_surfaceTextMaskCache.Count,
_surfaceTextMaskCache.Capacity,
_surfaceTextMaskCache.Hits,
_surfaceTextMaskCache.Misses);
public int MessageGlyphDelayMilliseconds
{
get
{
lock (_textLock) return _messageGlyphDelayMilliseconds;
}
}
public void SetMessageGlyphDelayMilliseconds(int milliseconds)
{
lock (_textLock) _messageGlyphDelayMilliseconds = System.Math.Max(0, milliseconds);
_timeline?.Event("message-glyph-delay", new() { ["delay_ms"] = milliseconds });
}
public void ShowText(int offset, string text)
=> throw new NotSupportedException(
"Godot gameplay text requires a retained layout binding.");
public void ShowText(AdvLiveTextRun run, int glyphDelayMilliseconds)
=> throw new NotSupportedException(
"Godot gameplay text requires a retained layout binding.");
public AdvRetainedTextRunResult? ShowText(
GfxState gfx,
AdvTextLayoutPresentationBinding binding,
AdvLiveTextRun run,
int glyphDelayMilliseconds)
{
if (binding.LayoutSlot != run.Layout.Slot
|| binding.FirstObjectHandle < 0
|| binding.ObjectCapacity <= 0)
throw new InvalidOperationException(
$"ADV layout {run.Layout.Slot} has no retained presentation binding.");
PrepareRetainedTextRun(
gfx, binding, run,
out RetainedAdvTextLayoutPresentation? presentation,
out AdvRetainedTextRunResult result);
Captured.Add((run.SourceOffset, run.Text));
_locator.Text(run.SourceOffset, run.Text);
int delay = System.Math.Max(0, glyphDelayMilliseconds);
int revealGlyphCount = System.Math.Max(
0, System.Math.Min(
result.GlyphCount,
presentation.PublishableGlyphCount - result.FirstGlyphIndex));
var state = new LiveTextState
{
Run = run,
StartedMs = _clock.NowMs,
GlyphDelayMilliseconds = delay,
FirstGlyphIndex = result.FirstGlyphIndex,
GlyphCount = revealGlyphCount,
};
lock (_textLock)
{
_liveText.Add(state);
_activeLiveText = state;
_advText = run.Text;
_advTextX = run.Layout.CursorX;
_advTextY = run.Layout.CursorY;
_currentAdvLayout = run.Layout.Slot;
_advTextStartedMs = state.StartedMs;
_activeGlyphDelayMilliseconds = delay;
_advTextForceComplete = _messageSkipActive;
IsTextRevealing =
revealGlyphCount > 0 && delay > 0 && !_messageSkipActive;
}
_timeline?.State("text-reveal", new()
{
["offset"] = $"0x{run.SourceOffset:x}",
["layout"] = run.Layout.Slot,
["x"] = run.Layout.OriginX + run.Layout.CursorX,
["y"] = run.Layout.OriginY + run.Layout.CursorY,
["glyphs"] = revealGlyphCount,
["records"] = result.GlyphCount,
["delay_ms"] = delay,
["presentation"] = "retained-glyphs",
});
int initiallyVisible = IsTextRevealing ? 1 : revealGlyphCount;
presentation.PublishThrough(
gfx, checked(result.FirstGlyphIndex + initiallyVisible));
Interlocked.Exchange(ref _presentRequested, 1);
if (!IsTextRevealing)
{
_timeline?.State("running", new() { ["text_reveal_complete"] = true });
return result;
}
bool scriptSuspended = SuspendScriptForPresentation();
try
{
RequestSynchronizedPresentation();
while (IsTextRevealing && !_stopping)
{
int visible;
lock (_textLock)
{
visible = _advTextForceComplete
? revealGlyphCount
: (int)System.Math.Clamp(
(_clock.NowMs - state.StartedMs) / delay + 1,
0, revealGlyphCount);
if (visible >= revealGlyphCount) IsTextRevealing = false;
}
int before = presentation.PublishedGlyphCount;
presentation.PublishThrough(
gfx, checked(result.FirstGlyphIndex + visible));
if (presentation.PublishedGlyphCount != before)
{
Interlocked.Exchange(ref _presentRequested, 1);
RequestSynchronizedPresentation();
}
if (IsTextRevealing) _frameSignal.WaitOne(50);
}
}
finally
{
ResumeScriptAfterPresentation(scriptSuspended);
}
_timeline?.State("running", new() { ["text_reveal_complete"] = true });
return result;
}
private void PrepareRetainedTextRun(
GfxState gfx,
AdvTextLayoutPresentationBinding binding,
AdvLiveTextRun run,
out RetainedAdvTextLayoutPresentation presentation,
out AdvRetainedTextRunResult result)
{
presentation = null!;
result = default;
if (run.Layout.Width <= 0 || run.Layout.Height <= 0)
throw new InvalidOperationException(
$"ADV layout {run.Layout.Slot} has invalid dimensions " +
$"{run.Layout.Width}x{run.Layout.Height}.");
RgbaImage destination = ResolveSurfacePixels(binding.SourceSurfaceSlot)
?? new RgbaImage(
run.Layout.Width,
run.Layout.Height,
new byte[checked(run.Layout.Width * run.Layout.Height * 4)]);
if (destination.Width != run.Layout.Width
|| destination.Height != run.Layout.Height)
throw new InvalidOperationException(
$"ADV layout {run.Layout.Slot} surface {binding.SourceSurfaceSlot} is " +
$"{destination.Width}x{destination.Height}; expected " +
$"{run.Layout.Width}x{run.Layout.Height}.");
var updated = new RgbaImage(
destination.Width, destination.Height, (byte[])destination.Pixels.Clone());
IReadOnlyList<GlyphRasterRequest> requests =
ImmediateSurfaceTextRenderer.CreateRequests(
run.Text, run.Style, _surfaceTextBackendInfo.Policy);
GlyphTextLayoutResult rendered = _retainedGlyphLayoutEngine.Render(
updated,
new GlyphTextLayoutOptions(
run.Layout.CursorX,
run.Layout.CursorY,
binding.ResetCursorX,
run.Layout.Right,
run.Layout.Bottom,
WrapHorizontally: true,
run.Style),
requests);
lock (_textLock)
{
if (!_retainedTextLayouts.TryGetValue(
binding.LayoutSlot, out presentation!))
{
presentation = new RetainedAdvTextLayoutPresentation(binding);
_retainedTextLayouts.Add(binding.LayoutSlot, presentation);
}
else if (presentation.Binding != binding)
throw new InvalidOperationException(
$"ADV layout {binding.LayoutSlot} changed its retained binding without reset.");
int first = presentation.Append(
rendered.Records,
rendered.PresentationRects,
run.Layout.OriginX,
run.Layout.OriginY);
result = new AdvRetainedTextRunResult(
binding.LayoutSlot,
first,
rendered.ConsumedGlyphs,
rendered.CursorX,
rendered.CursorY);
}
lock (_imageLock)
_surfaceImages[binding.SourceSurfaceSlot] = updated;
_slotDims[binding.SourceSurfaceSlot] = (updated.Width, updated.Height);
gfx.CreateSurface(binding.SourceSurfaceSlot);
}
public void SetAdvTextCursor(int layoutSlot, int x, int y)
{
lock (_textLock)
{
if (layoutSlot != 0) _currentAdvLayout = layoutSlot;
_advTextX = x;
_advTextY = y;
}
_timeline?.Event("text-cursor", new() { ["slot"] = layoutSlot, ["x"] = x, ["y"] = y });
}
public void DrawStringToSurface(int surfaceSlot, int x, int y, string text)
=> DrawStringToSurface(surfaceSlot, x, y, text, AdvTextStyle.Default);
public void DrawStringToSurface(int surfaceSlot, int x, int y, string text, AdvTextStyle style)
{
DrawStringPixels(surfaceSlot, x, y, text, style);
_timeline?.Event("draw-string", new()
{
["surface"] = surfaceSlot,
["x"] = x,
["y"] = y,
["text"] = text,
["presentation"] = "rgba-glyph-mask",
["backend"] = _surfaceTextBackendInfo.Id,
});
}
private void DrawStringPixels(
int surfaceSlot, int x, int y, string text, AdvTextStyle style)
{
RgbaImage? destination = ResolveSurfacePixels(surfaceSlot);
if (destination == null
&& _slotDims.TryGetValue(surfaceSlot, out var dimensions)
&& dimensions.W > 0
&& dimensions.H > 0)
destination = new RgbaImage(
dimensions.W, dimensions.H,
new byte[checked(dimensions.W * dimensions.H * 4)]);
if (destination == null || destination.Width <= 0 || destination.Height <= 0)
throw new InvalidOperationException(
$"Surface {surfaceSlot} has no rasterizable pixel allocation.");
var updated = new RgbaImage(
destination.Width, destination.Height, (byte[])destination.Pixels.Clone());
_surfaceTextPixelRenderer.Render(updated, x, y, text, style);
lock (_imageLock) _surfaceImages[surfaceSlot] = updated;
System.Threading.Interlocked.Exchange(ref _presentRequested, 1);
}
public (string Text, int X, int Y, int VisibleGlyphs, bool Revealing) SnapshotAdvText()
{
lock (_textLock)
{
int visible = _advTextForceComplete || !IsTextRevealing
? _advText.Length
: (int)System.Math.Clamp(
(_clock.NowMs - _advTextStartedMs) / System.Math.Max(1, _activeGlyphDelayMilliseconds) + 1,
0, _advText.Length);
return (_advText, _advTextX, _advTextY, visible, IsTextRevealing);
}
}
/// <summary>
/// Layout that owns the ordinary retained ADV page. Nested callback scripts such as HISTORY can select
/// and mutate other layouts while the parent wait remains parked; those transient selections must not
/// move the parent page when its retained range is restored.
/// </summary>
public int AdvPageLayoutSlot
{
get
{
lock (_textLock)
return IsWaiting && _activeWaitLayout != 0 ? _activeWaitLayout : _currentAdvLayout;
}
}
public void ClearRenderedAdvTextLayout(int layoutSlot)
{
lock (_textLock)
{
int slot = layoutSlot == 0 ? _currentAdvLayout : layoutSlot;
if (_retainedTextLayouts.ContainsKey(slot)) return;
_liveText.RemoveAll(state => state.Run.Layout.Slot == slot);
if (_activeLiveText?.Run.Layout.Slot == slot)
{
_activeLiveText = null;
_advText = "";
_advTextForceComplete = false;
IsTextRevealing = false;
}
}
}
public void ResetRenderedAdvTextLayout(
GfxState gfx, AdvTextLayoutPresentationBinding binding)
{
lock (_textLock)
{
_retainedHistoryLayouts.Remove(binding.LayoutSlot);
_retainedTextLayouts.Remove(binding.LayoutSlot);
_liveText.RemoveAll(
state => state.Run.Layout.Slot == binding.LayoutSlot);
if (_activeLiveText?.Run.Layout.Slot == binding.LayoutSlot)
{
_activeLiveText = null;
_advText = "";
_advTextForceComplete = false;
IsTextRevealing = false;
}
if (_suspendedRetainedTextLayoutSlot == binding.LayoutSlot)
_suspendedRetainedTextLayoutSlot = 0;
}
if (_slotDims.TryGetValue(
binding.SourceSurfaceSlot, out var dimensions)
&& dimensions.W > 0
&& dimensions.H > 0)
{
lock (_imageLock)
_surfaceImages[binding.SourceSurfaceSlot] = new RgbaImage(
dimensions.W,
dimensions.H,
new byte[checked(dimensions.W * dimensions.H * 4)]);
}
Interlocked.Exchange(ref _presentRequested, 1);
_timeline?.Event("adv-text-layout-reset", new()
{
["layout"] = binding.LayoutSlot,
["surface"] = binding.SourceSurfaceSlot,
["first_handle"] = binding.FirstObjectHandle,
["capacity"] = binding.ObjectCapacity,
});
}
public bool RenderTextHistory(
GfxState gfx,
AdvTextLayoutPresentationBinding binding,
AdvTextHistoryRenderBatch batch)
{
var run = new AdvLiveTextRun(
batch.FirstRecordIndex,
batch.Layout,
batch.Style,
batch.Text,
Array.Empty<string>());
if (binding.LayoutSlot != batch.LayoutSlot
|| binding.FirstObjectHandle < 0
|| binding.ObjectCapacity <= 0)
throw new InvalidOperationException(
$"History layout {batch.LayoutSlot} has no retained presentation binding.");
PrepareRetainedTextRun(
gfx, binding, run,
out RetainedAdvTextLayoutPresentation? presentation,
out AdvRetainedTextRunResult result);
presentation.PublishThrough(
gfx,
checked(result.FirstGlyphIndex + result.GlyphCount));
lock (_textLock)
{
_retainedHistoryLayouts.Add(batch.LayoutSlot);
}
Interlocked.Exchange(ref _presentRequested, 1);
_timeline?.Event("history-render", new()
{
["layout"] = batch.LayoutSlot,
["record"] = batch.FirstRecordIndex,
["x"] = batch.Layout.OriginX + batch.Layout.CursorX,
["y"] = batch.Layout.OriginY + batch.Layout.CursorY,
["text"] = batch.Text,
["glyphs"] = result.GlyphCount,
["presentation"] = "retained-glyphs",
});
return true;
}
public void EndTextHistoryPresentation(GfxState gfx)
{
RetainedAdvTextLayoutPresentation[] retained;
int[] surfaceSlots;
lock (_textLock)
{
retained = _retainedHistoryLayouts
.Select(slot => _retainedTextLayouts.GetValueOrDefault(slot))
.Where(presentation => presentation != null)
.Cast<RetainedAdvTextLayoutPresentation>()
.ToArray();
surfaceSlots = retained
.Select(presentation => presentation.Binding.SourceSurfaceSlot)
.Distinct()
.ToArray();
foreach (int slot in _retainedHistoryLayouts)
_retainedTextLayouts.Remove(slot);
_retainedHistoryLayouts.Clear();
}
foreach (RetainedAdvTextLayoutPresentation presentation in retained)
presentation.ErasePublished(gfx);
foreach (int surfaceSlot in surfaceSlots)
ClearAllocatedSurfacePixels(surfaceSlot);
Interlocked.Exchange(ref _presentRequested, 1);
_timeline?.Event("history-presentation-end", new()
{
["retained_layouts"] = retained.Length,
});
}
private void ClearAllocatedSurfacePixels(int surfaceSlot)
{
if (!_slotDims.TryGetValue(surfaceSlot, out var dimensions)
|| dimensions.W <= 0
|| dimensions.H <= 0)
return;
lock (_imageLock)
_surfaceImages[surfaceSlot] = new RgbaImage(
dimensions.W,
dimensions.H,
new byte[checked(dimensions.W * dimensions.H * 4)]);
}
public int MessageWindowAlphaSetting => _messageWindowAlphaSetting;
public void SetMessageWindowAlphaSetting(int value)
{
_messageWindowAlphaSetting = value;
_timeline?.Event("message-window-alpha", new() { ["value"] = value });
}
public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config)
{
lock (_textLock)
{
_waitIndicators[config.LayoutSlot] = config;
RebuildRetainedWaitIndicator(config.LayoutSlot);
}
_timeline?.Event("wait-indicator-config", new()
{
["layout"] = config.LayoutSlot, ["x"] = config.X, ["y"] = config.Y,
["surface"] = config.SurfaceSlot, ["cell_w"] = config.CellWidth,
["cell_h"] = config.CellHeight, ["terminal_frame"] = config.TerminalFrame,
["period_ms"] = config.FramePeriodMs,
});
}
public void BindAdvWaitIndicator(
AdvTextLayoutPresentationBinding binding,
AdvTextLayoutSnapshot layout)
{
lock (_textLock)
{
_waitIndicatorBindings[binding.LayoutSlot] = (binding, layout);
RebuildRetainedWaitIndicator(binding.LayoutSlot);
}
_timeline?.Event("wait-indicator-bind", new()
{
["layout"] = binding.LayoutSlot,
["handle"] = binding.WaitIndicatorObjectHandle,
["origin_x"] = layout.OriginX,
["origin_y"] = layout.OriginY,
});
}
private void RebuildRetainedWaitIndicator(int layoutSlot)
{
if (!_waitIndicators.TryGetValue(layoutSlot, out AdvWaitIndicatorConfig config)
|| !_waitIndicatorBindings.TryGetValue(
layoutSlot,
out (AdvTextLayoutPresentationBinding Binding, AdvTextLayoutSnapshot Layout) retained)
|| retained.Binding.WaitIndicatorObjectHandle < 0)
{
_retainedWaitIndicators.Remove(layoutSlot);
return;
}
_retainedWaitIndicators[layoutSlot] =
new RetainedAdvWaitIndicatorPresentation(
config,
retained.Binding,
retained.Layout.OriginX,
retained.Layout.OriginY);
}
public void SetAdvWaitIndicatorEnabled(bool enabled)
{
lock (_textLock)
{
if (enabled && !_waitIndicatorEnabled) _waitIndicatorStartedMs = _clock.NowMs;
_waitIndicatorEnabled = enabled;
}
Interlocked.Exchange(ref _presentRequested, 1);
_timeline?.Event("wait-indicator-enabled", new() { ["enabled"] = enabled });
}
public void PublishAdvTextLayout(int layoutSlot)
{
System.Threading.Interlocked.Exchange(ref _presentRequested, 1);
_timeline?.Event("adv-text-layout-publish", new() { ["layout"] = layoutSlot });
}
public void PublishAdvTextLayout(
GfxState gfx, AdvTextLayoutPresentationBinding binding)
{
lock (_textLock)
{
if (_retainedTextLayouts.TryGetValue(
binding.LayoutSlot, out RetainedAdvTextLayoutPresentation? presentation)
&& presentation.Binding == binding)
presentation.Republish(gfx);
if (_retainedWaitIndicators.TryGetValue(
binding.LayoutSlot,
out RetainedAdvWaitIndicatorPresentation? indicator))
indicator.Republish(
gfx,
_clock.NowMs - _waitIndicatorStartedMs,
WaitIndicatorShouldBeVisible(binding.LayoutSlot));
}
PublishAdvTextLayout(binding.LayoutSlot);
}
private bool WaitIndicatorShouldBeVisible(int layoutSlot)
=> IsWaiting
&& !_advPagePresentationSuspended
&& _waitIndicatorEnabled
&& _activeWaitLayout == layoutSlot;
private void UpdateRetainedWaitIndicator(GfxState gfx)
{
lock (_textLock)
{
foreach ((int layoutSlot, RetainedAdvWaitIndicatorPresentation presentation)
in _retainedWaitIndicators)
presentation.Update(
gfx,
_clock.NowMs - _waitIndicatorStartedMs,
WaitIndicatorShouldBeVisible(layoutSlot));
}
}
}

View File

@@ -22,7 +22,7 @@ public readonly record struct BackbufferPublicationPolicy(
bool PreserveExistingPixels, bool PreserveExistingPixels,
bool AppendGpuLayers); bool AppendGpuLayers);
public sealed class GodotAdvHost : IHost public sealed partial class GodotAdvHost : IHost
{ {
private readonly Main _main; private readonly Main _main;
private readonly ResourceMap _res; private readonly ResourceMap _res;
@@ -64,39 +64,6 @@ public sealed class GodotAdvHost : IHost
private readonly PageLocatorState _locator; private readonly PageLocatorState _locator;
private readonly System.Threading.AutoResetEvent _frameSignal = new(false); private readonly System.Threading.AutoResetEvent _frameSignal = new(false);
private volatile bool _stopping; private volatile bool _stopping;
private readonly object _textLock = new();
private readonly CachedGlyphMaskRasterizer _surfaceTextMaskCache;
private readonly ImmediateSurfaceTextRenderer _surfaceTextPixelRenderer;
private readonly RetainedGlyphLayoutEngine _retainedGlyphLayoutEngine;
private readonly GlyphRasterizerBackendInfo _surfaceTextBackendInfo;
private readonly HashSet<int> _retainedHistoryLayouts = new();
private sealed class LiveTextState
{
public required AdvLiveTextRun Run;
public required long StartedMs;
public required int GlyphDelayMilliseconds;
public int FirstGlyphIndex;
public int GlyphCount;
}
private readonly List<LiveTextState> _liveText = new();
private readonly Dictionary<int, RetainedAdvTextLayoutPresentation>
_retainedTextLayouts = new();
private LiveTextState? _activeLiveText;
private int _suspendedRetainedTextLayoutSlot;
private string _advText = "";
private int _advTextX = 100, _advTextY = 47;
private int _currentAdvLayout = 1; // SYSTEM4's ordinary SC0000 ADV layout
private long _advTextStartedMs;
private int _activeGlyphDelayMilliseconds = 50;
private int _messageGlyphDelayMilliseconds = 50;
private volatile int _messageWindowAlphaSetting;
private bool _advTextForceComplete;
private readonly Dictionary<int, AdvWaitIndicatorConfig> _waitIndicators = new();
private readonly Dictionary<int, (
AdvTextLayoutPresentationBinding Binding,
AdvTextLayoutSnapshot Layout)> _waitIndicatorBindings = new();
private readonly Dictionary<int, RetainedAdvWaitIndicatorPresentation>
_retainedWaitIndicators = new();
private readonly object _messageSkipLock = new(); private readonly object _messageSkipLock = new();
private bool _scriptMessageSkipActive; private bool _scriptMessageSkipActive;
private bool _physicalMessageSkipActive; private bool _physicalMessageSkipActive;
@@ -105,9 +72,6 @@ public sealed class GodotAdvHost : IHost
private (AudioPayload Audio, int PlaybackVariant)? _queuedSkippedVoice; private (AudioPayload Audio, int PlaybackVariant)? _queuedSkippedVoice;
private readonly object _scheduledVoiceLock = new(); private readonly object _scheduledVoiceLock = new();
private (AudioPayload Audio, int PlaybackVariant, uint DelayMs, uint? StartMs)? _scheduledVoice; private (AudioPayload Audio, int PlaybackVariant, uint DelayMs, uint? StartMs)? _scheduledVoice;
private int _activeWaitLayout;
private long _waitIndicatorStartedMs;
private bool _waitIndicatorEnabled;
private volatile bool _advPagePresentationSuspended; private volatile bool _advPagePresentationSuspended;
private volatile bool _modalMovieWaiting; private volatile bool _modalMovieWaiting;
private volatile bool _modalMovieCancelled; private volatile bool _modalMovieCancelled;
@@ -123,13 +87,10 @@ public sealed class GodotAdvHost : IHost
public volatile bool IsWaiting; public volatile bool IsWaiting;
public volatile bool IsTransitionWaiting; public volatile bool IsTransitionWaiting;
public volatile bool IsSleeping; public volatile bool IsSleeping;
public volatile bool IsTextRevealing;
public bool IsModalMovieWaiting => _modalMovieWaiting; public bool IsModalMovieWaiting => _modalMovieWaiting;
private int _presentRequested = 1; private int _presentRequested = 1;
private long _transitionStartedAtMs = -1; private long _transitionStartedAtMs = -1;
public long TransitionStartedAtMs => System.Threading.Interlocked.Read(ref _transitionStartedAtMs); public long TransitionStartedAtMs => System.Threading.Interlocked.Read(ref _transitionStartedAtMs);
public readonly List<(int Offset, string Text)> Captured = new();
public GodotAdvHost(Main main, ResourceMap res, string scene, Age.Engine.Hosting.FrameClock clock, public GodotAdvHost(Main main, ResourceMap res, string scene, Age.Engine.Hosting.FrameClock clock,
PageLocatorState locator, Sys4LogicalCanvas logicalCanvas, PageLocatorState locator, Sys4LogicalCanvas logicalCanvas,
IGlyphMaskRasterizer surfaceTextRasterizer, IGlyphMaskRasterizer surfaceTextRasterizer,
@@ -159,15 +120,6 @@ public sealed class GodotAdvHost : IHost
} }
public Sys4LogicalCanvas LogicalCanvas => new(_screenWidth, _screenHeight); public Sys4LogicalCanvas LogicalCanvas => new(_screenWidth, _screenHeight);
public bool UsesSurfaceTextPixels => true;
public GlyphRasterizerBackendInfo SurfaceTextBackendInfo => _surfaceTextBackendInfo;
public (int Count, int Capacity, long Hits, long Misses) SurfaceTextMaskCacheStats
=> (
_surfaceTextMaskCache.Count,
_surfaceTextMaskCache.Capacity,
_surfaceTextMaskCache.Hits,
_surfaceTextMaskCache.Misses);
public void ReportWarning(string message) => System.Console.Error.WriteLine(message); public void ReportWarning(string message) => System.Console.Error.WriteLine(message);
public void ShowDiagnosticMessage(DiagnosticMessage message) public void ShowDiagnosticMessage(DiagnosticMessage message)
@@ -246,433 +198,6 @@ public sealed class GodotAdvHost : IHost
_presentationRequestConsumed.WaitOne(50); _presentationRequestConsumed.WaitOne(50);
} }
public int MessageGlyphDelayMilliseconds
{
get
{
lock (_textLock) return _messageGlyphDelayMilliseconds;
}
}
public void SetMessageGlyphDelayMilliseconds(int milliseconds)
{
lock (_textLock) _messageGlyphDelayMilliseconds = System.Math.Max(0, milliseconds);
_timeline?.Event("message-glyph-delay", new() { ["delay_ms"] = milliseconds });
}
public void ShowText(int offset, string text)
=> throw new NotSupportedException(
"Godot gameplay text requires a retained layout binding.");
public void ShowText(AdvLiveTextRun run, int glyphDelayMilliseconds)
=> throw new NotSupportedException(
"Godot gameplay text requires a retained layout binding.");
public AdvRetainedTextRunResult? ShowText(
GfxState gfx,
AdvTextLayoutPresentationBinding binding,
AdvLiveTextRun run,
int glyphDelayMilliseconds)
{
if (binding.LayoutSlot != run.Layout.Slot
|| binding.FirstObjectHandle < 0
|| binding.ObjectCapacity <= 0)
throw new InvalidOperationException(
$"ADV layout {run.Layout.Slot} has no retained presentation binding.");
PrepareRetainedTextRun(
gfx, binding, run,
out RetainedAdvTextLayoutPresentation? presentation,
out AdvRetainedTextRunResult result);
Captured.Add((run.SourceOffset, run.Text));
_locator.Text(run.SourceOffset, run.Text);
int delay = System.Math.Max(0, glyphDelayMilliseconds);
int revealGlyphCount = System.Math.Max(
0, System.Math.Min(
result.GlyphCount,
presentation.PublishableGlyphCount - result.FirstGlyphIndex));
var state = new LiveTextState
{
Run = run,
StartedMs = _clock.NowMs,
GlyphDelayMilliseconds = delay,
FirstGlyphIndex = result.FirstGlyphIndex,
GlyphCount = revealGlyphCount,
};
lock (_textLock)
{
_liveText.Add(state);
_activeLiveText = state;
_advText = run.Text;
_advTextX = run.Layout.CursorX;
_advTextY = run.Layout.CursorY;
_currentAdvLayout = run.Layout.Slot;
_advTextStartedMs = state.StartedMs;
_activeGlyphDelayMilliseconds = delay;
_advTextForceComplete = _messageSkipActive;
IsTextRevealing =
revealGlyphCount > 0 && delay > 0 && !_messageSkipActive;
}
_timeline?.State("text-reveal", new()
{
["offset"] = $"0x{run.SourceOffset:x}",
["layout"] = run.Layout.Slot,
["x"] = run.Layout.OriginX + run.Layout.CursorX,
["y"] = run.Layout.OriginY + run.Layout.CursorY,
["glyphs"] = revealGlyphCount,
["records"] = result.GlyphCount,
["delay_ms"] = delay,
["presentation"] = "retained-glyphs",
});
int initiallyVisible = IsTextRevealing ? 1 : revealGlyphCount;
presentation.PublishThrough(
gfx, checked(result.FirstGlyphIndex + initiallyVisible));
Interlocked.Exchange(ref _presentRequested, 1);
if (!IsTextRevealing)
{
_timeline?.State("running", new() { ["text_reveal_complete"] = true });
return result;
}
bool scriptSuspended = SuspendScriptForPresentation();
try
{
RequestSynchronizedPresentation();
while (IsTextRevealing && !_stopping)
{
int visible;
lock (_textLock)
{
visible = _advTextForceComplete
? revealGlyphCount
: (int)System.Math.Clamp(
(_clock.NowMs - state.StartedMs) / delay + 1,
0, revealGlyphCount);
if (visible >= revealGlyphCount) IsTextRevealing = false;
}
int before = presentation.PublishedGlyphCount;
presentation.PublishThrough(
gfx, checked(result.FirstGlyphIndex + visible));
if (presentation.PublishedGlyphCount != before)
{
Interlocked.Exchange(ref _presentRequested, 1);
RequestSynchronizedPresentation();
}
if (IsTextRevealing) _frameSignal.WaitOne(50);
}
}
finally
{
ResumeScriptAfterPresentation(scriptSuspended);
}
_timeline?.State("running", new() { ["text_reveal_complete"] = true });
return result;
}
private void PrepareRetainedTextRun(
GfxState gfx,
AdvTextLayoutPresentationBinding binding,
AdvLiveTextRun run,
out RetainedAdvTextLayoutPresentation presentation,
out AdvRetainedTextRunResult result)
{
presentation = null!;
result = default;
if (run.Layout.Width <= 0 || run.Layout.Height <= 0)
throw new InvalidOperationException(
$"ADV layout {run.Layout.Slot} has invalid dimensions " +
$"{run.Layout.Width}x{run.Layout.Height}.");
RgbaImage destination = ResolveSurfacePixels(binding.SourceSurfaceSlot)
?? new RgbaImage(
run.Layout.Width,
run.Layout.Height,
new byte[checked(run.Layout.Width * run.Layout.Height * 4)]);
if (destination.Width != run.Layout.Width
|| destination.Height != run.Layout.Height)
throw new InvalidOperationException(
$"ADV layout {run.Layout.Slot} surface {binding.SourceSurfaceSlot} is " +
$"{destination.Width}x{destination.Height}; expected " +
$"{run.Layout.Width}x{run.Layout.Height}.");
var updated = new RgbaImage(
destination.Width, destination.Height, (byte[])destination.Pixels.Clone());
IReadOnlyList<GlyphRasterRequest> requests =
ImmediateSurfaceTextRenderer.CreateRequests(
run.Text, run.Style, _surfaceTextBackendInfo.Policy);
GlyphTextLayoutResult rendered = _retainedGlyphLayoutEngine.Render(
updated,
new GlyphTextLayoutOptions(
run.Layout.CursorX,
run.Layout.CursorY,
binding.ResetCursorX,
run.Layout.Right,
run.Layout.Bottom,
WrapHorizontally: true,
run.Style),
requests);
lock (_textLock)
{
if (!_retainedTextLayouts.TryGetValue(
binding.LayoutSlot, out presentation!))
{
presentation = new RetainedAdvTextLayoutPresentation(binding);
_retainedTextLayouts.Add(binding.LayoutSlot, presentation);
}
else if (presentation.Binding != binding)
throw new InvalidOperationException(
$"ADV layout {binding.LayoutSlot} changed its retained binding without reset.");
int first = presentation.Append(
rendered.Records,
rendered.PresentationRects,
run.Layout.OriginX,
run.Layout.OriginY);
result = new AdvRetainedTextRunResult(
binding.LayoutSlot,
first,
rendered.ConsumedGlyphs,
rendered.CursorX,
rendered.CursorY);
}
lock (_imageLock)
_surfaceImages[binding.SourceSurfaceSlot] = updated;
_slotDims[binding.SourceSurfaceSlot] = (updated.Width, updated.Height);
gfx.CreateSurface(binding.SourceSurfaceSlot);
}
public void SetAdvTextCursor(int layoutSlot, int x, int y)
{
lock (_textLock)
{
if (layoutSlot != 0) _currentAdvLayout = layoutSlot;
_advTextX = x;
_advTextY = y;
}
_timeline?.Event("text-cursor", new() { ["slot"] = layoutSlot, ["x"] = x, ["y"] = y });
}
public void DrawStringToSurface(int surfaceSlot, int x, int y, string text)
=> DrawStringToSurface(surfaceSlot, x, y, text, AdvTextStyle.Default);
public void DrawStringToSurface(int surfaceSlot, int x, int y, string text, AdvTextStyle style)
{
DrawStringPixels(surfaceSlot, x, y, text, style);
_timeline?.Event("draw-string", new()
{
["surface"] = surfaceSlot,
["x"] = x,
["y"] = y,
["text"] = text,
["presentation"] = "rgba-glyph-mask",
["backend"] = _surfaceTextBackendInfo.Id,
});
}
private void DrawStringPixels(
int surfaceSlot, int x, int y, string text, AdvTextStyle style)
{
RgbaImage? destination = ResolveSurfacePixels(surfaceSlot);
if (destination == null
&& _slotDims.TryGetValue(surfaceSlot, out var dimensions)
&& dimensions.W > 0
&& dimensions.H > 0)
destination = new RgbaImage(
dimensions.W, dimensions.H,
new byte[checked(dimensions.W * dimensions.H * 4)]);
if (destination == null || destination.Width <= 0 || destination.Height <= 0)
throw new InvalidOperationException(
$"Surface {surfaceSlot} has no rasterizable pixel allocation.");
var updated = new RgbaImage(
destination.Width, destination.Height, (byte[])destination.Pixels.Clone());
_surfaceTextPixelRenderer.Render(updated, x, y, text, style);
lock (_imageLock) _surfaceImages[surfaceSlot] = updated;
System.Threading.Interlocked.Exchange(ref _presentRequested, 1);
}
public (string Text, int X, int Y, int VisibleGlyphs, bool Revealing) SnapshotAdvText()
{
lock (_textLock)
{
int visible = _advTextForceComplete || !IsTextRevealing
? _advText.Length
: (int)System.Math.Clamp(
(_clock.NowMs - _advTextStartedMs) / System.Math.Max(1, _activeGlyphDelayMilliseconds) + 1,
0, _advText.Length);
return (_advText, _advTextX, _advTextY, visible, IsTextRevealing);
}
}
/// <summary>
/// Layout that owns the ordinary retained ADV page. Nested callback scripts such as HISTORY can select
/// and mutate other layouts while the parent wait remains parked; those transient selections must not
/// move the parent page when its retained range is restored.
/// </summary>
public int AdvPageLayoutSlot
{
get
{
lock (_textLock)
return IsWaiting && _activeWaitLayout != 0 ? _activeWaitLayout : _currentAdvLayout;
}
}
public void ClearRenderedAdvTextLayout(int layoutSlot)
{
lock (_textLock)
{
int slot = layoutSlot == 0 ? _currentAdvLayout : layoutSlot;
if (_retainedTextLayouts.ContainsKey(slot)) return;
_liveText.RemoveAll(state => state.Run.Layout.Slot == slot);
if (_activeLiveText?.Run.Layout.Slot == slot)
{
_activeLiveText = null;
_advText = "";
_advTextForceComplete = false;
IsTextRevealing = false;
}
}
}
public void ResetRenderedAdvTextLayout(
GfxState gfx, AdvTextLayoutPresentationBinding binding)
{
lock (_textLock)
{
_retainedHistoryLayouts.Remove(binding.LayoutSlot);
_retainedTextLayouts.Remove(binding.LayoutSlot);
_liveText.RemoveAll(
state => state.Run.Layout.Slot == binding.LayoutSlot);
if (_activeLiveText?.Run.Layout.Slot == binding.LayoutSlot)
{
_activeLiveText = null;
_advText = "";
_advTextForceComplete = false;
IsTextRevealing = false;
}
if (_suspendedRetainedTextLayoutSlot == binding.LayoutSlot)
_suspendedRetainedTextLayoutSlot = 0;
}
if (_slotDims.TryGetValue(
binding.SourceSurfaceSlot, out var dimensions)
&& dimensions.W > 0
&& dimensions.H > 0)
{
lock (_imageLock)
_surfaceImages[binding.SourceSurfaceSlot] = new RgbaImage(
dimensions.W,
dimensions.H,
new byte[checked(dimensions.W * dimensions.H * 4)]);
}
Interlocked.Exchange(ref _presentRequested, 1);
_timeline?.Event("adv-text-layout-reset", new()
{
["layout"] = binding.LayoutSlot,
["surface"] = binding.SourceSurfaceSlot,
["first_handle"] = binding.FirstObjectHandle,
["capacity"] = binding.ObjectCapacity,
});
}
public bool RenderTextHistory(
GfxState gfx,
AdvTextLayoutPresentationBinding binding,
AdvTextHistoryRenderBatch batch)
{
var run = new AdvLiveTextRun(
batch.FirstRecordIndex,
batch.Layout,
batch.Style,
batch.Text,
Array.Empty<string>());
if (binding.LayoutSlot != batch.LayoutSlot
|| binding.FirstObjectHandle < 0
|| binding.ObjectCapacity <= 0)
throw new InvalidOperationException(
$"History layout {batch.LayoutSlot} has no retained presentation binding.");
PrepareRetainedTextRun(
gfx, binding, run,
out RetainedAdvTextLayoutPresentation? presentation,
out AdvRetainedTextRunResult result);
presentation.PublishThrough(
gfx,
checked(result.FirstGlyphIndex + result.GlyphCount));
lock (_textLock)
{
_retainedHistoryLayouts.Add(batch.LayoutSlot);
}
Interlocked.Exchange(ref _presentRequested, 1);
_timeline?.Event("history-render", new()
{
["layout"] = batch.LayoutSlot,
["record"] = batch.FirstRecordIndex,
["x"] = batch.Layout.OriginX + batch.Layout.CursorX,
["y"] = batch.Layout.OriginY + batch.Layout.CursorY,
["text"] = batch.Text,
["glyphs"] = result.GlyphCount,
["presentation"] = "retained-glyphs",
});
return true;
}
public void EndTextHistoryPresentation(GfxState gfx)
{
RetainedAdvTextLayoutPresentation[] retained;
int[] surfaceSlots;
lock (_textLock)
{
retained = _retainedHistoryLayouts
.Select(slot => _retainedTextLayouts.GetValueOrDefault(slot))
.Where(presentation => presentation != null)
.Cast<RetainedAdvTextLayoutPresentation>()
.ToArray();
surfaceSlots = retained
.Select(presentation => presentation.Binding.SourceSurfaceSlot)
.Distinct()
.ToArray();
foreach (int slot in _retainedHistoryLayouts)
_retainedTextLayouts.Remove(slot);
_retainedHistoryLayouts.Clear();
}
foreach (RetainedAdvTextLayoutPresentation presentation in retained)
presentation.ErasePublished(gfx);
foreach (int surfaceSlot in surfaceSlots)
ClearAllocatedSurfacePixels(surfaceSlot);
Interlocked.Exchange(ref _presentRequested, 1);
_timeline?.Event("history-presentation-end", new()
{
["retained_layouts"] = retained.Length,
});
}
private void ClearAllocatedSurfacePixels(int surfaceSlot)
{
if (!_slotDims.TryGetValue(surfaceSlot, out var dimensions)
|| dimensions.W <= 0
|| dimensions.H <= 0)
return;
lock (_imageLock)
_surfaceImages[surfaceSlot] = new RgbaImage(
dimensions.W,
dimensions.H,
new byte[checked(dimensions.W * dimensions.H * 4)]);
}
public int MessageWindowAlphaSetting => _messageWindowAlphaSetting;
public void SetMessageWindowAlphaSetting(int value)
{
_messageWindowAlphaSetting = value;
_timeline?.Event("message-window-alpha", new() { ["value"] = value });
}
public void FillSurfaceRect(SurfaceRectFill fill) public void FillSurfaceRect(SurfaceRectFill fill)
{ {
RgbaImage? destination = ResolveSurfacePixels(fill.SurfaceSlot); RgbaImage? destination = ResolveSurfacePixels(fill.SurfaceSlot);
@@ -756,116 +281,6 @@ public sealed class GodotAdvHost : IHost
return new BackbufferPublicationPolicy(preserveExistingPixels, appendGpuLayers); return new BackbufferPublicationPolicy(preserveExistingPixels, appendGpuLayers);
} }
public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config)
{
lock (_textLock)
{
_waitIndicators[config.LayoutSlot] = config;
RebuildRetainedWaitIndicator(config.LayoutSlot);
}
_timeline?.Event("wait-indicator-config", new()
{
["layout"] = config.LayoutSlot, ["x"] = config.X, ["y"] = config.Y,
["surface"] = config.SurfaceSlot, ["cell_w"] = config.CellWidth,
["cell_h"] = config.CellHeight, ["terminal_frame"] = config.TerminalFrame,
["period_ms"] = config.FramePeriodMs,
});
}
public void BindAdvWaitIndicator(
AdvTextLayoutPresentationBinding binding,
AdvTextLayoutSnapshot layout)
{
lock (_textLock)
{
_waitIndicatorBindings[binding.LayoutSlot] = (binding, layout);
RebuildRetainedWaitIndicator(binding.LayoutSlot);
}
_timeline?.Event("wait-indicator-bind", new()
{
["layout"] = binding.LayoutSlot,
["handle"] = binding.WaitIndicatorObjectHandle,
["origin_x"] = layout.OriginX,
["origin_y"] = layout.OriginY,
});
}
private void RebuildRetainedWaitIndicator(int layoutSlot)
{
if (!_waitIndicators.TryGetValue(layoutSlot, out AdvWaitIndicatorConfig config)
|| !_waitIndicatorBindings.TryGetValue(
layoutSlot,
out (AdvTextLayoutPresentationBinding Binding, AdvTextLayoutSnapshot Layout) retained)
|| retained.Binding.WaitIndicatorObjectHandle < 0)
{
_retainedWaitIndicators.Remove(layoutSlot);
return;
}
_retainedWaitIndicators[layoutSlot] =
new RetainedAdvWaitIndicatorPresentation(
config,
retained.Binding,
retained.Layout.OriginX,
retained.Layout.OriginY);
}
public void SetAdvWaitIndicatorEnabled(bool enabled)
{
lock (_textLock)
{
if (enabled && !_waitIndicatorEnabled) _waitIndicatorStartedMs = _clock.NowMs;
_waitIndicatorEnabled = enabled;
}
Interlocked.Exchange(ref _presentRequested, 1);
_timeline?.Event("wait-indicator-enabled", new() { ["enabled"] = enabled });
}
public void PublishAdvTextLayout(int layoutSlot)
{
System.Threading.Interlocked.Exchange(ref _presentRequested, 1);
_timeline?.Event("adv-text-layout-publish", new() { ["layout"] = layoutSlot });
}
public void PublishAdvTextLayout(
GfxState gfx, AdvTextLayoutPresentationBinding binding)
{
lock (_textLock)
{
if (_retainedTextLayouts.TryGetValue(
binding.LayoutSlot, out RetainedAdvTextLayoutPresentation? presentation)
&& presentation.Binding == binding)
presentation.Republish(gfx);
if (_retainedWaitIndicators.TryGetValue(
binding.LayoutSlot,
out RetainedAdvWaitIndicatorPresentation? indicator))
indicator.Republish(
gfx,
_clock.NowMs - _waitIndicatorStartedMs,
WaitIndicatorShouldBeVisible(binding.LayoutSlot));
}
PublishAdvTextLayout(binding.LayoutSlot);
}
private bool WaitIndicatorShouldBeVisible(int layoutSlot)
=> IsWaiting
&& !_advPagePresentationSuspended
&& _waitIndicatorEnabled
&& _activeWaitLayout == layoutSlot;
private void UpdateRetainedWaitIndicator(GfxState gfx)
{
lock (_textLock)
{
foreach ((int layoutSlot, RetainedAdvWaitIndicatorPresentation presentation)
in _retainedWaitIndicators)
presentation.Update(
gfx,
_clock.NowMs - _waitIndicatorStartedMs,
WaitIndicatorShouldBeVisible(layoutSlot));
}
}
public volatile int Pages; // VM-thread page counter (incremented before IsWaiting so shot-gating can't race) public volatile int Pages; // VM-thread page counter (incremented before IsWaiting so shot-gating can't race)
public void WaitForInput() => WaitForInput(0); public void WaitForInput() => WaitForInput(0);