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
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -524,7 +524,7 @@ requirements.
|
||||
|
||||
#### 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
|
||||
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`,
|
||||
@@ -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
|
||||
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
|
||||
deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after
|
||||
each domain move.
|
||||
@@ -928,8 +932,9 @@ layer's rendering diverges from ADV; save layout.
|
||||
---
|
||||
|
||||
## 8. Immediate next step
|
||||
Begin 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,
|
||||
then move one existing domain at a time while preserving public types, commands, and generated output.
|
||||
Continue step 2 of the **codebase consolidation** maintenance slice: behavior-neutral physical splits backed
|
||||
by the tracked launcher and layered validation driver. With the embedded `Main` self-test surface isolated,
|
||||
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
|
||||
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 —";
|
||||
|
||||
// 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.
|
||||
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)})");
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
string fileName = $"page-map-{scene.ToUpperInvariant()}.jsonl";
|
||||
@@ -2407,55 +2051,4 @@ public partial class Main : Godot.Control
|
||||
#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