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,
|
│ ALF asset store, script provider, AGF/LZSS and Windows CUR decoders,
|
||||||
│ and resource facade
|
│ and resource facade
|
||||||
│ ├── Age.Engine/Text/ backend-neutral glyph-mask requests/results, deterministic AGE
|
│ ├── Age.Engine/Text/ backend-neutral glyph-mask requests/results, deterministic AGE
|
||||||
│ compositor/layout, retained live-glyph publication, and bounded
|
│ compositor/layout, retained live/History glyph and wait-atlas
|
||||||
│ font/glyph cache primitives
|
│ publication, and bounded font/glyph cache primitives
|
||||||
│ ├── Age.Engine/Persistence/ native S3SD/S4SD + S3RT codecs, shared payload/ReadTextDB,
|
│ ├── Age.Engine/Persistence/ native S3SD/S4SD + S3RT codecs, shared payload/ReadTextDB,
|
||||||
│ layout-3 numbered state/history/gfx, DAT/STH pair + BMP codec,
|
│ layout-3 numbered state/history/gfx, DAT/STH pair + BMP codec,
|
||||||
│ and profile-owned state
|
│ 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
|
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
|
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
|
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/
|
game call site. The initial port used replaceable labels keyed by target layout. The retained-glyph migration
|
||||||
effect snapshot, and clears stale labels whenever `0x71` resets that layout.
|
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
|
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),
|
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
|
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
|
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
|
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
|
core now drives immediate surface strings, live ADV, and retained History.
|
||||||
next migration step.
|
|
||||||
|
|
||||||
The independent Windows reference backend landed alongside, but outside, the neutral engine as
|
The independent Windows reference backend landed alongside, but outside, the neutral engine as
|
||||||
`Age.Engine.Text.Windows.WindowsGdiGlyphMaskRasterizer`. It holds one `CreateICA("DISPLAY")` information
|
`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,
|
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
|
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.
|
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.
|
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`),
|
`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`
|
**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;
|
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
|
`GodotAdvHost` retains configurations by layout and pairs them with op `0x212`'s script-owned handle and
|
||||||
Godot atlas overlay advances on the 48 ms host clock only while `IsWaiting`, then hides immediately when
|
layout origin. `RetainedAdvWaitIndicatorPresentation` binds the current atlas cell as an ordinary
|
||||||
input releases the wait. A 2026-07-27 polish fix removed the port's erroneous `TerminalFrame + 1`
|
`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
|
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`.
|
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.
|
The retained presenter requests a new frame only when that source cell changes, preserving the static-wait
|
||||||
Full SYSTEM4 replay remains Phase-B work; the shortcut carries the same state in the meantime.
|
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)
|
### 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 |
|
| 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 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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
|
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
|
`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.
|
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
|
6. **Migrate History and the ADV wait indicator through the same retained path. — Completed
|
||||||
binds into its target layout instead of creating a layout-keyed Label. Bind the configured wait atlas at
|
2026-07-30.** Op `0x1d1` now rasterizes each HISTORY row into its target layout surface and publishes its
|
||||||
the op-`0x212` handle so it follows native z-order and source-cell animation. Once SC0000, HISTORY,
|
native glyph interval through ordinary `GfxState` bindings. Row reset, wheel redraw, the broad native exit
|
||||||
STUDY/MAMES, HIDEWIN, and BUNKI pass, delete the game-text Label pools and the modal/raw-callback visibility
|
erase, recording re-enable, and surface release all remove the transient presentation without changing
|
||||||
exceptions whose only purpose was compensating for detached overlays.
|
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
|
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)
|
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,
|
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));
|
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]
|
[Fact]
|
||||||
public void WaitIndicatorTerminalFrameIsExclusive()
|
public void WaitIndicatorTerminalFrameIsExclusive()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ public class HistoryPresentationOpsTests
|
|||||||
Assert.Equal((24, 0xffffffL, 0x606060L, 8, "MS 明朝"),
|
Assert.Equal((24, 0xffffffL, 0x606060L, 8, "MS 明朝"),
|
||||||
(render.Style.PrimaryFontSize, render.Style.TextColor, render.Style.EffectColor,
|
(render.Style.PrimaryFontSize, render.Style.TextColor, render.Style.EffectColor,
|
||||||
render.Style.LineSpacing, render.Style.FontFace));
|
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]
|
[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 Slot, int X, int Y)> TextCursors = new();
|
||||||
public readonly List<(int Surface, int X, int Y, string Text)> SurfaceStrings = new();
|
public readonly List<(int Surface, int X, int Y, string Text)> SurfaceStrings = new();
|
||||||
public readonly List<AdvTextHistoryRenderBatch> HistoryRenders = new();
|
public readonly List<AdvTextHistoryRenderBatch> HistoryRenders = new();
|
||||||
|
public readonly List<(AdvTextLayoutPresentationBinding Binding, AdvTextHistoryRenderBatch Batch)>
|
||||||
|
RetainedHistoryRenders = new();
|
||||||
public readonly Dictionary<int, AdvTextHistoryRenderBatch> ActiveHistoryRenders = new();
|
public readonly Dictionary<int, AdvTextHistoryRenderBatch> ActiveHistoryRenders = new();
|
||||||
public int HistoryPresentationEnds;
|
public int HistoryPresentationEnds;
|
||||||
public readonly List<int> ClearedTextLayouts = new();
|
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)>
|
public readonly List<(int Slot, int SourceX, int SourceY, int Width, int Height, int X, int Y)>
|
||||||
TextureDraws = new();
|
TextureDraws = new();
|
||||||
public readonly List<AdvWaitIndicatorConfig> WaitIndicators = 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<bool> WaitIndicatorEnabledChanges = new();
|
||||||
public readonly List<int> PublishedAdvTextLayouts = new();
|
public readonly List<int> PublishedAdvTextLayouts = new();
|
||||||
public readonly List<long> SleptDurations = new();
|
public readonly List<long> SleptDurations = new();
|
||||||
@@ -129,11 +133,21 @@ internal class RecordingHost : IHost
|
|||||||
HistoryRenders.Add(batch);
|
HistoryRenders.Add(batch);
|
||||||
ActiveHistoryRenders[batch.LayoutSlot] = 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()
|
public void EndTextHistoryPresentation()
|
||||||
{
|
{
|
||||||
HistoryPresentationEnds++;
|
HistoryPresentationEnds++;
|
||||||
ActiveHistoryRenders.Clear();
|
ActiveHistoryRenders.Clear();
|
||||||
}
|
}
|
||||||
|
public void EndTextHistoryPresentation(GfxState gfx) => EndTextHistoryPresentation();
|
||||||
public int MessageWindowAlphaSetting { get; set; }
|
public int MessageWindowAlphaSetting { get; set; }
|
||||||
public void SetMessageWindowAlphaSetting(int value) => MessageWindowAlphaSetting = value;
|
public void SetMessageWindowAlphaSetting(int value) => MessageWindowAlphaSetting = value;
|
||||||
public void FillSurfaceRect(SurfaceRectFill fill) => SurfaceFills.Add(fill);
|
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)
|
public void PresentObjectRange(GfxState gfx, long firstHandle, long count)
|
||||||
=> PresentedRanges.Add((firstHandle, count));
|
=> PresentedRanges.Add((firstHandle, count));
|
||||||
public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) => WaitIndicators.Add(config);
|
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 SetAdvWaitIndicatorEnabled(bool enabled) => WaitIndicatorEnabledChanges.Add(enabled);
|
||||||
public void PublishAdvTextLayout(int layoutSlot) => PublishedAdvTextLayouts.Add(layoutSlot);
|
public void PublishAdvTextLayout(int layoutSlot) => PublishedAdvTextLayouts.Add(layoutSlot);
|
||||||
public void SetAdvPagePresentationSuspended(bool suspended)
|
public void SetAdvPagePresentationSuspended(bool suspended)
|
||||||
|
|||||||
@@ -86,15 +86,28 @@ public interface IHost
|
|||||||
GfxState gfx, AdvTextLayoutPresentationBinding binding)
|
GfxState gfx, AdvTextLayoutPresentationBinding binding)
|
||||||
=> ClearRenderedAdvTextLayout(binding.LayoutSlot);
|
=> ClearRenderedAdvTextLayout(binding.LayoutSlot);
|
||||||
void RenderTextHistory(AdvTextHistoryRenderBatch batch) { }
|
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
|
// 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.
|
// recording re-enable at exit ends that presentation and drops every bound target layout.
|
||||||
void EndTextHistoryPresentation() { }
|
void EndTextHistoryPresentation() { }
|
||||||
|
void EndTextHistoryPresentation(GfxState gfx) => EndTextHistoryPresentation();
|
||||||
int MessageWindowAlphaSetting => 0;
|
int MessageWindowAlphaSetting => 0;
|
||||||
void SetMessageWindowAlphaSetting(int value) { }
|
void SetMessageWindowAlphaSetting(int value) { }
|
||||||
void FillSurfaceRect(SurfaceRectFill fill) { }
|
void FillSurfaceRect(SurfaceRectFill fill) { }
|
||||||
void CopySurfaceRect(SurfaceRectCopy copy) { }
|
void CopySurfaceRect(SurfaceRectCopy copy) { }
|
||||||
void PresentObjectRange(GfxState gfx, long firstHandle, long count) { }
|
void PresentObjectRange(GfxState gfx, long firstHandle, long count) { }
|
||||||
void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) { }
|
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.
|
// Op 0x1ce explicitly starts/stops the same animated marker that op 0x72 starts for an ADV wait.
|
||||||
void SetAdvWaitIndicatorEnabled(bool enabled) { }
|
void SetAdvWaitIndicatorEnabled(bool enabled) { }
|
||||||
// Op 0x20a republishes one retained ADV text layout and includes the current marker frame when active.
|
// 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>
|
/// <summary>
|
||||||
/// Resolve layouts whose complete native retained-glyph handle interval is covered by one op-0x1f7
|
/// 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
|
/// erase. Hosts use this notification to discard fallback metadata or preserve exact transient records
|
||||||
/// teardown removes the corresponding detached presentation atomically.
|
/// for a following native op-0x20a republication, according to the active presentation path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<int> LayoutsCoveredByTextObjectErase(long firstHandle, long count)
|
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;
|
bool enabled = Read(a[0]) == 1;
|
||||||
TextHistory.SetRecordingEnabled(enabled);
|
TextHistory.SetRecordingEnabled(enabled);
|
||||||
if (enabled) _host.EndTextHistoryPresentation();
|
if (enabled) _host.EndTextHistoryPresentation(Gfx);
|
||||||
}
|
}
|
||||||
return pc + 1;
|
return pc + 1;
|
||||||
case "append-text-history-metadata": // 0x1d2: (metadata type, value)
|
case "append-text-history-metadata": // 0x1d2: (metadata type, value)
|
||||||
@@ -2370,11 +2370,17 @@ public sealed class VirtualMachine
|
|||||||
int flags = (int)Read(a[2]);
|
int flags = (int)Read(a[2]);
|
||||||
if ((flags & 4) == 0 && TextHistory.TryBuildRenderBatch(
|
if ((flags & 4) == 0 && TextHistory.TryBuildRenderBatch(
|
||||||
(int)Read(a[0]), (int)Read(a[1]), flags, Read(a[3]), Read(a[4]), out var batch))
|
(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.
|
// Native History uses the text manager's current leading, not a retained-record field.
|
||||||
Style = batch.Style with { LineSpacing = _advTextStyle.LineSpacing }
|
Style = batch.Style with { LineSpacing = _advTextStyle.LineSpacing }
|
||||||
});
|
};
|
||||||
|
_host.RenderTextHistory(
|
||||||
|
Gfx,
|
||||||
|
TextHistory.GetPresentationBinding(batch.LayoutSlot),
|
||||||
|
batch);
|
||||||
|
}
|
||||||
return pc + 1;
|
return pc + 1;
|
||||||
}
|
}
|
||||||
case "u0041BB90":
|
case "u0041BB90":
|
||||||
@@ -2820,7 +2826,13 @@ public sealed class VirtualMachine
|
|||||||
return pc + 1;
|
return pc + 1;
|
||||||
case "set-adv-wait-indicator-handle": // 0x212 (layout)(retained handle)
|
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;
|
return pc + 1;
|
||||||
}
|
}
|
||||||
case "set-adv-text-object-range": // 0x213 (layout)(first handle)(count)
|
case "set-adv-text-object-range": // 0x213 (layout)(first handle)(count)
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
private string _surfaceTextFallbackReason;
|
private string _surfaceTextFallbackReason;
|
||||||
private bool _surfaceTextFallbackWarningReported;
|
private bool _surfaceTextFallbackWarningReported;
|
||||||
private readonly Dictionary<int, AdvTextHistoryRenderBatch> _historyText = new();
|
private readonly Dictionary<int, AdvTextHistoryRenderBatch> _historyText = new();
|
||||||
|
private readonly HashSet<int> _retainedHistoryLayouts = new();
|
||||||
private sealed class LiveTextState
|
private sealed class LiveTextState
|
||||||
{
|
{
|
||||||
public required AdvLiveTextRun Run;
|
public required AdvLiveTextRun Run;
|
||||||
@@ -96,6 +97,11 @@ public sealed class GodotAdvHost : IHost
|
|||||||
private volatile int _messageWindowAlphaSetting;
|
private volatile int _messageWindowAlphaSetting;
|
||||||
private bool _advTextForceComplete;
|
private bool _advTextForceComplete;
|
||||||
private readonly Dictionary<int, AdvWaitIndicatorConfig> _waitIndicators = new();
|
private readonly Dictionary<int, AdvWaitIndicatorConfig> _waitIndicators = new();
|
||||||
|
private readonly Dictionary<int, (
|
||||||
|
AdvTextLayoutPresentationBinding Binding,
|
||||||
|
AdvTextLayoutSnapshot Layout)> _waitIndicatorBindings = new();
|
||||||
|
private readonly Dictionary<int, RetainedAdvWaitIndicatorPresentation>
|
||||||
|
_retainedWaitIndicators = new();
|
||||||
private readonly object _messageSkipLock = new();
|
private readonly object _messageSkipLock = new();
|
||||||
private bool _scriptMessageSkipActive;
|
private bool _scriptMessageSkipActive;
|
||||||
private bool _physicalMessageSkipActive;
|
private bool _physicalMessageSkipActive;
|
||||||
@@ -728,6 +734,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
lock (_textLock)
|
lock (_textLock)
|
||||||
{
|
{
|
||||||
_historyText.Remove(binding.LayoutSlot);
|
_historyText.Remove(binding.LayoutSlot);
|
||||||
|
_retainedHistoryLayouts.Remove(binding.LayoutSlot);
|
||||||
_retainedTextLayouts.Remove(binding.LayoutSlot);
|
_retainedTextLayouts.Remove(binding.LayoutSlot);
|
||||||
_liveText.RemoveAll(
|
_liveText.RemoveAll(
|
||||||
state => state.Run.Layout.Slot == binding.LayoutSlot);
|
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()
|
public void EndTextHistoryPresentation()
|
||||||
{
|
{
|
||||||
lock (_textLock) _historyText.Clear();
|
lock (_textLock)
|
||||||
|
{
|
||||||
|
_historyText.Clear();
|
||||||
|
_retainedHistoryLayouts.Clear();
|
||||||
|
}
|
||||||
_timeline?.Event("history-presentation-end");
|
_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()
|
public IReadOnlyList<AdvTextHistoryRenderBatch> SnapshotRenderedTextHistory()
|
||||||
{
|
{
|
||||||
lock (_textLock) return _historyText.Values.OrderBy(batch => batch.LayoutSlot).ToArray();
|
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 int MessageWindowAlphaSetting => _messageWindowAlphaSetting;
|
||||||
|
|
||||||
public void SetMessageWindowAlphaSetting(int value)
|
public void SetMessageWindowAlphaSetting(int value)
|
||||||
@@ -890,7 +992,11 @@ public sealed class GodotAdvHost : IHost
|
|||||||
|
|
||||||
public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config)
|
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()
|
_timeline?.Event("wait-indicator-config", new()
|
||||||
{
|
{
|
||||||
["layout"] = config.LayoutSlot, ["x"] = config.X, ["y"] = config.Y,
|
["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)
|
public void SetAdvWaitIndicatorEnabled(bool enabled)
|
||||||
{
|
{
|
||||||
lock (_textLock)
|
lock (_textLock)
|
||||||
@@ -907,6 +1051,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
if (enabled && !_waitIndicatorEnabled) _waitIndicatorStartedMs = _clock.NowMs;
|
if (enabled && !_waitIndicatorEnabled) _waitIndicatorStartedMs = _clock.NowMs;
|
||||||
_waitIndicatorEnabled = enabled;
|
_waitIndicatorEnabled = enabled;
|
||||||
}
|
}
|
||||||
|
Interlocked.Exchange(ref _presentRequested, 1);
|
||||||
_timeline?.Event("wait-indicator-enabled", new() { ["enabled"] = enabled });
|
_timeline?.Event("wait-indicator-enabled", new() { ["enabled"] = enabled });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -920,29 +1065,39 @@ public sealed class GodotAdvHost : IHost
|
|||||||
GfxState gfx, AdvTextLayoutPresentationBinding binding)
|
GfxState gfx, AdvTextLayoutPresentationBinding binding)
|
||||||
{
|
{
|
||||||
lock (_textLock)
|
lock (_textLock)
|
||||||
|
{
|
||||||
if (_retainedTextLayouts.TryGetValue(
|
if (_retainedTextLayouts.TryGetValue(
|
||||||
binding.LayoutSlot, out RetainedAdvTextLayoutPresentation? presentation)
|
binding.LayoutSlot, out RetainedAdvTextLayoutPresentation? presentation)
|
||||||
&& presentation.Binding == binding)
|
&& presentation.Binding == binding)
|
||||||
presentation.Republish(gfx);
|
presentation.Republish(gfx);
|
||||||
|
if (_retainedWaitIndicators.TryGetValue(
|
||||||
|
binding.LayoutSlot,
|
||||||
|
out RetainedAdvWaitIndicatorPresentation? indicator))
|
||||||
|
indicator.Republish(
|
||||||
|
gfx,
|
||||||
|
_clock.NowMs - _waitIndicatorStartedMs,
|
||||||
|
WaitIndicatorShouldBeVisible(binding.LayoutSlot));
|
||||||
|
}
|
||||||
PublishAdvTextLayout(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)
|
lock (_textLock)
|
||||||
{
|
{
|
||||||
if (!_waitIndicatorEnabled) return null;
|
foreach ((int layoutSlot, RetainedAdvWaitIndicatorPresentation presentation)
|
||||||
if (!_waitIndicators.TryGetValue(_activeWaitLayout, out config)) return null;
|
in _retainedWaitIndicators)
|
||||||
if (!_surfaceResources.TryGetValue(config.SurfaceSlot, out resourceId)) return null;
|
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)
|
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;
|
IsWaiting = false;
|
||||||
lock (_textLock) _waitIndicatorEnabled = false;
|
lock (_textLock) _waitIndicatorEnabled = false;
|
||||||
|
Interlocked.Exchange(ref _presentRequested, 1);
|
||||||
_timeline?.State("running", new()
|
_timeline?.State("running", new()
|
||||||
{
|
{
|
||||||
["input"] = messageSkipped ? "message-skip" : autoAdvanced ? "auto" : "user",
|
["input"] = messageSkipped ? "message-skip" : autoAdvanced ? "auto" : "user",
|
||||||
@@ -1166,10 +1322,13 @@ public sealed class GodotAdvHost : IHost
|
|||||||
: _currentAdvLayout;
|
: _currentAdvLayout;
|
||||||
if (_retainedTextLayouts.TryGetValue(
|
if (_retainedTextLayouts.TryGetValue(
|
||||||
slot, out RetainedAdvTextLayoutPresentation? presentation))
|
slot, out RetainedAdvTextLayoutPresentation? presentation))
|
||||||
{
|
|
||||||
presentation.ErasePublished(gfx);
|
presentation.ErasePublished(gfx);
|
||||||
|
if (_retainedWaitIndicators.TryGetValue(
|
||||||
|
slot, out RetainedAdvWaitIndicatorPresentation? indicator))
|
||||||
|
indicator.Erase(gfx);
|
||||||
|
if (_retainedTextLayouts.ContainsKey(slot)
|
||||||
|
|| _retainedWaitIndicators.ContainsKey(slot))
|
||||||
_suspendedRetainedTextLayoutSlot = slot;
|
_suspendedRetainedTextLayoutSlot = slot;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (_suspendedRetainedTextLayoutSlot != 0)
|
else if (_suspendedRetainedTextLayoutSlot != 0)
|
||||||
{
|
{
|
||||||
@@ -1177,6 +1336,15 @@ public sealed class GodotAdvHost : IHost
|
|||||||
_suspendedRetainedTextLayoutSlot,
|
_suspendedRetainedTextLayoutSlot,
|
||||||
out RetainedAdvTextLayoutPresentation? presentation))
|
out RetainedAdvTextLayoutPresentation? presentation))
|
||||||
presentation.Republish(gfx);
|
presentation.Republish(gfx);
|
||||||
|
if (_retainedWaitIndicators.TryGetValue(
|
||||||
|
_suspendedRetainedTextLayoutSlot,
|
||||||
|
out RetainedAdvWaitIndicatorPresentation? indicator))
|
||||||
|
indicator.Republish(
|
||||||
|
gfx,
|
||||||
|
_clock.NowMs - _waitIndicatorStartedMs,
|
||||||
|
IsWaiting
|
||||||
|
&& _waitIndicatorEnabled
|
||||||
|
&& _activeWaitLayout == _suspendedRetainedTextLayoutSlot);
|
||||||
_suspendedRetainedTextLayoutSlot = 0;
|
_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
|
// 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
|
// once, then continue only while the sampled retained scene can actually change. The wait atlas advances
|
||||||
// Godot Label; waiting/sleeping alone do not alter background pixels.
|
// its ordinary retained source cell here so it contributes only when the frame actually changes.
|
||||||
public HostPresentationReason ConsumePresentationReasons(GfxState gfx)
|
public HostPresentationReason ConsumePresentationReasons(GfxState gfx)
|
||||||
{
|
{
|
||||||
|
UpdateRetainedWaitIndicator(gfx);
|
||||||
bool screenTransitionActive;
|
bool screenTransitionActive;
|
||||||
lock (_screenTransitionLock) screenTransitionActive = _screenTransition != null;
|
lock (_screenTransitionLock) screenTransitionActive = _screenTransition != null;
|
||||||
var reasons = HostPresentationReason.None;
|
var reasons = HostPresentationReason.None;
|
||||||
@@ -1489,6 +1658,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
_surfaceText.Clear();
|
_surfaceText.Clear();
|
||||||
_surfaceResources.Clear();
|
_surfaceResources.Clear();
|
||||||
_historyText.Clear();
|
_historyText.Clear();
|
||||||
|
_retainedHistoryLayouts.Clear();
|
||||||
_liveText.Clear();
|
_liveText.Clear();
|
||||||
_retainedTextLayouts.Clear();
|
_retainedTextLayouts.Clear();
|
||||||
_activeLiveText = null;
|
_activeLiveText = null;
|
||||||
@@ -1498,6 +1668,8 @@ public sealed class GodotAdvHost : IHost
|
|||||||
_advTextY = 47;
|
_advTextY = 47;
|
||||||
_advTextForceComplete = false;
|
_advTextForceComplete = false;
|
||||||
_waitIndicators.Clear();
|
_waitIndicators.Clear();
|
||||||
|
_waitIndicatorBindings.Clear();
|
||||||
|
_retainedWaitIndicators.Clear();
|
||||||
_activeWaitLayout = 0;
|
_activeWaitLayout = 0;
|
||||||
_waitIndicatorEnabled = false;
|
_waitIndicatorEnabled = false;
|
||||||
}
|
}
|
||||||
@@ -1681,6 +1853,14 @@ public sealed class GodotAdvHost : IHost
|
|||||||
{
|
{
|
||||||
_surfaceText.Remove(slot);
|
_surfaceText.Remove(slot);
|
||||||
_surfaceResources.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);
|
_slotDims[slot] = (image.Width, image.Height);
|
||||||
return true;
|
return true;
|
||||||
@@ -1974,6 +2154,14 @@ public sealed class GodotAdvHost : IHost
|
|||||||
{
|
{
|
||||||
_surfaceText.Remove(slot);
|
_surfaceText.Remove(slot);
|
||||||
_surfaceResources.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);
|
_slotDims.Remove(slot);
|
||||||
if (movieRelease.Kind == MovieSurfaceReleaseKind.Released)
|
if (movieRelease.Kind == MovieSurfaceReleaseKind.Released)
|
||||||
@@ -2138,6 +2326,8 @@ public sealed class GodotAdvHost : IHost
|
|||||||
.Select(pair => pair.Key)
|
.Select(pair => pair.Key)
|
||||||
.ToArray())
|
.ToArray())
|
||||||
_retainedTextLayouts.Remove(layoutSlot);
|
_retainedTextLayouts.Remove(layoutSlot);
|
||||||
|
_retainedHistoryLayouts.RemoveWhere(layoutSlot =>
|
||||||
|
!_retainedTextLayouts.ContainsKey(layoutSlot));
|
||||||
}
|
}
|
||||||
foreach (MovieSurfaceBinding binding in stoppedMovies)
|
foreach (MovieSurfaceBinding binding in stoppedMovies)
|
||||||
{
|
{
|
||||||
@@ -2493,6 +2683,3 @@ internal sealed class LegacyScreenTransition
|
|||||||
DurationMs = durationMs;
|
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 GpuRetainedRenderer _gpuRenderer = null!;
|
||||||
private bool _useGpuBackend = true;
|
private bool _useGpuBackend = true;
|
||||||
private ImageTexture? _ageCursorTexture;
|
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
|
// 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.
|
// completed frame crosses the Godot Image boundary, avoiding a full GetData/SetData round-trip per layer.
|
||||||
private byte[] _screenPixels = [];
|
private byte[] _screenPixels = [];
|
||||||
@@ -195,17 +191,6 @@ public partial class Main : Godot.Control
|
|||||||
_screenView.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
_screenView.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
||||||
_gpuRenderer = new GpuRetainedRenderer(this);
|
_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 };
|
_text = new Label { AutowrapMode = TextServer.AutowrapMode.WordSmart, MouseFilter = MouseFilterEnum.Ignore };
|
||||||
AddChild(_text);
|
AddChild(_text);
|
||||||
_text.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
_text.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
||||||
@@ -471,6 +456,11 @@ public partial class Main : Godot.Control
|
|||||||
_vm.Gfx.SetSurface(0x0c, waitIndicator.PackedId, 0xff00);
|
_vm.Gfx.SetSurface(0x0c, waitIndicator.PackedId, 0xff00);
|
||||||
_host.ConfigureAdvWaitIndicator(new AdvWaitIndicatorConfig(
|
_host.ConfigureAdvWaitIndicator(new AdvWaitIndicatorConfig(
|
||||||
1, 385, 140, 0x0c, 0, 0, 30, 27, 12, 48));
|
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
|
// --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.
|
// 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;
|
phase = perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||||
allocationPhase = perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
allocationPhase = perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
||||||
if (!_selftest && _host != null) UpdateAdvTextPresentation();
|
if (!_selftest && _host != null) UpdateAdvTextPresentation();
|
||||||
if (!_selftest && _host != null) UpdateAdvWaitIndicatorPresentation();
|
|
||||||
if (!_selftest && _host != null) UpdateHistoryTextPresentation();
|
if (!_selftest && _host != null) UpdateHistoryTextPresentation();
|
||||||
perf?.RecordUiAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
perf?.RecordUiAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
||||||
perf?.RecordUi(PerformanceFrameLog.Timestamp() - phase);
|
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);
|
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)
|
private static string ColorTimeline(Age.Engine.Model.ColorTransitionState? state)
|
||||||
=> state is { } c
|
=> state is { } c
|
||||||
? $" color=0x{c.Current:x8}->0x{c.Target:x8} colorProgress={c.Progress:0.000}"
|
? $" 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);
|
&& !string.IsNullOrWhiteSpace(_host.SurfaceTextFallbackReason);
|
||||||
liveRetainedTextMode = "label-fallback";
|
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;
|
Window rootWindow = GetTree().Root;
|
||||||
bool logicalCanvasOk = _host.LogicalCanvas == new Sys4LogicalCanvas(_screenWidth, _screenHeight)
|
bool logicalCanvasOk = _host.LogicalCanvas == new Sys4LogicalCanvas(_screenWidth, _screenHeight)
|
||||||
&& _screen.GetWidth() == _screenWidth
|
&& _screen.GetWidth() == _screenWidth
|
||||||
@@ -2650,6 +2668,7 @@ public partial class Main : Godot.Control
|
|||||||
&& bgmStopReleaseOk && textEffectModesOk && fontCalibrationOk
|
&& bgmStopReleaseOk && textEffectModesOk && fontCalibrationOk
|
||||||
&& immediateSurfaceTextOk
|
&& immediateSurfaceTextOk
|
||||||
&& liveRetainedTextOk
|
&& liveRetainedTextOk
|
||||||
|
&& historyRetainedTextOk
|
||||||
&& logicalCanvasOk && backbufferPreservationOk;
|
&& logicalCanvasOk && backbufferPreservationOk;
|
||||||
if (ok) GD.Print($"SELFTEST OK: threaded host matches headless ({actual.Count} lines, full handling); " +
|
if (ok) GD.Print($"SELFTEST OK: threaded host matches headless ({actual.Count} lines, full handling); " +
|
||||||
$"debug launcher catalog/UI smoke ({debugEntries.Count} packed scripts); " +
|
$"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; " +
|
$"text-effect-modes=ok; font-calibration=ok; " +
|
||||||
$"immediate-surface-text={immediateSurfaceTextMode}; " +
|
$"immediate-surface-text={immediateSurfaceTextMode}; " +
|
||||||
$"live-adv-text={liveRetainedTextMode}; " +
|
$"live-adv-text={liveRetainedTextMode}; " +
|
||||||
|
$"history-text={historyRetainedTextMode}; " +
|
||||||
$"backbuffer-preservation=ok; " +
|
$"backbuffer-preservation=ok; " +
|
||||||
$"logical-canvas={_screenWidth}x{_screenHeight}; " +
|
$"logical-canvas={_screenWidth}x{_screenHeight}; " +
|
||||||
$"window-request={_windowOptions.Width}x{_windowOptions.Height}");
|
$"window-request={_windowOptions.Width}x{_windowOptions.Height}");
|
||||||
@@ -2672,6 +2692,7 @@ public partial class Main : Godot.Control
|
|||||||
$"text-effect-modes={textEffectModesOk}; font-calibration={fontCalibrationOk}; " +
|
$"text-effect-modes={textEffectModesOk}; font-calibration={fontCalibrationOk}; " +
|
||||||
$"immediate-surface-text={immediateSurfaceTextOk}({immediateSurfaceTextMode}); " +
|
$"immediate-surface-text={immediateSurfaceTextOk}({immediateSurfaceTextMode}); " +
|
||||||
$"live-adv-text={liveRetainedTextOk}({liveRetainedTextMode}); " +
|
$"live-adv-text={liveRetainedTextOk}({liveRetainedTextMode}); " +
|
||||||
|
$"history-text={historyRetainedTextOk}({historyRetainedTextMode}); " +
|
||||||
$"backbuffer-preservation={backbufferPreservationOk}; " +
|
$"backbuffer-preservation={backbufferPreservationOk}; " +
|
||||||
$"logical-canvas={logicalCanvasOk}({_screenWidth}x{_screenHeight}); " +
|
$"logical-canvas={logicalCanvasOk}({_screenWidth}x{_screenHeight}); " +
|
||||||
$"window-request={_windowOptions.Width}x{_windowOptions.Height}");
|
$"window-request={_windowOptions.Width}x{_windowOptions.Height}");
|
||||||
|
|||||||
Reference in New Issue
Block a user