Split Main self-test into partial class
This commit is contained in:
@@ -135,6 +135,10 @@ The Godot deliverable includes `Himegari.sln` because Godot's .NET exporter requ
|
|||||||
the project do not enter its import or export scan. `tools/export-linux-x64.ps1` produces and validates the
|
the project do not enter its import or export scan. `tools/export-linux-x64.ps1` produces and validates the
|
||||||
complete disposable artifact under `build/export/linux-x64/`.
|
complete disposable artifact under `build/export/linux-x64/`.
|
||||||
|
|
||||||
|
`godot/Main.cs` retains the front-end's startup and runtime coordination. Its synthetic threaded/headless
|
||||||
|
regression harness lives in the behavior-neutral partial-class companion `godot/Main.SelfTest.cs`, keeping
|
||||||
|
the validation surface independently navigable without changing the Godot node type or invocation path.
|
||||||
|
|
||||||
The disposable `build/page-map-<SCENE>.jsonl` files are produced by editor/development Godot runs and map
|
The disposable `build/page-map-<SCENE>.jsonl` files are produced by editor/development Godot runs and map
|
||||||
runtime ADV page ordinals to their authoritative script offsets for `tools/locate_page.py`. Packaged exports
|
runtime ADV page ordinals to their authoritative script offsets for `tools/locate_page.py`. Packaged exports
|
||||||
have no repository output tree and write their automatic maps below `user://diagnostics/page-maps` instead.
|
have no repository output tree and write their automatic maps below `user://diagnostics/page-maps` instead.
|
||||||
|
|||||||
@@ -524,7 +524,7 @@ requirements.
|
|||||||
|
|
||||||
#### Planned maintenance slice — codebase consolidation
|
#### Planned maintenance slice — codebase consolidation
|
||||||
|
|
||||||
**Status (2026-08-02): step 1 complete; step 2 is next.** The runtime and tooling now have enough independent
|
**Status (2026-08-02): step 1 complete; step 2 underway.** The runtime and tooling now have enough independent
|
||||||
regression coverage to support behavior-preserving cleanup: 590 engine tests, eleven directly runnable Python
|
regression coverage to support behavior-preserving cleanup: 590 engine tests, eleven directly runnable Python
|
||||||
tool suites, the Godot build/self-test, and the installed-corpus gates. The project layout itself is sound,
|
tool suites, the Godot build/self-test, and the installed-corpus gates. The project layout itself is sound,
|
||||||
but a few files have become navigation and ownership bottlenecks: `VirtualMachine.cs`, `GfxState.cs`,
|
but a few files have become navigation and ownership bottlenecks: `VirtualMachine.cs`, `GfxState.cs`,
|
||||||
@@ -559,6 +559,10 @@ do not mix mechanical moves with semantic changes.
|
|||||||
stage, battle, card, routine, gallery, and general table implementations plus tests into importable
|
stage, battle, card, routine, gallery, and general table implementations plus tests into importable
|
||||||
modules.
|
modules.
|
||||||
|
|
||||||
|
**Progress (2026-08-02):** the first bounded split moved `Main`'s synthetic scene builder and threaded
|
||||||
|
self-test harness into `godot/Main.SelfTest.cs`. The partial class retains the same node type and private
|
||||||
|
calls; runtime validation, including all 590 engine tests and the Godot threaded self-test, remains green.
|
||||||
|
|
||||||
**Gate:** no externally visible behavior or command changes; generated artifacts are byte-identical where
|
**Gate:** no externally visible behavior or command changes; generated artifacts are byte-identical where
|
||||||
deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after
|
deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after
|
||||||
each domain move.
|
each domain move.
|
||||||
@@ -928,8 +932,9 @@ layer's rendering diverges from ADV; save layout.
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 8. Immediate next step
|
## 8. Immediate next step
|
||||||
Begin step 2 of the **codebase consolidation** maintenance slice: behavior-neutral physical splits backed
|
Continue step 2 of the **codebase consolidation** maintenance slice: behavior-neutral physical splits backed
|
||||||
by the tracked launcher and layered validation driver. Start with the embedded `Main` self-test surface,
|
by the tracked launcher and layered validation driver. With the embedded `Main` self-test surface isolated,
|
||||||
then move one existing domain at a time while preserving public types, commands, and generated output.
|
move the `Main` audio control surface next, then one existing domain at a time while preserving public types,
|
||||||
|
commands, and generated output.
|
||||||
Concrete playthrough blockers may still preempt this bounded maintenance work; the consolidation effort does
|
Concrete playthrough blockers may still preempt this bounded maintenance work; the consolidation effort does
|
||||||
not replace Phase B gameplay validation or the open cross-platform gates.
|
not replace Phase B gameplay validation or the open cross-platform gates.
|
||||||
|
|||||||
420
godot/Main.SelfTest.cs
Normal file
420
godot/Main.SelfTest.cs
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Godot;
|
||||||
|
using Age.Engine.Diagnostics;
|
||||||
|
using Age.Engine.Hosting;
|
||||||
|
using Age.Engine.Model;
|
||||||
|
using Age.Engine.Sys4;
|
||||||
|
using Age.Engine.Text;
|
||||||
|
using Age.Engine.Vm;
|
||||||
|
using Script = Age.Engine.Model.Script;
|
||||||
|
|
||||||
|
public partial class Main
|
||||||
|
{
|
||||||
|
// Verifies that the Godot plumbing (background thread, semaphore suspension on input,
|
||||||
|
// and deferred main-thread calls) drives the VM identically to a headless run.
|
||||||
|
private void RunSelfTest()
|
||||||
|
{
|
||||||
|
var table = HimegariRuntimeMetadata.LoadOpcodeTable();
|
||||||
|
var (script, provider) = BuildSelfTestScene(table);
|
||||||
|
var headless = new VirtualMachine(script, table, new CaptureHost(), null, provider);
|
||||||
|
headless.Run();
|
||||||
|
var expected = headless.Emitted.ConvertAll(e => e.Offset);
|
||||||
|
|
||||||
|
var actual = _host.Captured.ConvertAll(c => c.Offset);
|
||||||
|
bool ok = actual.Count == expected.Count;
|
||||||
|
for (int i = 0; ok && i < actual.Count; i++) ok = actual[i] == expected[i];
|
||||||
|
var debugEntries = DebugSceneCatalog.Build(_catalog);
|
||||||
|
bool launcherOk = debugEntries.Any(entry => entry.Name == "DEBUG.BIN" && entry.Launchable)
|
||||||
|
&& debugEntries.Select(entry => entry.PackedId).Distinct().Count() == debugEntries.Count;
|
||||||
|
var launcherSmoke = new DebugSceneLauncher();
|
||||||
|
AddChild(launcherSmoke);
|
||||||
|
launcherSmoke.Open(debugEntries, "SYSTEM4.BIN > TITLE.BIN", present: false);
|
||||||
|
launcherSmoke.Free();
|
||||||
|
bool sleepMinimumOk = GodotAdvHost.NormalizeSleepMilliseconds(0, 1.0) == 1
|
||||||
|
&& GodotAdvHost.NormalizeSleepMilliseconds(100, 1.0) == 100;
|
||||||
|
bool inputTranslationOk = Win32VirtualKeyTranslator.TryTranslate(
|
||||||
|
new InputEventKey { PhysicalKeycode = Key.Z }, out int zVk) && zVk == 0x5a
|
||||||
|
&& Win32VirtualKeyTranslator.TryTranslate(
|
||||||
|
new InputEventKey { PhysicalKeycode = Key.Up }, out int upVk) && upVk == 0x26
|
||||||
|
&& Win32VirtualKeyTranslator.TryTranslate(
|
||||||
|
new InputEventKey { PhysicalKeycode = Key.Ctrl }, out int ctrlVk) && ctrlVk == 0x11;
|
||||||
|
var selftestResources = new ResourceMap(_catalog, _assetStore);
|
||||||
|
AudioPayload glowSfx = selftestResources.ReadAudio(selftestResources.ResolveSoundEffect(0x28)!);
|
||||||
|
byte[] glowGodotWav = RiffWaveSanitizer.PrepareForGodot(glowSfx.Bytes);
|
||||||
|
bool cp932WavMetadataOk = glowGodotWav.Length == 688_336
|
||||||
|
&& AudioStreamWav.LoadFromBuffer(glowGodotWav) != null;
|
||||||
|
AudioPayload bossSfx = selftestResources.ReadAudio(
|
||||||
|
selftestResources.ResolveSoundEffect(0x125)!);
|
||||||
|
byte[] bossGodotWav = RiffWaveSanitizer.PrepareForGodot(bossSfx.Bytes);
|
||||||
|
bool firstRiffBoundaryOk = bossSfx.Bytes.Length == 323_009
|
||||||
|
&& bossGodotWav.Length == 157_940
|
||||||
|
&& AudioStreamWav.LoadFromBuffer(bossGodotWav) != null;
|
||||||
|
AudioPayload bgm = selftestResources.ReadAudio(selftestResources.ResolveBgm(5)!);
|
||||||
|
FadeBgm(0, 10.0);
|
||||||
|
bool bgmFadeStarted = _bgmFadeTween?.IsValid() == true;
|
||||||
|
PlayBgm(bgm.Bytes, bgm.Name);
|
||||||
|
bool bgmReplacementCancelsFade = bgmFadeStarted
|
||||||
|
&& _bgmFadeTween == null
|
||||||
|
&& System.Math.Abs(_bgm.VolumeDb) < 0.001f;
|
||||||
|
RestartBgm(bgm.Bytes, bgm.Name, 0);
|
||||||
|
bool bgmOneShotModeOk = (_bgm.Stream as AudioStreamOggVorbis)?.Loop == false;
|
||||||
|
RestartBgm(bgm.Bytes, bgm.Name, 1);
|
||||||
|
bool bgmLoopModeOk = (_bgm.Stream as AudioStreamOggVorbis)?.Loop == true;
|
||||||
|
StopBgm();
|
||||||
|
bool bgmStopReleaseOk = _bgm.Stream == null
|
||||||
|
&& !_bgm.Playing
|
||||||
|
&& _bgmFadeTween == null
|
||||||
|
&& System.Math.Abs(_bgm.VolumeDb) < 0.001f;
|
||||||
|
IReadOnlyList<(int X, int Y)> outline =
|
||||||
|
AgeGlyphMaskCompositor.GetMode3OutlineOffsets(1, 1);
|
||||||
|
bool textEffectModesOk =
|
||||||
|
outline.Count == 12
|
||||||
|
&& outline.Contains((1, 0))
|
||||||
|
&& outline.Contains((-1, 0))
|
||||||
|
&& outline.Contains((0, 1))
|
||||||
|
&& outline.Contains((0, -1));
|
||||||
|
bool fontCalibrationOk =
|
||||||
|
ImmediateSurfaceTextRenderer.NativePixelHeight(24) == 24
|
||||||
|
&& ImmediateSurfaceTextRenderer.NativePixelHeight(25) == 24
|
||||||
|
&& ImmediateSurfaceTextRenderer.NativePixelHeight(32) == 31
|
||||||
|
&& ImmediateSurfaceTextRenderer.NativePixelHeight(33) == 31;
|
||||||
|
bool immediateSurfaceTextOk;
|
||||||
|
string immediateSurfaceTextMode;
|
||||||
|
_host.CreateTexture(997, 64, 24);
|
||||||
|
var immediateStyle = AdvTextStyle.Default with
|
||||||
|
{
|
||||||
|
PrimaryFontSize = 16,
|
||||||
|
TextColor = 0xffffff,
|
||||||
|
FontFace = ImmediateSurfaceTextRenderer.DefaultFontFace,
|
||||||
|
};
|
||||||
|
_host.DrawStringToSurface(997, 1, 1, "A姫", immediateStyle);
|
||||||
|
if (_host.UsesSurfaceTextPixels)
|
||||||
|
{
|
||||||
|
RgbaImage? pixels = _host.CaptureSurfacePixels(997);
|
||||||
|
GlyphRasterizerBackendInfo? backend = _host.SurfaceTextBackendInfo;
|
||||||
|
var cache = _host.SurfaceTextMaskCacheStats;
|
||||||
|
bool pixelsPresent = pixels != null
|
||||||
|
&& pixels.Pixels.Where((_, index) => index % 4 == 3)
|
||||||
|
.Any(alpha => alpha != 0);
|
||||||
|
bool gpuAccepted = false;
|
||||||
|
if (pixels != null)
|
||||||
|
{
|
||||||
|
_gpuRenderer.BeginFrame(false);
|
||||||
|
gpuAccepted = _gpuRenderer.DrawTexture(
|
||||||
|
pixels, int.MinValue + 997, -1,
|
||||||
|
0, 0, pixels.Width, pixels.Height,
|
||||||
|
new Affine2D(1, 0, 0, 1, 2, 3),
|
||||||
|
0xff8080, 255, 0.5f, true,
|
||||||
|
dynamic: true, dynamicKey: 997, BlendKind.Alpha);
|
||||||
|
GpuRetainedRenderer.FrameStats stats = _gpuRenderer.EndFrame();
|
||||||
|
gpuAccepted &= stats.DrawItems == 1 && stats.TextureUploads == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
_host.CreateTexture(996, 64, 24);
|
||||||
|
_host.CopySurfaceRect(new SurfaceRectCopy(
|
||||||
|
997, 996, 0, 0, 64, 24, 0, 0));
|
||||||
|
RgbaImage? copied = _host.CaptureSurfacePixels(996);
|
||||||
|
bool copiedPixels = pixels != null
|
||||||
|
&& copied != null
|
||||||
|
&& pixels.Pixels.SequenceEqual(copied.Pixels);
|
||||||
|
_host.FillSurfaceRect(new SurfaceRectFill(
|
||||||
|
996, 0, 0, 64, 24, 0, 0));
|
||||||
|
RgbaImage? cleared = _host.CaptureSurfacePixels(996);
|
||||||
|
bool clearedPixels = cleared != null && cleared.Pixels.All(value => value == 0);
|
||||||
|
immediateSurfaceTextOk =
|
||||||
|
pixelsPresent
|
||||||
|
&& backend is not null
|
||||||
|
&& (backend.Policy == GlyphRasterPolicy.NativeCp932Gray4
|
||||||
|
? backend is
|
||||||
|
{
|
||||||
|
Id: "windows-gdi-gray4",
|
||||||
|
NativePixelExact: true,
|
||||||
|
}
|
||||||
|
: backend is
|
||||||
|
{
|
||||||
|
Id: "portable-godot-textserver",
|
||||||
|
Policy: GlyphRasterPolicy.PortableUnicode,
|
||||||
|
NativePixelExact: false,
|
||||||
|
})
|
||||||
|
&& cache.Count is > 0 and <= 2048
|
||||||
|
&& cache.Capacity == 2048
|
||||||
|
&& gpuAccepted
|
||||||
|
&& copiedPixels
|
||||||
|
&& clearedPixels;
|
||||||
|
immediateSurfaceTextMode =
|
||||||
|
$"rgba:{backend?.Id}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
immediateSurfaceTextOk = false;
|
||||||
|
immediateSurfaceTextMode = "unavailable";
|
||||||
|
}
|
||||||
|
_host.ReleaseSurface(996);
|
||||||
|
_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)
|
||||||
|
&& _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;built={builtCompleteLine};objects={liveObjects.Count};" +
|
||||||
|
$"republished={republishedPartialErase};suspended={suspended};" +
|
||||||
|
$"restored={restored};reset={reset}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
liveRetainedTextOk = false;
|
||||||
|
liveRetainedTextMode = "unavailable";
|
||||||
|
}
|
||||||
|
_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);
|
||||||
|
_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;used={historyUsedRetained}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
historyRetainedTextOk = false;
|
||||||
|
_host.EndTextHistoryPresentation(_vm.Gfx);
|
||||||
|
historyRetainedTextMode = "unavailable";
|
||||||
|
}
|
||||||
|
Window rootWindow = GetTree().Root;
|
||||||
|
bool logicalCanvasOk = _host.LogicalCanvas == new Sys4LogicalCanvas(_screenWidth, _screenHeight)
|
||||||
|
&& _screen.GetWidth() == _screenWidth
|
||||||
|
&& _screen.GetHeight() == _screenHeight
|
||||||
|
&& _screenPixels.Length == checked(_screenWidth * _screenHeight * 4)
|
||||||
|
&& rootWindow.ContentScaleSize
|
||||||
|
== new Vector2I(_screenWidth, _screenHeight)
|
||||||
|
&& _windowOptions == WindowLaunchOptions.Resolve(
|
||||||
|
OS.GetCmdlineUserArgs(),
|
||||||
|
new Sys4LogicalCanvas(_screenWidth, _screenHeight));
|
||||||
|
var backbufferSnapshot = new List<RenderObject>();
|
||||||
|
_host.PresentFrame(_vm.Gfx);
|
||||||
|
BackbufferPublicationPolicy fullPolicy =
|
||||||
|
_host.SnapshotBackbufferObjects(
|
||||||
|
_vm.Gfx, _clock.NowMs, backbufferSnapshot);
|
||||||
|
bool backbufferPreservationOk =
|
||||||
|
fullPolicy
|
||||||
|
== new BackbufferPublicationPolicy(
|
||||||
|
PreserveExistingPixels: true,
|
||||||
|
AppendGpuLayers: backbufferSnapshot.Count == 0)
|
||||||
|
&& GodotAdvHost.ResolveBackbufferPublicationPolicy(
|
||||||
|
true, GfxHandleRange.All, 1)
|
||||||
|
== new BackbufferPublicationPolicy(true, false)
|
||||||
|
&& GodotAdvHost.ResolveBackbufferPublicationPolicy(
|
||||||
|
true, new GfxHandleRange(0, 60000), 1)
|
||||||
|
== new BackbufferPublicationPolicy(true, true);
|
||||||
|
_host.ClearRenderTarget(-1);
|
||||||
|
_host.PresentFrame(_vm.Gfx);
|
||||||
|
BackbufferPublicationPolicy clearedPolicy =
|
||||||
|
_host.SnapshotBackbufferObjects(
|
||||||
|
_vm.Gfx, _clock.NowMs, backbufferSnapshot);
|
||||||
|
backbufferPreservationOk &=
|
||||||
|
clearedPolicy == new BackbufferPublicationPolicy(false, false);
|
||||||
|
ok &= launcherOk && sleepMinimumOk && inputTranslationOk && cp932WavMetadataOk
|
||||||
|
&& firstRiffBoundaryOk
|
||||||
|
&& bgmReplacementCancelsFade && bgmOneShotModeOk && bgmLoopModeOk
|
||||||
|
&& bgmStopReleaseOk && textEffectModesOk && fontCalibrationOk
|
||||||
|
&& immediateSurfaceTextOk
|
||||||
|
&& liveRetainedTextOk
|
||||||
|
&& historyRetainedTextOk
|
||||||
|
&& logicalCanvasOk && backbufferPreservationOk;
|
||||||
|
if (ok) GD.Print($"SELFTEST OK: threaded host matches headless ({actual.Count} lines, full handling); " +
|
||||||
|
$"debug launcher catalog/UI smoke ({debugEntries.Count} packed scripts); " +
|
||||||
|
$"sleep-min=1ms; native-key-translation=ok; cp932-wav-info=ok; " +
|
||||||
|
$"first-riff-boundary=ok; " +
|
||||||
|
$"bgm-fade-replacement=ok; bgm-start-modes-stop=ok; " +
|
||||||
|
$"text-effect-modes=ok; font-calibration=ok; " +
|
||||||
|
$"immediate-surface-text={immediateSurfaceTextMode}; " +
|
||||||
|
$"live-adv-text={liveRetainedTextMode}; " +
|
||||||
|
$"history-text={historyRetainedTextMode}; " +
|
||||||
|
$"backbuffer-preservation=ok; " +
|
||||||
|
$"logical-canvas={_screenWidth}x{_screenHeight}; " +
|
||||||
|
$"window-request={_windowOptions.Width}x{_windowOptions.Height}");
|
||||||
|
else GD.Print($"SELFTEST FAIL: threaded={actual.Count} vs headless={expected.Count}; " +
|
||||||
|
$"debug-launcher={launcherOk}; sleep-min={sleepMinimumOk}; " +
|
||||||
|
$"native-key-translation={inputTranslationOk}; cp932-wav-info={cp932WavMetadataOk}; " +
|
||||||
|
$"first-riff-boundary={firstRiffBoundaryOk}; " +
|
||||||
|
$"bgm-fade-replacement={bgmReplacementCancelsFade}; " +
|
||||||
|
$"bgm-one-shot={bgmOneShotModeOk}; bgm-loop={bgmLoopModeOk}; " +
|
||||||
|
$"bgm-stop-release={bgmStopReleaseOk}; " +
|
||||||
|
$"text-effect-modes={textEffectModesOk}; font-calibration={fontCalibrationOk}; " +
|
||||||
|
$"immediate-surface-text={immediateSurfaceTextOk}({immediateSurfaceTextMode}); " +
|
||||||
|
$"live-adv-text={liveRetainedTextOk}({liveRetainedTextMode}); " +
|
||||||
|
$"history-text={historyRetainedTextOk}({historyRetainedTextMode}); " +
|
||||||
|
$"backbuffer-preservation={backbufferPreservationOk}; " +
|
||||||
|
$"logical-canvas={logicalCanvasOk}({_screenWidth}x{_screenHeight}); " +
|
||||||
|
$"window-request={_windowOptions.Width}x{_windowOptions.Height}");
|
||||||
|
GetTree().Quit(ok ? 0 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RunSelfTestAndQuitOnFailure()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RunSelfTest();
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
GD.PushError($"SELFTEST FAILED: {exception}");
|
||||||
|
GetTree().Quit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A deterministic synthesized scene: show-text, wait-for-input (exercises the suspend plumbing), a
|
||||||
|
// nested call-script into a synthetic subroutine (exercises call-script handling), shared globals.
|
||||||
|
private static (Script, IScriptProvider) BuildSelfTestScene(OpcodeTable table)
|
||||||
|
{
|
||||||
|
(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[]) 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[]) MovGI(int d, long v) => (0x55, new[] { new Operand(3, d), new Operand(0, v) });
|
||||||
|
(int, Operand[]) Exit() => (0x2, System.Array.Empty<Operand>());
|
||||||
|
|
||||||
|
var callee = ScriptAssembler.Assemble(table, "SUBSCENE",
|
||||||
|
new List<(int, Operand[])> { ShowText(0), MovGI(0x31, 42), Exit() }, new[] { "Sub" });
|
||||||
|
var caller = ScriptAssembler.Assemble(table, "SELFTEST",
|
||||||
|
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" });
|
||||||
|
return (caller, new SelfTestProvider(callee));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class SelfTestProvider : IScriptProvider
|
||||||
|
{
|
||||||
|
private readonly Script _callee;
|
||||||
|
public SelfTestProvider(Script callee) => _callee = callee;
|
||||||
|
public Script? GetById(long id) => id == 5 ? _callee : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
407
godot/Main.cs
407
godot/Main.cs
@@ -2026,10 +2026,6 @@ public partial class Main : Godot.Control
|
|||||||
}
|
}
|
||||||
public void ShowEnd() => _status.Text = "— end —";
|
public void ShowEnd() => _status.Text = "— end —";
|
||||||
|
|
||||||
// The selftest verifies the GODOT PLUMBING (background thread + semaphore suspend on wait-for-input
|
|
||||||
// + CallDeferred marshalling) drives the VM faithfully — i.e. produces the SAME output as a plain
|
|
||||||
// in-process run of the identical scene. Full op handling is on (the synthetic scene includes a real
|
|
||||||
// nested call-script); the expected value is computed live from a headless run, not a frozen golden.
|
|
||||||
// Reports (from the main thread) the call-scripts the VM executed as nested subroutines this run.
|
// Reports (from the main thread) the call-scripts the VM executed as nested subroutines this run.
|
||||||
private void ReportSubroutines()
|
private void ReportSubroutines()
|
||||||
{
|
{
|
||||||
@@ -2041,358 +2037,6 @@ public partial class Main : Godot.Control
|
|||||||
GD.Print($"[subroutines] {ids.Count} call-scripts executed as nested frames ({distinct.Count} distinct: {string.Join(", ", distinct)})");
|
GD.Print($"[subroutines] {ids.Count} call-scripts executed as nested frames ({distinct.Count} distinct: {string.Join(", ", distinct)})");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RunSelfTest()
|
|
||||||
{
|
|
||||||
var table = HimegariRuntimeMetadata.LoadOpcodeTable();
|
|
||||||
var (script, provider) = BuildSelfTestScene(table);
|
|
||||||
var headless = new VirtualMachine(script, table, new CaptureHost(), null, provider);
|
|
||||||
headless.Run();
|
|
||||||
var expected = headless.Emitted.ConvertAll(e => e.Offset);
|
|
||||||
|
|
||||||
var actual = _host.Captured.ConvertAll(c => c.Offset);
|
|
||||||
bool ok = actual.Count == expected.Count;
|
|
||||||
for (int i = 0; ok && i < actual.Count; i++) ok = actual[i] == expected[i];
|
|
||||||
var debugEntries = DebugSceneCatalog.Build(_catalog);
|
|
||||||
bool launcherOk = debugEntries.Any(entry => entry.Name == "DEBUG.BIN" && entry.Launchable)
|
|
||||||
&& debugEntries.Select(entry => entry.PackedId).Distinct().Count() == debugEntries.Count;
|
|
||||||
var launcherSmoke = new DebugSceneLauncher();
|
|
||||||
AddChild(launcherSmoke);
|
|
||||||
launcherSmoke.Open(debugEntries, "SYSTEM4.BIN > TITLE.BIN", present: false);
|
|
||||||
launcherSmoke.Free();
|
|
||||||
bool sleepMinimumOk = GodotAdvHost.NormalizeSleepMilliseconds(0, 1.0) == 1
|
|
||||||
&& GodotAdvHost.NormalizeSleepMilliseconds(100, 1.0) == 100;
|
|
||||||
bool inputTranslationOk = Win32VirtualKeyTranslator.TryTranslate(
|
|
||||||
new InputEventKey { PhysicalKeycode = Key.Z }, out int zVk) && zVk == 0x5a
|
|
||||||
&& Win32VirtualKeyTranslator.TryTranslate(
|
|
||||||
new InputEventKey { PhysicalKeycode = Key.Up }, out int upVk) && upVk == 0x26
|
|
||||||
&& Win32VirtualKeyTranslator.TryTranslate(
|
|
||||||
new InputEventKey { PhysicalKeycode = Key.Ctrl }, out int ctrlVk) && ctrlVk == 0x11;
|
|
||||||
var selftestResources = new ResourceMap(_catalog, _assetStore);
|
|
||||||
AudioPayload glowSfx = selftestResources.ReadAudio(selftestResources.ResolveSoundEffect(0x28)!);
|
|
||||||
byte[] glowGodotWav = RiffWaveSanitizer.PrepareForGodot(glowSfx.Bytes);
|
|
||||||
bool cp932WavMetadataOk = glowGodotWav.Length == 688_336
|
|
||||||
&& AudioStreamWav.LoadFromBuffer(glowGodotWav) != null;
|
|
||||||
AudioPayload bossSfx = selftestResources.ReadAudio(
|
|
||||||
selftestResources.ResolveSoundEffect(0x125)!);
|
|
||||||
byte[] bossGodotWav = RiffWaveSanitizer.PrepareForGodot(bossSfx.Bytes);
|
|
||||||
bool firstRiffBoundaryOk = bossSfx.Bytes.Length == 323_009
|
|
||||||
&& bossGodotWav.Length == 157_940
|
|
||||||
&& AudioStreamWav.LoadFromBuffer(bossGodotWav) != null;
|
|
||||||
AudioPayload bgm = selftestResources.ReadAudio(selftestResources.ResolveBgm(5)!);
|
|
||||||
FadeBgm(0, 10.0);
|
|
||||||
bool bgmFadeStarted = _bgmFadeTween?.IsValid() == true;
|
|
||||||
PlayBgm(bgm.Bytes, bgm.Name);
|
|
||||||
bool bgmReplacementCancelsFade = bgmFadeStarted
|
|
||||||
&& _bgmFadeTween == null
|
|
||||||
&& System.Math.Abs(_bgm.VolumeDb) < 0.001f;
|
|
||||||
RestartBgm(bgm.Bytes, bgm.Name, 0);
|
|
||||||
bool bgmOneShotModeOk = (_bgm.Stream as AudioStreamOggVorbis)?.Loop == false;
|
|
||||||
RestartBgm(bgm.Bytes, bgm.Name, 1);
|
|
||||||
bool bgmLoopModeOk = (_bgm.Stream as AudioStreamOggVorbis)?.Loop == true;
|
|
||||||
StopBgm();
|
|
||||||
bool bgmStopReleaseOk = _bgm.Stream == null
|
|
||||||
&& !_bgm.Playing
|
|
||||||
&& _bgmFadeTween == null
|
|
||||||
&& System.Math.Abs(_bgm.VolumeDb) < 0.001f;
|
|
||||||
IReadOnlyList<(int X, int Y)> outline =
|
|
||||||
AgeGlyphMaskCompositor.GetMode3OutlineOffsets(1, 1);
|
|
||||||
bool textEffectModesOk =
|
|
||||||
outline.Count == 12
|
|
||||||
&& outline.Contains((1, 0))
|
|
||||||
&& outline.Contains((-1, 0))
|
|
||||||
&& outline.Contains((0, 1))
|
|
||||||
&& outline.Contains((0, -1));
|
|
||||||
bool fontCalibrationOk =
|
|
||||||
ImmediateSurfaceTextRenderer.NativePixelHeight(24) == 24
|
|
||||||
&& ImmediateSurfaceTextRenderer.NativePixelHeight(25) == 24
|
|
||||||
&& ImmediateSurfaceTextRenderer.NativePixelHeight(32) == 31
|
|
||||||
&& ImmediateSurfaceTextRenderer.NativePixelHeight(33) == 31;
|
|
||||||
bool immediateSurfaceTextOk;
|
|
||||||
string immediateSurfaceTextMode;
|
|
||||||
_host.CreateTexture(997, 64, 24);
|
|
||||||
var immediateStyle = AdvTextStyle.Default with
|
|
||||||
{
|
|
||||||
PrimaryFontSize = 16,
|
|
||||||
TextColor = 0xffffff,
|
|
||||||
FontFace = ImmediateSurfaceTextRenderer.DefaultFontFace,
|
|
||||||
};
|
|
||||||
_host.DrawStringToSurface(997, 1, 1, "A姫", immediateStyle);
|
|
||||||
if (_host.UsesSurfaceTextPixels)
|
|
||||||
{
|
|
||||||
RgbaImage? pixels = _host.CaptureSurfacePixels(997);
|
|
||||||
GlyphRasterizerBackendInfo? backend = _host.SurfaceTextBackendInfo;
|
|
||||||
var cache = _host.SurfaceTextMaskCacheStats;
|
|
||||||
bool pixelsPresent = pixels != null
|
|
||||||
&& pixels.Pixels.Where((_, index) => index % 4 == 3)
|
|
||||||
.Any(alpha => alpha != 0);
|
|
||||||
bool gpuAccepted = false;
|
|
||||||
if (pixels != null)
|
|
||||||
{
|
|
||||||
_gpuRenderer.BeginFrame(false);
|
|
||||||
gpuAccepted = _gpuRenderer.DrawTexture(
|
|
||||||
pixels, int.MinValue + 997, -1,
|
|
||||||
0, 0, pixels.Width, pixels.Height,
|
|
||||||
new Affine2D(1, 0, 0, 1, 2, 3),
|
|
||||||
0xff8080, 255, 0.5f, true,
|
|
||||||
dynamic: true, dynamicKey: 997, BlendKind.Alpha);
|
|
||||||
GpuRetainedRenderer.FrameStats stats = _gpuRenderer.EndFrame();
|
|
||||||
gpuAccepted &= stats.DrawItems == 1 && stats.TextureUploads == 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
_host.CreateTexture(996, 64, 24);
|
|
||||||
_host.CopySurfaceRect(new SurfaceRectCopy(
|
|
||||||
997, 996, 0, 0, 64, 24, 0, 0));
|
|
||||||
RgbaImage? copied = _host.CaptureSurfacePixels(996);
|
|
||||||
bool copiedPixels = pixels != null
|
|
||||||
&& copied != null
|
|
||||||
&& pixels.Pixels.SequenceEqual(copied.Pixels);
|
|
||||||
_host.FillSurfaceRect(new SurfaceRectFill(
|
|
||||||
996, 0, 0, 64, 24, 0, 0));
|
|
||||||
RgbaImage? cleared = _host.CaptureSurfacePixels(996);
|
|
||||||
bool clearedPixels = cleared != null && cleared.Pixels.All(value => value == 0);
|
|
||||||
immediateSurfaceTextOk =
|
|
||||||
pixelsPresent
|
|
||||||
&& backend is not null
|
|
||||||
&& (backend.Policy == GlyphRasterPolicy.NativeCp932Gray4
|
|
||||||
? backend is
|
|
||||||
{
|
|
||||||
Id: "windows-gdi-gray4",
|
|
||||||
NativePixelExact: true,
|
|
||||||
}
|
|
||||||
: backend is
|
|
||||||
{
|
|
||||||
Id: "portable-godot-textserver",
|
|
||||||
Policy: GlyphRasterPolicy.PortableUnicode,
|
|
||||||
NativePixelExact: false,
|
|
||||||
})
|
|
||||||
&& cache.Count is > 0 and <= 2048
|
|
||||||
&& cache.Capacity == 2048
|
|
||||||
&& gpuAccepted
|
|
||||||
&& copiedPixels
|
|
||||||
&& clearedPixels;
|
|
||||||
immediateSurfaceTextMode =
|
|
||||||
$"rgba:{backend?.Id}";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
immediateSurfaceTextOk = false;
|
|
||||||
immediateSurfaceTextMode = "unavailable";
|
|
||||||
}
|
|
||||||
_host.ReleaseSurface(996);
|
|
||||||
_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)
|
|
||||||
&& _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;built={builtCompleteLine};objects={liveObjects.Count};" +
|
|
||||||
$"republished={republishedPartialErase};suspended={suspended};" +
|
|
||||||
$"restored={restored};reset={reset}";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
liveRetainedTextOk = false;
|
|
||||||
liveRetainedTextMode = "unavailable";
|
|
||||||
}
|
|
||||||
_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);
|
|
||||||
_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;used={historyUsedRetained}";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
historyRetainedTextOk = false;
|
|
||||||
_host.EndTextHistoryPresentation(_vm.Gfx);
|
|
||||||
historyRetainedTextMode = "unavailable";
|
|
||||||
}
|
|
||||||
Window rootWindow = GetTree().Root;
|
|
||||||
bool logicalCanvasOk = _host.LogicalCanvas == new Sys4LogicalCanvas(_screenWidth, _screenHeight)
|
|
||||||
&& _screen.GetWidth() == _screenWidth
|
|
||||||
&& _screen.GetHeight() == _screenHeight
|
|
||||||
&& _screenPixels.Length == checked(_screenWidth * _screenHeight * 4)
|
|
||||||
&& rootWindow.ContentScaleSize
|
|
||||||
== new Vector2I(_screenWidth, _screenHeight)
|
|
||||||
&& _windowOptions == WindowLaunchOptions.Resolve(
|
|
||||||
OS.GetCmdlineUserArgs(),
|
|
||||||
new Sys4LogicalCanvas(_screenWidth, _screenHeight));
|
|
||||||
var backbufferSnapshot = new List<RenderObject>();
|
|
||||||
_host.PresentFrame(_vm.Gfx);
|
|
||||||
BackbufferPublicationPolicy fullPolicy =
|
|
||||||
_host.SnapshotBackbufferObjects(
|
|
||||||
_vm.Gfx, _clock.NowMs, backbufferSnapshot);
|
|
||||||
bool backbufferPreservationOk =
|
|
||||||
fullPolicy
|
|
||||||
== new BackbufferPublicationPolicy(
|
|
||||||
PreserveExistingPixels: true,
|
|
||||||
AppendGpuLayers: backbufferSnapshot.Count == 0)
|
|
||||||
&& GodotAdvHost.ResolveBackbufferPublicationPolicy(
|
|
||||||
true, GfxHandleRange.All, 1)
|
|
||||||
== new BackbufferPublicationPolicy(true, false)
|
|
||||||
&& GodotAdvHost.ResolveBackbufferPublicationPolicy(
|
|
||||||
true, new GfxHandleRange(0, 60000), 1)
|
|
||||||
== new BackbufferPublicationPolicy(true, true);
|
|
||||||
_host.ClearRenderTarget(-1);
|
|
||||||
_host.PresentFrame(_vm.Gfx);
|
|
||||||
BackbufferPublicationPolicy clearedPolicy =
|
|
||||||
_host.SnapshotBackbufferObjects(
|
|
||||||
_vm.Gfx, _clock.NowMs, backbufferSnapshot);
|
|
||||||
backbufferPreservationOk &=
|
|
||||||
clearedPolicy == new BackbufferPublicationPolicy(false, false);
|
|
||||||
ok &= launcherOk && sleepMinimumOk && inputTranslationOk && cp932WavMetadataOk
|
|
||||||
&& firstRiffBoundaryOk
|
|
||||||
&& bgmReplacementCancelsFade && bgmOneShotModeOk && bgmLoopModeOk
|
|
||||||
&& bgmStopReleaseOk && textEffectModesOk && fontCalibrationOk
|
|
||||||
&& immediateSurfaceTextOk
|
|
||||||
&& liveRetainedTextOk
|
|
||||||
&& historyRetainedTextOk
|
|
||||||
&& logicalCanvasOk && backbufferPreservationOk;
|
|
||||||
if (ok) GD.Print($"SELFTEST OK: threaded host matches headless ({actual.Count} lines, full handling); " +
|
|
||||||
$"debug launcher catalog/UI smoke ({debugEntries.Count} packed scripts); " +
|
|
||||||
$"sleep-min=1ms; native-key-translation=ok; cp932-wav-info=ok; " +
|
|
||||||
$"first-riff-boundary=ok; " +
|
|
||||||
$"bgm-fade-replacement=ok; bgm-start-modes-stop=ok; " +
|
|
||||||
$"text-effect-modes=ok; font-calibration=ok; " +
|
|
||||||
$"immediate-surface-text={immediateSurfaceTextMode}; " +
|
|
||||||
$"live-adv-text={liveRetainedTextMode}; " +
|
|
||||||
$"history-text={historyRetainedTextMode}; " +
|
|
||||||
$"backbuffer-preservation=ok; " +
|
|
||||||
$"logical-canvas={_screenWidth}x{_screenHeight}; " +
|
|
||||||
$"window-request={_windowOptions.Width}x{_windowOptions.Height}");
|
|
||||||
else GD.Print($"SELFTEST FAIL: threaded={actual.Count} vs headless={expected.Count}; " +
|
|
||||||
$"debug-launcher={launcherOk}; sleep-min={sleepMinimumOk}; " +
|
|
||||||
$"native-key-translation={inputTranslationOk}; cp932-wav-info={cp932WavMetadataOk}; " +
|
|
||||||
$"first-riff-boundary={firstRiffBoundaryOk}; " +
|
|
||||||
$"bgm-fade-replacement={bgmReplacementCancelsFade}; " +
|
|
||||||
$"bgm-one-shot={bgmOneShotModeOk}; bgm-loop={bgmLoopModeOk}; " +
|
|
||||||
$"bgm-stop-release={bgmStopReleaseOk}; " +
|
|
||||||
$"text-effect-modes={textEffectModesOk}; font-calibration={fontCalibrationOk}; " +
|
|
||||||
$"immediate-surface-text={immediateSurfaceTextOk}({immediateSurfaceTextMode}); " +
|
|
||||||
$"live-adv-text={liveRetainedTextOk}({liveRetainedTextMode}); " +
|
|
||||||
$"history-text={historyRetainedTextOk}({historyRetainedTextMode}); " +
|
|
||||||
$"backbuffer-preservation={backbufferPreservationOk}; " +
|
|
||||||
$"logical-canvas={logicalCanvasOk}({_screenWidth}x{_screenHeight}); " +
|
|
||||||
$"window-request={_windowOptions.Width}x{_windowOptions.Height}");
|
|
||||||
GetTree().Quit(ok ? 0 : 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RunSelfTestAndQuitOnFailure()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
RunSelfTest();
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
GD.PushError($"SELFTEST FAILED: {exception}");
|
|
||||||
GetTree().Quit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string DefaultPageMapPath(string scene)
|
private static string DefaultPageMapPath(string scene)
|
||||||
{
|
{
|
||||||
string fileName = $"page-map-{scene.ToUpperInvariant()}.jsonl";
|
string fileName = $"page-map-{scene.ToUpperInvariant()}.jsonl";
|
||||||
@@ -2407,55 +2051,4 @@ public partial class Main : Godot.Control
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// A deterministic synthesized scene: show-text, wait-for-input (exercises the suspend plumbing), a
|
|
||||||
// nested call-script into a synthetic subroutine (exercises call-script handling), shared globals.
|
|
||||||
private static (Script, IScriptProvider) BuildSelfTestScene(OpcodeTable table)
|
|
||||||
{
|
|
||||||
(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[]) 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[]) MovGI(int d, long v) => (0x55, new[] { new Operand(3, d), new Operand(0, v) });
|
|
||||||
(int, Operand[]) Exit() => (0x2, System.Array.Empty<Operand>());
|
|
||||||
|
|
||||||
var callee = ScriptAssembler.Assemble(table, "SUBSCENE",
|
|
||||||
new List<(int, Operand[])> { ShowText(0), MovGI(0x31, 42), Exit() }, new[] { "Sub" });
|
|
||||||
var caller = ScriptAssembler.Assemble(table, "SELFTEST",
|
|
||||||
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" });
|
|
||||||
return (caller, new SelfTestProvider(callee));
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class SelfTestProvider : IScriptProvider
|
|
||||||
{
|
|
||||||
private readonly Script _callee;
|
|
||||||
public SelfTestProvider(Script callee) => _callee = callee;
|
|
||||||
public Script? GetById(long id) => id == 5 ? _callee : null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user