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
|
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.
|
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/
|
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.
|
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.
|
`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
|
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
|
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
|
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
|
transparent coverage. A focused Windows regression reproduces the original 0-versus-4 result and verifies that
|
||||||
reproduces the original 0-versus-4 result and verifies that the ideographic space advances without drawing ink;
|
the ideographic space advances without drawing ink; nonzero buffers smaller than the metric-defined rows remain
|
||||||
the SYSTEM4/DEBUGMAP boundary regression remains in place. The user then repeated the normal exact-GDI
|
hard failures. The SYSTEM4/DEBUGMAP boundary regression remains in place. The user then repeated the normal
|
||||||
DEBUGMAP entry and confirmed that the dialogue now continues into the map without the worker failure, accepting
|
exact-GDI DEBUGMAP entry and confirmed that the dialogue now continues into the map without the worker failure,
|
||||||
the fix end to end.
|
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
|
## Stage B0 — Ground-truth reconnaissance
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Age.Engine.Model;
|
||||||
using Age.Engine.Text;
|
using Age.Engine.Text;
|
||||||
using Age.Engine.Text.Windows;
|
using Age.Engine.Text.Windows;
|
||||||
|
|
||||||
@@ -73,6 +74,48 @@ public class WindowsGdiGlyphMaskRasterizerTests
|
|||||||
Assert.All(space.Coverage.ToArray(), value => Assert.Equal(0, value));
|
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]
|
[Fact]
|
||||||
public void FontHandlesUseTheSharedBoundedLruAndDisposeCleanly()
|
public void FontHandlesUseTheSharedBoundedLruAndDisposeCleanly()
|
||||||
{
|
{
|
||||||
@@ -135,17 +178,19 @@ public class WindowsGdiGlyphMaskRasterizerTests
|
|||||||
int glyphWidth = checked((int)metrics.BlackBoxX);
|
int glyphWidth = checked((int)metrics.BlackBoxX);
|
||||||
int glyphHeight = checked((int)metrics.BlackBoxY);
|
int glyphHeight = checked((int)metrics.BlackBoxY);
|
||||||
int stride = checked((glyphWidth + 3) & ~3);
|
int stride = checked((glyphWidth + 3) & ~3);
|
||||||
Assert.Equal(checked(stride * glyphHeight), (int)size);
|
int expectedBytes = checked(stride * glyphHeight);
|
||||||
byte[] coverage = new byte[size];
|
Assert.True(size >= expectedBytes);
|
||||||
|
byte[] rawCoverage = new byte[size];
|
||||||
if (size > 0)
|
if (size > 0)
|
||||||
{
|
{
|
||||||
identity = Mat2.Identity;
|
identity = Mat2.Identity;
|
||||||
uint written = Native.GetGlyphOutlineW(
|
uint written = Native.GetGlyphOutlineW(
|
||||||
dc, (uint)scalar, GgoGray4Bitmap,
|
dc, (uint)scalar, GgoGray4Bitmap,
|
||||||
out GlyphMetrics second, size, coverage, ref identity);
|
out GlyphMetrics second, size, rawCoverage, ref identity);
|
||||||
Assert.Equal(size, written);
|
Assert.Equal(size, written);
|
||||||
Assert.Equal(metrics, second);
|
Assert.Equal(metrics, second);
|
||||||
}
|
}
|
||||||
|
byte[] coverage = rawCoverage.AsSpan(0, expectedBytes).ToArray();
|
||||||
|
|
||||||
string text = char.ConvertFromUtf32(scalar);
|
string text = char.ConvertFromUtf32(scalar);
|
||||||
if (!Native.GetTextExtentPoint32W(dc, text, text.Length, out NativeSize cell))
|
if (!Native.GetTextExtentPoint32W(dc, text, text.Length, out NativeSize cell))
|
||||||
@@ -154,7 +199,7 @@ public class WindowsGdiGlyphMaskRasterizerTests
|
|||||||
glyphWidth, glyphHeight, stride,
|
glyphWidth, glyphHeight, stride,
|
||||||
metrics.GlyphOrigin.X, metrics.GlyphOrigin.Y,
|
metrics.GlyphOrigin.X, metrics.GlyphOrigin.Y,
|
||||||
metrics.CellIncrementX, metrics.CellIncrementY,
|
metrics.CellIncrementX, metrics.CellIncrementY,
|
||||||
cell.Width, cell.Height, coverage);
|
cell.Width, cell.Height, coverage, checked((int)size));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -178,7 +223,8 @@ public class WindowsGdiGlyphMaskRasterizerTests
|
|||||||
int CellAdvanceY,
|
int CellAdvanceY,
|
||||||
int CellWidth,
|
int CellWidth,
|
||||||
int CellHeight,
|
int CellHeight,
|
||||||
byte[] Coverage);
|
byte[] Coverage,
|
||||||
|
int RawBufferSize);
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
private struct NativePoint
|
private struct NativePoint
|
||||||
|
|||||||
@@ -126,22 +126,31 @@ public sealed class WindowsGdiGlyphMaskRasterizer
|
|||||||
int expectedBytes = checked(stride * height);
|
int expectedBytes = checked(stride * height);
|
||||||
// CP932 0x8140 (U+3000 IDEOGRAPHIC SPACE) is a zero-ink spacing glyph. GDI reports
|
// 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.
|
// 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
|
// Preserve those metrics and materialize the implied transparent mask. GDI can also
|
||||||
// short/oversized payload still means that the bitmap contract is inconsistent.
|
// return storage beyond the metric-defined rows (for example, bold 24px MS Mincho 'p'
|
||||||
if (size != 0 && size != expectedBytes)
|
// 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(
|
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];
|
byte[] coverage = new byte[expectedBytes];
|
||||||
if (size > 0)
|
if (size > 0)
|
||||||
{
|
{
|
||||||
|
byte[] gdiBuffer = new byte[checked((int)size)];
|
||||||
identity = Mat2.Identity;
|
identity = Mat2.Identity;
|
||||||
uint written = NativeMethods.GetGlyphOutlineA(
|
uint written = NativeMethods.GetGlyphOutlineA(
|
||||||
_displayIc.DangerousGetHandle(), code, GgoGray4Bitmap,
|
_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 == GdiError) ThrowWin32($"GetGlyphOutlineA read failed for CP932 0x{code:x4}");
|
||||||
if (written != size || !metrics.Equals(secondMetrics))
|
if (written != size || !metrics.Equals(secondMetrics))
|
||||||
throw new InvalidOperationException("GDI glyph metrics changed between query and read.");
|
throw new InvalidOperationException("GDI glyph metrics changed between query and read.");
|
||||||
|
gdiBuffer.AsSpan(0, expectedBytes).CopyTo(coverage);
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] encoded = EncodeCp932Code(code);
|
byte[] encoded = EncodeCp932Code(code);
|
||||||
|
|||||||
Reference in New Issue
Block a user