Split Godot host ADV text into partial class
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
597
godot/GodotAdvHost.AdvText.cs
Normal file
597
godot/GodotAdvHost.AdvText.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user