Split Main self-test into partial class

This commit is contained in:
gamer147
2026-08-02 15:52:34 -04:00
parent 2ad993f556
commit f395b77a93
4 changed files with 433 additions and 411 deletions

View File

@@ -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;
}
}