From 1ffd8a08536fc4a495a4227e6ec935d94c97c1d4 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sun, 2 Aug 2026 19:11:34 -0400 Subject: [PATCH] Split Godot host ADV text into partial class --- docs/PROJECT-STRUCTURE.md | 4 + docs/remake-architecture-and-roadmap.md | 12 +- godot/GodotAdvHost.AdvText.cs | 597 ++++++++++++++++++++++++ godot/GodotAdvHost.cs | 587 +---------------------- 4 files changed, 611 insertions(+), 589 deletions(-) create mode 100644 godot/GodotAdvHost.AdvText.cs diff --git a/docs/PROJECT-STRUCTURE.md b/docs/PROJECT-STRUCTURE.md index 7be890a..6941b0e 100644 --- a/docs/PROJECT-STRUCTURE.md +++ b/docs/PROJECT-STRUCTURE.md @@ -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 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-.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 have no repository output tree and write their automatic maps below `user://diagnostics/page-maps` instead. diff --git a/docs/remake-architecture-and-roadmap.md b/docs/remake-architecture-and-roadmap.md index e5333d4..cd3b377 100644 --- a/docs/remake-architecture-and-roadmap.md +++ b/docs/remake-architecture-and-roadmap.md @@ -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 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 deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after each domain move. @@ -939,8 +945,8 @@ layer's rendering diverges from ADV; save layout. ## 8. Immediate next step 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 -`GodotAdvHost` with its ADV-text surface, then continue one existing domain at a time while preserving public -types, commands, and generated output. +by the tracked launcher and layered validation driver. With the planned `Main` domains and the first +`GodotAdvHost` ADV-text surface isolated, move host presentation/input next, then continue one existing domain +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 not replace Phase B gameplay validation or the open cross-platform gates. diff --git a/godot/GodotAdvHost.AdvText.cs b/godot/GodotAdvHost.AdvText.cs new file mode 100644 index 0000000..68f177a --- /dev/null +++ b/godot/GodotAdvHost.AdvText.cs @@ -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 _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 _liveText = new(); + private readonly Dictionary + _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 _waitIndicators = new(); + private readonly Dictionary _waitIndicatorBindings = new(); + private readonly Dictionary + _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 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); + } + } + + /// + /// 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. + /// + 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()); + 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() + .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)); + } + } + +} diff --git a/godot/GodotAdvHost.cs b/godot/GodotAdvHost.cs index c945d46..8e247f5 100644 --- a/godot/GodotAdvHost.cs +++ b/godot/GodotAdvHost.cs @@ -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 _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 _liveText = new(); - private readonly Dictionary - _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 _waitIndicators = new(); - private readonly Dictionary _waitIndicatorBindings = new(); - private readonly Dictionary - _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 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); - } - } - - /// - /// 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. - /// - 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()); - 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() - .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);