fix(font): close stage-2 GPU behavioral findings

Normalize source bytes to representable shaped-cluster cursors and
reflow the code buffer whenever gutter or minimap geometry changes.

Measure alternate monospace advances across complete shaped runs and
load hermetic fixture faces before FontSystem construction. Complete
the rendered geometry, popup, caret, snapshot, and fixture acceptance
coverage, and record the review fixes in framing revision 5.
This commit is contained in:
Levi Neuwirth 2026-07-18 15:29:25 +01:00
parent 6cd78f0870
commit b3d326d937
4 changed files with 1006 additions and 169 deletions

View File

@ -1,8 +1,31 @@
# GPU font preference — framing (Arc 4 stage 2, `pmacs.gpu.set_font`)
**Revision 4 — 2026-07-15. Status: implemented on branch
**Revision 5 — 2026-07-18. Status: implemented on branch
`gpu-set-font` (protocol v17); awaiting PR review.**
Revision 5 (PR review, findings 15): source bytes that fall inside a
shaped cluster are normalized to an explicit representable cosmic-text
cursor before geometry or following; combining-mark and real-ligature
fixtures pin that they can no longer fall back to the source-line start
(finding 1). Code-buffer reflow is now one transaction for every
dynamic horizontal input, not only font application: line-number mode,
gutter digit-count transitions, minimap appearance/disappearance, full
text replacement, incremental CRDT edits, and byte-identical
`BufferSnapshot` summary clearing all synchronize the shaping width
before the final reshape and re-follow only a previously painted caret
(finding 2). The monospace advance probe divides the total shaped-run
width by the probe's cell count, rather than sampling its first glyph;
the embedded alternate monospace carries a real multi-cell ligature so
the old result is observably wrong (finding 3). All four fixture fonts
now enter the explicit database before `FontSystem` construction and
their retained IDs are checked against cosmic-text's actual
`is_monospace` classification, removing the post-construction test
approximation (finding 4). Acceptance 10, 11, 17, and 18 now exercise
their complete claims at both size bounds, including rendered band
containment, selection/hit row identity, wrapped completion anchors,
gutter continuation alignment, reverse caret-free reflow, and both
directions of caret-free resize (finding 5).
Revision 4 (framing round 3, findings 15): caret preservation is now
VISUAL-RUN aware rather than source-line-only. The code buffer keeps a
normalized cosmic-text `Scroll`, full reshapes reapply it, and the
@ -443,6 +466,14 @@ On a `FontFacts` arrival the GPU replaces its font state wholesale:
one final size/shape pass; no frame is submitted between them.
`resize()` goes through the same dimension helper for ALL seven
buffers, closing the existing four-of-seven resize skew.
The same helper and settle transaction also own every runtime input
that changes the code clip without changing the font: line-number
mode, a gutter digit-count transition after full or incremental text,
minimap presence after `FileStyleSummary`, and summary removal during
even a byte-identical `BufferSnapshot`. Each path captures the old
painted-caret predicate before changing geometry, synchronizes the
final buffer dimensions before shaping, and follows only when that
predicate was true.
- **Rows stay rows**: menu, minibuffer-candidate, and completion buffers
explicitly use `Wrap::None`. Their protocols, row-window calculations,
selection quads, and hit tests all assign exactly one row-height to one
@ -453,9 +484,13 @@ On a `FontFacts` arrival the GPU replaces its font state wholesale:
may clip within their single derived band.
- **Font-dependent advances without default drift**: the internal
measure pass shapes a fixed ASCII probe in the resolved family at the
relevant metrics, independent of document contents. It records the
selected/default advance ratio. The empty-code gutter fallback and
`menu_char_w` use today's exact constants multiplied by that ratio;
relevant metrics, independent of document contents. It sums every
shaped run's width and divides by the probe's logical cell count;
sampling one glyph is invalid because even a monospaced face may
shape several probe characters into one multi-cell ligature. It
records the selected/default advance ratio. The empty-code gutter
fallback and `menu_char_w` use today's exact constants multiplied by
that ratio;
the sanitized per-process default is therefore ratio 1 and remains
byte-identical, while an alternate monospace family cannot leave stale
JetBrains-only gutter/menu geometry. The measured NORMAL-face advance
@ -505,7 +540,11 @@ On a `FontFacts` arrival the GPU replaces its font state wholesale:
bytes when inline adornments are present — using the earliest
projected boundary for an adornment anchor (the current left-gravity
caret placement), then uses cosmic-text's `layout_cursor`/affinity so
a wrap boundary selects the same run as `shape_until_cursor`. A shared
a wrap boundary selects the same run as `shape_until_cursor`. A source
byte inside a combining or ligature cluster is snapped explicitly to
the cluster's logical end with `Before` affinity; it is never handed
to cosmic-text as an unrepresentable interior cursor, whose fallback
is the source-line start. A shared
`ensure_caret_painted` helper first performs the existing coarse
source-line `scroll_to_cursor` and rebuild when the byte is outside the
shaped slice, then maps the byte to a cosmic-text `Cursor` and calls
@ -665,7 +704,9 @@ route tests in pmacs-gpu's headless suite (`PMACS_REQUIRE_GPU=1`).
resets the normalized code scroll with the other BUFFER-scoped view
state so B cannot inherit A's visual residual. The new buffer shapes
under the same preference without waiting for a redundant global
fact.
fact. A byte-identical snapshot also removes the prior buffer's
minimap reservation before reshaping, so identical text cannot retain
the old buffer's narrower code clip.
5. **Version gate**: a v16 peer session never receives `FontFacts`
(producer `for_peer` + daemon skip arm, the real-daemon probe
shape from stage 1).
@ -740,8 +781,10 @@ route tests in pmacs-gpu's headless suite (`PMACS_REQUIRE_GPU=1`).
preservation); a same-family BOLD proportional collision is removed
during database assembly, styled
code still selects only monospaced faces, and the bundled ID is
present in cosmic-text's monospace-ID set. Direct resolver units
cover normal, bold, italic, and bold-italic queries. Both
present in cosmic-text's monospace-ID set. All four fixture IDs are
retained from the pre-`FontSystem` assembly and checked against
cosmic-text's real `is_monospace` classification. Direct resolver
units cover normal, bold, italic, and bold-italic queries. Both
rejected-request routes and the collision-safe default reset render
byte-identically to never-set.
13. **GPU shaping-cache invalidation**: with composed band strings
@ -770,7 +813,9 @@ route tests in pmacs-gpu's headless suite (`PMACS_REQUIRE_GPU=1`).
for gutter; no buffer retains construction-time or prior-size
dimensions. A family whose advance changes the gutter width forces
the final code-width reflow, and the resulting wrap/hit map and
rendered clip use that same width.
rendered clip use that same width. Line-number enable/disable,
gutter digit transitions, and minimap appearance/removal exercise
the same dynamic reflow transaction independently of a font change.
18. **Popup row invariance at both size bounds**: long menu,
minibuffer-candidate, and completion labels remain one layout run per
wire row at 600 and 7200 (`Wrap::None`). Their selection quads and

