engine: retain History and wait presentation
This commit is contained in:
@@ -73,6 +73,7 @@ public sealed class GodotAdvHost : IHost
|
||||
private string _surfaceTextFallbackReason;
|
||||
private bool _surfaceTextFallbackWarningReported;
|
||||
private readonly Dictionary<int, AdvTextHistoryRenderBatch> _historyText = new();
|
||||
private readonly HashSet<int> _retainedHistoryLayouts = new();
|
||||
private sealed class LiveTextState
|
||||
{
|
||||
public required AdvLiveTextRun Run;
|
||||
@@ -96,6 +97,11 @@ public sealed class GodotAdvHost : IHost
|
||||
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;
|
||||
@@ -728,6 +734,7 @@ public sealed class GodotAdvHost : IHost
|
||||
lock (_textLock)
|
||||
{
|
||||
_historyText.Remove(binding.LayoutSlot);
|
||||
_retainedHistoryLayouts.Remove(binding.LayoutSlot);
|
||||
_retainedTextLayouts.Remove(binding.LayoutSlot);
|
||||
_liveText.RemoveAll(
|
||||
state => state.Run.Layout.Slot == binding.LayoutSlot);
|
||||
@@ -775,17 +782,112 @@ public sealed class GodotAdvHost : IHost
|
||||
});
|
||||
}
|
||||
|
||||
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 (_retainedGlyphLayoutEngine == null
|
||||
|| binding.LayoutSlot != batch.LayoutSlot
|
||||
|| binding.FirstObjectHandle < 0
|
||||
|| binding.ObjectCapacity <= 0
|
||||
|| !TryPrepareRetainedTextRun(
|
||||
gfx, binding, run,
|
||||
out RetainedAdvTextLayoutPresentation? presentation,
|
||||
out AdvRetainedTextRunResult result))
|
||||
{
|
||||
RenderTextHistory(batch);
|
||||
return false;
|
||||
}
|
||||
|
||||
presentation.PublishThrough(
|
||||
gfx,
|
||||
checked(result.FirstGlyphIndex + result.GlyphCount));
|
||||
lock (_textLock)
|
||||
{
|
||||
_historyText.Remove(batch.LayoutSlot);
|
||||
_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()
|
||||
{
|
||||
lock (_textLock) _historyText.Clear();
|
||||
lock (_textLock)
|
||||
{
|
||||
_historyText.Clear();
|
||||
_retainedHistoryLayouts.Clear();
|
||||
}
|
||||
_timeline?.Event("history-presentation-end");
|
||||
}
|
||||
|
||||
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();
|
||||
_historyText.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,
|
||||
});
|
||||
}
|
||||
|
||||
public IReadOnlyList<AdvTextHistoryRenderBatch> SnapshotRenderedTextHistory()
|
||||
{
|
||||
lock (_textLock) return _historyText.Values.OrderBy(batch => batch.LayoutSlot).ToArray();
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -890,7 +992,11 @@ public sealed class GodotAdvHost : IHost
|
||||
|
||||
public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config)
|
||||
{
|
||||
lock (_textLock) _waitIndicators[config.LayoutSlot] = 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,
|
||||
@@ -900,6 +1006,44 @@ public sealed class GodotAdvHost : IHost
|
||||
});
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -907,6 +1051,7 @@ public sealed class GodotAdvHost : IHost
|
||||
if (enabled && !_waitIndicatorEnabled) _waitIndicatorStartedMs = _clock.NowMs;
|
||||
_waitIndicatorEnabled = enabled;
|
||||
}
|
||||
Interlocked.Exchange(ref _presentRequested, 1);
|
||||
_timeline?.Event("wait-indicator-enabled", new() { ["enabled"] = enabled });
|
||||
}
|
||||
|
||||
@@ -920,29 +1065,39 @@ public sealed class GodotAdvHost : IHost
|
||||
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);
|
||||
}
|
||||
|
||||
public AdvWaitIndicatorSnapshot? SnapshotAdvWaitIndicator()
|
||||
private bool WaitIndicatorShouldBeVisible(int layoutSlot)
|
||||
=> IsWaiting
|
||||
&& !_advPagePresentationSuspended
|
||||
&& _waitIndicatorEnabled
|
||||
&& _activeWaitLayout == layoutSlot;
|
||||
|
||||
private void UpdateRetainedWaitIndicator(GfxState gfx)
|
||||
{
|
||||
if (!IsWaiting || _advPagePresentationSuspended) return null;
|
||||
AdvWaitIndicatorConfig config;
|
||||
long resourceId;
|
||||
lock (_textLock)
|
||||
{
|
||||
if (!_waitIndicatorEnabled) return null;
|
||||
if (!_waitIndicators.TryGetValue(_activeWaitLayout, out config)) return null;
|
||||
if (!_surfaceResources.TryGetValue(config.SurfaceSlot, out resourceId)) return null;
|
||||
foreach ((int layoutSlot, RetainedAdvWaitIndicatorPresentation presentation)
|
||||
in _retainedWaitIndicators)
|
||||
presentation.Update(
|
||||
gfx,
|
||||
_clock.NowMs - _waitIndicatorStartedMs,
|
||||
WaitIndicatorShouldBeVisible(layoutSlot));
|
||||
}
|
||||
var asset = _res.ResolveTexture(resourceId);
|
||||
var image = asset != null ? Decode(asset) : null;
|
||||
if (asset == null || image == null || config.CellWidth <= 0 || config.CellHeight <= 0) return null;
|
||||
int frame = config.FrameAt(_clock.NowMs - _waitIndicatorStartedMs);
|
||||
return new AdvWaitIndicatorSnapshot(image, asset.Name, asset.PackedId, config, frame);
|
||||
}
|
||||
|
||||
public volatile int Pages; // VM-thread page counter (incremented before IsWaiting so shot-gating can't race)
|
||||
@@ -1029,6 +1184,7 @@ public sealed class GodotAdvHost : IHost
|
||||
}
|
||||
IsWaiting = false;
|
||||
lock (_textLock) _waitIndicatorEnabled = false;
|
||||
Interlocked.Exchange(ref _presentRequested, 1);
|
||||
_timeline?.State("running", new()
|
||||
{
|
||||
["input"] = messageSkipped ? "message-skip" : autoAdvanced ? "auto" : "user",
|
||||
@@ -1166,10 +1322,13 @@ public sealed class GodotAdvHost : IHost
|
||||
: _currentAdvLayout;
|
||||
if (_retainedTextLayouts.TryGetValue(
|
||||
slot, out RetainedAdvTextLayoutPresentation? presentation))
|
||||
{
|
||||
presentation.ErasePublished(gfx);
|
||||
if (_retainedWaitIndicators.TryGetValue(
|
||||
slot, out RetainedAdvWaitIndicatorPresentation? indicator))
|
||||
indicator.Erase(gfx);
|
||||
if (_retainedTextLayouts.ContainsKey(slot)
|
||||
|| _retainedWaitIndicators.ContainsKey(slot))
|
||||
_suspendedRetainedTextLayoutSlot = slot;
|
||||
}
|
||||
}
|
||||
else if (_suspendedRetainedTextLayoutSlot != 0)
|
||||
{
|
||||
@@ -1177,6 +1336,15 @@ public sealed class GodotAdvHost : IHost
|
||||
_suspendedRetainedTextLayoutSlot,
|
||||
out RetainedAdvTextLayoutPresentation? presentation))
|
||||
presentation.Republish(gfx);
|
||||
if (_retainedWaitIndicators.TryGetValue(
|
||||
_suspendedRetainedTextLayoutSlot,
|
||||
out RetainedAdvWaitIndicatorPresentation? indicator))
|
||||
indicator.Republish(
|
||||
gfx,
|
||||
_clock.NowMs - _waitIndicatorStartedMs,
|
||||
IsWaiting
|
||||
&& _waitIndicatorEnabled
|
||||
&& _activeWaitLayout == _suspendedRetainedTextLayoutSlot);
|
||||
_suspendedRetainedTextLayoutSlot = 0;
|
||||
}
|
||||
}
|
||||
@@ -1436,10 +1604,11 @@ public sealed class GodotAdvHost : IHost
|
||||
}
|
||||
|
||||
// Native retained-object writes are not front-buffer writes. Publish explicit/service-boundary dirtiness
|
||||
// once, then continue only while the sampled retained scene can actually change. Text reveal is a separate
|
||||
// Godot Label; waiting/sleeping alone do not alter background pixels.
|
||||
// once, then continue only while the sampled retained scene can actually change. The wait atlas advances
|
||||
// its ordinary retained source cell here so it contributes only when the frame actually changes.
|
||||
public HostPresentationReason ConsumePresentationReasons(GfxState gfx)
|
||||
{
|
||||
UpdateRetainedWaitIndicator(gfx);
|
||||
bool screenTransitionActive;
|
||||
lock (_screenTransitionLock) screenTransitionActive = _screenTransition != null;
|
||||
var reasons = HostPresentationReason.None;
|
||||
@@ -1489,6 +1658,7 @@ public sealed class GodotAdvHost : IHost
|
||||
_surfaceText.Clear();
|
||||
_surfaceResources.Clear();
|
||||
_historyText.Clear();
|
||||
_retainedHistoryLayouts.Clear();
|
||||
_liveText.Clear();
|
||||
_retainedTextLayouts.Clear();
|
||||
_activeLiveText = null;
|
||||
@@ -1498,6 +1668,8 @@ public sealed class GodotAdvHost : IHost
|
||||
_advTextY = 47;
|
||||
_advTextForceComplete = false;
|
||||
_waitIndicators.Clear();
|
||||
_waitIndicatorBindings.Clear();
|
||||
_retainedWaitIndicators.Clear();
|
||||
_activeWaitLayout = 0;
|
||||
_waitIndicatorEnabled = false;
|
||||
}
|
||||
@@ -1681,6 +1853,14 @@ public sealed class GodotAdvHost : IHost
|
||||
{
|
||||
_surfaceText.Remove(slot);
|
||||
_surfaceResources.Remove(slot);
|
||||
foreach (int layoutSlot in _retainedTextLayouts
|
||||
.Where(pair => pair.Value.Binding.SourceSurfaceSlot == slot)
|
||||
.Select(pair => pair.Key)
|
||||
.ToArray())
|
||||
{
|
||||
_retainedTextLayouts.Remove(layoutSlot);
|
||||
_retainedHistoryLayouts.Remove(layoutSlot);
|
||||
}
|
||||
}
|
||||
_slotDims[slot] = (image.Width, image.Height);
|
||||
return true;
|
||||
@@ -1974,6 +2154,14 @@ public sealed class GodotAdvHost : IHost
|
||||
{
|
||||
_surfaceText.Remove(slot);
|
||||
_surfaceResources.Remove(slot);
|
||||
foreach (int layoutSlot in _retainedTextLayouts
|
||||
.Where(pair => pair.Value.Binding.SourceSurfaceSlot == slot)
|
||||
.Select(pair => pair.Key)
|
||||
.ToArray())
|
||||
{
|
||||
_retainedTextLayouts.Remove(layoutSlot);
|
||||
_retainedHistoryLayouts.Remove(layoutSlot);
|
||||
}
|
||||
}
|
||||
_slotDims.Remove(slot);
|
||||
if (movieRelease.Kind == MovieSurfaceReleaseKind.Released)
|
||||
@@ -2138,6 +2326,8 @@ public sealed class GodotAdvHost : IHost
|
||||
.Select(pair => pair.Key)
|
||||
.ToArray())
|
||||
_retainedTextLayouts.Remove(layoutSlot);
|
||||
_retainedHistoryLayouts.RemoveWhere(layoutSlot =>
|
||||
!_retainedTextLayouts.ContainsKey(layoutSlot));
|
||||
}
|
||||
foreach (MovieSurfaceBinding binding in stoppedMovies)
|
||||
{
|
||||
@@ -2493,6 +2683,3 @@ internal sealed class LegacyScreenTransition
|
||||
DurationMs = durationMs;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct AdvWaitIndicatorSnapshot(
|
||||
RgbaImage Image, string Name, int AssetId, AdvWaitIndicatorConfig Config, int Frame);
|
||||
|
||||
121
godot/Main.cs
121
godot/Main.cs
@@ -37,10 +37,6 @@ public partial class Main : Godot.Control
|
||||
private GpuRetainedRenderer _gpuRenderer = null!;
|
||||
private bool _useGpuBackend = true;
|
||||
private ImageTexture? _ageCursorTexture;
|
||||
private TextureRect _waitIndicator = null!;
|
||||
private ImageTexture? _waitIndicatorSheet;
|
||||
private AtlasTexture? _waitIndicatorAtlas;
|
||||
private int _waitIndicatorAssetId = -1;
|
||||
// One managed composition target for the entire frame. Layer helpers mutate it in place; only the
|
||||
// completed frame crosses the Godot Image boundary, avoiding a full GetData/SetData round-trip per layer.
|
||||
private byte[] _screenPixels = [];
|
||||
@@ -195,17 +191,6 @@ public partial class Main : Godot.Control
|
||||
_screenView.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
||||
_gpuRenderer = new GpuRetainedRenderer(this);
|
||||
|
||||
// Native ADV wait marker: a tiny independently animated atlas region. Keeping it separate from the
|
||||
// logical software backbuffer avoids recompositing the entire retained scene throughout static waits.
|
||||
_waitIndicator = new TextureRect
|
||||
{
|
||||
Visible = false,
|
||||
ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize,
|
||||
StretchMode = TextureRect.StretchModeEnum.Keep,
|
||||
MouseFilter = MouseFilterEnum.Ignore,
|
||||
};
|
||||
AddChild(_waitIndicator);
|
||||
|
||||
_text = new Label { AutowrapMode = TextServer.AutowrapMode.WordSmart, MouseFilter = MouseFilterEnum.Ignore };
|
||||
AddChild(_text);
|
||||
_text.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
||||
@@ -471,6 +456,11 @@ public partial class Main : Godot.Control
|
||||
_vm.Gfx.SetSurface(0x0c, waitIndicator.PackedId, 0xff00);
|
||||
_host.ConfigureAdvWaitIndicator(new AdvWaitIndicatorConfig(
|
||||
1, 385, 140, 0x0c, 0, 0, 30, 27, 12, 48));
|
||||
AdvTextLayoutSnapshot waitLayout =
|
||||
_vm.TextHistory.GetLayoutSnapshot(1);
|
||||
_host.BindAdvWaitIndicator(
|
||||
_vm.TextHistory.GetPresentationBinding(1),
|
||||
waitLayout);
|
||||
}
|
||||
// --boot: run SYSTEM4's state prefix (INITCONFIG/INIT2/INIT) so the scene sees boot state — chiefly
|
||||
// INIT2's gfx handle array 0x62455.. (skips the UI scripts LOGO/OP/TITLE). State carries via globals.
|
||||
@@ -580,7 +570,6 @@ public partial class Main : Godot.Control
|
||||
phase = perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||
allocationPhase = perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
||||
if (!_selftest && _host != null) UpdateAdvTextPresentation();
|
||||
if (!_selftest && _host != null) UpdateAdvWaitIndicatorPresentation();
|
||||
if (!_selftest && _host != null) UpdateHistoryTextPresentation();
|
||||
perf?.RecordUiAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
||||
perf?.RecordUi(PerformanceFrameLog.Timestamp() - phase);
|
||||
@@ -1706,40 +1695,6 @@ public partial class Main : Godot.Control
|
||||
return new Color(((rgb >> 16) & 0xff) / 255f, ((rgb >> 8) & 0xff) / 255f, (rgb & 0xff) / 255f, 1);
|
||||
}
|
||||
|
||||
private void UpdateAdvWaitIndicatorPresentation()
|
||||
{
|
||||
// As with the ordinary dialogue Label, the enclosing page's separately animated marker must sit
|
||||
// out while HISTORY/HIDEWIN owns raw input; native retained composition naturally covers it.
|
||||
if (_vm.IsRawInputCallbackActive)
|
||||
{
|
||||
_waitIndicator.Visible = false;
|
||||
return;
|
||||
}
|
||||
var snapshot = _host.SnapshotAdvWaitIndicator();
|
||||
if (snapshot == null)
|
||||
{
|
||||
_waitIndicator.Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var s = snapshot.Value;
|
||||
if (_waitIndicatorAssetId != s.AssetId)
|
||||
{
|
||||
var image = Image.CreateFromData(s.Image.Width, s.Image.Height, false, Image.Format.Rgba8, s.Image.Pixels);
|
||||
_waitIndicatorSheet = ImageTexture.CreateFromImage(image);
|
||||
_waitIndicatorAtlas = new AtlasTexture { Atlas = _waitIndicatorSheet };
|
||||
_waitIndicator.Texture = _waitIndicatorAtlas;
|
||||
_waitIndicatorAssetId = s.AssetId;
|
||||
}
|
||||
|
||||
var c = s.Config;
|
||||
_waitIndicatorAtlas!.Region = new Rect2(
|
||||
c.SourceX + s.Frame * c.CellWidth, c.SourceY, c.CellWidth, c.CellHeight);
|
||||
_waitIndicator.Position = new Vector2(c.X, 430 + c.Y);
|
||||
_waitIndicator.Size = new Vector2(c.CellWidth, c.CellHeight);
|
||||
_waitIndicator.Visible = true;
|
||||
}
|
||||
|
||||
private static string ColorTimeline(Age.Engine.Model.ColorTransitionState? state)
|
||||
=> state is { } c
|
||||
? $" color=0x{c.Current:x8}->0x{c.Target:x8} colorProgress={c.Progress:0.000}"
|
||||
@@ -2610,6 +2565,69 @@ public partial class Main : Godot.Control
|
||||
&& !string.IsNullOrWhiteSpace(_host.SurfaceTextFallbackReason);
|
||||
liveRetainedTextMode = "label-fallback";
|
||||
}
|
||||
_vm.TextHistory.DefineLayout(2, 256, 64, 10, 100);
|
||||
_vm.TextHistory.SetResetCursor(2, 1, 1);
|
||||
_vm.TextHistory.SetBounds(2, 255, 63);
|
||||
_vm.TextHistory.SetTextObjectRange(2, 710000, 64);
|
||||
_vm.TextHistory.ResetLayout(2);
|
||||
AdvTextLayoutSnapshot historyLayout =
|
||||
_vm.TextHistory.GetLayoutSnapshot(2);
|
||||
AdvTextLayoutPresentationBinding historyBinding =
|
||||
_vm.TextHistory.GetPresentationBinding(2);
|
||||
_host.ResetRenderedAdvTextLayout(_vm.Gfx, historyBinding);
|
||||
var historyBatch = new AdvTextHistoryRenderBatch(
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
historyLayout,
|
||||
"History",
|
||||
immediateStyle);
|
||||
bool historyUsedRetained =
|
||||
_host.RenderTextHistory(
|
||||
_vm.Gfx,
|
||||
historyBinding,
|
||||
historyBatch);
|
||||
bool historyRetainedTextOk;
|
||||
string historyRetainedTextMode;
|
||||
if (_host.UsesSurfaceTextPixels)
|
||||
{
|
||||
RgbaImage? historySurface =
|
||||
_host.CaptureSurfacePixels(historyBinding.SourceSurfaceSlot);
|
||||
historyRetainedTextOk =
|
||||
historyUsedRetained
|
||||
&& _vm.Gfx.SnapshotVisibleObjects()
|
||||
.Count(item =>
|
||||
item.Handle >= historyBinding.FirstObjectHandle
|
||||
&& item.Handle - historyBinding.FirstObjectHandle
|
||||
< historyBinding.ObjectCapacity)
|
||||
== historyBatch.Text.Length
|
||||
&& historySurface != null
|
||||
&& historySurface.Pixels.Where((_, index) => index % 4 == 3)
|
||||
.Any(alpha => alpha != 0)
|
||||
&& _host.SnapshotRenderedTextHistory().Count == 0;
|
||||
_vm.Gfx.EraseRange(
|
||||
historyBinding.FirstObjectHandle,
|
||||
historyBinding.ObjectCapacity);
|
||||
_host.ClearRenderedAdvTextLayout(historyBinding.LayoutSlot);
|
||||
_host.EndTextHistoryPresentation(_vm.Gfx);
|
||||
historyRetainedTextOk &=
|
||||
!_vm.Gfx.SnapshotVisibleObjects()
|
||||
.Any(item =>
|
||||
item.Handle >= historyBinding.FirstObjectHandle
|
||||
&& item.Handle - historyBinding.FirstObjectHandle
|
||||
< historyBinding.ObjectCapacity)
|
||||
&& _host.CaptureSurfacePixels(historyBinding.SourceSurfaceSlot)
|
||||
?.Pixels.All(value => value == 0) == true;
|
||||
historyRetainedTextMode = "retained-glyphs";
|
||||
}
|
||||
else
|
||||
{
|
||||
historyRetainedTextOk =
|
||||
!historyUsedRetained
|
||||
&& _host.SnapshotRenderedTextHistory().Count == 1;
|
||||
_host.EndTextHistoryPresentation(_vm.Gfx);
|
||||
historyRetainedTextMode = "label-fallback";
|
||||
}
|
||||
Window rootWindow = GetTree().Root;
|
||||
bool logicalCanvasOk = _host.LogicalCanvas == new Sys4LogicalCanvas(_screenWidth, _screenHeight)
|
||||
&& _screen.GetWidth() == _screenWidth
|
||||
@@ -2650,6 +2668,7 @@ public partial class Main : Godot.Control
|
||||
&& bgmStopReleaseOk && textEffectModesOk && fontCalibrationOk
|
||||
&& immediateSurfaceTextOk
|
||||
&& liveRetainedTextOk
|
||||
&& historyRetainedTextOk
|
||||
&& logicalCanvasOk && backbufferPreservationOk;
|
||||
if (ok) GD.Print($"SELFTEST OK: threaded host matches headless ({actual.Count} lines, full handling); " +
|
||||
$"debug launcher catalog/UI smoke ({debugEntries.Count} packed scripts); " +
|
||||
@@ -2659,6 +2678,7 @@ public partial class Main : Godot.Control
|
||||
$"text-effect-modes=ok; font-calibration=ok; " +
|
||||
$"immediate-surface-text={immediateSurfaceTextMode}; " +
|
||||
$"live-adv-text={liveRetainedTextMode}; " +
|
||||
$"history-text={historyRetainedTextMode}; " +
|
||||
$"backbuffer-preservation=ok; " +
|
||||
$"logical-canvas={_screenWidth}x{_screenHeight}; " +
|
||||
$"window-request={_windowOptions.Width}x{_windowOptions.Height}");
|
||||
@@ -2672,6 +2692,7 @@ public partial class Main : Godot.Control
|
||||
$"text-effect-modes={textEffectModesOk}; font-calibration={fontCalibrationOk}; " +
|
||||
$"immediate-surface-text={immediateSurfaceTextOk}({immediateSurfaceTextMode}); " +
|
||||
$"live-adv-text={liveRetainedTextOk}({liveRetainedTextMode}); " +
|
||||
$"history-text={historyRetainedTextOk}({historyRetainedTextMode}); " +
|
||||
$"backbuffer-preservation={backbufferPreservationOk}; " +
|
||||
$"logical-canvas={logicalCanvasOk}({_screenWidth}x{_screenHeight}); " +
|
||||
$"window-request={_windowOptions.Width}x{_windowOptions.Height}");
|
||||
|
||||
Reference in New Issue
Block a user