Split Godot host ADV text into partial class
This commit is contained in:
@@ -22,7 +22,7 @@ public readonly record struct BackbufferPublicationPolicy(
|
||||
bool PreserveExistingPixels,
|
||||
bool AppendGpuLayers);
|
||||
|
||||
public sealed class GodotAdvHost : IHost
|
||||
public sealed partial class GodotAdvHost : IHost
|
||||
{
|
||||
private readonly Main _main;
|
||||
private readonly ResourceMap _res;
|
||||
@@ -64,39 +64,6 @@ public sealed class GodotAdvHost : IHost
|
||||
private readonly PageLocatorState _locator;
|
||||
private readonly System.Threading.AutoResetEvent _frameSignal = new(false);
|
||||
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 bool _scriptMessageSkipActive;
|
||||
private bool _physicalMessageSkipActive;
|
||||
@@ -105,9 +72,6 @@ public sealed class GodotAdvHost : IHost
|
||||
private (AudioPayload Audio, int PlaybackVariant)? _queuedSkippedVoice;
|
||||
private readonly object _scheduledVoiceLock = new();
|
||||
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 _modalMovieWaiting;
|
||||
private volatile bool _modalMovieCancelled;
|
||||
@@ -123,13 +87,10 @@ public sealed class GodotAdvHost : IHost
|
||||
public volatile bool IsWaiting;
|
||||
public volatile bool IsTransitionWaiting;
|
||||
public volatile bool IsSleeping;
|
||||
public volatile bool IsTextRevealing;
|
||||
public bool IsModalMovieWaiting => _modalMovieWaiting;
|
||||
private int _presentRequested = 1;
|
||||
private long _transitionStartedAtMs = -1;
|
||||
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,
|
||||
PageLocatorState locator, Sys4LogicalCanvas logicalCanvas,
|
||||
IGlyphMaskRasterizer surfaceTextRasterizer,
|
||||
@@ -159,15 +120,6 @@ public sealed class GodotAdvHost : IHost
|
||||
}
|
||||
|
||||
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 ShowDiagnosticMessage(DiagnosticMessage message)
|
||||
@@ -246,433 +198,6 @@ public sealed class GodotAdvHost : IHost
|
||||
_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)
|
||||
{
|
||||
RgbaImage? destination = ResolveSurfacePixels(fill.SurfaceSlot);
|
||||
@@ -756,116 +281,6 @@ public sealed class GodotAdvHost : IHost
|
||||
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 void WaitForInput() => WaitForInput(0);
|
||||
|
||||
Reference in New Issue
Block a user