engine: materialize live ADV glyphs
This commit is contained in:
@@ -105,7 +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
|
||||||
│ text compositor/layout, and bounded font/glyph cache primitives
|
│ compositor/layout, retained live-glyph 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
|
||||||
|
|||||||
@@ -2982,8 +2982,8 @@ 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 is not yet connected to live ADV or History; those paths still use Labels pending their retained-glyph
|
core now drives immediate surface strings and live ADV; retained History remains on its Label pending the
|
||||||
materialization steps.
|
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,8 +3014,31 @@ 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.
|
||||||
Live ADV, History, and the wait indicator remain on their existing presentation paths for the later staged
|
History and the wait indicator remain on their existing presentation paths for the later staged migration.
|
||||||
migration.
|
|
||||||
|
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`),
|
||||||
|
first object handle, capacity, wait-indicator handle, and reset cursor without changing the persisted
|
||||||
|
History record layout. The VM passes that binding and its `GfxState` to the host for each `show-text` run,
|
||||||
|
then writes the returned GDI-measured cursor into the canonical live layout before processing another
|
||||||
|
literal/dynamic run.
|
||||||
|
|
||||||
|
`RetainedAdvTextLayoutPresentation` stores each five-dword record together with the layout origin active
|
||||||
|
when that run was built. Publication binds `first_handle+index` from the record's source edges to
|
||||||
|
`run_origin+(left,top)`, so MAMES-style origin changes do not move earlier glyphs. The complete run is
|
||||||
|
rasterized once into the layout RGBA surface; timed reveal only adds another `GfxState` binding and never
|
||||||
|
rerasterizes or reuploads the complete line. The configured capacity bounds timed publication. Click or
|
||||||
|
Skip sets the existing force-complete flag, binds every remaining publishable record, synchronizes that
|
||||||
|
endpoint, and returns without releasing the subsequent stable input wait.
|
||||||
|
|
||||||
|
Because these are normal handles, a partial `0x1f7` removes only the selected glyph. Op `0x20a` walks the
|
||||||
|
retained records through the current reveal count and recreates missing bindings. HIDEWIN suspension erases
|
||||||
|
the active page's published range and restore republishes it; layout reset erases the configured interval,
|
||||||
|
drops transient records, and zeroes the source surface. Whole scene reset/release drops the transient layout
|
||||||
|
state. Numbered History serialization still owns none of these pixels or records: the saved-frame script
|
||||||
|
path reconstructs them, and a reconstruction test proves identical handle/source/destination bindings.
|
||||||
|
Exact runs are omitted from `SnapshotLiveAdvText`, eliminating the detached always-on-top Label while the
|
||||||
|
unavailable-backend fallback continues to use that snapshot.
|
||||||
|
|
||||||
#### ADV wait indicator -- ops `0x73` / `0x72` (2026-07-11)
|
#### ADV wait indicator -- ops `0x73` / `0x72` (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 now selects the separate `Age.Engine.Text.Windows` backend for immediate `0x204`/`0x205` strings when Windows ACP 932 is available; exact masks are cached and composited into numbered RGBA surfaces. Live ADV and History still use the existing `Label` path, which loads Windows `MS 明朝`/`MS ゴシック` when available and otherwise chooses the Japanese/default fallback with FreeType embolden and a Godot outline | Three Mincho/Gothic regular/bold samples byte-match an independent Unicode GDI oracle. Immediate surface text now obeys retained ordering, alpha/tint/fade, transforms, clipping, capture, and surface mutation in both render paths. Non-Windows/non-932 runs report why exact selection failed and atomically retain the old surface-label projection; they do not claim GDI parity | Add an explicitly non-identical portable backend and face-substitution policy, then migrate live ADV/History and remove 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 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 |
|
||||||
| 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 |
|
||||||
|
|||||||
@@ -612,13 +612,22 @@ primitive, which cannot be made equivalent by choosing another embolden constant
|
|||||||
dynamic GPU upload, and surface mutation path. Backend absence, an unrepresentable glyph, or an unresolved
|
dynamic GPU upload, and surface mutation path. Backend absence, an unrepresentable glyph, or an unresolved
|
||||||
surface falls back atomically to the prior metadata/Label projection with an explicit diagnostic; that
|
surface falls back atomically to the prior metadata/Label projection with an explicit diagnostic; that
|
||||||
projection remains only for this temporary unavailable-backend case until the portable backend lands.
|
projection remains only for this temporary unavailable-backend case until the portable backend lands.
|
||||||
5. **Materialize live ADV glyphs as ordinary retained objects.** Extend the transient layout-presentation
|
5. **Materialize live ADV glyphs as ordinary retained objects.** Completed 2026-07-30 for the exact Windows
|
||||||
binding with its source surface, first handle, and capacity without changing the persisted History record
|
path. The VM passes `GfxState` plus the transient layout binding into presentation and accepts the measured
|
||||||
ABI. Build the complete line before reveal, update the canonical live cursor from measured metrics, then
|
final cursor in return; the persisted History ABI is unchanged. `RetainedAdvTextLayoutPresentation`
|
||||||
bind one normal `GfxState` object per revealed record. Click/Skip publishes the remaining records and keeps
|
retains each native edge record with the layout origin active for that run, builds the complete line into
|
||||||
the existing consumed-click boundary. Layout reset erases its range and clears its surface; partial
|
the layout surface once, and binds `first_handle + glyph_index` in reveal order. Consecutive
|
||||||
`0x1f7` erases work per handle naturally; `0x20a` republishes from retained records. Consecutive
|
literal/dynamic runs therefore begin at GDI's returned cursor instead of relying on Label concatenation.
|
||||||
literal/dynamic runs advance by returned metrics rather than relying on Label concatenation.
|
Timed reveal stops at capacity, while click/Skip publishes the remaining available handles through the
|
||||||
|
existing consumed-click branch. Partial `0x1f7` erases ordinary handles; `0x20a` recreates all records
|
||||||
|
through the current reveal count. HIDEWIN suspension erases that range and restoration republishes it.
|
||||||
|
Layout reset erases the interval, clears its RGBA surface and transient records, and saved-frame-style
|
||||||
|
reconstruction deterministically rebuilds the same bindings. Exact runs are excluded from the live Label
|
||||||
|
snapshot; unavailable-backend runs retain it unchanged. Focused tests cover handle order/capacity,
|
||||||
|
per-run origins, partial erase/republication, suspension, measured cursor handoff, reset, and
|
||||||
|
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
|
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
|
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,
|
the op-`0x212` handle so it follows native z-order and source-cell animation. Once SC0000, HISTORY,
|
||||||
|
|||||||
@@ -216,6 +216,48 @@ public class AdvTextOpsTests
|
|||||||
host.LiveTextRuns.Select(emitted => emitted.Run.Text));
|
host.LiveTextRuns.Select(emitted => emitted.Run.Text));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RetainedGlyphMetricsAdvanceCanonicalCursorBetweenRuns()
|
||||||
|
{
|
||||||
|
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||||
|
static Operand I(long value) => new(0, value);
|
||||||
|
static Operand S(int index) => new(2, index);
|
||||||
|
var script = ScriptAssembler.Assemble(table, "RETAINED_CURSOR",
|
||||||
|
new List<(int, Operand[])>
|
||||||
|
{
|
||||||
|
(0x70, new[] { I(1), I(100), I(50), I(10), I(20) }),
|
||||||
|
(0x79, new[] { I(1), I(2), I(3) }),
|
||||||
|
(0x71, new[] { I(1) }),
|
||||||
|
(0x213, new[] { I(1), I(100), I(10) }),
|
||||||
|
(0x6e, new[] { I(0), S(0) }),
|
||||||
|
(0x6e, new[] { I(0), S(1) }),
|
||||||
|
(0x2, Array.Empty<Operand>()),
|
||||||
|
},
|
||||||
|
new[] { "A", "BC" });
|
||||||
|
var host = new RecordingHost
|
||||||
|
{
|
||||||
|
OnRetainedText = run => new AdvRetainedTextRunResult(
|
||||||
|
run.Layout.Slot,
|
||||||
|
FirstGlyphIndex: 0,
|
||||||
|
GlyphCount: run.Text.Length,
|
||||||
|
CursorX: run.Layout.CursorX + run.Text.Length * 4,
|
||||||
|
CursorY: run.Layout.CursorY),
|
||||||
|
};
|
||||||
|
var vm = new VirtualMachine(script, table, host);
|
||||||
|
|
||||||
|
vm.Run();
|
||||||
|
|
||||||
|
Assert.Equal(new[] { (2, 3), (6, 3) },
|
||||||
|
host.LiveTextRuns.Select(run =>
|
||||||
|
(run.Run.Layout.CursorX, run.Run.Layout.CursorY)));
|
||||||
|
Assert.Equal(new[] { (2, 3), (6, 3) },
|
||||||
|
vm.TextHistory.Records.Select(record =>
|
||||||
|
(record.Layout.CursorX, record.Layout.CursorY)));
|
||||||
|
Assert.Equal((14, 3),
|
||||||
|
(vm.TextHistory.GetLayoutSnapshot(1).CursorX,
|
||||||
|
vm.TextHistory.GetLayoutSnapshot(1).CursorY));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RealMamesResearchDescriptionUsesImmediateRetainedTextPath()
|
public void RealMamesResearchDescriptionUsesImmediateRetainedTextPath()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ public class NativeNumberedSaveCodecTests
|
|||||||
new AdvTextLayoutSnapshot(3, 320, 90, 20, 400, 0, 0, 300, 80),
|
new AdvTextLayoutSnapshot(3, 320, 90, 20, 400, 0, 0, 300, 80),
|
||||||
liveHistory.GetLayoutSnapshot(3));
|
liveHistory.GetLayoutSnapshot(3));
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
new AdvTextLayoutPresentationBinding(3, 0x17, 0x2000, 500, 0x1234),
|
new AdvTextLayoutPresentationBinding(3, 0x17, 0x2000, 500, 0x1234, 45, 42),
|
||||||
liveHistory.GetPresentationBinding(3));
|
liveHistory.GetPresentationBinding(3));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
using Age.Engine.Model;
|
||||||
|
using Age.Engine.Text;
|
||||||
|
|
||||||
|
public class RetainedAdvTextLayoutPresentationTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void PublishesNativeRectsInHandleOrderAndStopsAtCapacity()
|
||||||
|
{
|
||||||
|
var binding = new AdvTextLayoutPresentationBinding(
|
||||||
|
1, 21, 100, 2, 900, 10, 20);
|
||||||
|
var presentation = new RetainedAdvTextLayoutPresentation(binding);
|
||||||
|
presentation.Append(
|
||||||
|
[
|
||||||
|
new AdvRetainedGlyphRecord(0, 1, 2, 5, 8),
|
||||||
|
new AdvRetainedGlyphRecord(0, 5, 2, 9, 8),
|
||||||
|
],
|
||||||
|
layoutOriginX: 30,
|
||||||
|
layoutOriginY: 40);
|
||||||
|
presentation.Append(
|
||||||
|
[new AdvRetainedGlyphRecord(0, 10, 3, 14, 9)],
|
||||||
|
layoutOriginX: 50,
|
||||||
|
layoutOriginY: 60);
|
||||||
|
var gfx = new GfxState();
|
||||||
|
gfx.CreateSurface(21);
|
||||||
|
|
||||||
|
Assert.Equal(2, presentation.PublishThrough(gfx, 3));
|
||||||
|
|
||||||
|
Assert.Collection(
|
||||||
|
gfx.SnapshotVisibleObjects(),
|
||||||
|
first => Assert.Equal(
|
||||||
|
(100L, 1, 2, 4, 6, 31, 42),
|
||||||
|
(first.Handle, first.SrcX, first.SrcY, first.W, first.H,
|
||||||
|
first.DstX, first.DstY)),
|
||||||
|
second => Assert.Equal(
|
||||||
|
(101L, 5, 2, 4, 6, 35, 42),
|
||||||
|
(second.Handle, second.SrcX, second.SrcY, second.W, second.H,
|
||||||
|
second.DstX, second.DstY)));
|
||||||
|
Assert.Null(gfx.TryGet(102));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RepublishRestoresPartialEraseWithoutAdvancingReveal()
|
||||||
|
{
|
||||||
|
var presentation = Presentation(capacity: 4);
|
||||||
|
var gfx = new GfxState();
|
||||||
|
gfx.CreateSurface(21);
|
||||||
|
presentation.PublishThrough(gfx, 2);
|
||||||
|
gfx.EraseRange(101, 1);
|
||||||
|
|
||||||
|
Assert.Single(gfx.SnapshotVisibleObjects());
|
||||||
|
Assert.Equal(2, presentation.Republish(gfx));
|
||||||
|
Assert.Equal(new long[] { 100, 101 },
|
||||||
|
gfx.SnapshotVisibleObjects().Select(item => item.Handle));
|
||||||
|
|
||||||
|
presentation.ErasePublished(gfx);
|
||||||
|
Assert.Empty(gfx.SnapshotVisibleObjects());
|
||||||
|
Assert.Equal(2, presentation.PublishedGlyphCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConsecutiveRunsRetainTheirOwnLayoutOrigins()
|
||||||
|
{
|
||||||
|
var binding = new AdvTextLayoutPresentationBinding(
|
||||||
|
7, 27, 200, 10, -1, 53, 10);
|
||||||
|
var presentation = new RetainedAdvTextLayoutPresentation(binding);
|
||||||
|
presentation.Append(
|
||||||
|
[new AdvRetainedGlyphRecord(0, 3, 4, 7, 10)],
|
||||||
|
275, 90);
|
||||||
|
presentation.Append(
|
||||||
|
[new AdvRetainedGlyphRecord(0, 7, 4, 11, 10)],
|
||||||
|
275, 135);
|
||||||
|
var gfx = new GfxState();
|
||||||
|
gfx.CreateSurface(27);
|
||||||
|
|
||||||
|
presentation.PublishThrough(gfx, 2);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
new[] { (278, 94), (282, 139) },
|
||||||
|
gfx.SnapshotVisibleObjects().Select(item => (item.DstX, item.DstY)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SavedFrameStyleReconstructionRebuildsTheSameBindings()
|
||||||
|
{
|
||||||
|
RetainedAdvTextLayoutPresentation before = Presentation(capacity: 4);
|
||||||
|
var gfx = new GfxState();
|
||||||
|
gfx.CreateSurface(21);
|
||||||
|
before.PublishThrough(gfx, 3);
|
||||||
|
RenderObject[] expected = gfx.SnapshotVisibleObjects().ToArray();
|
||||||
|
|
||||||
|
gfx.EraseRange(
|
||||||
|
before.Binding.FirstObjectHandle,
|
||||||
|
before.Binding.ObjectCapacity);
|
||||||
|
RetainedAdvTextLayoutPresentation reconstructed =
|
||||||
|
Presentation(capacity: 4);
|
||||||
|
reconstructed.PublishThrough(gfx, 3);
|
||||||
|
|
||||||
|
Assert.Equal(expected, gfx.SnapshotVisibleObjects());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RetainedAdvTextLayoutPresentation Presentation(int capacity)
|
||||||
|
{
|
||||||
|
var presentation = new RetainedAdvTextLayoutPresentation(
|
||||||
|
new AdvTextLayoutPresentationBinding(
|
||||||
|
1, 21, 100, capacity, -1, 10, 20));
|
||||||
|
presentation.Append(
|
||||||
|
[
|
||||||
|
new AdvRetainedGlyphRecord(0, 1, 2, 5, 8),
|
||||||
|
new AdvRetainedGlyphRecord(0, 5, 2, 9, 8),
|
||||||
|
new AdvRetainedGlyphRecord(0, 9, 2, 13, 8),
|
||||||
|
],
|
||||||
|
30,
|
||||||
|
40);
|
||||||
|
return presentation;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -95,6 +95,16 @@ internal class RecordingHost : IHost
|
|||||||
Lines.Add((run.SourceOffset, run.Text));
|
Lines.Add((run.SourceOffset, run.Text));
|
||||||
LiveTextRuns.Add((run, glyphDelayMilliseconds));
|
LiveTextRuns.Add((run, glyphDelayMilliseconds));
|
||||||
}
|
}
|
||||||
|
public Func<AdvLiveTextRun, AdvRetainedTextRunResult?>? OnRetainedText;
|
||||||
|
public virtual AdvRetainedTextRunResult? ShowText(
|
||||||
|
GfxState gfx,
|
||||||
|
AdvTextLayoutPresentationBinding binding,
|
||||||
|
AdvLiveTextRun run,
|
||||||
|
int glyphDelayMilliseconds)
|
||||||
|
{
|
||||||
|
ShowText(run, glyphDelayMilliseconds);
|
||||||
|
return OnRetainedText?.Invoke(run);
|
||||||
|
}
|
||||||
public void SetMessageGlyphDelayMilliseconds(int milliseconds)
|
public void SetMessageGlyphDelayMilliseconds(int milliseconds)
|
||||||
=> MessageGlyphDelayMilliseconds = milliseconds;
|
=> MessageGlyphDelayMilliseconds = milliseconds;
|
||||||
public RgbaImage? CaptureSurfacePixels(int slot)
|
public RgbaImage? CaptureSurfacePixels(int slot)
|
||||||
|
|||||||
@@ -64,6 +64,15 @@ public interface IHost
|
|||||||
void ShowText(int offset, string text);
|
void ShowText(int offset, string text);
|
||||||
void ShowText(AdvLiveTextRun run, int glyphDelayMilliseconds)
|
void ShowText(AdvLiveTextRun run, int glyphDelayMilliseconds)
|
||||||
=> ShowText(run.SourceOffset, run.Text);
|
=> ShowText(run.SourceOffset, run.Text);
|
||||||
|
AdvRetainedTextRunResult? ShowText(
|
||||||
|
GfxState gfx,
|
||||||
|
AdvTextLayoutPresentationBinding binding,
|
||||||
|
AdvLiveTextRun run,
|
||||||
|
int glyphDelayMilliseconds)
|
||||||
|
{
|
||||||
|
ShowText(run, glyphDelayMilliseconds);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
int MessageGlyphDelayMilliseconds => 50;
|
int MessageGlyphDelayMilliseconds => 50;
|
||||||
void SetMessageGlyphDelayMilliseconds(int milliseconds) { }
|
void SetMessageGlyphDelayMilliseconds(int milliseconds) { }
|
||||||
// Native ADV text subsystem: op 0x7a updates the selected layout's last 20-byte cursor record;
|
// Native ADV text subsystem: op 0x7a updates the selected layout's last 20-byte cursor record;
|
||||||
@@ -73,6 +82,9 @@ public interface IHost
|
|||||||
void DrawStringToSurface(int surfaceSlot, int x, int y, string text, AdvTextStyle style)
|
void DrawStringToSurface(int surfaceSlot, int x, int y, string text, AdvTextStyle style)
|
||||||
=> DrawStringToSurface(surfaceSlot, x, y, text);
|
=> DrawStringToSurface(surfaceSlot, x, y, text);
|
||||||
void ClearRenderedAdvTextLayout(int layoutSlot) { }
|
void ClearRenderedAdvTextLayout(int layoutSlot) { }
|
||||||
|
void ResetRenderedAdvTextLayout(
|
||||||
|
GfxState gfx, AdvTextLayoutPresentationBinding binding)
|
||||||
|
=> ClearRenderedAdvTextLayout(binding.LayoutSlot);
|
||||||
void RenderTextHistory(AdvTextHistoryRenderBatch batch) { }
|
void RenderTextHistory(AdvTextHistoryRenderBatch batch) { }
|
||||||
// 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.
|
||||||
@@ -87,10 +99,15 @@ public interface IHost
|
|||||||
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.
|
||||||
void PublishAdvTextLayout(int layoutSlot) { }
|
void PublishAdvTextLayout(int layoutSlot) { }
|
||||||
|
void PublishAdvTextLayout(
|
||||||
|
GfxState gfx, AdvTextLayoutPresentationBinding binding)
|
||||||
|
=> PublishAdvTextLayout(binding.LayoutSlot);
|
||||||
// Op 0x199 temporarily yields the active ADV page into its registered hide-window coroutine.
|
// Op 0x199 temporarily yields the active ADV page into its registered hide-window coroutine.
|
||||||
// The retained scene continues to render, but the text layout and its wait marker are suspended
|
// The retained scene continues to render, but the text layout and its wait marker are suspended
|
||||||
// until op 0x7c restores the saved page PC.
|
// until op 0x7c restores the saved page PC.
|
||||||
void SetAdvPagePresentationSuspended(bool suspended) { }
|
void SetAdvPagePresentationSuspended(bool suspended) { }
|
||||||
|
void SetAdvPagePresentationSuspended(GfxState gfx, bool suspended)
|
||||||
|
=> SetAdvPagePresentationSuspended(suspended);
|
||||||
void WaitForInput();
|
void WaitForInput();
|
||||||
void WaitForInput(int layoutSlot) => WaitForInput();
|
void WaitForInput(int layoutSlot) => WaitForInput();
|
||||||
// Interactive hosts service script callbacks on the VM thread while the enclosing ADV page remains
|
// Interactive hosts service script callbacks on the VM thread while the enclosing ADV page remains
|
||||||
|
|||||||
@@ -30,7 +30,21 @@ public readonly record struct AdvTextLayoutPresentationBinding(
|
|||||||
int SourceSurfaceSlot,
|
int SourceSurfaceSlot,
|
||||||
long FirstObjectHandle,
|
long FirstObjectHandle,
|
||||||
long ObjectCapacity,
|
long ObjectCapacity,
|
||||||
long WaitIndicatorObjectHandle);
|
long WaitIndicatorObjectHandle,
|
||||||
|
int ResetCursorX,
|
||||||
|
int ResetCursorY);
|
||||||
|
|
||||||
|
public readonly record struct AdvRetainedGlyphPlacement(
|
||||||
|
AdvRetainedGlyphRecord Record,
|
||||||
|
int LayoutOriginX,
|
||||||
|
int LayoutOriginY);
|
||||||
|
|
||||||
|
public readonly record struct AdvRetainedTextRunResult(
|
||||||
|
int LayoutSlot,
|
||||||
|
int FirstGlyphIndex,
|
||||||
|
int GlyphCount,
|
||||||
|
int CursorX,
|
||||||
|
int CursorY);
|
||||||
|
|
||||||
/// <summary>Platform-neutral rules established from AGE's native retained-glyph workers.</summary>
|
/// <summary>Platform-neutral rules established from AGE's native retained-glyph workers.</summary>
|
||||||
public static class AdvRetainedTextContract
|
public static class AdvRetainedTextContract
|
||||||
|
|||||||
@@ -201,7 +201,8 @@ public sealed class AdvTextHistory
|
|||||||
var layout = GetOrCreateLayout(slot);
|
var layout = GetOrCreateLayout(slot);
|
||||||
return new AdvTextLayoutPresentationBinding(
|
return new AdvTextLayoutPresentationBinding(
|
||||||
slot, checked(slot + 0x14), layout.TextObjectRangeFirst,
|
slot, checked(slot + 0x14), layout.TextObjectRangeFirst,
|
||||||
layout.TextObjectRangeCount, layout.WaitIndicatorObjectHandle);
|
layout.TextObjectRangeCount, layout.WaitIndicatorObjectHandle,
|
||||||
|
layout.ResetCursorX, layout.ResetCursorY);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
86
engine/Age.Engine/Text/RetainedAdvTextLayoutPresentation.cs
Normal file
86
engine/Age.Engine/Text/RetainedAdvTextLayoutPresentation.cs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
using Age.Engine.Model;
|
||||||
|
|
||||||
|
namespace Age.Engine.Text;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transient live-layout presentation state. History persistence owns neither this list nor its Gfx
|
||||||
|
/// bindings; saved-frame script reconstruction rebuilds both.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RetainedAdvTextLayoutPresentation
|
||||||
|
{
|
||||||
|
private readonly List<AdvRetainedGlyphPlacement> _glyphs = new();
|
||||||
|
|
||||||
|
public RetainedAdvTextLayoutPresentation(AdvTextLayoutPresentationBinding binding)
|
||||||
|
{
|
||||||
|
if (binding.LayoutSlot <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(binding));
|
||||||
|
if (binding.SourceSurfaceSlot < 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(binding));
|
||||||
|
if (binding.FirstObjectHandle < 0 || binding.ObjectCapacity <= 0)
|
||||||
|
throw new ArgumentException(
|
||||||
|
"A retained ADV layout requires a nonnegative first handle and positive capacity.",
|
||||||
|
nameof(binding));
|
||||||
|
Binding = binding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdvTextLayoutPresentationBinding Binding { get; }
|
||||||
|
public IReadOnlyList<AdvRetainedGlyphPlacement> Glyphs => _glyphs;
|
||||||
|
public int PublishedGlyphCount { get; private set; }
|
||||||
|
public int PublishableGlyphCount
|
||||||
|
=> (int)Math.Min(_glyphs.Count, Binding.ObjectCapacity);
|
||||||
|
|
||||||
|
public int Append(
|
||||||
|
IReadOnlyList<AdvRetainedGlyphRecord> records,
|
||||||
|
int layoutOriginX,
|
||||||
|
int layoutOriginY)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(records);
|
||||||
|
int first = _glyphs.Count;
|
||||||
|
foreach (AdvRetainedGlyphRecord record in records)
|
||||||
|
_glyphs.Add(new AdvRetainedGlyphPlacement(
|
||||||
|
record, layoutOriginX, layoutOriginY));
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Publish newly revealed glyphs through a layout-global target count.</summary>
|
||||||
|
public int PublishThrough(GfxState gfx, int targetGlyphCount)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(gfx);
|
||||||
|
int target = Math.Clamp(targetGlyphCount, 0, PublishableGlyphCount);
|
||||||
|
for (int index = PublishedGlyphCount; index < target; index++)
|
||||||
|
Bind(gfx, index);
|
||||||
|
PublishedGlyphCount = Math.Max(PublishedGlyphCount, target);
|
||||||
|
return PublishedGlyphCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Recreate every currently revealed binding after op 0x20a or temporary suspension.</summary>
|
||||||
|
public int Republish(GfxState gfx)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(gfx);
|
||||||
|
for (int index = 0; index < PublishedGlyphCount; index++)
|
||||||
|
Bind(gfx, index);
|
||||||
|
return PublishedGlyphCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ErasePublished(GfxState gfx)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(gfx);
|
||||||
|
gfx.EraseRange(Binding.FirstObjectHandle, Binding.ObjectCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Bind(GfxState gfx, int index)
|
||||||
|
{
|
||||||
|
AdvRetainedGlyphPlacement placement = _glyphs[index];
|
||||||
|
AdvRetainedGlyphRecord record = placement.Record;
|
||||||
|
if (record.Width <= 0 || record.Height <= 0) return;
|
||||||
|
gfx.BindDraw(
|
||||||
|
checked(Binding.FirstObjectHandle + index),
|
||||||
|
Binding.SourceSurfaceSlot,
|
||||||
|
record.Left,
|
||||||
|
record.Top,
|
||||||
|
record.Width,
|
||||||
|
record.Height,
|
||||||
|
checked(placement.LayoutOriginX + record.Left),
|
||||||
|
checked(placement.LayoutOriginY + record.Top));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1809,7 +1809,7 @@ public sealed class VirtualMachine
|
|||||||
{
|
{
|
||||||
_cur.CoroutineResumePc = pc + 1;
|
_cur.CoroutineResumePc = pc + 1;
|
||||||
_cur.CoroutineYieldActive = true;
|
_cur.CoroutineYieldActive = true;
|
||||||
_host.SetAdvPagePresentationSuspended(true);
|
_host.SetAdvPagePresentationSuspended(Gfx, true);
|
||||||
targetOffset = _cur.CoroutineYieldHandlerA;
|
targetOffset = _cur.CoroutineYieldHandlerA;
|
||||||
}
|
}
|
||||||
else targetOffset = _cur.CoroutineYieldHandlerB;
|
else targetOffset = _cur.CoroutineYieldHandlerB;
|
||||||
@@ -1824,7 +1824,7 @@ public sealed class VirtualMachine
|
|||||||
{
|
{
|
||||||
_cur.CoroutineResumePc = null;
|
_cur.CoroutineResumePc = null;
|
||||||
_cur.CoroutineYieldActive = false;
|
_cur.CoroutineYieldActive = false;
|
||||||
_host.SetAdvPagePresentationSuspended(false);
|
_host.SetAdvPagePresentationSuspended(Gfx, false);
|
||||||
return resumePc;
|
return resumePc;
|
||||||
}
|
}
|
||||||
return pc + 1; // cold bounded scene-entry path
|
return pc + 1; // cold bounded scene-entry path
|
||||||
@@ -1991,7 +1991,13 @@ public sealed class VirtualMachine
|
|||||||
var liveRun = new AdvLiveTextRun(
|
var liveRun = new AdvLiveTextRun(
|
||||||
off, TextHistory.GetLayoutSnapshot(layoutSlot), _advTextStyle, text, scriptStack);
|
off, TextHistory.GetLayoutSnapshot(layoutSlot), _advTextStyle, text, scriptStack);
|
||||||
TextHistory.AppendText(layoutSlot, off, text, _advTextStyle);
|
TextHistory.AppendText(layoutSlot, off, text, _advTextStyle);
|
||||||
_host.ShowText(liveRun, _messageGlyphDelayMilliseconds);
|
AdvTextLayoutPresentationBinding binding =
|
||||||
|
TextHistory.GetPresentationBinding(liveRun.Layout.Slot);
|
||||||
|
AdvRetainedTextRunResult? retained = _host.ShowText(
|
||||||
|
Gfx, binding, liveRun, _messageGlyphDelayMilliseconds);
|
||||||
|
if (retained is { } result)
|
||||||
|
TextHistory.SetCursor(
|
||||||
|
result.LayoutSlot, result.CursorX, result.CursorY);
|
||||||
}
|
}
|
||||||
return pc + 1;
|
return pc + 1;
|
||||||
case "define-adv-text-layout": // 0x70: configure layout and begin a logical retained group
|
case "define-adv-text-layout": // 0x70: configure layout and begin a logical retained group
|
||||||
@@ -2003,8 +2009,12 @@ public sealed class VirtualMachine
|
|||||||
int requestedSlot = (int)Read(a[0]);
|
int requestedSlot = (int)Read(a[0]);
|
||||||
TextHistory.ResetLayout(requestedSlot);
|
TextHistory.ResetLayout(requestedSlot);
|
||||||
var layout = TextHistory.GetLayoutSnapshot(requestedSlot);
|
var layout = TextHistory.GetLayoutSnapshot(requestedSlot);
|
||||||
|
AdvTextLayoutPresentationBinding binding =
|
||||||
|
TextHistory.GetPresentationBinding(layout.Slot);
|
||||||
|
if (binding.FirstObjectHandle >= 0 && binding.ObjectCapacity > 0)
|
||||||
|
Gfx.EraseRange(binding.FirstObjectHandle, binding.ObjectCapacity);
|
||||||
_host.SetAdvTextCursor(layout.Slot, layout.CursorX, layout.CursorY);
|
_host.SetAdvTextCursor(layout.Slot, layout.CursorX, layout.CursorY);
|
||||||
_host.ClearRenderedAdvTextLayout(layout.Slot);
|
_host.ResetRenderedAdvTextLayout(Gfx, binding);
|
||||||
_cur.ReadMessageOffset = ins.Offset;
|
_cur.ReadMessageOffset = ins.Offset;
|
||||||
_sharedProfile.ReadText.CommitPending();
|
_sharedProfile.ReadText.CommitPending();
|
||||||
RefreshAdvReadSkipState();
|
RefreshAdvReadSkipState();
|
||||||
@@ -2031,8 +2041,13 @@ public sealed class VirtualMachine
|
|||||||
return pc + 1;
|
return pc + 1;
|
||||||
case "u00420CE0":
|
case "u00420CE0":
|
||||||
case "publish-adv-text-layout": // 0x20a: publish layout and current marker frame if active
|
case "publish-adv-text-layout": // 0x20a: publish layout and current marker frame if active
|
||||||
_host.PublishAdvTextLayout((int)Read(a[0]));
|
{
|
||||||
|
int requestedSlot = (int)Read(a[0]);
|
||||||
|
AdvTextLayoutPresentationBinding binding =
|
||||||
|
TextHistory.GetPresentationBinding(requestedSlot);
|
||||||
|
_host.PublishAdvTextLayout(Gfx, binding);
|
||||||
return pc + 1;
|
return pc + 1;
|
||||||
|
}
|
||||||
case "draw-string": // 0x204 (surface slot, x, y, string)
|
case "draw-string": // 0x204 (surface slot, x, y, string)
|
||||||
_host.DrawStringToSurface((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), ReadStr(a[3]),
|
_host.DrawStringToSurface((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), ReadStr(a[3]),
|
||||||
_advTextStyle);
|
_advTextStyle);
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
private readonly Dictionary<int, List<SurfaceTextDraw>> _surfaceText = new();
|
private readonly Dictionary<int, List<SurfaceTextDraw>> _surfaceText = new();
|
||||||
private readonly CachedGlyphMaskRasterizer? _surfaceTextMaskCache;
|
private readonly CachedGlyphMaskRasterizer? _surfaceTextMaskCache;
|
||||||
private readonly ImmediateSurfaceTextRenderer? _surfaceTextPixelRenderer;
|
private readonly ImmediateSurfaceTextRenderer? _surfaceTextPixelRenderer;
|
||||||
|
private readonly RetainedGlyphLayoutEngine? _retainedGlyphLayoutEngine;
|
||||||
private readonly GlyphRasterizerBackendInfo? _surfaceTextBackendInfo;
|
private readonly GlyphRasterizerBackendInfo? _surfaceTextBackendInfo;
|
||||||
private string _surfaceTextFallbackReason;
|
private string _surfaceTextFallbackReason;
|
||||||
private bool _surfaceTextFallbackWarningReported;
|
private bool _surfaceTextFallbackWarningReported;
|
||||||
@@ -77,9 +78,15 @@ public sealed class GodotAdvHost : IHost
|
|||||||
public required AdvLiveTextRun Run;
|
public required AdvLiveTextRun Run;
|
||||||
public required long StartedMs;
|
public required long StartedMs;
|
||||||
public required int GlyphDelayMilliseconds;
|
public required int GlyphDelayMilliseconds;
|
||||||
|
public bool RetainedPixels;
|
||||||
|
public int FirstGlyphIndex;
|
||||||
|
public int GlyphCount;
|
||||||
}
|
}
|
||||||
private readonly List<LiveTextState> _liveText = new();
|
private readonly List<LiveTextState> _liveText = new();
|
||||||
|
private readonly Dictionary<int, RetainedAdvTextLayoutPresentation>
|
||||||
|
_retainedTextLayouts = new();
|
||||||
private LiveTextState? _activeLiveText;
|
private LiveTextState? _activeLiveText;
|
||||||
|
private int _suspendedRetainedTextLayoutSlot;
|
||||||
private string _advText = "";
|
private string _advText = "";
|
||||||
private int _advTextX = 100, _advTextY = 47;
|
private int _advTextX = 100, _advTextY = 47;
|
||||||
private int _currentAdvLayout = 1; // SYSTEM4's ordinary SC0000 ADV layout
|
private int _currentAdvLayout = 1; // SYSTEM4's ordinary SC0000 ADV layout
|
||||||
@@ -143,6 +150,8 @@ public sealed class GodotAdvHost : IHost
|
|||||||
surfaceTextRasterizer, capacity: 2048);
|
surfaceTextRasterizer, capacity: 2048);
|
||||||
_surfaceTextPixelRenderer =
|
_surfaceTextPixelRenderer =
|
||||||
new ImmediateSurfaceTextRenderer(_surfaceTextMaskCache);
|
new ImmediateSurfaceTextRenderer(_surfaceTextMaskCache);
|
||||||
|
_retainedGlyphLayoutEngine =
|
||||||
|
new RetainedGlyphLayoutEngine(_surfaceTextMaskCache);
|
||||||
}
|
}
|
||||||
_surfaceTextFallbackReason = surfaceTextFallbackReason
|
_surfaceTextFallbackReason = surfaceTextFallbackReason
|
||||||
?? (surfaceTextRasterizer == null
|
?? (surfaceTextRasterizer == null
|
||||||
@@ -330,6 +339,205 @@ public sealed class GodotAdvHost : IHost
|
|||||||
_timeline?.State("running", new() { ["text_reveal_complete"] = true });
|
_timeline?.State("running", new() { ["text_reveal_complete"] = true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AdvRetainedTextRunResult? ShowText(
|
||||||
|
GfxState gfx,
|
||||||
|
AdvTextLayoutPresentationBinding binding,
|
||||||
|
AdvLiveTextRun run,
|
||||||
|
int glyphDelayMilliseconds)
|
||||||
|
{
|
||||||
|
if (_retainedGlyphLayoutEngine == null
|
||||||
|
|| binding.LayoutSlot != run.Layout.Slot
|
||||||
|
|| binding.FirstObjectHandle < 0
|
||||||
|
|| binding.ObjectCapacity <= 0
|
||||||
|
|| !TryPrepareRetainedTextRun(
|
||||||
|
gfx, binding, run,
|
||||||
|
out RetainedAdvTextLayoutPresentation? presentation,
|
||||||
|
out AdvRetainedTextRunResult result))
|
||||||
|
{
|
||||||
|
ShowText(run, glyphDelayMilliseconds);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Captured.Add((run.SourceOffset, run.Text));
|
||||||
|
_locator.Text(run.SourceOffset, run.Text);
|
||||||
|
int delay = System.Math.Max(0, glyphDelayMilliseconds);
|
||||||
|
int revealGlyphCount = System.Math.Max(
|
||||||
|
0, System.Math.Min(
|
||||||
|
result.GlyphCount,
|
||||||
|
presentation.PublishableGlyphCount - result.FirstGlyphIndex));
|
||||||
|
var state = new LiveTextState
|
||||||
|
{
|
||||||
|
Run = run,
|
||||||
|
StartedMs = _clock.NowMs,
|
||||||
|
GlyphDelayMilliseconds = delay,
|
||||||
|
RetainedPixels = true,
|
||||||
|
FirstGlyphIndex = result.FirstGlyphIndex,
|
||||||
|
GlyphCount = revealGlyphCount,
|
||||||
|
};
|
||||||
|
lock (_textLock)
|
||||||
|
{
|
||||||
|
_liveText.Add(state);
|
||||||
|
_activeLiveText = state;
|
||||||
|
_advText = run.Text;
|
||||||
|
_advTextX = run.Layout.CursorX;
|
||||||
|
_advTextY = run.Layout.CursorY;
|
||||||
|
_currentAdvLayout = run.Layout.Slot;
|
||||||
|
_advTextStartedMs = state.StartedMs;
|
||||||
|
_activeGlyphDelayMilliseconds = delay;
|
||||||
|
_advTextForceComplete = _messageSkipActive;
|
||||||
|
IsTextRevealing =
|
||||||
|
revealGlyphCount > 0 && delay > 0 && !_messageSkipActive;
|
||||||
|
}
|
||||||
|
_timeline?.State("text-reveal", new()
|
||||||
|
{
|
||||||
|
["offset"] = $"0x{run.SourceOffset:x}",
|
||||||
|
["layout"] = run.Layout.Slot,
|
||||||
|
["x"] = run.Layout.OriginX + run.Layout.CursorX,
|
||||||
|
["y"] = run.Layout.OriginY + run.Layout.CursorY,
|
||||||
|
["glyphs"] = revealGlyphCount,
|
||||||
|
["records"] = result.GlyphCount,
|
||||||
|
["delay_ms"] = delay,
|
||||||
|
["presentation"] = "retained-glyphs",
|
||||||
|
});
|
||||||
|
|
||||||
|
int initiallyVisible = IsTextRevealing ? 1 : revealGlyphCount;
|
||||||
|
presentation.PublishThrough(
|
||||||
|
gfx, checked(result.FirstGlyphIndex + initiallyVisible));
|
||||||
|
Interlocked.Exchange(ref _presentRequested, 1);
|
||||||
|
if (!IsTextRevealing)
|
||||||
|
{
|
||||||
|
_timeline?.State("running", new() { ["text_reveal_complete"] = true });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool scriptSuspended = SuspendScriptForPresentation();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RequestSynchronizedPresentation();
|
||||||
|
while (IsTextRevealing && !_stopping)
|
||||||
|
{
|
||||||
|
int visible;
|
||||||
|
lock (_textLock)
|
||||||
|
{
|
||||||
|
visible = _advTextForceComplete
|
||||||
|
? revealGlyphCount
|
||||||
|
: (int)System.Math.Clamp(
|
||||||
|
(_clock.NowMs - state.StartedMs) / delay + 1,
|
||||||
|
0, revealGlyphCount);
|
||||||
|
if (visible >= revealGlyphCount) IsTextRevealing = false;
|
||||||
|
}
|
||||||
|
int before = presentation.PublishedGlyphCount;
|
||||||
|
presentation.PublishThrough(
|
||||||
|
gfx, checked(result.FirstGlyphIndex + visible));
|
||||||
|
if (presentation.PublishedGlyphCount != before)
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _presentRequested, 1);
|
||||||
|
RequestSynchronizedPresentation();
|
||||||
|
}
|
||||||
|
if (IsTextRevealing) _frameSignal.WaitOne(50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ResumeScriptAfterPresentation(scriptSuspended);
|
||||||
|
}
|
||||||
|
_timeline?.State("running", new() { ["text_reveal_complete"] = true });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryPrepareRetainedTextRun(
|
||||||
|
GfxState gfx,
|
||||||
|
AdvTextLayoutPresentationBinding binding,
|
||||||
|
AdvLiveTextRun run,
|
||||||
|
out RetainedAdvTextLayoutPresentation presentation,
|
||||||
|
out AdvRetainedTextRunResult result)
|
||||||
|
{
|
||||||
|
presentation = null!;
|
||||||
|
result = default;
|
||||||
|
if (run.Layout.Width <= 0 || run.Layout.Height <= 0)
|
||||||
|
{
|
||||||
|
ReportSurfaceTextFallback(
|
||||||
|
$"ADV layout {run.Layout.Slot} has invalid dimensions " +
|
||||||
|
$"{run.Layout.Width}x{run.Layout.Height}.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RgbaImage destination = ResolveSurfacePixels(binding.SourceSurfaceSlot)
|
||||||
|
?? new RgbaImage(
|
||||||
|
run.Layout.Width,
|
||||||
|
run.Layout.Height,
|
||||||
|
new byte[checked(run.Layout.Width * run.Layout.Height * 4)]);
|
||||||
|
if (destination.Width != run.Layout.Width
|
||||||
|
|| destination.Height != run.Layout.Height)
|
||||||
|
{
|
||||||
|
ReportSurfaceTextFallback(
|
||||||
|
$"ADV layout {run.Layout.Slot} surface {binding.SourceSurfaceSlot} is " +
|
||||||
|
$"{destination.Width}x{destination.Height}; expected " +
|
||||||
|
$"{run.Layout.Width}x{run.Layout.Height}.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var updated = new RgbaImage(
|
||||||
|
destination.Width, destination.Height, (byte[])destination.Pixels.Clone());
|
||||||
|
GlyphTextLayoutResult rendered;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IReadOnlyList<GlyphRasterRequest> requests =
|
||||||
|
ImmediateSurfaceTextRenderer.CreateRequests(run.Text, run.Style);
|
||||||
|
rendered = _retainedGlyphLayoutEngine!.Render(
|
||||||
|
updated,
|
||||||
|
new GlyphTextLayoutOptions(
|
||||||
|
run.Layout.CursorX,
|
||||||
|
run.Layout.CursorY,
|
||||||
|
binding.ResetCursorX,
|
||||||
|
run.Layout.Right,
|
||||||
|
run.Layout.Bottom,
|
||||||
|
WrapHorizontally: true,
|
||||||
|
run.Style),
|
||||||
|
requests);
|
||||||
|
}
|
||||||
|
catch (Exception error) when (
|
||||||
|
error is ArgumentException
|
||||||
|
or InvalidOperationException
|
||||||
|
or PlatformNotSupportedException
|
||||||
|
or System.ComponentModel.Win32Exception)
|
||||||
|
{
|
||||||
|
ReportSurfaceTextFallback(
|
||||||
|
$"{error.GetType().Name}: {error.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_textLock)
|
||||||
|
{
|
||||||
|
if (!_retainedTextLayouts.TryGetValue(
|
||||||
|
binding.LayoutSlot, out presentation!))
|
||||||
|
{
|
||||||
|
presentation = new RetainedAdvTextLayoutPresentation(binding);
|
||||||
|
_retainedTextLayouts.Add(binding.LayoutSlot, presentation);
|
||||||
|
}
|
||||||
|
else if (presentation.Binding != binding)
|
||||||
|
{
|
||||||
|
ReportSurfaceTextFallback(
|
||||||
|
$"ADV layout {binding.LayoutSlot} changed its retained binding without reset.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int first = presentation.Append(
|
||||||
|
rendered.Records, run.Layout.OriginX, run.Layout.OriginY);
|
||||||
|
result = new AdvRetainedTextRunResult(
|
||||||
|
binding.LayoutSlot,
|
||||||
|
first,
|
||||||
|
rendered.ConsumedGlyphs,
|
||||||
|
rendered.CursorX,
|
||||||
|
rendered.CursorY);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_imageLock)
|
||||||
|
_surfaceImages[binding.SourceSurfaceSlot] = updated;
|
||||||
|
_slotDims[binding.SourceSurfaceSlot] = (updated.Width, updated.Height);
|
||||||
|
gfx.CreateSurface(binding.SourceSurfaceSlot);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public void SetAdvTextCursor(int layoutSlot, int x, int y)
|
public void SetAdvTextCursor(int layoutSlot, int x, int y)
|
||||||
{
|
{
|
||||||
lock (_textLock)
|
lock (_textLock)
|
||||||
@@ -450,17 +658,17 @@ public sealed class GodotAdvHost : IHost
|
|||||||
{
|
{
|
||||||
lock (_textLock)
|
lock (_textLock)
|
||||||
{
|
{
|
||||||
var snapshot = new LiveAdvTextSnapshot[_liveText.Count];
|
var snapshot = new List<LiveAdvTextSnapshot>(_liveText.Count);
|
||||||
for (int i = 0; i < _liveText.Count; i++)
|
foreach (LiveTextState state in _liveText)
|
||||||
{
|
{
|
||||||
var state = _liveText[i];
|
if (state.RetainedPixels) continue;
|
||||||
bool revealing = ReferenceEquals(state, _activeLiveText) && IsTextRevealing;
|
bool revealing = ReferenceEquals(state, _activeLiveText) && IsTextRevealing;
|
||||||
int visible = !revealing || _advTextForceComplete || state.GlyphDelayMilliseconds == 0
|
int visible = !revealing || _advTextForceComplete || state.GlyphDelayMilliseconds == 0
|
||||||
? state.Run.Text.Length
|
? state.Run.Text.Length
|
||||||
: (int)System.Math.Clamp(
|
: (int)System.Math.Clamp(
|
||||||
(_clock.NowMs - state.StartedMs) / state.GlyphDelayMilliseconds + 1,
|
(_clock.NowMs - state.StartedMs) / state.GlyphDelayMilliseconds + 1,
|
||||||
0, state.Run.Text.Length);
|
0, state.Run.Text.Length);
|
||||||
snapshot[i] = new LiveAdvTextSnapshot(state.Run, visible, revealing);
|
snapshot.Add(new LiveAdvTextSnapshot(state.Run, visible, revealing));
|
||||||
}
|
}
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
@@ -502,6 +710,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
{
|
{
|
||||||
int slot = layoutSlot == 0 ? _currentAdvLayout : layoutSlot;
|
int slot = layoutSlot == 0 ? _currentAdvLayout : layoutSlot;
|
||||||
_historyText.Remove(slot);
|
_historyText.Remove(slot);
|
||||||
|
if (_retainedTextLayouts.ContainsKey(slot)) return;
|
||||||
_liveText.RemoveAll(state => state.Run.Layout.Slot == slot);
|
_liveText.RemoveAll(state => state.Run.Layout.Slot == slot);
|
||||||
if (_activeLiveText?.Run.Layout.Slot == slot)
|
if (_activeLiveText?.Run.Layout.Slot == slot)
|
||||||
{
|
{
|
||||||
@@ -513,6 +722,47 @@ public sealed class GodotAdvHost : IHost
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ResetRenderedAdvTextLayout(
|
||||||
|
GfxState gfx, AdvTextLayoutPresentationBinding binding)
|
||||||
|
{
|
||||||
|
lock (_textLock)
|
||||||
|
{
|
||||||
|
_historyText.Remove(binding.LayoutSlot);
|
||||||
|
_retainedTextLayouts.Remove(binding.LayoutSlot);
|
||||||
|
_liveText.RemoveAll(
|
||||||
|
state => state.Run.Layout.Slot == binding.LayoutSlot);
|
||||||
|
if (_activeLiveText?.Run.Layout.Slot == binding.LayoutSlot)
|
||||||
|
{
|
||||||
|
_activeLiveText = null;
|
||||||
|
_advText = "";
|
||||||
|
_advTextForceComplete = false;
|
||||||
|
IsTextRevealing = false;
|
||||||
|
}
|
||||||
|
if (_suspendedRetainedTextLayoutSlot == binding.LayoutSlot)
|
||||||
|
_suspendedRetainedTextLayoutSlot = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_slotDims.TryGetValue(
|
||||||
|
binding.SourceSurfaceSlot, out var dimensions)
|
||||||
|
&& dimensions.W > 0
|
||||||
|
&& dimensions.H > 0)
|
||||||
|
{
|
||||||
|
lock (_imageLock)
|
||||||
|
_surfaceImages[binding.SourceSurfaceSlot] = new RgbaImage(
|
||||||
|
dimensions.W,
|
||||||
|
dimensions.H,
|
||||||
|
new byte[checked(dimensions.W * dimensions.H * 4)]);
|
||||||
|
}
|
||||||
|
Interlocked.Exchange(ref _presentRequested, 1);
|
||||||
|
_timeline?.Event("adv-text-layout-reset", new()
|
||||||
|
{
|
||||||
|
["layout"] = binding.LayoutSlot,
|
||||||
|
["surface"] = binding.SourceSurfaceSlot,
|
||||||
|
["first_handle"] = binding.FirstObjectHandle,
|
||||||
|
["capacity"] = binding.ObjectCapacity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public void RenderTextHistory(AdvTextHistoryRenderBatch batch)
|
public void RenderTextHistory(AdvTextHistoryRenderBatch batch)
|
||||||
{
|
{
|
||||||
lock (_textLock) _historyText[batch.LayoutSlot] = batch;
|
lock (_textLock) _historyText[batch.LayoutSlot] = batch;
|
||||||
@@ -666,6 +916,17 @@ public sealed class GodotAdvHost : IHost
|
|||||||
_timeline?.Event("adv-text-layout-publish", new() { ["layout"] = layoutSlot });
|
_timeline?.Event("adv-text-layout-publish", new() { ["layout"] = layoutSlot });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void PublishAdvTextLayout(
|
||||||
|
GfxState gfx, AdvTextLayoutPresentationBinding binding)
|
||||||
|
{
|
||||||
|
lock (_textLock)
|
||||||
|
if (_retainedTextLayouts.TryGetValue(
|
||||||
|
binding.LayoutSlot, out RetainedAdvTextLayoutPresentation? presentation)
|
||||||
|
&& presentation.Binding == binding)
|
||||||
|
presentation.Republish(gfx);
|
||||||
|
PublishAdvTextLayout(binding.LayoutSlot);
|
||||||
|
}
|
||||||
|
|
||||||
public AdvWaitIndicatorSnapshot? SnapshotAdvWaitIndicator()
|
public AdvWaitIndicatorSnapshot? SnapshotAdvWaitIndicator()
|
||||||
{
|
{
|
||||||
if (!IsWaiting || _advPagePresentationSuspended) return null;
|
if (!IsWaiting || _advPagePresentationSuspended) return null;
|
||||||
@@ -894,6 +1155,35 @@ public sealed class GodotAdvHost : IHost
|
|||||||
_timeline?.State(suspended ? "adv-page-suspended" : "adv-page-restored", new());
|
_timeline?.State(suspended ? "adv-page-suspended" : "adv-page-restored", new());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetAdvPagePresentationSuspended(GfxState gfx, bool suspended)
|
||||||
|
{
|
||||||
|
lock (_textLock)
|
||||||
|
{
|
||||||
|
if (suspended)
|
||||||
|
{
|
||||||
|
int slot = IsWaiting && _activeWaitLayout != 0
|
||||||
|
? _activeWaitLayout
|
||||||
|
: _currentAdvLayout;
|
||||||
|
if (_retainedTextLayouts.TryGetValue(
|
||||||
|
slot, out RetainedAdvTextLayoutPresentation? presentation))
|
||||||
|
{
|
||||||
|
presentation.ErasePublished(gfx);
|
||||||
|
_suspendedRetainedTextLayoutSlot = slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (_suspendedRetainedTextLayoutSlot != 0)
|
||||||
|
{
|
||||||
|
if (_retainedTextLayouts.TryGetValue(
|
||||||
|
_suspendedRetainedTextLayoutSlot,
|
||||||
|
out RetainedAdvTextLayoutPresentation? presentation))
|
||||||
|
presentation.Republish(gfx);
|
||||||
|
_suspendedRetainedTextLayoutSlot = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Interlocked.Exchange(ref _presentRequested, 1);
|
||||||
|
SetAdvPagePresentationSuspended(suspended);
|
||||||
|
}
|
||||||
|
|
||||||
public void InputCallbackCompleted(GfxState gfx)
|
public void InputCallbackCompleted(GfxState gfx)
|
||||||
{
|
{
|
||||||
// Callback completion itself is not native graphics dirtiness. Any retained writes made by the
|
// Callback completion itself is not native graphics dirtiness. Any retained writes made by the
|
||||||
@@ -1200,7 +1490,9 @@ public sealed class GodotAdvHost : IHost
|
|||||||
_surfaceResources.Clear();
|
_surfaceResources.Clear();
|
||||||
_historyText.Clear();
|
_historyText.Clear();
|
||||||
_liveText.Clear();
|
_liveText.Clear();
|
||||||
|
_retainedTextLayouts.Clear();
|
||||||
_activeLiveText = null;
|
_activeLiveText = null;
|
||||||
|
_suspendedRetainedTextLayoutSlot = 0;
|
||||||
_advText = "";
|
_advText = "";
|
||||||
_advTextX = 100;
|
_advTextX = 100;
|
||||||
_advTextY = 47;
|
_advTextY = 47;
|
||||||
@@ -1839,6 +2131,13 @@ 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 >= firstSlot
|
||||||
|
&& pair.Value.Binding.SourceSurfaceSlot < end)
|
||||||
|
.Select(pair => pair.Key)
|
||||||
|
.ToArray())
|
||||||
|
_retainedTextLayouts.Remove(layoutSlot);
|
||||||
}
|
}
|
||||||
foreach (MovieSurfaceBinding binding in stoppedMovies)
|
foreach (MovieSurfaceBinding binding in stoppedMovies)
|
||||||
{
|
{
|
||||||
|
|||||||
102
godot/Main.cs
102
godot/Main.cs
@@ -2541,6 +2541,75 @@ public partial class Main : Godot.Control
|
|||||||
}
|
}
|
||||||
_host.ReleaseSurface(996);
|
_host.ReleaseSurface(996);
|
||||||
_host.ReleaseSurface(997);
|
_host.ReleaseSurface(997);
|
||||||
|
bool liveRetainedTextOk;
|
||||||
|
string liveRetainedTextMode;
|
||||||
|
AdvTextLayoutPresentationBinding liveBinding =
|
||||||
|
_vm.TextHistory.GetPresentationBinding(1);
|
||||||
|
if (_host.UsesSurfaceTextPixels)
|
||||||
|
{
|
||||||
|
static bool InLiveRange(
|
||||||
|
RenderObject item, AdvTextLayoutPresentationBinding binding)
|
||||||
|
=> item.Handle >= binding.FirstObjectHandle
|
||||||
|
&& item.Handle - binding.FirstObjectHandle < binding.ObjectCapacity;
|
||||||
|
IReadOnlyList<RenderObject> liveObjects = _vm.Gfx.SnapshotVisibleObjects()
|
||||||
|
.Where(item => InLiveRange(item, liveBinding))
|
||||||
|
.ToArray();
|
||||||
|
RgbaImage? liveSurface =
|
||||||
|
_host.CaptureSurfacePixels(liveBinding.SourceSurfaceSlot);
|
||||||
|
bool builtCompleteLine =
|
||||||
|
liveObjects.Count == "HelloWorldSub".Length
|
||||||
|
&& liveObjects.Select(item => item.Handle)
|
||||||
|
.SequenceEqual(Enumerable.Range(0, liveObjects.Count)
|
||||||
|
.Select(index => liveBinding.FirstObjectHandle + index))
|
||||||
|
&& liveSurface != null
|
||||||
|
&& liveSurface.Pixels.Where((_, index) => index % 4 == 3)
|
||||||
|
.Any(alpha => alpha != 0)
|
||||||
|
&& _host.SnapshotLiveAdvText().Count == 0
|
||||||
|
&& _vm.TextHistory.GetLayoutSnapshot(1).CursorX
|
||||||
|
> liveBinding.ResetCursorX;
|
||||||
|
|
||||||
|
_vm.Gfx.EraseRange(liveBinding.FirstObjectHandle + 1, 1);
|
||||||
|
_host.PublishAdvTextLayout(_vm.Gfx, liveBinding);
|
||||||
|
bool republishedPartialErase =
|
||||||
|
_vm.Gfx.SnapshotVisibleObjects()
|
||||||
|
.Count(item => InLiveRange(item, liveBinding))
|
||||||
|
== liveObjects.Count;
|
||||||
|
|
||||||
|
_host.SetAdvPagePresentationSuspended(_vm.Gfx, true);
|
||||||
|
bool suspended =
|
||||||
|
!_vm.Gfx.SnapshotVisibleObjects()
|
||||||
|
.Any(item => InLiveRange(item, liveBinding));
|
||||||
|
_host.SetAdvPagePresentationSuspended(_vm.Gfx, false);
|
||||||
|
bool restored =
|
||||||
|
_vm.Gfx.SnapshotVisibleObjects()
|
||||||
|
.Count(item => InLiveRange(item, liveBinding))
|
||||||
|
== liveObjects.Count;
|
||||||
|
|
||||||
|
_vm.Gfx.EraseRange(
|
||||||
|
liveBinding.FirstObjectHandle, liveBinding.ObjectCapacity);
|
||||||
|
_host.ResetRenderedAdvTextLayout(_vm.Gfx, liveBinding);
|
||||||
|
RgbaImage? resetSurface =
|
||||||
|
_host.CaptureSurfacePixels(liveBinding.SourceSurfaceSlot);
|
||||||
|
bool reset =
|
||||||
|
resetSurface != null
|
||||||
|
&& resetSurface.Pixels.All(value => value == 0)
|
||||||
|
&& !_vm.Gfx.SnapshotVisibleObjects()
|
||||||
|
.Any(item => InLiveRange(item, liveBinding));
|
||||||
|
liveRetainedTextOk =
|
||||||
|
builtCompleteLine
|
||||||
|
&& republishedPartialErase
|
||||||
|
&& suspended
|
||||||
|
&& restored
|
||||||
|
&& reset;
|
||||||
|
liveRetainedTextMode = "retained-glyphs";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
liveRetainedTextOk =
|
||||||
|
_host.SnapshotLiveAdvText().Count == 3
|
||||||
|
&& !string.IsNullOrWhiteSpace(_host.SurfaceTextFallbackReason);
|
||||||
|
liveRetainedTextMode = "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
|
||||||
@@ -2580,6 +2649,7 @@ public partial class Main : Godot.Control
|
|||||||
&& bgmReplacementCancelsFade && bgmOneShotModeOk && bgmLoopModeOk
|
&& bgmReplacementCancelsFade && bgmOneShotModeOk && bgmLoopModeOk
|
||||||
&& bgmStopReleaseOk && textEffectModesOk && fontCalibrationOk
|
&& bgmStopReleaseOk && textEffectModesOk && fontCalibrationOk
|
||||||
&& immediateSurfaceTextOk
|
&& immediateSurfaceTextOk
|
||||||
|
&& liveRetainedTextOk
|
||||||
&& 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); " +
|
||||||
@@ -2588,6 +2658,7 @@ public partial class Main : Godot.Control
|
|||||||
$"bgm-fade-replacement=ok; bgm-start-modes-stop=ok; " +
|
$"bgm-fade-replacement=ok; bgm-start-modes-stop=ok; " +
|
||||||
$"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}; " +
|
||||||
$"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}");
|
||||||
@@ -2600,6 +2671,7 @@ public partial class Main : Godot.Control
|
|||||||
$"bgm-stop-release={bgmStopReleaseOk}; " +
|
$"bgm-stop-release={bgmStopReleaseOk}; " +
|
||||||
$"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}); " +
|
||||||
$"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}");
|
||||||
@@ -2610,7 +2682,7 @@ public partial class Main : Godot.Control
|
|||||||
// nested call-script into a synthetic subroutine (exercises call-script handling), shared globals.
|
// nested call-script into a synthetic subroutine (exercises call-script handling), shared globals.
|
||||||
private static (Script, IScriptProvider) BuildSelfTestScene(OpcodeTable table)
|
private static (Script, IScriptProvider) BuildSelfTestScene(OpcodeTable table)
|
||||||
{
|
{
|
||||||
(int, Operand[]) ShowText(int s) => (0x6e, new[] { new Operand(2, s), new Operand(0, 0) });
|
(int, Operand[]) ShowText(int s) => (0x6e, new[] { new Operand(0, 0), new Operand(2, s) });
|
||||||
(int, Operand[]) Wait() => (0x72, new[] { new Operand(0, 0) });
|
(int, Operand[]) Wait() => (0x72, new[] { new Operand(0, 0) });
|
||||||
(int, Operand[]) CallScript(long id) => (0x3, new[] { new Operand(0, id) });
|
(int, Operand[]) CallScript(long id) => (0x3, new[] { new Operand(0, id) });
|
||||||
(int, Operand[]) MovGG(int d, int s) => (0x55, new[] { new Operand(3, d), new Operand(3, s) });
|
(int, Operand[]) MovGG(int d, int s) => (0x55, new[] { new Operand(3, d), new Operand(3, s) });
|
||||||
@@ -2620,7 +2692,33 @@ public partial class Main : Godot.Control
|
|||||||
var callee = ScriptAssembler.Assemble(table, "SUBSCENE",
|
var callee = ScriptAssembler.Assemble(table, "SUBSCENE",
|
||||||
new List<(int, Operand[])> { ShowText(0), MovGI(0x31, 42), Exit() }, new[] { "Sub" });
|
new List<(int, Operand[])> { ShowText(0), MovGI(0x31, 42), Exit() }, new[] { "Sub" });
|
||||||
var caller = ScriptAssembler.Assemble(table, "SELFTEST",
|
var caller = ScriptAssembler.Assemble(table, "SELFTEST",
|
||||||
new List<(int, Operand[])> { ShowText(0), Wait(), ShowText(1), CallScript(5), MovGG(0x30, 0x31), Exit() },
|
new List<(int, Operand[])>
|
||||||
|
{
|
||||||
|
(0x70, new[]
|
||||||
|
{
|
||||||
|
new Operand(0, 1), new Operand(0, 512), new Operand(0, 64),
|
||||||
|
new Operand(0, 0), new Operand(0, 0),
|
||||||
|
}),
|
||||||
|
(0x79, new[]
|
||||||
|
{
|
||||||
|
new Operand(0, 1), new Operand(0, 1), new Operand(0, 1),
|
||||||
|
}),
|
||||||
|
(0x71, new[] { new Operand(0, 1) }),
|
||||||
|
(0x1c1, new[]
|
||||||
|
{
|
||||||
|
new Operand(0, 1), new Operand(0, 511), new Operand(0, 63),
|
||||||
|
}),
|
||||||
|
(0x213, new[]
|
||||||
|
{
|
||||||
|
new Operand(0, 1), new Operand(0, 700000), new Operand(0, 64),
|
||||||
|
}),
|
||||||
|
ShowText(0),
|
||||||
|
Wait(),
|
||||||
|
ShowText(1),
|
||||||
|
CallScript(5),
|
||||||
|
MovGG(0x30, 0x31),
|
||||||
|
Exit(),
|
||||||
|
},
|
||||||
new[] { "Hello", "World" });
|
new[] { "Hello", "World" });
|
||||||
return (caller, new SelfTestProvider(callee));
|
return (caller, new SelfTestProvider(callee));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user