fix retained glyph bottom clipping
This commit is contained in:
@@ -2894,6 +2894,16 @@ The shipped effect profiles use only modes 1 and 3:
|
|||||||
`360 / (sqrt(effect_x²+effect_y²)*8)`, blits the effect-colored glyph at every rounded offset, then
|
`360 / (sqrt(effect_x²+effect_y²)*8)`, blits the effect-colored glyph at every rounded offset, then
|
||||||
draws the primary glyph. All 164 paired sites use `(1,1)`, producing the normal one-pixel outline.
|
draws the primary glyph. All 164 paired sites use `(1,1)`, producing the normal one-pixel outline.
|
||||||
|
|
||||||
|
**Measured cell versus raster ink (2026-07-30).** `adv_text_build_glyph_records@0x4576c0` stores
|
||||||
|
the `GetTextExtentPoint32A` measured cell edges in each retained record, and
|
||||||
|
`adv_text_publish_next_glyph@0x451220` uses those edges when it binds the native object.
|
||||||
|
`text_raster_string_cached@0x45b600`, however, composites the complete `GetGlyphOutlineA` black-box
|
||||||
|
bitmap and effect copies into the shared layout surface. Direct 24-pixel bold Mincho probes show several
|
||||||
|
ordinary Japanese glyphs reaching below the 24-pixel measured cell even before the mode-3 `(1,1)` outline.
|
||||||
|
Thus the record is a layout/publication cell, not a guaranteed ink bounding box. The port preserves that
|
||||||
|
native record and derives a separate transient vertical publication crop from mask origin/height and effect
|
||||||
|
offsets; its horizontal crop remains the measured cell to avoid exposing the next glyph during timed reveal.
|
||||||
|
|
||||||
The ordinary ADV preset is therefore 24-pixel `MS 明朝`, weight 700, white fill, mode-3 `(1,1)` outline
|
The ordinary ADV preset is therefore 24-pixel `MS 明朝`, weight 700, white fill, mode-3 `(1,1)` outline
|
||||||
in `0x606060`, with 8 pixels of line leading. The small menu-description preset is 16-pixel
|
in `0x606060`, with 8 pixels of line leading. The small menu-description preset is 16-pixel
|
||||||
`MS ゴシック`, weight 0, white fill, mode 1 with zero displacement, and 9 pixels of leading.
|
`MS ゴシック`, weight 0, white fill, mode 1 with zero displacement, and 9 pixels of leading.
|
||||||
|
|||||||
@@ -3674,6 +3674,15 @@ surface's z-order, alpha, tint, fade, transform, clipping, capture, and lifetime
|
|||||||
Validation: engine **580/580**, zero-warning Godot build, and threaded `SELFTEST OK` in both forced
|
Validation: engine **580/580**, zero-warning Godot build, and threaded `SELFTEST OK` in both forced
|
||||||
`portable-godot-textserver` and forced `windows-gdi-gray4` modes.
|
`portable-godot-textserver` and forced `windows-gdi-gray4` modes.
|
||||||
|
|
||||||
|
**Bottom-edge clipping follow-up (2026-07-30):** retained publication originally reused the native
|
||||||
|
measured-cell rectangle as its exact source crop. Direct GDI probing confirmed that the glyph black-box
|
||||||
|
bitmap, plus the mode-3 outline, can extend below that cell even though the pixels are present in the
|
||||||
|
layout surface. The native `AdvRetainedGlyphRecord` remains unchanged for layout and reconstruction; each
|
||||||
|
transient placement now carries a separate vertically ink-safe publication rectangle. Horizontal edges
|
||||||
|
remain cell-bounded so timed reveal cannot expose a neighboring glyph. Synthetic compositor/publication
|
||||||
|
regressions pass, the Godot build is clean, and forced exact and portable self-tests both report
|
||||||
|
`SELFTEST OK`.
|
||||||
|
|
||||||
## Persistence native-format reconnaissance complete (2026-07-24)
|
## Persistence native-format reconnaissance complete (2026-07-24)
|
||||||
|
|
||||||
The deferred save/profile ownership question now has a compatibility-mode answer. The remaining native
|
The deferred save/profile ownership question now has a compatibility-mode answer. The remaining native
|
||||||
|
|||||||
@@ -627,7 +627,10 @@ primitive, which cannot be made equivalent by choosing another embolden constant
|
|||||||
per-run origins, partial erase/republication, suspension, measured cursor handoff, reset, and
|
per-run origins, partial erase/republication, suspension, measured cursor handoff, reset, and
|
||||||
reconstruction. The synthesized Godot VM self-test exercises timed exact reveal and reports
|
reconstruction. The synthesized Godot VM self-test exercises timed exact reveal and reports
|
||||||
`live-adv-text=retained-glyphs`; real GPU captures of SC0000 pages 1 and 7 confirm ordinary and voiced
|
`live-adv-text=retained-glyphs`; real GPU captures of SC0000 pages 1 and 7 confirm ordinary and voiced
|
||||||
dialogue placement/wrapping alongside the immediate speaker-name surface.
|
dialogue placement/wrapping alongside the immediate speaker-name surface. A 2026-07-30 clipping
|
||||||
|
follow-up keeps those native measured-cell records intact but gives transient placements a separate
|
||||||
|
vertically ink-safe source crop, because backend glyph black boxes and mode-3 outlines can extend below
|
||||||
|
the measured cell; horizontal crops remain cell-bounded to preserve reveal isolation.
|
||||||
6. **Migrate History and the ADV wait indicator through the same retained path. — Completed
|
6. **Migrate History and the ADV wait indicator through the same retained path. — Completed
|
||||||
2026-07-30.** Op `0x1d1` now rasterizes each HISTORY row into its target layout surface and publishes its
|
2026-07-30.** Op `0x1d1` now rasterizes each HISTORY row into its target layout surface and publishes its
|
||||||
native glyph interval through ordinary `GfxState` bindings. Row reset, wheel redraw, the broad native exit
|
native glyph interval through ordinary `GfxState` bindings. Row reset, wheel redraw, the broad native exit
|
||||||
@@ -645,7 +648,8 @@ primitive, which cannot be made equivalent by choosing another embolden constant
|
|||||||
sizes. `auto` selects exact GDI when its ACP-932 gate passes and otherwise selects the explicitly
|
sizes. `auto` selects exact GDI when its ACP-932 gate passes and otherwise selects the explicitly
|
||||||
non-pixel-exact Unicode backend; development runs can force either with `--text-backend`. Both modes pass
|
non-pixel-exact Unicode backend; development runs can force either with `--text-backend`. Both modes pass
|
||||||
the same immediate/live/History retained-glyph self-test. Gameplay `Label` pools and surface-text overlay
|
the same immediate/live/History retained-glyph self-test. Gameplay `Label` pools and surface-text overlay
|
||||||
projection are removed, so text always participates in the ordinary surface compositor.
|
projection are removed, so text always participates in the ordinary surface compositor. Publication uses
|
||||||
|
the transient ink-safe crop established in step 5 in both backends.
|
||||||
|
|
||||||
**Acceptance gates:**
|
**Acceptance gates:**
|
||||||
|
|
||||||
|
|||||||
@@ -193,6 +193,40 @@ public class GlyphMaskCompositorTests
|
|||||||
Assert.Equal(255, Pixel(destination, 2, 7).A);
|
Assert.Equal(255, Pixel(destination, 2, 7).A);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RetainedPresentationCropIncludesInkBelowTheNativeCell()
|
||||||
|
{
|
||||||
|
var mask = new GlyphMask(
|
||||||
|
width: 1, height: 4, stride: 1,
|
||||||
|
originX: 0, originY: 3,
|
||||||
|
cellAdvanceX: 4, cellAdvanceY: 0,
|
||||||
|
cellWidth: 4, cellHeight: 5,
|
||||||
|
coverage: [16, 16, 16, 16]);
|
||||||
|
var destination = Image(16, 16);
|
||||||
|
var engine = new RetainedGlyphLayoutEngine(
|
||||||
|
new CountingRasterizer(_ => mask));
|
||||||
|
|
||||||
|
GlyphTextLayoutResult result = engine.Render(
|
||||||
|
destination,
|
||||||
|
new GlyphTextLayoutOptions(
|
||||||
|
CursorX: 2, CursorY: 1, LineOriginX: 2,
|
||||||
|
RightBound: 16, BottomBound: 16, WrapHorizontally: true,
|
||||||
|
Style(renderMode: 3) with
|
||||||
|
{
|
||||||
|
EffectOffsetX = 1,
|
||||||
|
EffectOffsetY = 1,
|
||||||
|
}),
|
||||||
|
[NativeRequest(0x82a0, 0x3042)]);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
new AdvRetainedGlyphRecord(0, 2, 1, 6, 6),
|
||||||
|
Assert.Single(result.Records));
|
||||||
|
Assert.Equal(
|
||||||
|
new AdvRetainedGlyphPresentationRect(2, 1, 6, 8),
|
||||||
|
Assert.Single(result.PresentationRects));
|
||||||
|
Assert.NotEqual(0, Pixel(destination, 2, 7).A);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ClosingPunctuationStaysOnOverflowingPrecedingLine()
|
public void ClosingPunctuationStaysOnOverflowingPrecedingLine()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -79,6 +79,29 @@ public class RetainedAdvTextLayoutPresentationTests
|
|||||||
gfx.SnapshotVisibleObjects().Select(item => (item.DstX, item.DstY)));
|
gfx.SnapshotVisibleObjects().Select(item => (item.DstX, item.DstY)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PublicationUsesInkSafeCropWithoutChangingNativeRecord()
|
||||||
|
{
|
||||||
|
var presentation = new RetainedAdvTextLayoutPresentation(
|
||||||
|
new AdvTextLayoutPresentationBinding(
|
||||||
|
1, 21, 100, 4, -1, 10, 20));
|
||||||
|
var record = new AdvRetainedGlyphRecord(0, 3, 4, 7, 10);
|
||||||
|
presentation.Append(
|
||||||
|
[record],
|
||||||
|
[new AdvRetainedGlyphPresentationRect(3, 3, 7, 12)],
|
||||||
|
layoutOriginX: 30,
|
||||||
|
layoutOriginY: 40);
|
||||||
|
var gfx = new GfxState();
|
||||||
|
gfx.CreateSurface(21);
|
||||||
|
|
||||||
|
presentation.PublishThrough(gfx, 1);
|
||||||
|
|
||||||
|
RenderObject item = Assert.Single(gfx.SnapshotVisibleObjects());
|
||||||
|
Assert.Equal((3, 3, 4, 9, 33, 43),
|
||||||
|
(item.SrcX, item.SrcY, item.W, item.H, item.DstX, item.DstY));
|
||||||
|
Assert.Equal(record, Assert.Single(presentation.Glyphs).Record);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SavedFrameStyleReconstructionRebuildsTheSameBindings()
|
public void SavedFrameStyleReconstructionRebuildsTheSameBindings()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,8 +34,23 @@ public readonly record struct AdvTextLayoutPresentationBinding(
|
|||||||
int ResetCursorX,
|
int ResetCursorX,
|
||||||
int ResetCursorY);
|
int ResetCursorY);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transient publication crop. Native records retain measured cell edges, but a backend mask/effect can
|
||||||
|
/// carry ink outside that cell. Keeping the expanded crop separate preserves the native layout ABI.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct AdvRetainedGlyphPresentationRect(
|
||||||
|
int Left,
|
||||||
|
int Top,
|
||||||
|
int Right,
|
||||||
|
int Bottom)
|
||||||
|
{
|
||||||
|
public int Width => Right - Left;
|
||||||
|
public int Height => Bottom - Top;
|
||||||
|
}
|
||||||
|
|
||||||
public readonly record struct AdvRetainedGlyphPlacement(
|
public readonly record struct AdvRetainedGlyphPlacement(
|
||||||
AdvRetainedGlyphRecord Record,
|
AdvRetainedGlyphRecord Record,
|
||||||
|
AdvRetainedGlyphPresentationRect PresentationRect,
|
||||||
int LayoutOriginX,
|
int LayoutOriginX,
|
||||||
int LayoutOriginY);
|
int LayoutOriginY);
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,30 @@ public sealed class RetainedAdvTextLayoutPresentation
|
|||||||
int first = _glyphs.Count;
|
int first = _glyphs.Count;
|
||||||
foreach (AdvRetainedGlyphRecord record in records)
|
foreach (AdvRetainedGlyphRecord record in records)
|
||||||
_glyphs.Add(new AdvRetainedGlyphPlacement(
|
_glyphs.Add(new AdvRetainedGlyphPlacement(
|
||||||
record, layoutOriginX, layoutOriginY));
|
record,
|
||||||
|
new AdvRetainedGlyphPresentationRect(
|
||||||
|
record.Left, record.Top, record.Right, record.Bottom),
|
||||||
|
layoutOriginX,
|
||||||
|
layoutOriginY));
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Append(
|
||||||
|
IReadOnlyList<AdvRetainedGlyphRecord> records,
|
||||||
|
IReadOnlyList<AdvRetainedGlyphPresentationRect> presentationRects,
|
||||||
|
int layoutOriginX,
|
||||||
|
int layoutOriginY)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(records);
|
||||||
|
ArgumentNullException.ThrowIfNull(presentationRects);
|
||||||
|
if (presentationRects.Count != records.Count)
|
||||||
|
throw new ArgumentException(
|
||||||
|
"Every retained glyph record requires one presentation rectangle.",
|
||||||
|
nameof(presentationRects));
|
||||||
|
int first = _glyphs.Count;
|
||||||
|
for (int index = 0; index < records.Count; index++)
|
||||||
|
_glyphs.Add(new AdvRetainedGlyphPlacement(
|
||||||
|
records[index], presentationRects[index], layoutOriginX, layoutOriginY));
|
||||||
return first;
|
return first;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,16 +94,16 @@ public sealed class RetainedAdvTextLayoutPresentation
|
|||||||
private void Bind(GfxState gfx, int index)
|
private void Bind(GfxState gfx, int index)
|
||||||
{
|
{
|
||||||
AdvRetainedGlyphPlacement placement = _glyphs[index];
|
AdvRetainedGlyphPlacement placement = _glyphs[index];
|
||||||
AdvRetainedGlyphRecord record = placement.Record;
|
AdvRetainedGlyphPresentationRect rect = placement.PresentationRect;
|
||||||
if (record.Width <= 0 || record.Height <= 0) return;
|
if (rect.Width <= 0 || rect.Height <= 0) return;
|
||||||
gfx.BindDraw(
|
gfx.BindDraw(
|
||||||
checked(Binding.FirstObjectHandle + index),
|
checked(Binding.FirstObjectHandle + index),
|
||||||
Binding.SourceSurfaceSlot,
|
Binding.SourceSurfaceSlot,
|
||||||
record.Left,
|
rect.Left,
|
||||||
record.Top,
|
rect.Top,
|
||||||
record.Width,
|
rect.Width,
|
||||||
record.Height,
|
rect.Height,
|
||||||
checked(placement.LayoutOriginX + record.Left),
|
checked(placement.LayoutOriginX + rect.Left),
|
||||||
checked(placement.LayoutOriginY + record.Top));
|
checked(placement.LayoutOriginY + rect.Top));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ public readonly record struct GlyphTextLayoutOptions(
|
|||||||
|
|
||||||
public sealed record GlyphTextLayoutResult(
|
public sealed record GlyphTextLayoutResult(
|
||||||
IReadOnlyList<AdvRetainedGlyphRecord> Records,
|
IReadOnlyList<AdvRetainedGlyphRecord> Records,
|
||||||
|
IReadOnlyList<AdvRetainedGlyphPresentationRect> PresentationRects,
|
||||||
int CursorX,
|
int CursorX,
|
||||||
int CursorY,
|
int CursorY,
|
||||||
int ConsumedGlyphs,
|
int ConsumedGlyphs,
|
||||||
@@ -40,6 +41,8 @@ public sealed class RetainedGlyphLayoutEngine
|
|||||||
ArgumentNullException.ThrowIfNull(destination);
|
ArgumentNullException.ThrowIfNull(destination);
|
||||||
ArgumentNullException.ThrowIfNull(glyphs);
|
ArgumentNullException.ThrowIfNull(glyphs);
|
||||||
var records = new List<AdvRetainedGlyphRecord>(glyphs.Count);
|
var records = new List<AdvRetainedGlyphRecord>(glyphs.Count);
|
||||||
|
var presentationRects =
|
||||||
|
new List<AdvRetainedGlyphPresentationRect>(glyphs.Count);
|
||||||
int cursorX = options.CursorX;
|
int cursorX = options.CursorX;
|
||||||
int cursorY = options.CursorY;
|
int cursorY = options.CursorY;
|
||||||
int wrappedLines = 0;
|
int wrappedLines = 0;
|
||||||
@@ -81,11 +84,59 @@ public sealed class RetainedGlyphLayoutEngine
|
|||||||
AgeGlyphMaskCompositor.DrawGlyph(
|
AgeGlyphMaskCompositor.DrawGlyph(
|
||||||
destination, mask, cursorX, cursorY, request.PixelHeight, options.Style);
|
destination, mask, cursorX, cursorY, request.PixelHeight, options.Style);
|
||||||
records.Add(new AdvRetainedGlyphRecord(0, cursorX, cursorY, right, bottom));
|
records.Add(new AdvRetainedGlyphRecord(0, cursorX, cursorY, right, bottom));
|
||||||
|
presentationRects.Add(PresentationRect(
|
||||||
|
destination.Height,
|
||||||
|
mask,
|
||||||
|
cursorX,
|
||||||
|
cursorY,
|
||||||
|
right,
|
||||||
|
bottom,
|
||||||
|
request.PixelHeight,
|
||||||
|
options.Style));
|
||||||
cursorX = checked(cursorX + mask.CellAdvanceX);
|
cursorX = checked(cursorX + mask.CellAdvanceX);
|
||||||
cursorY = checked(cursorY + mask.CellAdvanceY);
|
cursorY = checked(cursorY + mask.CellAdvanceY);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new GlyphTextLayoutResult(
|
return new GlyphTextLayoutResult(
|
||||||
records, cursorX, cursorY, records.Count, wrappedLines, observed, stopped);
|
records, presentationRects, cursorX, cursorY, records.Count,
|
||||||
|
wrappedLines, observed, stopped);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AdvRetainedGlyphPresentationRect PresentationRect(
|
||||||
|
int surfaceHeight,
|
||||||
|
GlyphMask mask,
|
||||||
|
int cellLeft,
|
||||||
|
int cellTop,
|
||||||
|
int cellRight,
|
||||||
|
int cellBottom,
|
||||||
|
int topToBaseline,
|
||||||
|
AdvTextStyle style)
|
||||||
|
{
|
||||||
|
int minimumEffectY = 0;
|
||||||
|
int maximumEffectY = 0;
|
||||||
|
if (style.RenderMode == 1)
|
||||||
|
{
|
||||||
|
minimumEffectY = Math.Min(0, style.EffectOffsetY);
|
||||||
|
maximumEffectY = Math.Max(0, style.EffectOffsetY);
|
||||||
|
}
|
||||||
|
else if (style.RenderMode == 3)
|
||||||
|
{
|
||||||
|
foreach ((int _, int y) in AgeGlyphMaskCompositor.GetMode3OutlineOffsets(
|
||||||
|
style.EffectOffsetX, style.EffectOffsetY))
|
||||||
|
{
|
||||||
|
minimumEffectY = Math.Min(minimumEffectY, y);
|
||||||
|
maximumEffectY = Math.Max(maximumEffectY, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int inkTop = checked(
|
||||||
|
cellTop + topToBaseline - mask.OriginY + minimumEffectY);
|
||||||
|
int inkBottom = checked(
|
||||||
|
cellTop + topToBaseline - mask.OriginY + mask.Height + maximumEffectY);
|
||||||
|
return new AdvRetainedGlyphPresentationRect(
|
||||||
|
cellLeft,
|
||||||
|
Math.Clamp(Math.Min(cellTop, inkTop), 0, surfaceHeight),
|
||||||
|
cellRight,
|
||||||
|
Math.Clamp(Math.Max(cellBottom, inkBottom), 0, surfaceHeight));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -425,7 +425,10 @@ public sealed class GodotAdvHost : IHost
|
|||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"ADV layout {binding.LayoutSlot} changed its retained binding without reset.");
|
$"ADV layout {binding.LayoutSlot} changed its retained binding without reset.");
|
||||||
int first = presentation.Append(
|
int first = presentation.Append(
|
||||||
rendered.Records, run.Layout.OriginX, run.Layout.OriginY);
|
rendered.Records,
|
||||||
|
rendered.PresentationRects,
|
||||||
|
run.Layout.OriginX,
|
||||||
|
run.Layout.OriginY);
|
||||||
result = new AdvRetainedTextRunResult(
|
result = new AdvRetainedTextRunResult(
|
||||||
binding.LayoutSlot,
|
binding.LayoutSlot,
|
||||||
first,
|
first,
|
||||||
|
|||||||
Reference in New Issue
Block a user