View File

@ -8,7 +8,9 @@ installed, so these tiny fixture faces are generated and committed:
PmacsTestMonoTwo-Regular.ttf "Pmacs Test Mono Two" monospaced,
advance 720/1000 (JetBrains Mono is
600/1000, so the measured advance
ratio is exactly 1.2)
ratio is exactly 1.2), with true
"01" and "fi" ligatures whose advances
preserve two cells
PmacsTestProportional-Regular.ttf "Pmacs Test Proportional" varying
advances, not monospaced
PmacsTestFamily-Regular.ttf "Pmacs Test Family" monospaced
@ -28,6 +30,7 @@ Run from this directory: python3 generate.py
Requires fontTools (any recent version).
"""
from fontTools.feaLib.builder import addOpenTypeFeaturesFromString
from fontTools.fontBuilder import FontBuilder
from fontTools.pens.ttGlyphPen import TTGlyphPen
@ -35,6 +38,9 @@ UPM = 1000
CHARS = " 0123456789abcdefghijklmnopqrstuvwxyz"
ASCENT = 800
DESCENT = -200
# Fixed at the first committed fixture generation so re-running this
# script changes only intentional font data, not the head timestamps.
FIXTURE_TIMESTAMP = 3866975598
def glyph_name(char):
@ -58,8 +64,18 @@ def empty_glyph():
return TTGlyphPen(None).glyph()
def build(path, family, style, weight, bold, fixed_pitch, advance_for):
def build(
path,
family,
style,
weight,
bold,
fixed_pitch,
advance_for,
ligatures=(),
):
order = [".notdef"] + [glyph_name(c) for c in CHARS]
order += [name for name, _ in ligatures]
fb = FontBuilder(UPM, isTTF=True)
fb.setupGlyphOrder(order)
fb.setupCharacterMap({ord(c): glyph_name(c) for c in CHARS})
@ -70,6 +86,10 @@ def build(path, family, style, weight, bold, fixed_pitch, advance_for):
name = glyph_name(c)
glyphs[name] = empty_glyph() if c == " " else rect_glyph(adv)
metrics[name] = (adv, 0 if c == " " else 60)
for name, components in ligatures:
advance = sum(advance_for(c) for c in components)
glyphs[name] = rect_glyph(advance)
metrics[name] = (advance, 60)
fb.setupGlyf(glyphs)
fb.setupHorizontalMetrics(metrics)
fb.setupHorizontalHeader(ascent=ASCENT, descent=DESCENT)
@ -87,6 +107,19 @@ def build(path, family, style, weight, bold, fixed_pitch, advance_for):
fb.setupPost(isFixedPitch=1 if fixed_pitch else 0)
if bold:
fb.font["head"].macStyle = 0x01
if ligatures:
substitutions = "\n".join(
"sub %s by %s;"
% (" ".join(glyph_name(c) for c in components), name)
for name, components in ligatures
)
addOpenTypeFeaturesFromString(
fb.font,
"feature liga {\n%s\n} liga;" % substitutions,
)
fb.font["head"].created = FIXTURE_TIMESTAMP
fb.font["head"].modified = FIXTURE_TIMESTAMP
fb.font.recalcTimestamp = False
fb.save(path)
print("wrote", path)
@ -108,6 +141,10 @@ build(
bold=False,
fixed_pitch=True,
advance_for=lambda c: 720,
ligatures=(
("zero_one.liga", "01"),
("f_i.liga", "fi"),
),
)
build(
"PmacsTestProportional-Regular.ttf",

File diff suppressed because it is too large Load Diff