allsorts icon indicating copy to clipboard operation
allsorts copied to clipboard

Is it unsound to perform text shaping while an OutlineBuilder exists?

Open LoganDark opened this issue 2 years ago • 9 comments

In order to get an OutlineBuilder, you have to do some parsing using the font's FontTableProvider. This means the OutlineBuilder borrows from the FontTableProvider, which means it borrows from the Font.

Then, if you need to shape some text, you need to... call Font::shape which takes &mut self.

This is aliasing a mutable reference with preexisting shared references.

That means the most obvious low-hanging-fruit for caching (obtaining the OutlineBuilder up-front at load time) is unsound.

I did not notice this because I have to use piles of unsafe code to store the font and related data in a pinned struct. Therefore the borrow checker is significantly inhibited. see #52

Now that I think about it, it seems like a huge API issue, as well as a performance issue. Parsing tables to get an OutlineBuilder each frame in a game for example (required not to violate the borrowing rules), sounds like a huge problem.

LoganDark avatar Jun 14 '22 02:06 LoganDark

OutlineBuilder is just a trait so it has no state of it's own. It only borrows the underlying table (mutably) when visit is called. Unless I'm missing a detail?

wezm avatar Jun 15 '22 00:06 wezm

OutlineBuilder is just a trait so it has no state of it's own. It only borrows the underlying table (mutably) when visit is called. Unless I'm missing a detail?

The actual implementations of OutlineBuilder require borrowing a bunch of data obtained from the DynamicFontTableProvider

LoganDark avatar Jun 15 '22 02:06 LoganDark

Right it seems that I was misunderstanding your question. Normally you would not be able to call shape while holding an OutlineBuilder impl because the borrow checker would not allow it. However if you do manage to achieve this via unsafe are you asking if it's unsound?

If so, shaping currently mutates the gsub_cache, gpos_cache, glyph_cache, and the lazy loaded GDEF table (if it's not already loaded) on Font. So it might be ok to hold onto the OutlineBuilder but I can't guarantee this will always be the case.

Now that I think about it, it seems like a huge API issue, as well as a performance issue. Parsing tables to get an OutlineBuilder each frame in a game for example (required not to violate the borrowing rules), sounds like a huge problem.

As you pointed out in #52 the library is not currently well structured for this type of use. The API evolved for our use case which is load a bunch of fonts, shape the text, and then generate a PDF. I'm not sure if/when we'll be able to dedicate the time to remedying that.

wezm avatar Jun 15 '22 04:06 wezm

If so, shaping currently mutates the gsub_cache, gpos_cache, glyph_cache, and the lazy loaded GDEF table (if it's not already loaded) on Font. So it might be ok to hold onto the OutlineBuilder but I can't guarantee this will always be the case.

I'm saying that it's definitely unsound because there are aliasing mutable references which is forbidden.

As you pointed out in #52 the library is not currently well structured for this type of use. The API evolved for our use case which is load a bunch of fonts, shape the text, and then generate a PDF. I'm not sure if/when we'll be able to dedicate the time to remedying that.

This is kind of sad, because allsorts is actually kind of fast and extremely featureful. I'm having no problems shaping/rasterizing text every single frame in software (the rasterizer is ab_glyph_rasterizer). It's just that the code currently doing that is invoking Slightly Undefined Behavior. :(

LoganDark avatar Jun 15 '22 04:06 LoganDark

This is kind of sad, because allsorts is actually kind of fast and extremely featureful. I'm having no problems shaping/rasterizing text every single frame in software (the rasterizer is ab_glyph_rasterizer). It's just that the code currently doing that is invoking Slightly Undefined Behavior. :(

Perhaps a silly idea: what if you parsed the font twice and used one instance for the OutlineBuilder and one for shaping?

wezm avatar Jun 15 '22 04:06 wezm

This is kind of sad, because allsorts is actually kind of fast and extremely featureful. I'm having no problems shaping/rasterizing text every single frame in software (the rasterizer is ab_glyph_rasterizer). It's just that the code currently doing that is invoking Slightly Undefined Behavior. :(

Perhaps a silly idea: what if you parsed the font twice and used one instance for the OutlineBuilder and one for shaping?

I had this idea once but I actually forgot to try it. This will probably work. In fact, I don't see any reason why it wouldn't. The data structures returned by a call to shape do not contain any references to the font, and therefore can just be... transferred. However you want.

This can be used to cause panics and crashes and unexpected behavior... or it can be used for performance.

I'm definitely going to try implementing that now, thank you!

LoganDark avatar Jun 15 '22 05:06 LoganDark

@wezm Is it possible for Font to implement Clone so that it can share some of the reference-counted data structures? Or would that break safety somehow?

LoganDark avatar Jun 23 '22 06:06 LoganDark

If the compiler allows it then there shouldn't be any safety issues.

wezm avatar Jun 23 '22 06:06 wezm

dang, DynamicFontTableProvider and its darn Box. probably should get rid of that one day

LoganDark avatar Jun 23 '22 06:06 LoganDark