The key of a raster made for content that shows a symbol includes the
font generation, and since c348b384 (#1723) the key for content
painted with a pattern includes it too. FontDb::add_font increases
that generation, and the six tests in tests_fontchange.rs call it. Two
tests in fx.rs draw such content and count the rasters moved between
frames: moved_rasters_need_the_same_content_and_effects and
a_moved_group_draws_the_shadow_of_the_instance_art_in_it_while_interacting.
Neither held fonts_steady(). When a font test added a font between
their frames, the renderer made the raster again and the test counted
0 rasters moved instead of 1. Under CPU load on Windows, with the font
tests in the same process, the first test failed in 12 of 100 runs and
the second failed in 36 of 70.
Both tests now hold fonts_steady().
translucent_blended_and_shadowed_type_redraws_when_its_font_becomes_available
added its font without the font lock that fonts_steady() holds shared,
and it now takes adding_fonts(), as the other five font tests do. With
the change, the first test passed in all 200 runs and the second
passed in all 120 runs under the same load.
Only type at full opacity, in Normal blending and without object
effects took its glyph outlines from the geometry the renderer keeps
for each type object. Type at less than 100% opacity or in a blend
mode, and the art drawn for type with a drop shadow, glow, blur or
other raster effect, were laid out again in every frame.
draw_text now takes the geometry from text_geom_of when the type it
draws is fx_owner, the node draw_arc is drawing, and the frame does
not snap type to pixels. While the effect-art cache keeps type art,
that art is drawn as fx_owner. The geometry is laid out again when the
type is edited or the fonts change, and frames with Type Optimized
anti-aliasing lay type out as before. On 8 threads at 2880 x 1800, a
page of 1,000 type objects at 80% opacity takes 13 ms a frame instead
of 23 ms, with the same pixels.
Opaque type kept the layout of earlier frames when a symbol it shows
as an inline graphic changed size and the document's inline graphics
had not been resolved again. text_geom_of now lays type out without
the cache when the text it is given is not the node's own, such as the
copy that Document::inline_resolved makes with the new size.
Docs: docs/target-app-parity.md.
The renderer kept the evaluation of each blend, envelope and repeat
while the object and a hash stayed the same: the addresses of the art of
the symbols the object showed and of the patterns it painted with, and
the patterns' tile bounds. When anything else the evaluation reads
changed, such as a pattern's tile type, symbols in symbol art, the
fonts or the color settings, a renderer that had drawn the object went
on drawing the old evaluation. The quads of a gradient mesh were kept
the same way after a change to the color settings, and the art drawn
for a live object with an effect of its own was kept by the object's
address alone.
resources_key now also walks the opacity-mask, symbol and pattern art
it finds. It hashes what it finds, the font generation when it finds
type or a placed document, and the color settings. Mesh quads are made
again when the color settings change. The art drawn for an object's
effects is made again when the hash changes, if it was made from
evaluated live objects.
With other builds running, a temporary release test timed the hash for
400 live objects at 17 to 506 microseconds a frame, against 10 to 16
before.
Docs: docs/target-app-parity.md, docs/gaps.md.
tests_fontchange adds fonts to the shared font database, which makes every renderer lay type
out again; a tests_memory test counting the type it kept between frames then failed when one ran
alongside it (about half of the runs of the render tests). The font tests now hold a lock alone,
and the frame cache tests hold it shared.
The renderer kept the rasters of an object's drop shadows, outer glows
and filtered content (Gaussian Blur, Feather and the Photoshop-style
filters), and drew them again while the object, the zoom and rotation
and the ink plane stayed the same. The content drawn into a raster also
reads things outside the object: patterns, symbols, brushes, images,
document, layer and color settings, the knockout a group inherits, the
fonts, and the options the frame is drawn with. When one of them
changed, a renderer that had drawn the object went on drawing the
raster it had kept, made from the old values.
Each kept raster now records what its content was drawn from: which of
those things the object reads, found by walking the object when the
raster is made, and a hash of their values then. The renderer draws
the raster only while that hash is the same in the current frame. Once
a frame, the renderer hashes the document's patterns, symbols and
images. It hashes their art and pixels by the addresses of their
shared values, and it keeps weak references to those values while a
raster made with them is kept, so no other value can take one of those
addresses. A filtered raster also records the color channels its
filters split, and under De-Interlace the raster resolution.
A temporary release test timed the inputs of 400 kept rasters at about
10 microseconds a frame.
The renderer drew each symbol instance by copying its symbol's art,
staining the copy for a stained instance and moving every object in it
by the instance's transform, in every frame. The copies were new
nodes, so the caches the renderer keys by a node's address (cull
bounds, path geometry, glyph layouts, live-effect geometry) missed for
every object of every instance, and the copies stayed in those caches
until they were pruned. Stroke outlines missed as well, because their
cache uses an entry only when the entry holds the node being drawn.
The renderer copied and moved the art of inline graphics in type the
same way.
The renderer now keeps the art it places in a frame cache with a
64 MiB budget, under the address of the symbol's art, the transform,
whether strokes scale with it, and the instance's stain. When the art
is a group or a layer without an appearance of its own, the renderer
copies none of it in a frame whose instances and symbols did not
change, and the objects in it hit the caches keyed by their own
address (cull bounds and paths, glyph layouts, live-effect geometry).
The stroke-outline cache keys a stroke by its address in the
appearance. In the art placed for an unstained instance, each object
without a gradient fill or stroke shares its appearance with the same
object in the symbol's art. When a symbol has two or more unstained
instances, the renderer therefore still outlines the strokes of those
objects again for each instance in every frame. Moving or restaining
an instance, or editing its symbol's art, places that art again, and
the other instances keep theirs. Other art is placed again in every
frame, as before, and so is art that holds a symbol instance, type
with an inline graphic, an image, a placed document, a blend, an
envelope, a repeat or a raster effect. Art that holds type is placed
again when the fonts change.
vectorcraft-cli bench of a symbol of eight paths (three filled and
stroked, two only filled, three only stroked) shown by 2,000
unstained instances took 51.6 ms a frame on main and 32.3 ms with
this change, at 2880x1800 on 8 threads; the same paths in 2,000
groups took 26.0 ms. The peak working set of a bench run at that size
went from 150 MiB to 83 MiB. In the tests, a renderer that keeps
placed art draws the same pixels as a new renderer, and instances
draw the same pixels as groups of their placed art.
Type > Font, the Character, Properties and Glyphs panels and Find Font list
every installed font by default, as the reference app does, instead of the
four bundled families. The family dropdowns get a search field that filters
the list as you type (Enter picks the first match) and open on the current
font. Show System Fonts, which no longer means anything, becomes Refresh Font
List (text.rescanFonts): it picks up fonts installed since the app started and
redraws type set in them without editing the document. text.fontList lets MCP
agents list the families and a family's styles.
Root cause: the font lists came from FontDb::families(), which only held the
bundled fonts until something scanned the system fonts (fixed with #130). The
Type > Font menu, rebuilt every frame, now builds its items once per font
change, and renderers lay type out again when the fonts change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>