Text-emoji artwork acceptance gate¶
Recorded: 2026-09-19. This page documents fixtures and regression coverage only. No font-routing, discovery, or fitting behavior is changed by the work it describes; the failing cases below are the renderer repairs still owed.
The font fallback review traced one incident: the application line
eprintln!("🛠 Installed {}-{}", name, version);
rendered tofu while the cursor advanced correctly. Three suites passed anyway.
tests/xvfb-emoji-artwork.sh is the gate that fails on it. The tracked emoji
and font-fallback priorities place it before any resolver change.
What the gate asserts¶
Each positive case requires all of the following, and a case that satisfies only some of them fails:
- a route whose role is not
tofu; - the expected effective font file, taken from the route log's
file=, because a family name is a preference and not an identity; - the expected presentation (
text,emoji, ornone); - ink that is not pixel-identical to the deterministic tofu box measured in the same universe at the same cell geometry;
- ink above a low floor, with margins inside its own cells;
- every region outside those cells accounted for: a blank neighbor must be genuinely blank, an occupied neighbor must be pixel-identical to a control render of the same line with a space in place of the symbol, and the internal border above and the whole row below must carry no ink.
Containment is established by those outside-the-cell comparisons, not by bounds measured inside an already cropped cell, which cannot see ink that left it. An occupied neighbor is never accepted merely for being nonblank, since overwritten text or a tofu box would satisfy that. Where two symbols are adjacent or spaced, both are graded in full as atoms rather than one being treated as the other's neighbor.
Cursor advance is asserted separately, from a CPR reply, and is never excused by a known gap. That separation is the point: a monochrome pixel class alone proves nothing, since deterministic tofu is monochrome ink, and a correct advance says nothing about whether a glyph was drawn.
A listed gap must also fail in exactly the documented way. The suite requires the
width-1 advance deferral in the log and a tofu route, and permits only the three
failure categories that follow from that miss (tofu-route, effective-file,
tofu-ink). A wrong presentation, an unexpected effective font, a damaged
neighbor, or ink outside the assigned cells is an independent regression and
fails the suite even for a listed case. Fault injection confirms each path:
altering one gap case's expected presentation, its neighbor expectation, or
listing a case that does not fail by this mechanism each produce FAIL with the
offending category named.
Negative cases are retained rather than converted. legacy-mono-absent renders
bare U+1F6E0 in the mono universe, whose Noto Emoji 1.05 face genuinely lacks
that base, and pua-absent renders U+E000 with systemFallback: false. Both
must produce exactly the tofu box, which is what keeps the positive assertions
above from degenerating into "any ink passes".
Fixtures and their identities¶
tools/stage-font-fixtures now stages two monochrome Noto Emoji faces. They
never share a universe, because both name the family Noto Emoji.
| File | Internal version | Source | Text-default bases | Role |
|---|---|---|---|---|
NotoEmoji-Regular.ttf |
1.050 | noto-emoji 2.028 source archive | 63 of 207 | Retained incomplete negative fixture |
NotoEmoji-Regular-3.003.ttf |
3.003 | Fedora google-noto-emoji-fonts 20250623-4 |
207 of 207 | Modern positive fixture |
The new file is byte-identical to the installed package file the review
diagnosed, /usr/share/fonts/google-noto-emoji-fonts/NotoEmoji-Regular.ttf,
SHA-256 b57ed895ae9d09ba7b4b19c343a75cf39aad57156c3450e339d9d11556d7edc1. It is
a test input under SIL OFL 1.1, staged from a pinned archive hash like every
other fixture; nothing is bundled into a runtime artifact. Package release
versions and internal head.fontRevision values are different identities, so
manifest.json now records version alongside sha256, and
text_default_bases records the coverage census that separates these two faces.
The census counts emoji bases whose default presentation is text, excluding the
ASCII keycap bases, as a scalar cmap count. It makes no claim about sequences,
artwork quality, or reachability.
mono-modern holds the modern face with the base text font. routing-modern
adds the CJK face and Noto COLRv1, mirroring routing, so the color and
monochrome alternatives overlap exactly as they do on a real system.
What repaired the reported failure¶
The gate reports 17 passed, 0 expected gaps, 0 unexpected. The eleven
width-1 entries that used to be listed in known_gap were removed one at a time
as their complete assertion sets passed; no assertion was weakened, and the two
negative tofu cases still pass unchanged.
What was measured before the repair, in routing-modern and mono-modern:
system discovery did reach the modern monochrome face, and the width-1 advance
rule then deferred it.
font: activated Xft fallback slot=0 style=0 entry=3 source=chain0 budget=1/50
font: deferred Xft fallback slot=0 entry=3 advance=28.207 cell=13 width=1 limit=10
font: route base=U+1F6E0 width=1 presentation=text role=tofu glyphs=0 file=(unknown)
XtpFontFallbackAdvanceFits in src/font_metrics.c returns true before
computing anything unless committed_width == 1U, so the rule is a one-cell
rule and the defect was one-cell advance rejection of a usable face with no
fitting policy to answer it. That is what the span fitting policy in
font-resolution(7) now supplies. limitFontWidth is
unchanged and still governs every other atom class; the fitted instance has to
satisfy it like anything else.
The same line now routes as:
font: fitted Xft fallback slot=0 span=13 advance=28.207 fitted-advance=12.902
font: route base=U+1F6E0 width=1 presentation=text role=fallback \
file=.../NotoEmoji-Regular-3.003.ttf
With faceNameEmojiText configured the same atom routes role=emoji-text from
the same file, which is the explicit lever for the same outcome.
Measured artwork at 16 point in a 13x27 cell: 90 inked pixels with bounds
1,12,12,11, so the glyph occupies roughly a 12x11 box inside its cell, with
margins on every side and no ink in the border above, the row below, or any
neighboring cell. It is small. Legibility remains a human judgment and belongs
to the TDN artwork probe; this suite establishes that a recognizable-sized,
correctly routed, contained glyph is drawn where tofu used to be.
A separate miss, since repaired¶
The older expected-tofu cases in tests/xvfb-emoji-routing.sh were not the
same defect as the width-1 advance rejection above. For info-text,
policy-text-grin and policy-text-color-wide in the routing universe, Noto
Emoji 1.05 does map the base, but the coverage-trimmed candidate sort dropped it
as redundant once the color face covered the same scalars: the log showed four
queued primary-slot candidates, the color face's empty outline being rejected,
and no monochrome candidate ever activated.
That was the discovery loss, and it is now repaired by the presentation-aware
second sort described in font-resolution(7). All three
cases render from NotoEmoji-Regular.ttf today, and their expectations in that
suite were updated from role=tofu to the serving role — a strictly stronger
assertion, since a role and a file are named where nothing was before.
tests/xvfb-font-discovery.sh is the suite for that repair: the reproduction
itself, clean defaults, a generic rule preferring color, a universe with no
monochrome face at all, an explicit fallbackFace1 still outranking discovery,
systemFallback: false, limitFontsets: 0, and both CJK routes. Nine cases,
each checking route identity, containment and the cursor column.
One thing that repair did not change is worth keeping in view: a final
role=tofu still cannot be told apart from absent coverage without reading the
earlier log lines. Distinguishing lost candidates, advance rejection and genuine
absence at the route itself is the diagnostics work, still outstanding.
The resources themselves¶
tests/xvfb-emoji-text-role.sh covers faceNameEmojiText and fitEmojiText,
which the gate above deliberately never sets: both chain entries, a hostile
generic-emoji pattern rule in the alias-emoji universe, an explicit
color=true in the user's own pattern being honored, the fitting opt-out at both
consult points, precedence against faceName entry 2 and fallbackFace1,
limitFontsets: 0 suppressing both rescue entries, the width-one scope of the
advance rule, instance reuse, a second span reached through a font-slot switch,
and styled output. Twenty cases pass, with route, pixel and cursor assertions
as applicable; these are not all asserted in every case.
Three of them are the real styled faces: the regular face fitted and serving, SGR bold selecting the bold file with that candidate fitted in its own right, and an italic candidate that cannot satisfy the policy being declined with the regular face retained. The two outcomes are distinguished rather than merged. The bold case asserts a second fitted instance exists and that bold's pixels differ from regular's; the italic case asserts the italic file never served, that the decline was logged, and that the italic attribute survived the decline — the serving face is the regular one, so nothing there claims the artwork looks slanted. Disabling the styled span check makes the bold case fail on the fitted instance and the italic case fail on the effective file, the serving face and the missing decline.
Paint clips to the atom's cells, so an unfitted oversized styled glyph is cropped rather than allowed to bleed. The assertions these cases make — containment, and bold's pixels differing from regular's — cannot reliably tell a cropped glyph from a fitted one. Assertions that could, such as a reference render or a check on the fixture's complete bar geometry, are not made here, which is why the bold case asserts the fitted instance instead of leaning on pixels. The reported AddressSanitizer run found no lifetime errors on the exercised paths. The slot-switch case observes a second fitted instance; an earlier log line alone does not establish that the first instance remains alive. Ownership is enforced by retaining fitted fonts until universe destruction.
The fitted-face table's boundary is a unit test, fitted-face table in
-self-test: reuse returns the same entry, the span is part of the key, a full
table refuses a new entry instead of displacing one, every entry handed out
earlier stays findable after exhaustion, and clearing the count models the
universe replacement that a reload performs.
The styled-candidate branch was the outstanding gap and is now covered by a
generated family rather than by inspection.
tools/font-fixtures/make-styled-emoji.py produces XTP Styled Emoji in three
real styles: Fontconfig reads weight 200 for Bold and slant 100 for Italic, with no
FC_EMBOLDEN and no FC_MATRIX on either, so neither is a synthesised style.
Regular and Bold map U+1F6E0 to a two-em glyph — a measured advance of 52.187px in
a 13px cell at 16 point — so both must be fitted. Italic maps it to a half-em glyph
that a GSUB multiple substitution in liga decomposes into two, so the run
advances too far while the face's own maximum advance already fits one cell, which
is a shape fitting cannot repair. That half-em maximum advance is load-bearing:
anything added to these faces has to keep it, which is why the sequence glyphs
described below are half an em too. Regular draws two bars and Bold three, so which
face served shows in pixels as well as in the route log. The styled-emoji
universe carries the family from fonts-styled/, outside the manifest, for the
same reason the crowd fillers are outside it: an adversary for one code path, not a
rendering fixture.
One coverage limit is worth naming rather than papering over: - Every real glyph in the staged monochrome face has the same 2600-unit advance, so no two atoms built from it can have different advances. Order independence is therefore established by construction — the atom's advance is not an input to the scale — and the suite asserts the observable consequence, that two atoms share one fitted instance.
Unicode coverage audit¶
tools/emoji-coverage-audit.py is the reproducible inventory behind the coverage
claims here. Its inputs are pinned: the emoji properties and the E<version> age of
each base come from the staged data/emoji-data.txt (Unicode 18.0, whose version
header font-fixture-info.py --check pins against the manifest), and the faces come
from the staged tree. It downloads nothing. Run it with
python3 tools/emoji-coverage-audit.py, --json for the full table, or --check to
verify the inventory alone.
Two things it deliberately does not do, because the first version of it did both and both produced unreliable numbers:
- It does not infer coverage by scanning the suites. A codepoint can be written in
a comment, in an unused variable, or assembled from shell variables a scanner never
sees, and a sequence's components are not the sequence. Coverage comes from
AUTOMATED_CASES, an explicit table naming the suite, the case, the atom and what that case asserts. The labels are read off the helper each case calls, so they distinguishrole(a named serving role is required) fromnon-tofu-role(only that it is not tofu), andshaping(the route was required to reportglyphs=1) is claimed only where a case actually requires it — in the artwork suite that means the cases usingcheck_every_route, becausecheck_routenever inspectsglyphs=. The scan survives only as a separate source mentions column, which means a codepoint is written down somewhere and nothing more. - It does not claim coverage outside its own scope. The scope is every sequence in the table and every base newer than E15.0. Older bases are reported as not inventoried: the suites assert a great deal about them — bare U+1F6E0 is this gate's own subject — and this table simply does not enumerate them, so the asserted/unasserted totals below are for the audited scope alone.
--checkis case-reference validation, nothing more. Each row must point at a case its suite still declares, matched against the suite's own invocation forms (run_case/run_unicode_casewith the universe first;start_sample/negative_case) with comment lines ignored, so a deleted case whose name survives in a comment, or a longer case name that contains a shorter one, is not a declaration. It cannot show that a case still makes the assertions its row claims; only reading the case can.- It does not shape with
hb-shape's defaults. Production shapes a composition-requiring atom withHB_BUFFER_FLAG_PRESERVE_DEFAULT_IGNORABLESand everything else withREMOVE_DEFAULT_IGNORABLES(XtpShapeUtf8ForCompositionandXtpShapeUtf8,src/glyph_shape.c), and the audit now selects the same flag per atom through a port of the classifier insrc/emoji_presentation.c, plus--bot --eot --cluster-level=1. This is not cosmetic: with ignorables removed, an unsupported tag payload disappears andTwitterColorEmoji-SVGinOT.ttfreports one plausible glyph for the Scotland flag it cannot draw. With production's flags the same face reportsmissing-glyph:7, which is what the renderer refuses. The column is namedshapingbecause one glyph is not the whole of acceptance either: coverage, ink, presentation and the advance rule are separate gates the audit does not model.
tests/emoji-coverage-audit.py (meson test emoji-coverage-audit, 13 tests) holds
each of these properties, all of which an earlier version got wrong: a commented and an
unused literal are mentions and not coverage; the two columns are shown to disagree; a
declaration deleted while a comment keeps its name is reported, and a longer case
name is not a declaration of a shorter one; the assertion labels are checked against
the helpers the cases call; U+1F6E0 is shown to be out of scope rather than unasserted;
and the removed-ignorables false positive is reproduced and then shown to be rejected
by the audit's own verdict, while a complete shaping verdict is shown to say nothing
about presentation or ink.
Counts after the Unicode 18 migration — 21 staged faces,
1447 Emoji bases, of which 43 are in the audited scope (E15.0+):
| bucket | bases |
|---|---|
| in scope, asserted by a named automated case | 5 |
| in scope, covered by a staged face, asserted by nothing | 29 |
| in scope, covered by no staged face | 9 — every E18.0 addition; no staged release covers them |
| not inventoried (older than E15.0) | 1404 |
| (written in a suite's source, coverage unproven) | 35 |
| (human-assessed probe samples, never automated evidence) | 49 |
Probe samples include the Go artwork probe (tools/probe/emoji_artwork.go) as well as
the width probes; U+1FAE9 is a probe sample and nothing else, which the report shows.
Per-face coverage of the 34 bases newer than E15.0, cmap only:
| face | E15.0+ bases |
|---|---|
| Noto-COLRv1, NotoColorEmoji (2.051), OpenMoji-COLRv0 | 34/34 |
| NotoEmoji-Regular-3.003 | 27/34 — missing U+1F6D8, U+1FA8A, U+1FA8E, U+1FAC8, U+1FACD, U+1FAEA, U+1FAEF |
| TwitterColorEmoji-SVGinOT | 20/34 |
| NotoColorEmoji-2.034, NotoEmoji-Regular (1.05), Twemoji | 0/34 |
The audit found no renderer defect. That conclusion rests on the live renderer cases below, not on the audit's shaping: every newer base sampled routes, shapes to one glyph and paints. The one gap exposed is a font gap, and it is recorded as one. The monochrome fixture predates E17.0, so forced text presentation has nothing to serve those seven bases with and the route is deliberate tofu — with the cursor advance still correct, which is the renderer's own contract and is asserted separately.
Representative cases, added to tests/xvfb-emoji-artwork.sh as rows rather than one
launch per codepoint:
| case | what it grades |
|---|---|
newer-bases-row |
U+1FA75 (E15.0), U+1FADF (E16.0), U+1FA8A and U+1FACD (E17.0) in mode 2027: each routed presentation=emoji glyphs=1 from Noto-COLRv1.ttf, color ink that is not the tofu box, margins inside its own two cells, blank following cell, blank row below and border above, and one CPR of column 9 for the row |
newer-bases-text |
the same E15.0 and E16.0 bases under forced text presentation: served from NotoEmoji-Regular-3.003.ttf, monochrome ink, same containment, CPR column 5 |
newest-bases-text-gap |
the two E17.0 bases under forced text: deliberate tofu, pixel-identical to the measured width-2 tofu box, advance still column 5. A face covering E17.0 should turn this into a positive case |
tag-flags-row |
RGI England and Wales plus the valid non-RGI usca sequence in mode 2027: every logged U+1F3F4 route shaped one glyph from the color face, each atom contained, and England's artwork differs from Wales's, so the tag payload demonstrably reached the font |
tag-flags-legacy |
the same row in the legacy width regime, which was measured to supply the same complete atoms here — the identical advance (column 7) and route evidence, not an assumption that legacy joins anything |
Tag sequences, shaped with production's flags: the three RGI subdivision flags
(gbeng, gbsct, gbwls) are complete in six faces including the monochrome one.
The non-RGI usca is complete in Noto-COLRv1.ttf — a real, distinct ligature, which
is why the painted case above is legitimate — and split:6 in the monochrome face,
which is a font limitation and not a defect: splitting a sequence the font cannot join
is the correct outcome.
Remaining limits:
- 9 in-scope bases are covered by no staged face: the Unicode 18.0 additions, which
every pinned font release predates.
e18-base-widthin the routing suite records that as a font gap with the committed width still asserted. - 29 in-scope bases are covered by a staged face and asserted by nothing, and 1404 older bases are not inventoried at all. That is by design: this is a routing and artwork gate, not a Unicode conformance matrix, and the inventory is a scoped instrument for picking the next sample rather than a census of what the suites assert.
- The inventory's assertion labels are maintained by hand against the suites' helpers.
--checkcatches a case that no longer exists; it cannot catch a case whose assertions changed while its name stayed, so the labels are only as good as the last reading of those cases. zwj-polar-bearis asserted by no automated case; it is a probe sample only.- The audit measures scalar coverage from cmaps and sequence shaping from
hb-shape. Neither says a glyph is legible, and nothing here promotes a visual-legibility record; the TDN capability records are unchanged by this slice. - Modifier, keycap and regional-indicator sequences are sampled by one entry each, already exercised by the routing suite; the new cases add none.
Styled whole-sequence selection¶
Style selection runs after a route is chosen, so a bold or italic request can
only replace the serving font, never the routing decision. The question that leaves
open is whether it can replace a complete sequence with an incomplete drawing of
its parts. Production accepts a styled candidate only if it shapes the whole cluster
to one glyph — requires_composition in FontHasCluster, src/font_router.c —
and the same condition governs the two places a styled candidate comes from: a
configured role (VtFontRoleStyle) and the same-family candidate list on a fallback
rung (FallbackStyleRangeWithCluster).
tests/xvfb-emoji-routing.sh grades that with eleven cases in the styled-emoji
universe, all in mode 2027, because the complete grapheme is the atom under test. The
same generated family now also carries one ZWJ sequence, U+1F468 ZWJ U+1F4BB: every
face maps both components and the joiner, and only Regular carries the liga rule
that joins them, verified with hb-shape — Regular shapes one glyph, Bold, Italic
and the partial family shape two, since HarfBuzz drops the default-ignorable joiner.
A second generated family, XTP Partial Sequence, covers the same components with
no ligature and nothing else, so it can stand in front as a preferred role face. One
more codepoint, U+1F4A1, is carried by the styled family alone, which is how an atom
is steered onto a fallback rung rather than to the preferred role face.
The sequence glyphs are half an em, the same as Italic's own U+1F6E0 glyph, so that
no face's maximum advance changes: that value is an input to the fitting decisions the
styled cases above grade, and a wider component glyph would have raised Italic's
advanceWidthMax from 500 to 700 and quietly moved that mechanism. The three faces'
advanceWidthMax, ascent, descent, weight class, fsSelection, italic angle,
macStyle and U+1F6E0 metrics are identical to the pre-change faces, and every
pre-existing glyph's outline and metrics compare byte-identical; only cmap entries
were added. Unchanged shaping alone would not have established that.
What the cases establish:
- the complete sequence is served by Regular with
glyphs=1, and a bold or italic request keeps that effective file — the real, covering styled candidate is declined; - the same request style on one component of that sequence is served by the bold or italic file itself, which is what makes the declines above attributable to complete-sequence acceptance rather than to coverage or to a synthesised style;
- a preferred role face that covers both components and cannot shape the sequence is refused whole: the atom falls through to the later candidate under regular, bold and italic requests, and that same face does serve one component;
- the decline happens on a fallback rung as well as in a configured role, reached by
removing the wide role so the atom is served at
emoji-fallback; - that rung has its own control: under the same configuration, U+1F4A1 is served by
the bold file at
emoji-fallback, so retaining Regular for the sequence is not merely the absence of a usable bold candidate on that rung; - every case keeps non-tofu ink, a blank following cell, a blank band below the whole span, and a CPR-measured advance of two columns, independent of which face served.
Disabling the acceptance check (dropping requires_composition && run->count != 1U)
makes both decisive cases fail. seq-styled-bold and seq-fallback-bold then accept
the Bold face for the incomplete atom: the route loses glyphs=1 and the painted ink
goes from 171 pixels in bounds=6,2,11,19 to 285 in bounds=0,2,23,19 — the two
components drawn side by side across the span instead of the joined glyph. The source
was restored afterwards and the diff is empty.
Two limits, stated rather than implied:
- The route line reports the requested bold and italic attributes, not the served
face's, so the effective file is the discriminator;
slant=does follow the served face and distinguishes the italic cases. - The fixture's component glyph is the same shape in all three faces, so pixels do not distinguish bold from italic here, and nothing in these cases claims the artwork looks bold or slanted. Human legibility is still not asserted anywhere.
- Two further cases use real Noto faces for a keycap and a regional flag under a style request. Neither family ships a real bold or italic, so they reach only the "no styled candidate exists" path: they grade that a style request leaves a composed atom whole, with its width and containment intact, and nothing more.
Reload and cache coverage¶
tests/xtp-font-reload.c, driven by tests/xvfb-font-reload.sh in the
routing-modern universe, carries the emoji transitions in one running widget
so that later steps inherit the state earlier ones produced. What it establishes,
in order:
- A one-cell text emoji atom is served by automatic monochrome discovery with role
fallbackfromNotoEmoji-Regular-3.003.ttf, and the serving font is the fitted table's entry for the current span. Routing it again returns the same instance and does not fit the face a second time. - A successful geometry reload of the active slot advances the generation exactly once and changes the cell width. The replaced universe starts with an empty fitted table; re-routing the atom fits the face to the new span and no entry for the previous span survives.
- A successful font reload that sets
faceNameEmojiTextadvances the generation once and changes the serving role fromfallbacktoemoji-text. The role is the assertion, because the same file reached through automatic discovery would prove nothing about the rescue taking over. - A rejected reload leaves the generation and the effective universe unchanged, and
the atom then still routes
emoji-text, from the same file, returning the same fitted instance, which is still the table's entry for the current span.
Two kinds of reuse are asserted separately, because the same font pointer comes back either way: the route cache gains exactly one entry for the atom and none when it is routed again, and the fitted table keeps exactly one instance. Both counters reset when the universe is replaced.
A count that does not grow is not a hit, since re-storing the same key would also
leave it unchanged, so the hit itself is read from the router's own debug logging
(src/font_router.c logs route-cache hit, miss and stale). The helper raises
the log level and prints a PHASE marker before each transition; the suite then
attributes the lines to phases and requires a miss for the atom's first route, a
hit for the repeat, and no route-cache stale line anywhere, since a stale
entry surviving a reload is exactly the defect this area is about.
Evidence is observable state — generation counters, role names, effective files and
the fitted table's spans. Nothing compares against a font pointer from a destroyed
universe: an allocator may hand the same address back, so identity there would
prove nothing about replacement. The
suite's snapshot check pins the generation sequence 2, 4, 4: advanced by each
successful reload, held by the rejected one. The whole suite also runs under
AddressSanitizer, since this is the path where a universe is destroyed while fitted
faces and route-cache entries point into it.
What these transitions do not cover:
- Nothing about pixels: routing is in-process here. Painting across the same transitions is covered by the separate helper below, which is why the routing helper keeps the stub backend.
- Only the monochrome text path. The color emoji role and the styled bold and italic faces are not exercised across a reload.
- The presentation reserve's interaction with reload is untested: a reload clears the reserve by replacing the universe, which no case asserts.
Painting across the same transitions¶
tests/xtp-font-reload-paint.c is the painted variant, run by the same suite. The
routing helper links src/terminal_stub.c (meson.build), and the stub ignores
input, emits no cells and reports column zero, so it can never paint: the painted
variant links the real backend instead and asserts that with
XtpTerminalBackendIsStub before it looks at any pixel. It keeps one widget
across every transition, as the routing helper does.
It is deliberately narrow. It paints through XtpTerminalFeedOutput and
XtpVtUpdate — the dirty-update path the application uses; XtpVtRedraw repaints
the cached frame, so every sample would have shown the first screen — and reads back
rectangles of the window with XGetImage. Two harness facts are established before
any emoji claim: plain ASCII paints ink in its own cell and advances one column, and
the reference character U+E000 routes to tofu — asserted, not assumed from a
nonblank drawing — and paints the box whose checksum is what "not tofu" then means at
that geometry. The reference is re-measured before every phase, since a reload
changes the cell.
Four phases follow in one widget: automatic fallback, the same after a geometry
reload, the explicit faceNameEmojiText rescue, and the rejected reload that has to
keep painting from the retained universe. Each asserts, for bare U+1F6E0: the
serving role expected of that phase (fallback, then emoji-text) and an
effective file of NotoEmoji-Regular-3.003.ttf for the very route the paint draws
from; then ink present, a checksum different from that geometry's tofu box, and
containment. The rescue phase is where role and file diverge in usefulness: the file
is the same one automatic discovery already found, so only the role shows the rescue
took over — and only the paint shows it still draws.
Containment is measured outside the atom: the cell to its right and the full two-cell-wide band in the row below must be blank. A bound computed inside the sampled cell, as an earlier version had, cannot fail, so it is gone rather than labelled. The containment sample is taken with the cursor hidden, since the cursor block inks the cell after the atom; the advance is read from a second paint with it visible, since the core reports a cursor column only for a visible cursor.
The widget is created with internalBorder 0 and white-on-black through
XtVaTypedArg: those resources are a Dimension and Pixels, so a plain string
argument is stored as its pointer value, which put the text origin off-window and
left the window a uniform garbage color — every cell then sampled identically and
the emoji looked "pixel-identical to tofu".
Each of these checks was shown to reject a wrong expectation: naming the
emoji-text role in the automatic phase, expecting DejaVuSansMono.ttf as the
effective file, using a served character as the tofu reference, and painting ink into
the cell beside the atom or the row below it each produce the corresponding failure.
What it does not cover: only the monochrome text path and this one atom, no color or styled faces, and no assertion that the artwork is recognizable — it distinguishes drawings, it does not read them.
Visual acceptance procedure¶
Everything above is machine-checkable. Legibility is not: no assertion in this tree says a hammer looks like a hammer. That judgment is a human's, it is recorded by hand, and until it is recorded no artwork capability carries a Revenant support record. This procedure is the way to produce one; it invents no results and promotes nothing on its own.
Use the existing probe — no new tooling. Establish what you are assessing first: the
binary's identity, not the checkout's. Nothing compiles a Revenant revision into the
executable, so the identity recorded is the version string, the backend revision and the
binary's hash; a revision is attached only when the tree was clean, and a dirty tree is
labelled as one rather than borrowing HEAD's name. The build must succeed before
anything else runs:
set -eu
repo=$PWD
binary=$repo/build-agent-gcc/revenant
ninja -C build-agent-gcc revenant || exit 1
source_state=clean
test -z "$(git status --porcelain)" || source_state=dirty
rev=$(git rev-parse HEAD)
build=$("$binary" -report-config 2>/dev/null |
sed -n 's/^! \(version\|backend revision\): /\1=/p' | paste -sd' ')
hash=$(sha256sum "$binary" | cut -d' ' -f1)
if test "$source_state" = clean
then
identity="$build binary-sha256=$hash built-from=git:$rev"
else
identity="$build binary-sha256=$hash source=dirty built-from=unknown (git:$rev does not describe it)"
fi
Then run the five cases for each size and environment. Every run writes into a fresh
session directory, so repeating the procedure cannot overwrite earlier evidence, and
colorGlyphs, emojiPresentation and the width regime are set explicitly and read
back from the same configuration before being recorded — a value that cannot be read
back stops the run instead of being asserted:
session=$(mktemp -d "${TMPDIR:-/tmp}/revenant-emoji-$(date -u +%Y%m%dT%H%M%SZ)-XXXXXX")
printf 'session %s\nidentity %s\n' "$session" "$identity" >"$session/session.txt"
grapheme=default
for size in 10 16 32
do
for env in installed staged
do
prefix=
test "$env" = installed || prefix="font-fixtures-stage/run routing-modern"
effective=$($prefix "$binary" -fs "$size" \
-xrm "xterm.vt100.graphemeWidth: $grapheme" \
-xrm 'xterm.vt100.colorGlyphs: true' \
-xrm 'xterm.vt100.emojiPresentation: unicode' \
-report-config 2>/dev/null |
sed -n 's/^XTerm\*VT100\*\(colorGlyphs\|emojiPresentation\|graphemeWidth\):[[:space:]]*/\1=/p' |
paste -sd' ')
# Never record a setting that was not read back.
for key in colorGlyphs emojiPresentation graphemeWidth
do
case " $effective " in
*" $key="*) ;;
*) echo "no effective $key for size=$size env=$env" >&2; exit 1 ;;
esac
done
for case in text-monochrome-emoji:monochrome-emoji \
text-emoji-presentation:emoji-presentation \
text-emoji-cell-fitting:emoji-cell-fitting \
text-emoji-sequences:emoji-sequences \
text-symbol-whitespace-expansion:symbol-whitespace-expansion
do
id=${case##*:}
stem=$session/$env-$size-$grapheme-$id
$prefix "$binary" -fs "$size" \
-xrm "xterm.vt100.graphemeWidth: $grapheme" \
-xrm 'xterm.vt100.colorGlyphs: true' \
-xrm 'xterm.vt100.emojiPresentation: unicode' \
-report-font-routing \
-xrm 'XTerm*vt100.translations: #override <Key>F12: report-font-routing()' \
-e sh -c 'cd "$1" && shift && exec just probe "$@"' sh "$repo" \
"${case%%:*}" "$id" --assess \
--terminal revenant --terminal-version "$identity" \
--configuration "faceSize=$size env=$env $effective serving-fonts=$stem.routing" \
--output "$stem.json" \
2>"$stem.routing"
done
done
done
Argument handling is positional throughout: the repository path and the metadata reach
just probe as arguments to sh -c, never interpolated into shell text, so a path
containing spaces or an apostrophe survives intact. This was exercised with stand-ins
from such a directory.
Run the set with grapheme=default (the legacy width regime, which is Revenant's
default) and, if you also want the mode-2027 regime, repeat with grapheme=unicode;
whichever you used is in the filename and in --configuration, never assumed. Three
sizes make fitting visible: a shrunken face has few pixels at 10, and a large cell
exposes positioning at 32.
Which font painted a sample is a routing question. fc-match and -welcome
report candidate information — what Fontconfig would offer for a pattern — and cannot
say what served an atom; -welcome states this about its own lookup. Press F12 in
the terminal while the samples are on screen to write a report-font-routing()
snapshot to $stem.routing, and read the effective files from its route records (file,
index, rung), or read the font: route base=… file=… lines from a -log debug
run. Record those paths in the observation. Candidate lists may still be recorded as
context, labelled as candidates.
Include the original line verbatim. It is the first sample of monochrome-emoji:
🛠 Installed demo-1.0
Judge it as a whole: a recognizable hammer-and-wrench in the first cell, the text beside it untouched, and nothing clipped.
Record, for every run, in the probe's --configuration and --observation strings —
the settings the run actually used, not a template:
- the binary's identity: its
versionandbackend revisionfrom-report-config, itssha256, and a checkout revision only if the tree was clean; a dirty tree is recorded assource=dirty built-from=unknown, because no revision describes it; - the exact
faceSize, and thegraphemeWidth,colorGlyphsandemojiPresentationvalues read back from the same configuration rather than the ones intended, plus which environment it was — installed fonts or the staged universe; - the serving font files for the atoms judged, taken from the routing snapshot or the debug route lines, one per atom class if they differ;
- release and internal font versions where they differ (installed monochrome 3.003 and the 1.05 fixture are different assets);
- any local Fontconfig rules
-welcomereports as substitutions, and candidate lists if useful — both labelled as candidate information, which is what they are.
Distinguish the two fitting questions, and do not merge their assessments:
- Strict in-cell fitting (
text-emoji-cell-fitting) is today's policy and the expected default: recognizable ink inside the cells the backend committed, nothing clipped, no neighboring cell touched. Theemoji-cell-fittingcase's spaced and adjacent rows are for this. - Whitespace expansion (
text-symbol-whitespace-expansion) is a separate optional behavior Revenant does not implement. Its case asks only whether the spaced row's artwork is larger than the adjacent row's. Equal rows are recorded as "no enlargement observed for these samples" — not as proof of strict drawing, and not as a defect either way. Its record is alreadyunsupportedwith the containment assertions as evidence; a visual assessment neither promotes nor contradicts that.
Where observations go: keep the JSON from --output with the review, and write the
judgment into tdn/terminals/revenant.yaml under the feature's record as
evidence: - kind: observation, with as-of: git:<full revision>, a checked date
and notes naming the sizes, the serving font files and what was seen. A record only
becomes supported on the strength of such an observation; a clean probe exit, a
passing suite, or correct cursor advance is not an assessment. If a sample is wrong,
record the failing label rather than a general verdict, and leave the record unknown
or partial accordingly.
Keep observation and cause apart. A difference between the installed and staged runs is
a difference between two runs; attributing it to a font needs the routing evidence from
both, showing which file served the atom in each. Without that, record what was seen
and say the cause is undetermined. just check-tdn validates the registry after any record change.
Running it¶
# The staging tree is never modified in place; restage and replace it.
tools/stage-font-fixtures --seed font-fixtures-stage/sources font-fixtures-next
tools/font-fixture-info.py --check font-fixtures-next/manifest.json
mv font-fixtures-stage font-fixtures-old && mv font-fixtures-next font-fixtures-stage
meson test -C build-agent-gcc xvfb-emoji-artwork --print-errorlogs
A repaired renderer will turn a gap into XPASS, which fails the suite until
its known_gap entry is removed. Remove entries one at a time, with the
observation that justifies each removal; do not lower an expectation to clear
one.