Fix oversized GDI glyph buffers
This commit is contained in:
@@ -3062,6 +3062,14 @@ height/width, weight, `DEFAULT_CHARSET`, and raw CP932 face bytes. Each request
|
||||
or two-byte CP932 code for `GetGlyphOutlineA(GGO_GRAY4_BITMAP)` and `GetTextExtentPoint32A`; returned masks
|
||||
flow through the same normalized `GlyphMask` contract without another pixel conversion.
|
||||
|
||||
`GetGlyphOutline`'s queried allocation size is not always identical to the rows described by
|
||||
`GLYPHMETRICS`. On the reference Windows installation, 24-pixel bold MS Mincho `p` reports a 12x15 box with a
|
||||
12-byte row stride but requests 192 bytes rather than 180; a live SC0000 run likewise produced 672 bytes for a
|
||||
25x23 box with 28-byte rows. The adapter therefore allocates and reads the complete queried size, as the API
|
||||
requires, while exposing only `stride * gmBlackBoxY` row bytes to the normalized mask/compositor. Extra storage
|
||||
is outside the drawable metric box and can contain unspecified data. Zero-byte spacing glyphs retain their
|
||||
separate transparent-mask handling; nonzero undersized buffers still fail.
|
||||
|
||||
This exact ANSI reference requires Windows system ACP 932. `TryGetAvailability` reports the active-platform/
|
||||
code-page incompatibility and the constructor refuses to masquerade as exact when unavailable.
|
||||
`GlyphRasterizerBackendInfo` identifies the backend as `windows-gdi-gray4`, native-CP932, and pixel-exact.
|
||||
|
||||
@@ -87,11 +87,30 @@ style, `GetGlyphOutlineA(GGO_GRAY4_BITMAP)` returns the spacing glyph's advance
|
||||
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.
|
||||
transparent coverage. A focused Windows regression reproduces the original 0-versus-4 result and verifies that
|
||||
the ideographic space advances without drawing ink; nonzero buffers smaller than the metric-defined rows remain
|
||||
hard failures. 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.
|
||||
|
||||
### SYSTEM4 P009 oversized GDI buffer corrected (2026-08-16)
|
||||
|
||||
A new-game run with Ctrl held reached `SYSTEM4 P009`, whose text is `SC0000@0xa77` and whose following wait is
|
||||
`SC0000@0xa7f`, then failed while rasterizing the retained line. GDI reported a 672-byte gray-4 requirement for
|
||||
a 25x23 metric box with 28-byte rows; the adapter rejected it because the metric-defined coverage is only 644
|
||||
bytes. This is not the earlier zero-ink spacing-glyph case. A focused independent ANSI/Unicode probe reproduces
|
||||
the same oversized-storage contract with 24-pixel bold MS Mincho `p`: `GLYPHMETRICS` reports 12x15 and a
|
||||
12-byte row stride, while `GetGlyphOutline` requests 192 bytes rather than the 180 bytes described by those
|
||||
rows. The surplus may contain unspecified data and is not an additional drawable metric row.
|
||||
|
||||
The adapter now follows the two returned contracts independently: it allocates and reads the complete size
|
||||
requested by GDI, but normalizes only `stride * gmBlackBoxY` bytes into `GlyphMask` for layout/composition.
|
||||
A nonzero undersized result, a failed read, or changed metrics between the query and read remains an error. The
|
||||
regression compares the ANSI adapter with an independent Unicode GDI call, proves that the raw buffer is larger
|
||||
than the normalized mask, and checks the exact metric-defined coverage. A second regression covers every glyph
|
||||
in the reported SC0000 line with its active 24-pixel bold Mincho style. The real direct-scene harness, exact GDI
|
||||
backend, and held-Ctrl input then recorded the reported P009 coordinate and continued through P166 without a
|
||||
worker failure; the probe stopped at the later interactive name-entry boundary.
|
||||
|
||||
## Stage B0 — Ground-truth reconnaissance
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Text;
|
||||
using Age.Engine.Text.Windows;
|
||||
|
||||
@@ -73,6 +74,48 @@ public class WindowsGdiGlyphMaskRasterizerTests
|
||||
Assert.All(space.Coverage.ToArray(), value => Assert.Equal(0, value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sc0000OpeningNarrationLineRasterizesEveryGlyph()
|
||||
{
|
||||
if (!WindowsGdiGlyphMaskRasterizer.TryGetAvailability(out _)) return;
|
||||
|
||||
const string text =
|
||||
"だが怨嗟に満ちた言葉は、呪いのように彼らの心に暗い影を落とした。";
|
||||
var style = AdvTextStyle.Default with
|
||||
{
|
||||
PrimaryFontSize = 24,
|
||||
Bold = true,
|
||||
FontFace = "MS 明朝",
|
||||
RenderMode = 3,
|
||||
LineSpacing = 8,
|
||||
};
|
||||
|
||||
using var rasterizer = new WindowsGdiGlyphMaskRasterizer();
|
||||
foreach (GlyphRasterRequest request in
|
||||
ImmediateSurfaceTextRenderer.CreateRequests(text, style))
|
||||
{
|
||||
Exception? error = Record.Exception(() => rasterizer.Rasterize(request));
|
||||
Assert.True(
|
||||
error == null,
|
||||
$"U+{request.UnicodeScalar:X4} CP932 0x{request.Cp932Code:X4}: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gray4StorageBeyondMetricRowsIsTrimmed()
|
||||
{
|
||||
if (!WindowsGdiGlyphMaskRasterizer.TryGetAvailability(out _)) return;
|
||||
|
||||
GlyphRasterRequest request = NativeRequest("MS 明朝", 24, -12, 700, 'p');
|
||||
DirectGlyph direct = RasterizeUnicodeDirect("MS 明朝", 24, -12, 700, 'p');
|
||||
using var rasterizer = new WindowsGdiGlyphMaskRasterizer();
|
||||
GlyphMask actual = rasterizer.Rasterize(request);
|
||||
|
||||
Assert.True(direct.RawBufferSize > direct.Coverage.Length);
|
||||
Assert.Equal(direct.Coverage, actual.Coverage.ToArray());
|
||||
Assert.Equal(actual.Stride * actual.Height, actual.Coverage.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FontHandlesUseTheSharedBoundedLruAndDisposeCleanly()
|
||||
{
|
||||
@@ -135,17 +178,19 @@ public class WindowsGdiGlyphMaskRasterizerTests
|
||||
int glyphWidth = checked((int)metrics.BlackBoxX);
|
||||
int glyphHeight = checked((int)metrics.BlackBoxY);
|
||||
int stride = checked((glyphWidth + 3) & ~3);
|
||||
Assert.Equal(checked(stride * glyphHeight), (int)size);
|
||||
byte[] coverage = new byte[size];
|
||||
int expectedBytes = checked(stride * glyphHeight);
|
||||
Assert.True(size >= expectedBytes);
|
||||
byte[] rawCoverage = new byte[size];
|
||||
if (size > 0)
|
||||
{
|
||||
identity = Mat2.Identity;
|
||||
uint written = Native.GetGlyphOutlineW(
|
||||
dc, (uint)scalar, GgoGray4Bitmap,
|
||||
out GlyphMetrics second, size, coverage, ref identity);
|
||||
out GlyphMetrics second, size, rawCoverage, ref identity);
|
||||
Assert.Equal(size, written);
|
||||
Assert.Equal(metrics, second);
|
||||
}
|
||||
byte[] coverage = rawCoverage.AsSpan(0, expectedBytes).ToArray();
|
||||
|
||||
string text = char.ConvertFromUtf32(scalar);
|
||||
if (!Native.GetTextExtentPoint32W(dc, text, text.Length, out NativeSize cell))
|
||||
@@ -154,7 +199,7 @@ public class WindowsGdiGlyphMaskRasterizerTests
|
||||
glyphWidth, glyphHeight, stride,
|
||||
metrics.GlyphOrigin.X, metrics.GlyphOrigin.Y,
|
||||
metrics.CellIncrementX, metrics.CellIncrementY,
|
||||
cell.Width, cell.Height, coverage);
|
||||
cell.Width, cell.Height, coverage, checked((int)size));
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -178,7 +223,8 @@ public class WindowsGdiGlyphMaskRasterizerTests
|
||||
int CellAdvanceY,
|
||||
int CellWidth,
|
||||
int CellHeight,
|
||||
byte[] Coverage);
|
||||
byte[] Coverage,
|
||||
int RawBufferSize);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct NativePoint
|
||||
|
||||
@@ -126,22 +126,31 @@ public sealed class WindowsGdiGlyphMaskRasterizer
|
||||
int expectedBytes = checked(stride * height);
|
||||
// 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)
|
||||
// Preserve those metrics and materialize the implied transparent mask. GDI can also
|
||||
// return storage beyond the metric-defined rows (for example, bold 24px MS Mincho 'p'
|
||||
// reports 12x15 but requests 192 bytes rather than 180). Treat the queried size as the
|
||||
// allocation contract and the GLYPHMETRICS box as the raster-consumption contract: read
|
||||
// exactly the requested buffer, then retain only the rows described by the metrics. Bytes
|
||||
// outside that box are not drawable coverage and are not required to be initialized to zero.
|
||||
if (size != 0 && size < expectedBytes)
|
||||
throw new InvalidOperationException(
|
||||
$"GDI gray-4 buffer size {size} disagrees with {width}x{height}, stride {stride}.");
|
||||
$"GDI gray-4 buffer size {size} for CP932 0x{code:x4} " +
|
||||
$"('{request.FontFace}', {request.PixelHeight}px, width {request.RequestedWidth}, " +
|
||||
$"weight {request.Weight}) is smaller than its {width}x{height} metric box, " +
|
||||
$"stride {stride} ({expectedBytes} bytes).");
|
||||
|
||||
byte[] coverage = new byte[expectedBytes];
|
||||
if (size > 0)
|
||||
{
|
||||
byte[] gdiBuffer = new byte[checked((int)size)];
|
||||
identity = Mat2.Identity;
|
||||
uint written = NativeMethods.GetGlyphOutlineA(
|
||||
_displayIc.DangerousGetHandle(), code, GgoGray4Bitmap,
|
||||
out GlyphMetrics secondMetrics, size, coverage, ref identity);
|
||||
out GlyphMetrics secondMetrics, size, gdiBuffer, ref identity);
|
||||
if (written == GdiError) ThrowWin32($"GetGlyphOutlineA read failed for CP932 0x{code:x4}");
|
||||
if (written != size || !metrics.Equals(secondMetrics))
|
||||
throw new InvalidOperationException("GDI glyph metrics changed between query and read.");
|
||||
gdiBuffer.AsSpan(0, expectedBytes).CopyTo(coverage);
|
||||
}
|
||||
|
||||
byte[] encoded = EncodeCp932Code(code);
|
||||
|
||||
Reference in New Issue
Block a user