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:
parent
6cd78f0870
commit
b3d326d937
|
|
@ -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 1–5): 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 1–5): 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
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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
Loading…
Reference in New Issue