engine: retain History and wait presentation
This commit is contained in:
@@ -105,8 +105,8 @@ S:\Game Hacking\Eushully\Himegari\ ← workspace root (three siblings)
|
||||
│ ALF asset store, script provider, AGF/LZSS and Windows CUR decoders,
|
||||
│ and resource facade
|
||||
│ ├── Age.Engine/Text/ backend-neutral glyph-mask requests/results, deterministic AGE
|
||||
│ compositor/layout, retained live-glyph publication, and bounded
|
||||
│ font/glyph cache primitives
|
||||
│ compositor/layout, retained live/History glyph and wait-atlas
|
||||
│ publication, and bounded font/glyph cache primitives
|
||||
│ ├── Age.Engine/Persistence/ native S3SD/S4SD + S3RT codecs, shared payload/ReadTextDB,
|
||||
│ layout-3 numbered state/history/gfx, DAT/STH pair + BMP codec,
|
||||
│ and profile-owned state
|
||||
|
||||
@@ -2546,8 +2546,13 @@ Op `0x1d1` asks `AdvTextHistory` to select one retained group, skip metadata/voi
|
||||
host-facing render batch using the target layout configured by `0x70/0x71`, positioned by `0x198`, and
|
||||
seeded by `0x7a`. `HISTORY.BIN` passes zero for flags and both color overrides, so this is its exact ordinary
|
||||
binding path; the native flag-4 raster-without-binding and flag-8 side-effect modes remain outside the live
|
||||
game call site. Godot owns only replaceable labels keyed by target layout, applies the retained font/color/
|
||||
effect snapshot, and clears stale labels whenever `0x71` resets that layout.
|
||||
game call site. The initial port used replaceable labels keyed by target layout. The retained-glyph migration
|
||||
now passes the target's transient surface/handle binding with the batch, rasterizes its text through the
|
||||
shared glyph-mask service, and publishes one ordinary object per edge record. Exact rows therefore inherit
|
||||
native z-order, alpha/tint/fade, clipping, transforms, capture, and range erasure. `0x71` resets the row's
|
||||
surface and records; HISTORY's broad exit erase plus recording re-enable remove all transient row bindings
|
||||
without touching the persisted backlog. The layout-keyed Label remains only as the explicit
|
||||
unavailable-rasterizer fallback.
|
||||
|
||||
The same slice implements the support calls at their natural seams. Op `0x131` reads a host configuration
|
||||
property (currently defaulting to zero; choosing a profile/config persistence backend remains deferred),
|
||||
@@ -2982,8 +2987,7 @@ and returns the five-dword edge records plus final cursor, observed overflow, wr
|
||||
state. It preserves native horizontal precedence: vertical-only overflow stops before compositing, while a
|
||||
simultaneous horizontal overflow follows the wrap/kinsoku path. A reusable bounded LRU provides eviction
|
||||
callbacks for future native font handles; `CachedGlyphMaskRasterizer` applies the same bound to masks. This
|
||||
core now drives immediate surface strings and live ADV; retained History remains on its Label pending the
|
||||
next migration step.
|
||||
core now drives immediate surface strings, live ADV, and retained History.
|
||||
|
||||
The independent Windows reference backend landed alongside, but outside, the neutral engine as
|
||||
`Age.Engine.Text.Windows.WindowsGdiGlyphMaskRasterizer`. It holds one `CreateICA("DISPLAY")` information
|
||||
@@ -3014,7 +3018,8 @@ offscreen capture, fill/copy, and preservation behavior apply without a detached
|
||||
If the exact backend is unavailable, string conversion fails, or the target surface cannot be resolved,
|
||||
the host records one explicit diagnostic and retains the old metadata/Label projection for that draw. The
|
||||
fallback is constructed before surface publication, so it cannot mix a partial new raster with the Label.
|
||||
History and the wait indicator remain on their existing presentation paths for the later staged migration.
|
||||
This fallback now covers immediate, live, and History text; the non-glyph wait indicator is always an
|
||||
ordinary retained atlas object when its script binding is valid.
|
||||
|
||||
Live ADV now uses the same exact surface pixels through ordinary retained objects.
|
||||
`AdvTextLayoutPresentationBinding` is transient engine state: it carries the layout surface (`slot+0x14`),
|
||||
@@ -3059,13 +3064,20 @@ the following click releases the wait.
|
||||
|
||||
**Port implementation:** the single-scene harness now injects SYSTEM4's exact SO000 surface-12 and op-`0x73`
|
||||
configuration alongside its existing SO001 shortcut. The VM forwards all ten operands through the host ABI;
|
||||
`GodotAdvHost` retains configurations by layout and resolves slot 0 to the active Phase-A layout. A separate
|
||||
Godot atlas overlay advances on the 48 ms host clock only while `IsWaiting`, then hides immediately when
|
||||
input releases the wait. A 2026-07-27 polish fix removed the port's erroneous `TerminalFrame + 1`
|
||||
`GodotAdvHost` retains configurations by layout and pairs them with op `0x212`'s script-owned handle and
|
||||
layout origin. `RetainedAdvWaitIndicatorPresentation` binds the current atlas cell as an ordinary
|
||||
`GfxState` object, mutating it only when the 48 ms frame boundary changes. Op `0x20a` recreates an erased
|
||||
current frame; HIDEWIN suspension and wait-service shutdown erase the handle. The former Godot
|
||||
`TextureRect` overlay and raw-callback visibility exception are removed, so normal handle order, source
|
||||
color key, capture, and surface/scene composition apply. A real Vulkan SC0000 page-one capture places the
|
||||
retained bat at `(385,570)`.
|
||||
|
||||
A 2026-07-27 polish fix removed the port's erroneous `TerminalFrame + 1`
|
||||
interpretation, which had selected nonexistent frame `12` once per cycle and made Godot render one empty
|
||||
atlas cell. Frame selection now treats the terminal value as the exclusive frame count and loops `0..11`.
|
||||
Keeping this 30x27 overlay outside the software backbuffer preserves the static-wait compositor optimization.
|
||||
Full SYSTEM4 replay remains Phase-B work; the shortcut carries the same state in the meantime.
|
||||
The retained presenter requests a new frame only when that source cell changes, preserving the static-wait
|
||||
compositor optimization. Full SYSTEM4 replay remains Phase-B work; the shortcut carries the same state in
|
||||
the meantime.
|
||||
|
||||
### SC0000 native SFX / BGM-fade family — `0xb4`/`0xb5`/`0xb6`/`0xba`/`0xc2`/`0xd9` (2026-07-11)
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ or replaced before claiming portable exports.
|
||||
| AGE movie decode (`0x236` scene movies; `0x20f` modal LOGO/OP/ED; `0x24d` movie masks) | `FfmpegMovieDecoder` is the sole factory over the project-owned `native/age_movie_ffmpeg` ABI | Windows-x64 passes the complete 213-payload installed video/audio corpus gate plus audible LOGO/OP/CHAPTER playback and real TEST.AGF green-mask decode | Add target-specific native builds and export packaging |
|
||||
| Movie integration | Each surface owns a unique playback-instance id; `MovieRuntime` owns `IMovieDecoder` from an injected factory; video-only streams use monotonic pacing while audio-bearing streams use the Godot output clock. Opcode `0x24d` redirects decoded green bytes through the platform-neutral retained rasterizer and exact managed packed-alpha mask helper | Concurrent/restarted uses of one asset have independent frame/audio/completion/teardown state. Ordinary and mask movies share VFS/FFmpeg ownership; the mask result is a backend-neutral dynamic RGBA surface consumed by either Godot renderer. Managed code is no longer Windows-annotated, while only the win-x64 native bundle exists today | Add Linux/macOS native builds and smoke gates |
|
||||
| Movie audio | ABI v2 returns timestamped stereo float PCM; bounded managed buffering feeds a per-playback Godot `AudioStreamGenerator` and routes native movie flags to engine buses | All 29 installed audio-bearing streams decode with signal; synchronized LOGO/OP/CHAPTER playback is audibly accepted | Treat absent, distorted, or unsynchronized audio from an audio-bearing movie as a runtime bug |
|
||||
| ADV font discovery/raster fidelity | `Age.Engine.Text` remains OS-neutral. Godot selects the separate `Age.Engine.Text.Windows` backend for immediate `0x204`/`0x205` strings and live ADV when Windows ACP 932 is available; exact masks are cached, composited into layout/numbered RGBA surfaces, and live glyphs become ordinary retained handles. History still uses the existing `Label` path. The fallback loads Windows `MS 明朝`/`MS ゴシック` when available and otherwise chooses the Japanese/default font with FreeType embolden and a Godot outline | Three Mincho/Gothic regular/bold samples byte-match an independent Unicode GDI oracle. Immediate and live text now obey retained ordering, alpha/tint/fade, transforms, clipping, capture, HIDEWIN suspension, partial erase/republication, and reset. Non-Windows/non-932 runs report why exact selection failed and atomically retain the old Label presentations; they do not claim GDI parity | Add an explicitly non-identical portable backend and face-substitution policy, then migrate History/wait-indicator presentation and remove the remaining Label compensations. Keep the GDI project out of non-Windows deliverables rather than hiding failed exact selection |
|
||||
| ADV font discovery/raster fidelity | `Age.Engine.Text` remains OS-neutral. Godot selects the separate `Age.Engine.Text.Windows` backend for immediate `0x204`/`0x205` strings, live ADV, and retained History when Windows ACP 932 is available; exact masks are cached, composited into layout/numbered RGBA surfaces, and published through ordinary retained handles. The wait atlas is likewise bound at its script-configured handle and no longer uses a detached Godot node. The fallback loads Windows `MS 明朝`/`MS ゴシック` when available and otherwise chooses the Japanese/default font with FreeType embolden and a Godot outline | Three Mincho/Gothic regular/bold samples byte-match an independent Unicode GDI oracle. Immediate, live, History, and wait-marker presentation now obey retained ordering, alpha/tint/fade, transforms, clipping, capture, HIDEWIN suspension, partial erase/republication, reset, and History teardown. Non-Windows/non-932 runs report why exact text selection failed and atomically retain the old Label presentations; they do not claim GDI parity | Add an explicitly non-identical portable backend and face-substitution policy, then remove the unavailable-rasterizer Label fallback and its remaining overlay compensations. Keep the GDI project out of non-Windows deliverables rather than hiding failed exact selection |
|
||||
| Filesystem semantics | Several filename and containment comparisons use `OrdinalIgnoreCase`; installed assets are conventionally uppercase | Needs validation on case-sensitive filesystems; may hide casing or containment mistakes | Add Linux/macOS tests with mixed-case synthetic roots and use filesystem-appropriate containment rules |
|
||||
| Save/profile/settings storage | `Sys4PersistencePaths` models AGE's independent `SAVEPATH` and `REGFILEPATH` resolutions. Godot replaces Himegari's related profile directory with `user://`, yielding `user://SAVE` for native S3SD/S4SD/S3RT files and thumbnails plus `user://SYS4REG.INI` for the BOM-less CP932 options file; the preserving writer changes only its nine `[sound]` keys | Save payloads and engine options are isolated together without changing either native format. Native/drop-in resolution remains available through `USEAPPDATAFOLDER` plus both SYS4INI paths. A single-root override is rejected when `SAVEPATH` is not beneath `REGFILEPATH`, preventing cross-profile guesses | Expose explicit profile/native selection through the future launcher and allow independent overrides for profiles whose two native paths are unrelated. Validate CP932 availability, replace/flush, case, permissions, and interrupted-write behavior on each export target |
|
||||
| Game-install and repository discovery | `GameRootSelection` accepts `--game-root`, then probes the executable directory and current working directory for `SYS4INI.BIN`; on Unix the frontend prefers inherited shell `PWD` because Godot may change the process directory during project startup. Godot injects the selected root into its catalog and loose-first ALF store. `Paths.cs` remains the development/test locator for generated opcode data, diagnostics, and CLI conveniences | Installed game data no longer depends on the workspace sibling layout, enabling an executable beside `AGE.EXE`, a terminal launch from the install, or a launcher-supplied absolute profile root. A packaged export still needs its generated runtime metadata bundled independently of repository discovery | Add export packaging for the opcode/profile artifacts, let the future profile launcher pass `--game-root`, and run executable-directory/CWD plus case/permission smoke gates on Linux and macOS |
|
||||
|
||||
@@ -628,11 +628,17 @@ primitive, which cannot be made equivalent by choosing another embolden constant
|
||||
reconstruction. The synthesized Godot VM self-test exercises timed exact reveal and reports
|
||||
`live-adv-text=retained-glyphs`; real GPU captures of SC0000 pages 1 and 7 confirm ordinary and voiced
|
||||
dialogue placement/wrapping alongside the immediate speaker-name surface.
|
||||
6. **Migrate History and the ADV wait indicator through the same retained path.** Op `0x1d1` rasterizes and
|
||||
binds into its target layout instead of creating a layout-keyed Label. Bind the configured wait atlas at
|
||||
the op-`0x212` handle so it follows native z-order and source-cell animation. Once SC0000, HISTORY,
|
||||
STUDY/MAMES, HIDEWIN, and BUNKI pass, delete the game-text Label pools and the modal/raw-callback visibility
|
||||
exceptions whose only purpose was compensating for detached overlays.
|
||||
6. **Migrate History and the ADV wait indicator through the same retained path. — Completed
|
||||
2026-07-30.** Op `0x1d1` now rasterizes each HISTORY row into its target layout surface and publishes its
|
||||
native glyph interval through ordinary `GfxState` bindings. Row reset, wheel redraw, the broad native exit
|
||||
erase, recording re-enable, and surface release all remove the transient presentation without changing
|
||||
the persisted backlog. The configured wait atlas now binds at op `0x212`'s handle, advances source cells
|
||||
only on the configured frame boundary, participates in op-`0x20a` republication and HIDEWIN suspension,
|
||||
and disappears when the wait service stops. The standalone Godot wait `TextureRect` and its raw-callback
|
||||
visibility exception are gone. Exact History no longer enters the layout-keyed Label pool; that code and
|
||||
the live/immediate Label pools remain solely as the explicit unavailable-rasterizer fallback until step 7
|
||||
supplies the portable glyph backend. Focused lifecycle tests, all 579 engine tests, the exact History
|
||||
Godot self-test, and a real SC0000 Vulkan capture cover the retained path.
|
||||
7. **Complete and select the portable backend.** Spike the smallest maintainable way to obtain portable
|
||||
font masks/metrics (Godot/TextServer offscreen extraction versus a bounded FreeType-compatible adapter)
|
||||
before choosing a dependency. Define face substitution and raster policy as profile/configuration data,
|
||||
|
||||
@@ -388,6 +388,34 @@ public class AdvTextOpsTests
|
||||
Assert.Single(host.WaitIndicators));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WaitIndicatorHandlePublishesItsResolvedLayoutBindingToHost()
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
static Operand I(long value) => new(0, value);
|
||||
var script = ScriptAssembler.Assemble(table, "WAITMARK_BINDING",
|
||||
new List<(int, Operand[])>
|
||||
{
|
||||
(0x70, new[] { I(1), I(800), I(160), I(0), I(430) }),
|
||||
(0x79, new[] { I(1), I(100), I(47) }),
|
||||
(0x213, new[] { I(1), I(0xd6d8), I(500) }),
|
||||
(0x212, new[] { I(1), I(0xd674) }),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
var host = new RecordingHost();
|
||||
|
||||
new VirtualMachine(script, table, host).Run();
|
||||
|
||||
var retained = Assert.Single(host.WaitIndicatorBindings);
|
||||
Assert.Equal(
|
||||
new AdvTextLayoutPresentationBinding(
|
||||
1, 21, 0xd6d8, 500, 0xd674, 100, 47),
|
||||
retained.Binding);
|
||||
Assert.Equal(
|
||||
new AdvTextLayoutSnapshot(1, 800, 160, 0, 430, 0, 0, 800, 160),
|
||||
retained.Layout);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WaitIndicatorTerminalFrameIsExclusive()
|
||||
{
|
||||
|
||||
@@ -45,6 +45,10 @@ public class HistoryPresentationOpsTests
|
||||
Assert.Equal((24, 0xffffffL, 0x606060L, 8, "MS 明朝"),
|
||||
(render.Style.PrimaryFontSize, render.Style.TextColor, render.Style.EffectColor,
|
||||
render.Style.LineSpacing, render.Style.FontFace));
|
||||
var retained = Assert.Single(host.RetainedHistoryRenders);
|
||||
Assert.Equal(4, retained.Binding.LayoutSlot);
|
||||
Assert.Equal(0x18, retained.Binding.SourceSurfaceSlot);
|
||||
Assert.Same(render, retained.Batch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
using Age.Engine.Hosting;
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Text;
|
||||
|
||||
public class RetainedAdvWaitIndicatorPresentationTests
|
||||
{
|
||||
private static readonly AdvWaitIndicatorConfig Config = new(
|
||||
LayoutSlot: 1,
|
||||
X: 385,
|
||||
Y: 140,
|
||||
SurfaceSlot: 12,
|
||||
SourceX: 0,
|
||||
SourceY: 0,
|
||||
CellWidth: 30,
|
||||
CellHeight: 27,
|
||||
TerminalFrame: 12,
|
||||
FramePeriodMs: 48);
|
||||
|
||||
private static readonly AdvTextLayoutPresentationBinding Binding = new(
|
||||
LayoutSlot: 1,
|
||||
SourceSurfaceSlot: 21,
|
||||
FirstObjectHandle: 0xd6d8,
|
||||
ObjectCapacity: 500,
|
||||
WaitIndicatorObjectHandle: 0xd674,
|
||||
ResetCursorX: 100,
|
||||
ResetCursorY: 47);
|
||||
|
||||
[Fact]
|
||||
public void PublishesAtlasCellAtLayoutRelativePosition()
|
||||
{
|
||||
var presentation = new RetainedAdvWaitIndicatorPresentation(
|
||||
Config, Binding, layoutOriginX: 0, layoutOriginY: 430);
|
||||
var gfx = new GfxState();
|
||||
gfx.SetSurface(12, 0x337c, 0xff00);
|
||||
|
||||
Assert.True(presentation.Update(gfx, elapsedMs: 96, visible: true));
|
||||
|
||||
RenderObject marker = Assert.Single(gfx.SnapshotVisibleObjects());
|
||||
Assert.Equal(
|
||||
(0xd674L, 12, 60, 0, 30, 27, 385, 570),
|
||||
(marker.Handle, gfx.TryGet(marker.Handle)!.SourceSlot, marker.SrcX, marker.SrcY,
|
||||
marker.W, marker.H, marker.DstX, marker.DstY));
|
||||
Assert.Equal(2, presentation.PublishedFrame);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MutatesOnlyWhenFrameChangesAndErasesWhenHidden()
|
||||
{
|
||||
var presentation = new RetainedAdvWaitIndicatorPresentation(
|
||||
Config, Binding, 0, 430);
|
||||
var gfx = new GfxState();
|
||||
|
||||
Assert.True(presentation.Update(gfx, 0, visible: true));
|
||||
Assert.False(presentation.Update(gfx, 47, visible: true));
|
||||
Assert.True(presentation.Update(gfx, 48, visible: true));
|
||||
Assert.Equal(30, Assert.Single(gfx.SnapshotVisibleObjects()).SrcX);
|
||||
|
||||
Assert.True(presentation.Update(gfx, 48, visible: false));
|
||||
Assert.Empty(gfx.SnapshotVisibleObjects());
|
||||
Assert.False(presentation.Update(gfx, 48, visible: false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RepublishRestoresAHandleErasedByScript()
|
||||
{
|
||||
var presentation = new RetainedAdvWaitIndicatorPresentation(
|
||||
Config, Binding, 0, 430);
|
||||
var gfx = new GfxState();
|
||||
presentation.Update(gfx, 144, visible: true);
|
||||
gfx.EraseRange(Binding.WaitIndicatorObjectHandle, 1);
|
||||
|
||||
Assert.True(presentation.Republish(gfx, 144, visible: true));
|
||||
|
||||
RenderObject marker = Assert.Single(gfx.SnapshotVisibleObjects());
|
||||
Assert.Equal((0xd674L, 90), (marker.Handle, marker.SrcX));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RejectsMissingHandleAndMismatchedLayout()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
new RetainedAdvWaitIndicatorPresentation(
|
||||
Config,
|
||||
Binding with { WaitIndicatorObjectHandle = -1 },
|
||||
0,
|
||||
430));
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
new RetainedAdvWaitIndicatorPresentation(
|
||||
Config with { LayoutSlot = 2 },
|
||||
Binding,
|
||||
0,
|
||||
430));
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@ internal class RecordingHost : IHost
|
||||
public readonly List<(int Slot, int X, int Y)> TextCursors = new();
|
||||
public readonly List<(int Surface, int X, int Y, string Text)> SurfaceStrings = new();
|
||||
public readonly List<AdvTextHistoryRenderBatch> HistoryRenders = new();
|
||||
public readonly List<(AdvTextLayoutPresentationBinding Binding, AdvTextHistoryRenderBatch Batch)>
|
||||
RetainedHistoryRenders = new();
|
||||
public readonly Dictionary<int, AdvTextHistoryRenderBatch> ActiveHistoryRenders = new();
|
||||
public int HistoryPresentationEnds;
|
||||
public readonly List<int> ClearedTextLayouts = new();
|
||||
@@ -30,6 +32,8 @@ internal class RecordingHost : IHost
|
||||
public readonly List<(int Slot, int SourceX, int SourceY, int Width, int Height, int X, int Y)>
|
||||
TextureDraws = new();
|
||||
public readonly List<AdvWaitIndicatorConfig> WaitIndicators = new();
|
||||
public readonly List<(AdvTextLayoutPresentationBinding Binding, AdvTextLayoutSnapshot Layout)>
|
||||
WaitIndicatorBindings = new();
|
||||
public readonly List<bool> WaitIndicatorEnabledChanges = new();
|
||||
public readonly List<int> PublishedAdvTextLayouts = new();
|
||||
public readonly List<long> SleptDurations = new();
|
||||
@@ -129,11 +133,21 @@ internal class RecordingHost : IHost
|
||||
HistoryRenders.Add(batch);
|
||||
ActiveHistoryRenders[batch.LayoutSlot] = batch;
|
||||
}
|
||||
public bool RenderTextHistory(
|
||||
GfxState gfx,
|
||||
AdvTextLayoutPresentationBinding binding,
|
||||
AdvTextHistoryRenderBatch batch)
|
||||
{
|
||||
RetainedHistoryRenders.Add((binding, batch));
|
||||
RenderTextHistory(batch);
|
||||
return false;
|
||||
}
|
||||
public void EndTextHistoryPresentation()
|
||||
{
|
||||
HistoryPresentationEnds++;
|
||||
ActiveHistoryRenders.Clear();
|
||||
}
|
||||
public void EndTextHistoryPresentation(GfxState gfx) => EndTextHistoryPresentation();
|
||||
public int MessageWindowAlphaSetting { get; set; }
|
||||
public void SetMessageWindowAlphaSetting(int value) => MessageWindowAlphaSetting = value;
|
||||
public void FillSurfaceRect(SurfaceRectFill fill) => SurfaceFills.Add(fill);
|
||||
@@ -141,6 +155,10 @@ internal class RecordingHost : IHost
|
||||
public void PresentObjectRange(GfxState gfx, long firstHandle, long count)
|
||||
=> PresentedRanges.Add((firstHandle, count));
|
||||
public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) => WaitIndicators.Add(config);
|
||||
public void BindAdvWaitIndicator(
|
||||
AdvTextLayoutPresentationBinding binding,
|
||||
AdvTextLayoutSnapshot layout)
|
||||
=> WaitIndicatorBindings.Add((binding, layout));
|
||||
public void SetAdvWaitIndicatorEnabled(bool enabled) => WaitIndicatorEnabledChanges.Add(enabled);
|
||||
public void PublishAdvTextLayout(int layoutSlot) => PublishedAdvTextLayouts.Add(layoutSlot);
|
||||
public void SetAdvPagePresentationSuspended(bool suspended)
|
||||
|
||||
@@ -86,15 +86,28 @@ public interface IHost
|
||||
GfxState gfx, AdvTextLayoutPresentationBinding binding)
|
||||
=> ClearRenderedAdvTextLayout(binding.LayoutSlot);
|
||||
void RenderTextHistory(AdvTextHistoryRenderBatch batch) { }
|
||||
bool RenderTextHistory(
|
||||
GfxState gfx,
|
||||
AdvTextLayoutPresentationBinding binding,
|
||||
AdvTextHistoryRenderBatch batch)
|
||||
{
|
||||
RenderTextHistory(batch);
|
||||
return false;
|
||||
}
|
||||
// History render batches are transient bindings, unlike the retained backlog itself. HISTORY.BIN's
|
||||
// recording re-enable at exit ends that presentation and drops every bound target layout.
|
||||
void EndTextHistoryPresentation() { }
|
||||
void EndTextHistoryPresentation(GfxState gfx) => EndTextHistoryPresentation();
|
||||
int MessageWindowAlphaSetting => 0;
|
||||
void SetMessageWindowAlphaSetting(int value) { }
|
||||
void FillSurfaceRect(SurfaceRectFill fill) { }
|
||||
void CopySurfaceRect(SurfaceRectCopy copy) { }
|
||||
void PresentObjectRange(GfxState gfx, long firstHandle, long count) { }
|
||||
void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) { }
|
||||
// Op 0x212 supplies the ordinary retained handle used to publish the configured atlas cell.
|
||||
void BindAdvWaitIndicator(
|
||||
AdvTextLayoutPresentationBinding binding,
|
||||
AdvTextLayoutSnapshot layout) { }
|
||||
// Op 0x1ce explicitly starts/stops the same animated marker that op 0x72 starts for an ADV wait.
|
||||
void SetAdvWaitIndicatorEnabled(bool enabled) { }
|
||||
// Op 0x20a republishes one retained ADV text layout and includes the current marker frame when active.
|
||||
|
||||
@@ -207,8 +207,8 @@ public sealed class AdvTextHistory
|
||||
|
||||
/// <summary>
|
||||
/// Resolve layouts whose complete native retained-glyph handle interval is covered by one op-0x1f7
|
||||
/// erase. The Label backend collapses those glyph objects into live text runs, so full native range
|
||||
/// teardown removes the corresponding detached presentation atomically.
|
||||
/// erase. Hosts use this notification to discard fallback metadata or preserve exact transient records
|
||||
/// for a following native op-0x20a republication, according to the active presentation path.
|
||||
/// </summary>
|
||||
public IReadOnlyList<int> LayoutsCoveredByTextObjectErase(long firstHandle, long count)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
using Age.Engine.Hosting;
|
||||
using Age.Engine.Model;
|
||||
|
||||
namespace Age.Engine.Text;
|
||||
|
||||
/// <summary>
|
||||
/// Transient retained-object projection of one ADV layout's animated input-wait atlas.
|
||||
/// The script owns the source surface and object handle; this object owns only the current binding.
|
||||
/// </summary>
|
||||
public sealed class RetainedAdvWaitIndicatorPresentation
|
||||
{
|
||||
private int _publishedFrame = -1;
|
||||
|
||||
public RetainedAdvWaitIndicatorPresentation(
|
||||
AdvWaitIndicatorConfig config,
|
||||
AdvTextLayoutPresentationBinding binding,
|
||||
int layoutOriginX,
|
||||
int layoutOriginY)
|
||||
{
|
||||
if (config.LayoutSlot != binding.LayoutSlot)
|
||||
throw new ArgumentException(
|
||||
"Wait-indicator configuration and retained binding must select the same layout.");
|
||||
if (binding.WaitIndicatorObjectHandle < 0)
|
||||
throw new ArgumentException(
|
||||
"A retained ADV wait indicator requires a nonnegative object handle.",
|
||||
nameof(binding));
|
||||
if (config.SurfaceSlot < 0 || config.CellWidth <= 0 || config.CellHeight <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(config));
|
||||
|
||||
Config = config;
|
||||
Binding = binding;
|
||||
LayoutOriginX = layoutOriginX;
|
||||
LayoutOriginY = layoutOriginY;
|
||||
}
|
||||
|
||||
public AdvWaitIndicatorConfig Config { get; }
|
||||
public AdvTextLayoutPresentationBinding Binding { get; }
|
||||
public int LayoutOriginX { get; }
|
||||
public int LayoutOriginY { get; }
|
||||
public int PublishedFrame => _publishedFrame;
|
||||
public bool IsPublished => _publishedFrame >= 0;
|
||||
|
||||
/// <summary>Publish a changed source cell, or erase the retained handle when inactive.</summary>
|
||||
public bool Update(GfxState gfx, long elapsedMs, bool visible)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gfx);
|
||||
if (!visible)
|
||||
{
|
||||
if (!IsPublished) return false;
|
||||
Erase(gfx);
|
||||
return true;
|
||||
}
|
||||
|
||||
int frame = Config.FrameAt(elapsedMs);
|
||||
if (frame == _publishedFrame) return false;
|
||||
Bind(gfx, frame);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Recreate the current frame after script-owned range teardown/publication.</summary>
|
||||
public bool Republish(GfxState gfx, long elapsedMs, bool visible)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gfx);
|
||||
if (!visible)
|
||||
{
|
||||
if (!IsPublished) return false;
|
||||
Erase(gfx);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bind(gfx, Config.FrameAt(elapsedMs));
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Erase(GfxState gfx)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gfx);
|
||||
gfx.EraseRange(Binding.WaitIndicatorObjectHandle, 1);
|
||||
_publishedFrame = -1;
|
||||
}
|
||||
|
||||
private void Bind(GfxState gfx, int frame)
|
||||
{
|
||||
gfx.BindDraw(
|
||||
Binding.WaitIndicatorObjectHandle,
|
||||
Config.SurfaceSlot,
|
||||
checked(Config.SourceX + frame * Config.CellWidth),
|
||||
Config.SourceY,
|
||||
Config.CellWidth,
|
||||
Config.CellHeight,
|
||||
checked(LayoutOriginX + Config.X),
|
||||
checked(LayoutOriginY + Config.Y));
|
||||
_publishedFrame = frame;
|
||||
}
|
||||
}
|
||||
@@ -2347,7 +2347,7 @@ public sealed class VirtualMachine
|
||||
{
|
||||
bool enabled = Read(a[0]) == 1;
|
||||
TextHistory.SetRecordingEnabled(enabled);
|
||||
if (enabled) _host.EndTextHistoryPresentation();
|
||||
if (enabled) _host.EndTextHistoryPresentation(Gfx);
|
||||
}
|
||||
return pc + 1;
|
||||
case "append-text-history-metadata": // 0x1d2: (metadata type, value)
|
||||
@@ -2370,11 +2370,17 @@ public sealed class VirtualMachine
|
||||
int flags = (int)Read(a[2]);
|
||||
if ((flags & 4) == 0 && TextHistory.TryBuildRenderBatch(
|
||||
(int)Read(a[0]), (int)Read(a[1]), flags, Read(a[3]), Read(a[4]), out var batch))
|
||||
_host.RenderTextHistory(batch with
|
||||
{
|
||||
batch = batch with
|
||||
{
|
||||
// Native History uses the text manager's current leading, not a retained-record field.
|
||||
Style = batch.Style with { LineSpacing = _advTextStyle.LineSpacing }
|
||||
});
|
||||
};
|
||||
_host.RenderTextHistory(
|
||||
Gfx,
|
||||
TextHistory.GetPresentationBinding(batch.LayoutSlot),
|
||||
batch);
|
||||
}
|
||||
return pc + 1;
|
||||
}
|
||||
case "u0041BB90":
|
||||
@@ -2820,7 +2826,13 @@ public sealed class VirtualMachine
|
||||
return pc + 1;
|
||||
case "set-adv-wait-indicator-handle": // 0x212 (layout)(retained handle)
|
||||
{
|
||||
TextHistory.SetWaitIndicatorObjectHandle((int)Read(a[0]), Read(a[1]));
|
||||
int requestedSlot = (int)Read(a[0]);
|
||||
TextHistory.SetWaitIndicatorObjectHandle(requestedSlot, Read(a[1]));
|
||||
AdvTextLayoutSnapshot layout =
|
||||
TextHistory.GetLayoutSnapshot(requestedSlot);
|
||||
_host.BindAdvWaitIndicator(
|
||||
TextHistory.GetPresentationBinding(layout.Slot),
|
||||
layout);
|
||||
return pc + 1;
|
||||
}
|
||||
case "set-adv-text-object-range": // 0x213 (layout)(first handle)(count)
|
||||
|
||||
@@ -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