Fix GDI zero-ink glyph handling
This commit is contained in:
@@ -71,6 +71,28 @@ When the escalation trigger is met:
|
||||
End the escalation once the reproduced discrepancy and its relevant contract are explained. Do not expand it
|
||||
into reconstructing unused handlers, recovering the whole native engine, or producing a byte-matching C build.
|
||||
|
||||
### DEBUGMAP P008/P009 GDI worker-end regression corrected (2026-08-15)
|
||||
|
||||
A live `SYSTEM4 > FIELD > SC0270` run first reported `[vm] ended: unknown` while the locator still showed P008
|
||||
(`SC0270@0x5b22`). The coordinate is an ordinary `wait-for-input`; its next instruction begins P009. A
|
||||
corpus-backed regression and automated portable-text runs crossed that boundary, but those runs did not exercise
|
||||
the exact Windows GDI rasterizer used by normal Windows play. The `unknown` message also hid the actionable
|
||||
exception because the worker's `finally` set the completion flag while the main loop printed only the unset VM
|
||||
halt reason. Godot now retains and prints the complete worker exception and automatically writes a
|
||||
`worker-failure` diagnostic snapshot at the last traced script/opcode coordinate.
|
||||
|
||||
The next live failure and that snapshot localized the defect to P009's `show-text` at `SC0270@0x5bea`. This line
|
||||
contains an ideographic full-width space (`U+3000`, CP932 `0x8140`). With `MS 明朝` at the active 24-pixel
|
||||
style, `GetGlyphOutlineA(GGO_GRAY4_BITMAP)` returns the spacing glyph's advance and a nominal 1x1 metric box but
|
||||
reports a zero-byte bitmap requirement. The adapter incorrectly required the reported buffer size to equal the
|
||||
four-byte aligned metric box and threw before P009 could be published. It now treats only a zero-byte result as
|
||||
a zero-ink glyph: the GDI placement and advance metrics are retained and the implied mask is filled with
|
||||
transparent coverage. Nonzero short or oversized payloads remain hard failures. A focused Windows regression
|
||||
reproduces the original 0-versus-4 result and verifies that the ideographic space advances without drawing ink;
|
||||
the SYSTEM4/DEBUGMAP boundary regression remains in place. The user then repeated the normal exact-GDI
|
||||
DEBUGMAP entry and confirmed that the dialogue now continues into the map without the worker failure, accepting
|
||||
the fix end to end.
|
||||
|
||||
## Stage B0 — Ground-truth reconnaissance
|
||||
|
||||
Before changing runtime architecture, record the original game's path from process start through the first
|
||||
|
||||
@@ -7,6 +7,36 @@ using Xunit;
|
||||
|
||||
public class DebugSceneLaunchTests
|
||||
{
|
||||
private sealed class ReachedDebugMapPage8ContinuationException : Exception { }
|
||||
|
||||
private sealed class LaunchDebugMapAndStopAfterPage8Sink : ITraceSink
|
||||
{
|
||||
public VirtualMachine Vm = null!;
|
||||
public bool TracingSteps => true;
|
||||
|
||||
public void Emit(in TraceEvent e)
|
||||
{
|
||||
if (e.Kind == TraceEventKind.FrameEnter
|
||||
&& e.Name?.Equals("TITLE.BIN", StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
DebugFrameSnapshot frame = Assert.IsType<DebugFrameSnapshot>(Vm.DebugFrame);
|
||||
Assert.True(Vm.TryRequestDebugFrameReturn(frame.FrameId, new Dictionary<int, long>
|
||||
{
|
||||
[0] = 1,
|
||||
[0xaba5c] = -1,
|
||||
[0x62ccf] = 0,
|
||||
[0x699] = 0x338c,
|
||||
}));
|
||||
}
|
||||
|
||||
if (e.Kind == TraceEventKind.Step
|
||||
&& e.Ins?.Offset == 0x5b25
|
||||
&& Vm.DebugFrame?.CurrentScript.Equals(
|
||||
"SC0270.BIN", StringComparison.OrdinalIgnoreCase) == true)
|
||||
throw new ReachedDebugMapPage8ContinuationException();
|
||||
}
|
||||
}
|
||||
|
||||
private static Operand I(long value) => new(0, value);
|
||||
private static Operand G(long address) => new(3, address);
|
||||
private static (int, Operand[]) Call(Operand id) => (0x3, new[] { id });
|
||||
@@ -15,6 +45,24 @@ public class DebugSceneLaunchTests
|
||||
private static (int, Operand[]) Sleep() => (0xc8, new[] { I(1) });
|
||||
private static (int, Operand[]) Exit() => (0x2, Array.Empty<Operand>());
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Workspace")]
|
||||
public void RealDebugMapLaunchContinuesPastSc0270Page8Wait()
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
var scripts = Sys4ScriptProvider.Load(table);
|
||||
var sink = new LaunchDebugMapAndStopAfterPage8Sink();
|
||||
var vm = new VirtualMachine(
|
||||
scripts.RequireByName("SYSTEM4.BIN"), table, new CaptureHost(),
|
||||
new VmOptions(MaxSteps: 1_000_000), scripts, sink);
|
||||
sink.Vm = vm;
|
||||
|
||||
Exception? exception = Record.Exception(() => vm.Run());
|
||||
|
||||
Assert.IsType<ReachedDebugMapPage8ContinuationException>(exception);
|
||||
Assert.Null(vm.HaltReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParkedTitleFrameReturnsToCoordinatorWhichDispatchesSelectedScript()
|
||||
{
|
||||
|
||||
@@ -60,6 +60,19 @@ public class WindowsGdiGlyphMaskRasterizerTests
|
||||
Assert.All(actual.Coverage.ToArray(), value => Assert.InRange(value, (byte)0, (byte)16));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IdeographicSpacePreservesAdvanceWithTransparentCoverage()
|
||||
{
|
||||
if (!WindowsGdiGlyphMaskRasterizer.TryGetAvailability(out _)) return;
|
||||
|
||||
using var rasterizer = new WindowsGdiGlyphMaskRasterizer();
|
||||
GlyphMask space = rasterizer.Rasterize(
|
||||
NativeRequest("MS 明朝", 24, -12, 0, 0x3000));
|
||||
|
||||
Assert.True(space.CellAdvanceX > 0);
|
||||
Assert.All(space.Coverage.ToArray(), value => Assert.Equal(0, value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FontHandlesUseTheSharedBoundedLruAndDisposeCleanly()
|
||||
{
|
||||
|
||||
@@ -124,7 +124,11 @@ public sealed class WindowsGdiGlyphMaskRasterizer
|
||||
int height = checked((int)metrics.BlackBoxY);
|
||||
int stride = checked((width + 3) & ~3);
|
||||
int expectedBytes = checked(stride * height);
|
||||
if (size != expectedBytes)
|
||||
// CP932 0x8140 (U+3000 IDEOGRAPHIC SPACE) is a zero-ink spacing glyph. GDI reports
|
||||
// its placement as a nominal 1x1 black box but returns a zero-byte required buffer.
|
||||
// Preserve those metrics and materialize the implied transparent mask; a nonzero
|
||||
// short/oversized payload still means that the bitmap contract is inconsistent.
|
||||
if (size != 0 && size != expectedBytes)
|
||||
throw new InvalidOperationException(
|
||||
$"GDI gray-4 buffer size {size} disagrees with {width}x{height}, stride {stride}.");
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ public partial class Main : Godot.Control
|
||||
private volatile bool _done;
|
||||
private bool _ended;
|
||||
private Task? _vmTask;
|
||||
private Exception? _vmFailure;
|
||||
private bool _selftest;
|
||||
private string? _shotPath; // --shot <png>: capture a page then quit (dev tool)
|
||||
private int _shotPage = 1; // --shot-page <n>: which page to capture (default 1)
|
||||
@@ -455,6 +456,7 @@ public partial class Main : Godot.Control
|
||||
_vmTask = Task.Run(() =>
|
||||
{
|
||||
try { _vm.Run(); }
|
||||
catch (Exception error) { _vmFailure = error; }
|
||||
finally { _done = true; }
|
||||
});
|
||||
|
||||
@@ -560,14 +562,24 @@ public partial class Main : Godot.Control
|
||||
{
|
||||
_ended = true;
|
||||
DumpHistogram();
|
||||
GD.Print($"[vm] ended: {_vm!.HaltReason ?? "unknown"} after {_vm.Steps} steps");
|
||||
ReportSubroutines();
|
||||
ShowEnd();
|
||||
if (_vm.HaltReason == "STEP-LIMIT")
|
||||
if (_vmFailure != null)
|
||||
{
|
||||
GodotTraceSnapshot haltTrace = _trace.HaltSnapshot ?? _trace.Snapshot();
|
||||
GD.Print(StepLimitDiagnosticFormatter.Format(haltTrace, _table!));
|
||||
CaptureStallDiagnostic(haltTrace, "step-limit");
|
||||
GD.PushError($"[vm] worker failed after {_vm!.Steps} steps: {_vmFailure}");
|
||||
ReportSubroutines();
|
||||
if (!_selftest)
|
||||
CaptureStallDiagnostic(_trace.Snapshot(), "worker-failure");
|
||||
}
|
||||
else
|
||||
{
|
||||
GD.Print($"[vm] ended: {_vm!.HaltReason ?? "unknown"} after {_vm.Steps} steps");
|
||||
ReportSubroutines();
|
||||
ShowEnd();
|
||||
if (_vm.HaltReason == "STEP-LIMIT")
|
||||
{
|
||||
GodotTraceSnapshot haltTrace = _trace.HaltSnapshot ?? _trace.Snapshot();
|
||||
GD.Print(StepLimitDiagnosticFormatter.Format(haltTrace, _table!));
|
||||
CaptureStallDiagnostic(haltTrace, "step-limit");
|
||||
}
|
||||
}
|
||||
if (_selftest) RunSelfTestAndQuitOnFailure();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user