feat(text)!: Pluggable text layout via Makie.layout_text - #5717
feat(text)!: Pluggable text layout via Makie.layout_text#5717jkrumbiegel wants to merge 73 commits into
Conversation
Introduces a `Glyphs` recipe that renders a flat batch of atlas glyphs (per-glyph positions + marker_offset + glyph indices/fonts, scalar-or-vector display attrs), similar to Scatter but kept separate. `Text` now computes its layout and creates a single always-present `Glyphs` child (plus the existing linesegments child for LaTeX rules) instead of rendering glyphs itself, and is no longer atomic. The glyph -> atlas-quad computations (sdf_uv, quad_offset/scale) and position transforms move into `Glyphs`; with per-glyph positions these become plain broadcasts and the per-char position expansion node is dropped. Backends render `Glyphs` (CairoMakie, GLMakie done; WGLMakie by analogy, needs browser/CI check) and descend into Text's children. Benchmark vs breaking-0.25 baseline (N=4000, ~27k glyphs): create 35.9 vs 44.7ms, color 31.9 vs 38.2ms, ~15-20% faster; the per-glyph position transform is not a net regression. Color still tracks a full re-layout, addressed in the next phase.
compute_glyph_collections! now inspects the compute graph's `changed`/`cached` state: when no layout-affecting input changed and every block's text has `display_independent_layout == true`, it reuses the cached glyph geometry (pointer-stable, so the Glyphs child never re-uploads) and recomputes only the per-glyph color/stroke arrays. Plain strings apply color/stroke after layout so they qualify; RichText and LaTeXString bake color into layout and recompute (the per-type overload decides via the trait). A colour tween over ~27k glyphs drops from 31.9ms to 3.6ms. Scalar->vector color updates remain unsupported (a pre-existing Makie attribute-slot-typing limitation that affects Scatter identically); out of scope.
Adds the `rasterize_marker_for_gpu(marker, markersize)` extension hook in
compute_marker_attributes so GPU backends can rasterize an opaque scatter marker
(e.g. a compiled LaTeX/Typst object) into an image at upload time, while CairoMakie
keeps drawing the original marker as vectors. Default returns nothing, so existing
markers are unaffected. The image-matrix marker path (single Matrix and per-marker
Vector{Matrix} with pack_images) already exists on this branch.
Text gains an always-present but empty `plotlist` child fed by a `text_specs`
output, the channel a text_handler will use for non-glyph, non-rule output
(images, arbitrary plots). Empty means no children and no render objects, so the
default glyph/rule paths are unaffected. LaTeX rules keep their dedicated merged
linesegments child (this branch already replaced the old per-frame plotlist diff).
Adds a themable `text_handler` plot attribute (`@inherit text_handler`, default nothing) and a pluggable layout protocol: - compile_text(handler, src, font, fonts, fontsize, lineheight, justification, word_wrap_width) -> CompiledGlyphs | nothing. Receives only layout-affecting inputs; returns an unaligned, backend-neutral glyph bundle (glyph columns + rules + bbox), or nothing to fall through to the built-in path. - place_text!(outputs, ::CompiledGlyphs, align, rotation, offset, color, ...): Makie-provided placement that applies alignment from the reported bbox, then rotation/offset/color, and merges glyphs into the shared Glyphs batch and rules into the linesegments child. Handlers with custom payloads override it. compute_glyph_collections! routes each block through the handler and falls back to convert_text_string! when compile_text returns nothing, so a single text! can mix handled and unhandled strings. Handler blocks opt out of the display-reuse fast path (they may bake color in); the handler owns any layout cache. MathTeXHandler lays out LaTeXString via MathTeXEngine through this protocol, producing output identical to the built-in LaTeX path (the migration path a third party like TeXLayout/Typst would follow). No backend changes needed: handled text flows through the same Glyphs/LineSegments children.
Text is now a container; sdf_uv/quad_offset/quad_scale/sdf_marker_shape live on its Glyphs child (plot.plots[1]) rather than on the Text plot itself.
…hs-only
Drops the dedicated LaTeX-rules LineSegments child (tex_linesegments!) and the
:linesegments/:linewidths/:linecolors/:lineindices node outputs. Rules now flow
through the same plotlist channel as any other non-glyph handler output: the built-in
LaTeX path and MathTeXHandler emit a LineSegments PlotSpec, which register_text_plotlist!
shifts by the block's projected position (per camera) and renders in markerspace. Plain
text carries no rules child at all.
CompiledGlyphs is now glyphs-only (glyphindices, fonts, origins, extents, scales) plus
bbox and baseline (needed for valign = :baseline, which bbox alone can't express).
compile_text returns (CompiledGlyphs, Vector{PlotSpec}); place_text! merges glyphs via
place_glyphs! and emits the specs (aligned, block-tagged) into text_specs, filling the
text color for specs that don't set their own. String bounding boxes fold in per-spec
bboxes instead of raw rule segments.
The scatter shader treats strokewidth as a uniform float; feeding it the per-glyph
text_strokewidth vector errored on WGLMakie (Type Vector{Float32} not supported).
Per-glyph text strokewidth is always uniform-valued in practice, so pass the scalar
plot strokewidth, matching how GL/WGL handled Text before the Glyphs extraction.
- CairoMakie draw_glyphs: replace `local` batch vars with a batch tuple accumulator (`local` bindings are unusual Julia style) - compile_text now returns a CompiledText struct with union-nothing glyphs/specs fields and dispatch constructors, instead of a (CompiledGlyphs, specs) tuple - Document why Text/text remain in PrimitivePlotTypes/atomic_functions (container still converts its own attributes and needs camera registration for plotlist projection; no recipe body)
…dlers Image-rendering text handlers (e.g. MakieTeX's LaTeX/Typst) bake appearance into their output at compile time, so compile_text now receives the per-block color, strokecolor and strokewidth (the same appearance attributes place_text! gets). Glyph-based handlers ignore them and let place_text! apply them to the glyph batch.
A callback returning `Ref{Any}(x)` (the type-narrowing opt-out) stores the
dereferenced value but was compared against the raw Ref wrapper, which never
equals it, so the output stayed perpetually dirty and re-fired every dependent.
Dereference once and compare the value.
…updates The text argument producer re-emits input_text whenever position changes (they share a node). It handed back an aliased array, and is_same treats a shared array as possibly-mutated (dirty), so the text layout re-ran on every position update during layout solves. For image handlers (MakieTeX LaTeX) that meant recompiling each pass (10 engine runs for 3 constant labels; now 3). Copy so the emitted array is distinct and compares equal.
Nothing reads the glyphcollections node output any more (backends consume the flat per-glyph arrays); it was reconstructed per block and threaded through the cache for nothing. Remove the field, the four pushes (including the String-path GlyphCollection that existed only to be pushed), and the gcs slot in reuse_glyph_layout. The GlyphCollection type stays for now (PathText + layout intermediates still use it).
The text node's per-glyph outputs were named inconsistently (glyph_origins vs font_per_char vs text_color). Rename them uniformly: glyphindices→glyph_indices, font_per_char→glyph_fonts, text_scales→glyph_scales, text_color→glyph_colors, text_rotation→glyph_rotations, text_strokecolor→glyph_strokecolors, text_strokewidth→glyph_strokewidths (glyph_origins/glyph_extents unchanged). This also removes the name collision with the unrelated text_color/text_rotation attributes on TextLabel/Contour/Tooltip. Scoped to the node outputs and their real consumers (register_glyphs!, editabletext, textbox, tests); the Glyphs recipe attributes and the GlyphCollection/layout-nt fields are left untouched.
The text layout node allocated a fresh set of 13 output arrays on every evaluation. It now keeps one GlyphBuffer across evaluations, emptying and refilling the cached arrays instead. Reuse is graph-safe: `is_same` treats an aliased array as changed, so the mutated buffer still propagates to the Glyphs child and the bounding box nodes. The buffer also hides the parallel-array bookkeeping behind push_glyph_block!, push_empty_block! and push_text_spec!, so a spec can't be pushed without its matching block index and bbox, nor a glyph block without its text_blocks entry. push_text_spec! takes an optional bbox for output whose extent isn't its positions (image markers). per_glyph_block is replaced by BlockAttribute, resolved while appending. Allocations for one relayout of 26893 glyphs drop from 63.6 to 53.9 MiB. A color-only update, which keeps the cached geometry, drops from 3382 to 4.3 KiB and no longer scales with glyph count. Cairo renders of plain, rich, LaTeX and handler text are byte-identical to before.
Text layout resolved alignment, rotation and offset itself, so changing any of them re-ran the layouter. For an expensive `text_handler` that meant recompiling a LaTeX label just to move it. Layout now emits a layout frame instead: unaligned, unrotated glyph origins plus a per-block bbox and baseline describing the frame. A downstream placement node consumes those with `align`, `rotation` and `offset` and produces the placed `glyph_origins`, `glyph_rotations`, `text_specs` and `text_spec_bboxes`. A handler no longer sees the placement attributes at all, and its specs get the block transform applied for it, including composing the block rotation into a `rotation` kwarg so an image marker turns with the text. `align` still reaches layout through one number: automatic justification follows halign, so it resolves to `resolved_justification` upstream. An align change that leaves that number alone is filtered by `is_same` and never triggers a relayout. Two behavior changes fall out of the shared placement path: - `valign = :baseline` now aligns `rich` text and `LaTeXString`s on their baseline. Both previously behaved like `:bottom`, because `:baseline` went through `get_yshift`'s `default` as the align fraction 0.0 rather than as a baseline position. Plain strings are unchanged. - With `justification = automatic`, a fractional `halign` now justifies by that fraction instead of falling back to 0.5. This makes plain strings agree with rich text, which already did this. Verified against the previous commit: CairoMakie renders over align, rotation, offset, justification and word-wrap grids are byte-identical except the two cells covering the changes above, MakieTeX's handler output is byte-identical including rotated markers, and the 80 recorded text/layout reference images differ only in anti-aliasing (worst score 0.005 against the 0.05 threshold, 10 pixels of 300,000) from the reordered float arithmetic.
The text layouters were the only thing building `GlyphCollection`, and the glyph buffer immediately took it apart again. They now return the flat per-glyph arrays directly, under one shared set of names (`glyphindices`, `fonts`, `origins`, `extents`, `bbox`, `baseline`), so all three layout paths read the same way at the call site. That leaves `GlyphCollection` and `ScalarOrVector` unused, along with the six `attr_broadcast_*` / `is_*_attribute` / `getindex` methods that existed only to make `ScalarOrVector` fields broadcastable, and `collect_vector`. `GlyphInfo` also loses its `rotation` field, dead since rotation moved to the placement node. No behavior change: the 80 recorded text and makielayout reference images are byte-identical to the previous commit, as are the Cairo placement grids.
Bending text along a path produces a position and a rotation per glyph, which is exactly what `Glyphs` consumes. `pathtext` was instead handing each character to `text` as its own single-character string, so every glyph paid for a full text block: a layout pass, a block bbox, a justification resolve, and an alignment shift that `align = (:left, :baseline)` was chosen to cancel out. It now resolves glyph indices and fallback fonts during layout and drives a `Glyphs` child directly, with the placed positions as the glyph origins and no marker offset. `_place_glyphs_on_path` no longer threads characters through just to hand them back as strings. The 80 recorded text and makielayout reference images, which cover pathtext over several halign values, plain and rich text, and polyline and Bézier paths, are byte-identical to the previous commit.
`GlyphCollection` is gone, so the names built on it no longer refer to anything: `glyph_collection` -> `layout_string`, `texelems_and_glyph_collection` -> `texelems_and_layout`, and `compute_glyph_collections!` -> `register_glyph_layout!`, which also pairs with `register_glyph_placement!` downstream of it. Deletes a block of dead helpers that came along for the ride: `get_from_collection` and `get_text_blocks` read fields of the removed type, `map_per_glyph` and the two `per_*_getindex` error paths reference an undefined `glyphs` binding (so they could never have run), and `per_text_block` / `per_glyph_attributes` had no callers left.
A vector `color`, `strokecolor`, `strokewidth` or `fontsize` whose length matched the total glyph count used to style a string character by character. That model does not survive proper text shaping: with HarfBuzz several code points can shape into a single glyph (ligatures, combining marks, Indic clusters), so there is no character-to-glyph correspondence to index by. Rather than keep an API that would have to break later, drop it now; `rich` text already covers styling parts of a string, and does so in terms of the text rather than the glyph run. Such a vector is now always one value per string, and a mismatched length errors pointing at `rich` text instead of silently reinterpreting the vector. Per-character `fontsize` was half-broken anyway: layout only ever saw the block's first entry, so glyphs grew while their advances stayed put and they overlapped. `Glyphs` is unaffected, being the per-glyph primitive; `pathtext` therefore still takes per-character vectors, which it will have to revisit when shaping lands.
`pathtext` drives `Glyphs` directly, so a vector `color`, `strokecolor` or `strokewidth` still styled the string character by character after `text` dropped that. Same reasoning applies: text shaping can merge several code points into one glyph, so a vector indexed per character has no well-defined mapping onto glyphs, and `pathtext` already accepts `RichText`, which expresses the same styling in terms of the text. Each of the three now takes one value and errors otherwise, pointing at `rich` text. The reference test that used a per-character viridis ramp now builds it with `rich` instead, and records byte-identically, so the replacement is exact rather than merely equivalent.
`_layout_richtext_for_path` hardcoded opaque black as the base color, so
`pathtext(path, text = rich("abc"), color = :red)` drew black; only the parts that
rich text styled itself came out right. Now that `rich` is the way to style parts
of the text, that base has to be the plot's color.
Resolving it means colormapping the color before layout, which recipe attributes
don't get for free: `to_color` first, then `register_colormapping!` on the
converted node. So a colormapped number still works, for the whole string.
That also moves `alpha` into the color here, so the `Glyphs` child must not apply
it a second time (it was doing 0.25 for `alpha = 0.5`, where `text` gives 0.5).
The one reference image that combines an annotation color with partly-styled rich
text changes accordingly: its "H₂O → " now takes the annotation's steelblue
instead of black, matching the arrow it labels.
Splitting the handler protocol in two made sense when placement needed align, rotation and offset: the engine step couldn't see them, so it handed a payload to a placement step that could. Since placement moved into its own node, the placement step's arguments are a strict subset of the engine step's, so the split carries no information and just makes a handler author learn two functions and an intermediate type. `emit_text!` is the whole protocol now: get the block's attributes, push into the buffer, return `true`, or return `false` to fall through. That deletes `CompiledText`, `CompiledGlyphs` and `place_text!`, since a handler that has the buffer has no reason to re-package what `push_glyph_block!` already takes. The fallback method is deliberately untyped in all arguments, including the buffer: typing the buffer makes the obvious handler method (typing the handler and the input type) ambiguous with the fallback rather than more specific.
Twelve positional arguments made the handler extension point brittle: adding a
text attribute would have broken every handler in existence, which is not a good
property for the one function third parties are meant to implement.
The attributes now travel as a struct, so a new field leaves existing handlers
alone. Keywords would have needed every handler to remember `kwargs...` to get the
same protection, and a struct also documents the set in one place.
Nothing is lost at the use site, since a handler can destructure what it wants:
function Makie.emit_text!(buffer, ::MyHandler, str::AbstractString, attributes)
(; fontsize, color) = attributes
...
end
`fontsize` is normalized to `Vec2f` on the way in, so a handler no longer has to
consider both a number and a `Vec2`; `MathTeXHandler` and MakieTeX both took the
first component anyway.
Signalling with `return false` had the handler tell Makie "I didn't do anything", which Makie has no way to verify: a handler that pushes and then returns `false` gets its block laid out twice, silently corrupting `text_blocks`. Nothing in the type system prevented that. The fallback method now runs Makie's own layout, so a handler that has no method for an input type falls through by not existing, and one that decides from the content hands the block back with `default_text_layout!`. Every path pushes exactly one block because every path ends in a push. `text_handler = nothing` also stops being a special case in the layout loop: `nothing` has no method, so it lands on the fallback like any other unhandled input. Two things this pulled in. Attributes are now resolved per block once, before layout, so `BlockAttribute` and its resolve-while-appending are gone; the per-string length check that lived there is a single `validate_per_string` pass over the attributes, which also names the offending attribute in the error. `layout_string` now takes the font and fontsize as one value for the whole string, which additionally fixes a `Vec2` fontsize erroring in the built-in path.
`emit_text!` has one obligation, and nothing was checking it. A handler that pushes nothing leaves `text_blocks` shorter than the input, and one that pushes twice leaves it longer, either way silently misaligning every per-block array downstream: the text just renders wrong. The layout loop now compares the block count against the string index after each call and says which handler broke the contract. It caught the test's own counting handler immediately, which had been relying on `return false` to opt out.
`default_text_layout!` was a second name for something the protocol could already express: `text_handler = nothing` means "use Makie's layout", so Makie's layout is just `emit_text!` with `handler === nothing`. Its three methods now dispatch on `::Nothing` and the extra function is gone, which also makes the built-in path an instance of the extension point rather than a thing beside it. A handler delegates with `emit_text!(buffer, nothing, src, attributes)`, mirroring the attribute value that means the same thing. The generic fallback delegating to `nothing` needs a recursion stop, so there is a `::Nothing` method for input types Makie has no layout for. It replaces what used to be a `MethodError` on an internal function with a message naming the type and the two ways out.
`emit_text!` took the node's `GlyphBuffer` and pushed into it, which put two things on the handler author: picking the right one of three push functions, and keeping the parallel arrays consistent. Both are Makie's bookkeeping, and neither is checkable at the boundary, so a handler that pushed nothing or twice silently misaligned every per-block array downstream. The previous commit could only catch that after the fact by counting blocks. A handler now returns one `TextLayout` for the block and Makie appends it, so "exactly one block per string" holds by construction and the count check, the three push functions and the buffer itself all leave the extension point. `append_text_layout!` validates the array lengths up front and names the field that doesn't match. `spec_bboxes` defaults to the bounding boxes of the specs' positions, as `push_text_spec!` did, so only a handler whose visual extent differs from its positions (an image marker) passes them.
Neither half of `emit_text!` describes the function any more: it stopped pushing into a buffer in the previous commit, and the text it names is what goes in (a `String`, `LaTeXString`, `RichText`, or a handler's own type), not what comes out. `layout_text` takes the text as the object of the verb, the way `layout_string` already does, and returns a `TextLayout`. That name belonged to the `RichText` layouter, which is internal with a single call site and becomes `layout_richtext`, leaving `layout_string` / `layout_richtext` / `texelems_and_layout` each named after what it lays out.
The `text` page gets a section on `text_handler` aimed at using one: how to set it on a plot, how to set it in the theme so axis and legend labels go through it too, and that a handler only claims the input types it has methods for. The LaTeX page links there, since rendering `LaTeXString`s with an engine other than MathTeXEngine.jl is the main reason to reach for a handler. Writing a handler is left to `layout_text`'s docstring, which gains the method sketch, the note that `align`/`rotation`/`offset` are applied downstream, and a warning about `LaTeXString <: AbstractString` (a method taking `AbstractString` claims LaTeX input and lays out its source). `MathTeXHandler` moves out of the changelog line, since it does what the default LaTeX path already does and is not something a user would set.
rasterize_marker_for_gpu ran in the compute graph, which has no screen, so a rasterized marker was fixed at nominal resolution: blurry on hidpi displays and in high-resolution saves. The hook takes px_per_unit now and GLMakie re-rasterizes single markers per screen through an input backed by screen.px_per_unit, which also re-runs when a save with a different px_per_unit reconfigures a displayed screen. The graph keeps the px_per_unit = 1 rasterization as the fallback for WGLMakie and for vectors of markers, whose uv packing happens graph-side.
Spec placement used to rewrite each spec's first positional argument and compose a rotation kwarg, which restricted specs to point-based plot types and rebuilt every spec's positions on each camera move. Placement (align, rotation, offset and the projected anchor) now composes into the spec child's model matrix, so any plot type works as a spec, including image, and camera moves diff a single matrix kwarg instead of argument arrays. Spec children run with transformation = :nothing and a model keyword now seeds the model input of such plots directly; the spec differ skips the construction-only transformation kwarg instead of erroring on it. transform_marker defaults to true on spec children where it exists, so scatter markers rotate with their block.
CairoMakie sorts drawing by the z of a plot's transformation matrix, which a spec child with transformation = :nothing reports as 0, hiding legend labels rendered as specs behind the legend background. Spec children now share one transformation inherited from the text plot, carrying its z and an identity transform_func, while their model input is driven exclusively by the model keyword: a plot constructed with an explicit model no longer syncs its transformation into the model input at all.
Recipes that forward their generic attributes, like spy and datashader, pass model = automatic to their children, which the model keyword handling tried to convert to a matrix. Non-matrix values now mean the transformation drives the model as usual.
…asterization nodes
…`, fix WGLMakie not rendering correctly
| map!(attr, inputs, :baked_display_attributes) do text, handler, color, strokecolor, strokewidth | ||
| # here rather than downstream so a bad length is reported when the plot is | ||
| # created, not when its colors are first pulled | ||
| for (name, value) in [(:color, color), (:strokecolor, strokecolor), (:strokewidth, strokewidth)] | ||
| validate_per_string(name, value, length(text)) | ||
| end | ||
| baked = !isempty(text) && any(str -> bakes_display_attributes(handler, str), text) | ||
| baked || return nothing | ||
| return (color, strokecolor, strokewidth) | ||
| end |
There was a problem hiding this comment.
return nothing is probably a bug here.
If the output is not initialized, return nothing will cause it to be initialized to nothing. I think that's what you want here.
If the output is already initialized return nothing causes the update to be discard. I.e. the old value remains and propagates as "up to date". So baked_display_attributes can not dynamically switch to nothing like this. I think after init nothing is always considered as "nothing changed here", so we'd need to disambiguate it.
Tangentially, if you have multiple outputs and return nothing in an init run, all outputs become nothing. I would've expected an error there. Maybe that should be changed?
| register_computation!(attr, inputs, [:_positions, :_text_update]) do inputs, changed, @nospecialize(cached) | ||
| a_pos, a_text, args... = values(inputs) | ||
| _, text_changed, args_changed... = values(changed) | ||
|
|
||
| # Note: Could add RichText | ||
| if args isa Tuple{<:Union{AbstractString, RichText}} | ||
| # position data will always be wrapped in a Vector, so strings should too | ||
| return ((a_pos,), Ref{Any}([args[1]])) | ||
| return ((a_pos,), text_update([args[1]], args_changed[1])) | ||
| elseif args isa Tuple{<:AbstractVector{<:Union{AbstractString, RichText}}} | ||
| return ((a_pos,), Ref{Any}(args[1])) | ||
| return ((a_pos,), text_update(args[1], args_changed[1])) | ||
| elseif args isa Tuple{<:AbstractVector{<:Tuple{<:Any, <:VecTypes}}} | ||
| # [(text, pos), ...] argument | ||
| return ((last.(args[1]),), Ref{Any}(first.(args[1]))) | ||
| return ((last.(args[1]),), text_update(first.(args[1]), args_changed[1])) | ||
| else # assume position data | ||
| return (args, Ref{Any}(to_string_arr(a_text))) | ||
| return (args, text_update(to_string_arr(a_text), text_changed)) | ||
| end | ||
| end |
There was a problem hiding this comment.
We're still not doing any kind of depwarn here or elsewhere for text(..., position = ...). Also, do we want to deprecate the tuple version as well?
| """ | ||
| struct TextAttributes | ||
| font::NativeFont | ||
| fonts::Any |
There was a problem hiding this comment.
Can we not fix the type to a Dict?
|
|
||
| map!(attr, [:marker, :scaled_color], :scatter_color) do marker, color | ||
| map!(attr, [:image, :scaled_color], :scatter_color) do marker, color | ||
| if marker isa AbstractMatrix |
There was a problem hiding this comment.
This fixes the rasterized marker thing in WGLMakie but still doesn't consider px_per_unit. I don't know how to include that without it being a mess...
For context, I moved the ppu handling into all_marker_computations so GLMakie doesn't rasterize twice. So all_marker_computations is adding a dummy px_per_unit = 1f0 node here and uses that for rasterization.
|
My commits are mostly cleanup. I'll push another to fix tests once they finish locally |
|
/benchmark 20 |
Benchmark ResultsSHA: af372c8da05789f1cfe6da2f986c851931b56619 · 20 samples Warning These results are subject to substantial noise because GitHub's CI runs on shared machines that are not ideally suited for benchmarking. |
|
/benchmark 100 |
Benchmark ResultsSHA: af372c8da05789f1cfe6da2f986c851931b56619 · 100 samples Warning These results are subject to substantial noise because GitHub's CI runs on shared machines that are not ideally suited for benchmarking. |
|
/benchmark 50 |
Benchmark ResultsSHA: c4271f66de01d4a8a5e878b52b22c6163aed6853 · 50 samples Warning These results are subject to substantial noise because GitHub's CI runs on shared machines that are not ideally suited for benchmarking. |
|
Heh, alright, benchmarking @profview for _ in 1:100
scene = Scene()
for _ in 1:100
text!(scene, 0, 0)
end
endTo optimize We could also look into making a specialized It might also be possible to just skip the plotlist plot entirely until there are specs to process? |
|
Have you actually diagnosed that these are what makes this slower? Or is it some compilation thing when plotting? Or is it what you say and it's just the larger number of text plots in a normal plot that cause this? |
|
I didn't directly compare breaking to this pr but since I've done a bunch of benchmarking on plot creation on breaking already I think I have a decent feeling for it.
|
|
Ugh, nvm, most of the time in |
Supersedes #5632, opened separately so that PR's review history stays readable. It refactors the text pipeline so other packages can hook in their own way of rendering text or text-like objects: real LaTeX through MakieTeX (companion PR MakieOrg/MakieTeX.jl#73), or something like TeXLayout.jl, which currently has to resort to type piracy for this.
The extension point is
Makie.layout_text, which returns glyph data, other plot specs, or both, for a handler type and/or a text type:Makie's own layout is the
handler === nothingimplementation of that same function, so there is one protocol rather than an extension API bolted next to the built-in string types, and a type a handler has no method for falls through to Makie without the handler doing anything.Picking the handler in the theme means a whole figure can be rendered with LaTeX text without threading handlers through every call:
Title, labels, tick labels and legend entries all come from a real TeX run here.
Glyphs and specs
Text is now a scatter-like plot of glyphs, and that idea gets its own plot type:
Glyphs.Textbecomes a container with aGlyphschild for the glyph batch and a plotlist for everything that isn't a glyph, which is how MathTeXEngine's rules and MakieTeX's image markers get drawn. Glyphs keep a fast path; the specs are slower, but they are flexible enough to switch between text and plot types dynamically, and these text systems aren't meant for performance sensitive work anyway. The point of MakieTeX is high quality static plots, not animating huge equations in a live GUI.Specs are placed through their plot's
modelmatrix rather than by rewriting their arguments, so any plot type can be a spec (animage, say), a spec's arguments stay in the block's layout frame, and moving the camera or the text updates one matrix per spec. Markers rasterized for the GPU backends (MakieTeX's PDF labels) are rasterized at the screen'spx_per_unitin GLMakie and re-rasterized when it changes, so they stay sharp on hidpi screens and in high-resolution saves of an already displayed figure.Moving text around doesn't relayout, because
align,rotationandoffsetare applied downstream of layout, which for a LaTeX engine is the difference between moving a label and recompiling it. Almost every other attribute does relayout, which seems fine given how rarely they are animated.The split also makes display-only updates cheap for plain text, since glyph geometry is reused instead of laid out again. This holds with a
text_handlerset: only blocks the handler actually claims (has alayout_textmethod for) re-run layout on a recolor, fall-through blocks keep the fast path.Breaking
Per-character styling goes away. A vector
color,strokecolor,strokewidth,fontsizeorrotationontextis one value per string now, and a length that doesn't fit errors. This is for shaping systems like HarfBuzz, where several code points become one glyph, so there is nothing for a per-character vector to index; we can style glyphs, not characters.richtext already covers the use case, since it styles ranges over a string rather than characters.Textis a container plot, so code that read glyph render attributes (sdf_uv,quad_offset,quad_scale, ...) off aTexthas to read them from itsGlyphschild.pathtextdraws into aGlyphschild instead of aTextone.align = (halign, :baseline)now putsrichtext andLaTeXStrings on their baseline instead of their bottom edge, matching plain strings; for multi-linerichtext that is the last line's baseline, also matching plain strings.justification = automatic, a fractionalhalignjustifies by that fraction instead of falling back to0.5.Also fixed on the way:
richtext inpathtextignored the plot'scolorfor the parts it didn't style itself, and appliedalphatwice.Caveats
LineSegmentsplots. A camera move only updates each spec'smodelmatrix, but merging same-type specs across blocks would still reduce the plot count; only text that emits specs pays anything at all, so keeping the implementation simple seems worth more until that turns out to be wrong.MathTeXHandlerdoesn't handleword_wrap_widthyet; the built-in LaTeX path still does.text("string", position = ...)form needs a deprecation cycle, so it stays as is here, per @ffreyer's note on feat(text): Add text_handler extension hook #5632.strokewidthstill collapses to one value in GLMakie and WGLMakie, whose shaders bind it to a uniform float. Same as on master, CairoMakie draws it per glyph.