Text and fonts
Verified against Minecraft 26.3 · Part X · a chat line in a language whose characters are not in the default font: six stages from a
Componentto a quad, one of which uploads a texture while pretending to measure.
Ask the client how wide a piece of text is and it will stitch a glyph into a
texture sheet and upload it to the GPU. Font.width resolves each codepoint
to a baked glyph and reads its advance — so measuring a codepoint nobody
has drawn yet is neither free nor read-only, and a layout pass over a
never-before-seen script is a series of texture uploads wearing arithmetic’s
clothes.
This page is the pipeline that ends in that glyph: flattening a component
tree into styled runs, measuring and wrapping it, reordering it for
bidirectional scripts, choosing a glyph for every codepoint from a chain of
providers, baking it the first time anyone asks, and emitting one renderable per glyph for the draw pass to turn into vertices.
What a Component is — its contents kinds, its Style, how it is
serialised — is Part II’s text
components, and how a chat message is
signed is chat and signing. This page
starts from “you have one”.
The cast
| class | what it decides | thread |
|---|---|---|
StringDecomposer | where one styled run ends and the next begins, and what a section sign means | Render thread |
StringSplitter | width, line breaks and word boundaries, given a width provider | Render thread |
FormattedBidiReorder | visual order, by handing the flattened text to ICU | Render thread |
FontManager | which GlyphSource a FontDescription resolves to — three different ways | worker pool, then Render thread |
FontSet | the provider chain for one font id, its codepoint cache, and the fishy split | Render thread |
GlyphStitcher | which FontTexture sheet a bitmap goes to — the sheet places and uploads it | Render thread |
Font | the per-codepoint loop, and the Font.PreparedText everything else walks | Render thread |
GuiRenderer | expanding prepared text into one GlyphRenderState per glyph | Render thread |
The six stages
flowchart TD
C["a Component"]
subgraph WHEN["when the text is laid out"]
RUNS["1 · flatten — Component.visit, then Style.applyTo"]
DEC["StringDecomposer, codepoint by codepoint"]
WRAP["2 · measure and wrap — StringSplitter"]
BIDI["3 · reorder — Language.getVisualOrder, ICU"]
RUNS --> DEC --> WRAP --> BIDI
end
subgraph PREP["inside Font.prepareText"]
RESOLVE["4 · resolve — FontManager, then the first provider"]
BAKE["5 · bake — first sight only, into a FontTexture"]
EMIT["6 · emit — a Font.PreparedText of TextRenderables"]
RESOLVE --> BAKE --> EMIT
end
GUI["at draw: GuiRenderer expands it into GlyphRenderState, or TextFeatureRenderer walks it for the world"]
WORLD["or SubmitNodeCollection.submitText, for the world"]
C --> RUNS
BIDI --> RESOLVE
WRAP -. "measuring resolves and bakes it too" .-> RESOLVE
EMIT --> GUI
BIDI --> WORLD
WORLD -. "TextFeatureRenderer runs four to six at draw" .-> RESOLVE
The numbers are the order the stages matter in, not one walk: for a chat line the top box runs when the line arrives and whenever the chat re-wraps, the bottom box on every record pass for the GUI and at draw time for text in the world, and the first dotted edge is why measuring a string you never draw still uploads its glyphs. In the GUI only what happens after stage six waits for the draw pass.
The numbering is the order the stages matter in, not a pipeline anything walks straight through. Stages one to three run when a chat line arrives (some callers run them on every frame), and four to six run inside Font.prepareText, which the GUI calls during the record pass — the render
tree explains
why, and why nothing is left for the draw pass but the expansion of the
finished Font.PreparedText into one GlyphRenderState a glyph. But four and five are
also reached from two: the width function Font hands its StringSplitter
asks the glyph source for each codepoint, and that call resolves the provider
and forces the bake. Measuring a string you never draw still uploads its
glyphs.
All of it is on the Render thread, glyph baking and GPU uploads included. The
only work that leaves the thread is loading, and it is an ordinary resource
reload with
one unusual step: FontManager’s prepare phase parses the font definitions,
loads each provider, resolves references between them, and then pre-warms the chain by asking it for every codepoint but the space that any provider claims, on the worker pool, each answered by the first provider that has it — so a TrueType font’s glyphs are loaded off the Render thread before the apply phase asks for every one of them again to build the by-width table. Baking still waits for first use, except for the missing box and the white effect glyph, which every reload bakes at once. The apply phase — closing the old font sets
and building new ones — is back on the Render thread.
1 · Flatten
Component and FormattedText provide the walk: Component.visit yields
(style, string) runs in logical order, applying Style.applyTo down the
sibling tree. StringDecomposer iterates those runs codepoint by codepoint
and is where a legacy section-sign colour code is interpreted.
FormattedCharSequence is the end product — a one-method interface that
pushes (index, style, codepoint) triples at a FormattedCharSink — and
ComponentCollector reassembles pieces back into formatted text.
2 · Measure and wrap
StringSplitter owns width and line breaking: StringSplitter.stringWidth,
StringSplitter.splitLines, StringSplitter.headByWidth,
StringSplitter.findLineBreak and StringSplitter.getWordPosition. It works
against a StringSplitter.WidthProvider, and on the real Font that
provider is what bakes. Font.width, Font.split,
Font.splitIgnoringLanguage, Font.wordWrapHeight and Font.getSplitter
are the public face of it.
Wrapping preserves styles rather than ignoring them. It cuts at a
character offset, captures the style in force at the cut, and re-applies it to
the continuation — which is why a colour code before a wrap point still
colours the line after it. And
Font.split reorders while Font.splitIgnoringLanguage does not — anything that will re-wrap must use the second.
3 · Reorder
Language.getVisualOrder is the entry point; on the client ClientLanguage
implements it with FormattedBidiReorder, which builds a SubStringSource —
the flattened plain text plus one Style per character — runs ICU’s Arabic shaping and bidi
algorithm over it, and re-emits each run through
SubStringSource.substring. MutableComponent.getVisualOrderText caches
the result against the identity of the current Language.
Font.bidirectionalShaping is a separate, much smaller thing: it shapes and reorders a bare string, and beside Font.prepareText’s own use of it the sign editor is
the only caller. Those two are half of the game’s ICU surface; the other two
are not text layout at all — CreateBuffetWorldScreen and the item
property LocalTime each use it for collation and calendars.
4 · Resolve
FontManager is the reload listener and the resolver: Font.Provider asks
it for a GlyphSource given a FontDescription, and it branches once per
kind of description — three of them.
A FontDescription.Resource resolves to a FontSet — the per-font-id object
holding the provider list, the codepoint cache (CodepointMap), a
GlyphStitcher and the by-width table used for obfuscation. A
FontDescription.AtlasSprite or FontDescription.PlayerSprite resolves
instead to a SingleSpriteSource from AtlasGlyphProvider or
PlayerGlyphProvider — a one-glyph font that returns the same sprite for every codepoint, with no texture sheet of its own; only an atlas id nobody knows falls back to the missing-font set.
Below that: GlyphProvider implementations chosen by GlyphProviderType,
declared in font/ JSON as GlyphProviderDefinitions. Five kinds, and their
definitions are worth naming because the differences between them are visible
on screen: BitmapProvider slices a PNG on a grid, TrueTypeGlyphProviderDefinition
runs FreeType over a real font file (FreeTypeUtil is the wrapper),
UnihexProvider reads the compact hex format the fallback font ships in,
SpaceProvider produces advance and no pixels, and
ProviderReferenceDefinition splices another font’s providers into this one’s
chain. Beside them SpecialGlyphs is not from a file at all: it is the
hard-coded missing-glyph box and the white square everything else draws
effects with.
5 · Bake
A provider returns an UnbakedGlyph, whose GlyphBitmap is handed to
GlyphStitcher.stitch, placed into a FontTexture sheet and uploaded,
producing a BakedGlyph. BakedGlyph is an interface; the sheet
implementation is BakedSheetGlyph, and EffectGlyph covers the solid quads
used for underlines and backgrounds.
There is one glyph atlas family per font id, each with its own stitcher, and colour and greyscale glyphs never share a sheet. The atlas is discarded, never compacted: a resource reload throws it away — and so does toggling the force-unicode or Japanese-variants option, which rebuilds every font set with no reload at all.
6 · Emit
What comes out is a TextRenderable per glyph inside a Font.PreparedText.
In the GUI, GuiGraphicsExtractor.text and its siblings build a
GuiTextRenderState that GuiRenderer later expands into
GlyphRenderStates. In the world, SubmitNodeCollection.submitText and
SubmitNodeCollection.submitNameTag both hand a FormattedCharSequence to TextFeatureRenderer, which runs Font.prepareText itself at draw, and
GlyphRenderTypes.select picks between the
normal, see-through and polygon-offset render types by Font.DisplayMode.
A chat line, through all six
sequenceDiagram
participant ChatC as ChatComponent
participant CRU as Component<br/>RenderUtils
participant SSpl as StringSplitter
participant FBR as Formatted<br/>BidiReorder
ChatC->>CRU: wrapComponents — the chat width over the chat scale
CRU->>SSpl: splitLines — over runs already translated
SSpl->>SSpl: splitLines cuts at a char offset, each run keeping its Style
CRU->>FBR: reorder, reached through Language.getVisualOrder
FBR->>FBR: SubStringSource, ICU bidi, one substring per run
Stages one to three, when the message arrives: four objects, and the measuring has already baked the glyphs it measured (an Arabic line’s shaped forms wait for stage five).
ComponentRenderUtils reaches FormattedBidiReorder through
Language.getVisualOrder, which ClientLanguage implements.
When the frame is next recorded, the same line goes through four other objects.
sequenceDiagram
participant ChatC as ChatComponent
participant Font as Font
participant FSet as FontSet
participant GStit as GlyphStitcher
participant GuiR as GuiRenderer
rect rgba(0, 0, 0, 0.04)
Note over ChatC,GuiR: recording the frame
ChatC->>Font: prepareText, via GuiTextRenderState asking its bounds
loop each codepoint
Font->>FSet: getGlyph — the first provider that has it
FSet->>GStit: first sight only: stitch it and upload
Font->>Font: a TextRenderable, effects, advance the pen
end
end
rect rgba(0, 0, 0, 0.04)
Note over ChatC,GuiR: drawing the same frame
GuiR->>GuiR: walks the Font.PreparedText with a Font.GlyphVisitor
GuiR->>GuiR: one GlyphRenderState a glyph — shadow, bold and italic belong to BakedSheetGlyph.<br/>renderChar
end
Stages four to six, then what happens to their result: the two bands are the
record pass and the draw pass of one frame, and GlyphStitcher appears in the first only for a glyph the measuring did not bake, such as a shaped form, because a glyph is baked once per font set, until a reload or a font option starts it again.
Two moments beyond the baking are worth pausing on. Translation is lazy and
cached on the TranslatableContents itself, so the first thing to visit a message is what translates it — for a chat line, usually the chat listener (a system message’s sender guess, a signed message’s trust check), otherwise the log line ChatComponent writes as it adds the message. And the continuation indent on a wrapped chat line is a
literal space codepoint prepended by ComponentRenderUtils — one of the places the pipeline invents a character rather than deriving it, beside a clipped line’s ellipsis and an object component’s replacement character.
Style is not a font, and the box is four failures
Two things the pipeline does not do are worth stating before the page ends, because both are assumed the other way round by nearly everyone.
Bold is not a font. Bold draws the glyph twice with a small offset and
thickens it; italic shears the top and bottom edges. Nothing anywhere in the
six stages knows what a bold face is. Shadow is a colour rather than a boolean,
and zero means none. Underlines, strikethroughs and text backgrounds always
come from the default font whatever the style names, because the effect
glyph is looked up separately from the text’s. Obfuscation is in the same
family: the glyph is swapped for a random one of the same width, from a
by-width table rebuilt with the font set, which is why obfuscated text does not shift a layout unless the chain has fractional advances or, in bold, mixes providers of different bold offsets — and the swap happens inside Font.prepareText, which already runs once per frame, though each glyph it swaps in is baked the first time it is chosen.
A sprite is a font. A block icon or a player head in a chat line is,
mechanically, one character in a font with one character in it: an object
component emits a single object replacement character with a synthetic font
description, which resolves through stage four to a one-glyph sprite source.
The plain-text walk of the same component yields a bracketed fallback string
instead, which is what the narrator and Component.getString see. A data pack
cannot reach this: the codec behind FontDescription encodes only the resource
kind, so a style can never name a sprite font, and the sprite descriptions
arise only from object contents.
And when the pipeline fails it fails one way, which makes the hollow box
ambiguous. Four different causes produce it: no provider has the codepoint; the
font id is unknown, so the whole font set is the missing-font set; the glyph’s
advance is fishy — outside a sane range — and the caller asked for the
filtered font; or the bitmap fit no sheet. The fishy case is the one with
machinery behind it. Every font set stores two suppliers per codepoint because
of it, and the game builds a second Font that filters the fishy ones out,
with exactly one use in the entire client: the chat input box, which cannot be
made to draw a character three screens wide.
What else walks a Font.PreparedText, and what the caches promise
The prepared text is walked twice by different things and that is deliberate.
GuiRenderer walks it to emit glyphs, and ActiveTextCollector walks the very
same object looking for the style under the cursor — which is how hovering a
link works, and why preparation can be asked to record empty areas, so that
hovering the space inside a hover-event run still finds the style. Glyph areas
deliberately extend to the full advance, so there are no dead gaps between
characters.
The caches down the pipeline are safe mostly by being single-threaded rather
than by being locked: they are meant to be touched only on the Render thread.
It is not a clean rule — some are keyed on identity and some on equality, and
the glyph layer does use volatile fields and a Guava cache. A component’s
cached visual order is invalidated by the Language object changing, and a
translatable component’s decomposition likewise. Two places leak, and both are worth knowing: the sign block entity caches its rendered lines on a class the server also ships, and a text display entity keeps its laid-out lines the same way; neither cache notices a font reload.
For a 1.21-era reader.
Fontcannot draw. Every drawInBatch and drawString is gone;Font.prepareTextreturns aFont.PreparedTextthat somebody else walks later.Style.getFontno longer returns an identifier — it returns aFontDescription, which may not name a font file at all. Also gone: Font.StringRenderOutput; RawGlyph and SheetGlyphInfo (nowUnbakedGlyphandGlyphBitmap); BakedGlyph as a class; GlyphProviderBuilder (nowGlyphProviderDefinition); andFontSet.getGlyphas public API.
Where to look
Font.prepareText — the per-codepoint loop is the centre of the page.
FontSet for the provider chain and the fishy-advance split, FontManager
for how a FontDescription becomes a GlyphSource, and
GlyphStitcher.stitch for the moment a glyph acquires a texture.
StringSplitter.splitLines for how a line break preserves styles, and
FormattedBidiReorder.reorder for the heaviest of the four places ICU is
used.
Rules: names, never code · how the system works, not how the code reads ·
newest version only · every backticked name passes tools/verify_names.py.