Skip to content

glifo: Fix interaction with complex paints#1668

Open
LaurenzV wants to merge 8 commits into
mainfrom
laurenz/grad_fix
Open

glifo: Fix interaction with complex paints#1668
LaurenzV wants to merge 8 commits into
mainfrom
laurenz/grad_fix

Conversation

@LaurenzV
Copy link
Copy Markdown
Collaborator

As part of our migration of glifo, we've basically completely broken the interaction between glyphs and complex paint transforms. This PR makes two changes to restore the expected behavior:

  1. In a glyph run, we keep track of the original paint transform and later on apply a correction to make sure that each glyph is drawn with the correct paint transform.
  2. We disable glyph caching for glyphs with complex paints since our image tinting API only supports solid color fills.

I added some more new tests to prevent future regressions. Thanks to @waywardmonkeys for finding this.

@LaurenzV LaurenzV requested a review from grebmeg May 25, 2026 07:54
@waywardmonkeys
Copy link
Copy Markdown
Collaborator

I will leave the actual review for someone else, but you should probably include at least one test using an image brush as well.

@waywardmonkeys
Copy link
Copy Markdown
Collaborator

Also, when I implemented brush transforms for glyphs for Vello Classic recently, I had some interesting tests that might be worth taking a peek at.

@LaurenzV
Copy link
Copy Markdown
Collaborator Author

Sure, can do that!

Comment thread glifo/src/glyph.rs
pub(crate) glyph_type: GlyphType<'a>,
/// The global transform of the glyph.
pub(crate) transform: Affine,
/// The transform of the paint, relative to [`PreparedGlyph::transform`].
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is subtly wrong, since it’s actually relative to transform.pre_scale(scale).

Comment thread glifo/src/glyph.rs
@@ -150,6 +151,8 @@ pub(crate) struct PreparedGlyph<'a> {
pub(crate) glyph_type: GlyphType<'a>,
/// The global transform of the glyph.
pub(crate) transform: Affine,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think transform is a bit overloaded across this file. Would it be better to rename it to something like the following?

    /// Per-glyph outline transform: maps the draw-unit glyph outline
    /// (after font-size absorption) to scene coordinates.
    pub(crate) outline_transform: Affine,

Comment thread glifo/src/glyph.rs
/// Global transform.
transform: Affine,
/// Absolute paint transform for the glyph run.
absolute_paint_transform: Affine,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: "absolute" here doesn't refer to a world frame, just "the composition in scene space, before glifo's per-glyph rewrite". Renaming to scene_paint_transform would pair naturally with relative_paint_transform (which is in glyph-local space) and make the invariant readable: scene_paint_transform == outline_draw_transform * relative_paint_transform.

Comment thread glifo/src/glyph.rs
Comment on lines +399 to +408
let outline_draw_transform = outline_transform.pre_scale(scale_props.draw_scale);

// We assume that the backend calculates the absolute paint transform
// by concatenating scene transform and (relative) paint transform.
// (This is currently the case for Vello CPU / Vello Hybrid, but will
// also be assumed to be the case for any other potential backend.)
// Therefore, we can calculate the relative paint transform for
// the glyph by pre-concatenating it with the inverted outline transform.
let relative_paint_transform =
outline_draw_transform.inverse() * absolute_paint_transform;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the outline cache hits (the common case for repeated characters), the code has already paid for an Affine::inverse() and Affine * Affine multiply that gets discarded. What about putting the computation into the outline-cache-miss branch, since that's the only place a relative_paint_transform produced here is consumed?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m worried we’re introducing quite a lot of math here on the hot path, especially for a general solid fill, since this math isn’t even needed in that case, can this be avoided at least for solid colors? And more generally, are there ways to avoid doing this math per glyph? One idea would be to factor out the run-constant linear part so that we end up with significantly fewer computations overall, although that would increase the complexity here.

Another idea, which seems preferable, would be to push this responsibility into the renderer, perhaps by introducing set_paint_transform_absolute(t: Affine) on GlyphRenderer which would flip the responsibility: glifo calls it once per run with absolute_paint_transform, the renderer stores it as an optional Option<Affine> on its RenderState, and every paint-encoding site uses that override directly when set instead of composing transform · paint_transform. Glifo can then freely mutate state.transform per glyph without any compensation, because the paint basis is pinned. The per-glyph math drops to zero, the inverse disappears entirely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants