Implement parley_core (analysis, shaping, reshaping breaks, vertical text, inline objects) - #634
Implement parley_core (analysis, shaping, reshaping breaks, vertical text, inline objects)#634tomcur wants to merge 12 commits into
parley_core (analysis, shaping, reshaping breaks, vertical text, inline objects)#634Conversation
7719e14 to
f9c7ede
Compare
There was a problem hiding this comment.
process_clusters, push_cluster, to_whitespace and some helpers in this file are based on the old parley/src/layout/data.rs.
There was a problem hiding this comment.
Based on the old parley/src/shape/cache.rs.
| /// A positioned glyph. | ||
| /// | ||
| /// `advance` is along the run's main (inline) axis; `x`/`y` are offsets from the pen position. | ||
| /// | ||
| /// For horizontal text the pen advances by `advance` in `x`; for vertical text it advances in `y`. | ||
| #[derive(Copy, Clone, Default, Debug, PartialEq)] | ||
| pub struct Glyph { | ||
| /// The glyph identifier within the run's font. | ||
| pub id: u32, | ||
| /// Horizontal offset from the pen position. | ||
| pub x: f32, | ||
| /// Vertical offset from the pen position. | ||
| pub y: f32, | ||
| /// Advance along the run's main axis. | ||
| pub advance: f32, | ||
| } |
There was a problem hiding this comment.
Old parley/src/layout/glyph.rs, but dropping style.
| /// Per-run storage. | ||
| /// | ||
| /// Ranges index into the parent [`ShapedText`]'s arrays. | ||
| #[derive(Clone, Debug)] | ||
| pub(crate) struct RunData { |
There was a problem hiding this comment.
Based on parley/src/layout/data.rs's RunData.
| /// Per-cluster storage: one per grapheme cluster. | ||
| /// | ||
| /// If `glyph_len == INLINE_GLYPH`, then `glyph_offset` is a glyph identifier, otherwise, it's the | ||
| /// relative offset into the glyph array with the base taken from the owning run. | ||
| #[derive(Copy, Clone, Debug)] | ||
| pub(crate) struct ClusterData { |
There was a problem hiding this comment.
Based on parley/src/layout/data.rs's Clusterdata.
| } | ||
|
|
||
| /// Itemizer over the analyzed character stream, created by [`Analysis::itemize_with`]. | ||
| pub struct Itemizer<'a, F> { |
There was a problem hiding this comment.
Iterator to itemize based on the run splitting in parley/src/shape/mod.rs.
There was a problem hiding this comment.
Some types (Boundary, Whitespace, etc.), ported from the old parley, some new types for vertical writing modes.
| /// A sequence of clusters sharing one font, size, script, direction and writing mode. | ||
| #[derive(Clone, Copy)] | ||
| pub struct Run<'a> { | ||
| shaped: &'a ShapedText, | ||
| data: &'a RunData, | ||
| index: usize, | ||
| } |
There was a problem hiding this comment.
Based on Run in parley/src/layout/run.rs.
| /// A cluster: mapping between a span of source text and a span of glyphs. | ||
| #[derive(Clone, Copy)] | ||
| pub struct Cluster<'a> { | ||
| run: Run<'a>, | ||
| data: &'a ClusterData, | ||
| } |
There was a problem hiding this comment.
Loosely based on Cluster in parley/src/layout/cluster.rs.
I think this could and should be broken up even further. As it is, the PR mixes logic changes (though I assume the behavior stays the same) with code structure changes with file moves. Maybe it makes a ton of sense to someone more familiar with the code, but as someone with minimal parley experience I have very little idea what's going on. To give an example, you move // OLD
pub(crate) struct ShapePlanKey<'a> {
/// The font collection's blob ID.
font_blob_id: u64,
/// The font's index in the font collection.
font_index: u32,
synthesis: &'a fontique::Synthesis,
direction: harfrust::Direction,
script: harfrust::Script,
language: Option<harfrust::Language>,
features: &'a [harfrust::Feature],
variations: Option<&'a [FontVariation]>,
}
// NEW
pub(super) struct ShapePlanKey<'a> {
/// The font collection's blob ID.
font_blob_id: u64,
/// The font's index in the font collection.
font_index: u32,
coords: &'a [NormalizedCoord],
direction: Direction,
script: Script,
language: Option<&'a Language>,
features: &'a [Feature],
}The I don't know how big a change that is, but my point is I only noticed the change because I visually compared the old and new file for changes. The Github UI didn't show a diff, because it treats them as two different files. For a refactor like this, what you want to do is first implement all the logic changes and visibility changes that you will need once items are refactored away, and then do a separate PR to move the items. |
|
(I'm willing to do work on splitting up the PR, if it helps you.) |
|
@tomcur Oh boy, another big (even bigger) diff. I've skimmed it, and can see that some of that is just moving files about in ways that seem quite reasonable, but I'm going to ask up front if this could be split into smaller chunks that we can review and land separately. In particular this seems to:
And it seems to me that there is no reason why those two things need to be in the same PR. EDIT: it seems that Olivier has the same feedback:
I am in agreement with this, although you could also potentially do it the other way around: move stuff around first, and then do the logic changes (and I might actually prefer this as I think the "moving the code around" changes will be less controversial and easier to review / quicker to land). Either way, mixing actual logic changes with file moves makes the diff very difficult to review. |
The reason I suggest doing logic changes first is that sometimes logic changes (and changes in visibility, module structure, etc) are necessary before the code can be split into a separate crate. |
|
This replaces a PR that I was yet to file (in part because I was waiting to see the shape of this one). That PR would have split up the layout data into shaped and line breaking data. I'd rather rebuild what I'm doing on top of this instead. |
|
Thank you! I agree with both of you, as I also find the current state hard to review. I spent last evening and late last night (early morning...) minimizing diffs and checking things were correctly ported, and then decided to just push what I had to start the discussion. Your suggestions make sense, but not sure yet which way around is going to be most straightforward. Before doing the work to make this more reviewable (again, very happy to do this), I'd love discussion on the general shape of the API; i.e., whether the "seam" between
Thank you for offering help splitting this up, @PoignardAzur! If we decide we want this shape we can discuss, though by default it may be most efficient if I do it.
This makes sense to me, and is one of the things I was struggling with, as I did move stuff around first. (As an aside, the LLM use here was a mixed experience: it was genuinely useful to learn about some of the new concepts, esp. vertical metrics in fonts and such, it also helped experimenting, but it probably slowed down the port itself by quite a bit as I've been spending a lot of time cleaning up things like gratuitous renames and sloppily ported algorithms; had I done it manually this may have been in a better state, and been there more quickly.) |
Fair enough, but then I'd suggest marking this PR as a draft.
If you're willing to redo some work from the ground up, I might do a zero-logic-changes refactoring PR of my own which you would work on top of. The changes I'm thinking of are stuff like splitting types across files, normalizing imports, moving stuff into a I think it would be a better base for a refactor, but also you'd never be able to rebase this PR on top of mine, hence why I say you'd redo work from the ground up. |
In my experience, current agents work best when given a very tight scope, e.g. "Refactor this crate and move functions around but don't touch any logic" or "Change the algorithm in this function without touching any code outside of the file". That approach gives you more prep work and follow-up work, but it has a much lower risk of going completely off-rails. |
…l text, inline objects) This introduces a `parley_core` implementing text analysis and shaping, but not layout. Callers (like `parley`) bring their own layout engine. New functionality in `parley_core` is reshaping of text when breaking the text into fragments (e.g. for line breaking), or when joining fragments back together. That ensures correct shaping across line breaks, but also allows implementing layout algorithms like Knuth-Plass on top of `parley_core`. This implements vertical writing modes in `parley_core`, including `text-orientation`. Inline objects are supported first-class using the `u+fffc` object replacement character, which gets correct analysis, bidi, etc. around the object (`parley` still uses its out-of-band encoding (for now?)). `parley` is refactored to be on top of `parley_core` for its analysis and shaping, but it doesn't use the reshaping in this PR yet. The diff is rather large, and there is new functionality here (and a bit of experimentation to get here), but I've attempted to port some of the core things over from `parley` without too many gratuitous changes (that means this is punting on some clean up). I have some follow-ups to this if this lands, like better emoji presentation classification (choosing between color emoji or pictographs). This benches on my machines as follows. <details> <summary>Desktop</summary> ``` $ cargo bench -q --bench=main -- compare target/benchmarks/main -p -t 4. Default Style - arabic 20 characters [ 8.9 us ... 8.6 us ] -3.06%* Default Style - latin 20 characters [ 4.3 us ... 4.1 us ] -4.20%* Default Style - japanese 20 characters [ 8.1 us ... 8.3 us ] +1.97%* Default Style - arabic 1 paragraph [ 48.1 us ... 45.2 us ] -5.91%* Default Style - latin 1 paragraph [ 17.2 us ... 14.9 us ] -13.22%* Default Style - japanese 1 paragraph [ 70.0 us ... 69.6 us ] -0.62% Default Style - arabic 4 paragraph [ 213.2 us ... 198.3 us ] -7.01%* Default Style - latin 4 paragraph [ 66.1 us ... 56.3 us ] -14.78%* Default Style - japanese 4 paragraph [ 99.5 us ... 98.1 us ] -1.43%* Styled - arabic 20 characters [ 9.8 us ... 10.2 us ] +4.03%* Styled - latin 20 characters [ 5.3 us ... 5.9 us ] +10.23%* Styled - japanese 20 characters [ 8.7 us ... 9.1 us ] +5.18%* Styled - arabic 1 paragraph [ 50.3 us ... 50.6 us ] +0.53% Styled - latin 1 paragraph [ 21.4 us ... 21.9 us ] +2.77%* Styled - japanese 1 paragraph [ 76.5 us ... 78.6 us ] +2.78%* Styled - arabic 4 paragraph [ 231.4 us ... 227.3 us ] -1.75%* Styled - latin 4 paragraph [ 83.0 us ... 85.0 us ] +2.45%* Styled - japanese 4 paragraph [ 108.8 us ... 111.4 us ] +2.37%* ``` </details> <details> <summary>Laptop</summary> ``` $ cargo bench -q --bench=main -- compare target/benchmarks/main -p -t 2. Default Style - arabic 20 characters [ 20.3 us ... 20.8 us ] +2.31% Default Style - latin 20 characters [ 8.1 us ... 8.0 us ] -1.09% Default Style - japanese 20 characters [ 17.2 us ... 18.6 us ] +7.87%* Default Style - arabic 1 paragraph [ 103.0 us ... 99.0 us ] -3.93% Default Style - latin 1 paragraph [ 33.3 us ... 29.6 us ] -10.97%* Default Style - japanese 1 paragraph [ 148.1 us ... 155.9 us ] +5.32%* Default Style - arabic 4 paragraph [ 443.5 us ... 445.2 us ] +0.38% Default Style - latin 4 paragraph [ 130.0 us ... 120.1 us ] -7.66%* Default Style - japanese 4 paragraph [ 216.5 us ... 231.4 us ] +6.88%* Styled - arabic 20 characters [ 21.4 us ... 21.4 us ] -0.15% Styled - latin 20 characters [ 11.4 us ... 11.4 us ] +0.42% Styled - japanese 20 characters [ 18.4 us ... 18.7 us ] +1.21% Styled - arabic 1 paragraph [ 108.5 us ... 115.3 us ] +6.23%* Styled - latin 1 paragraph [ 41.5 us ... 41.6 us ] +0.08% Styled - japanese 1 paragraph [ 155.1 us ... 166.4 us ] +7.26%* Styled - arabic 4 paragraph [ 463.5 us ... 462.3 us ] -0.27% Styled - latin 4 paragraph [ 161.7 us ... 155.1 us ] -4.09%* Styled - japanese 4 paragraph [ 236.6 us ... 238.1 us ] +0.63% ``` </details>
We now just always clear it, appending analyses isn't really useful anyway as resolving isn't correct over separate `&str`'s.
I've skimmed the API and implementation - I like it. I can review it when I'm back from holidays next week if that helps - perhaps we can have a couple reviewers? It might be worth trying to port the proposed line breaking solution to parley (even if it's just an LLM draft) so that we can better understand how it might be integrated (and surface any points of friction). One question that I am wondering: Do we want to force consumers to use I'm not attached to the idea and it's certainly something we can do after the initial implementation. It might be worth hearing other's opinions on the matter. |
Neat! Enjoy your holidays. I'm starting a cycling/camping trip tonight until mid of next week. I'll be able to see messages every now and then. If it rains enough late next week I'll do some implementation work (we're staying at family), and will be back home June 18th.
Do you mean having (I played around a bit with splitting up the PR after Olivier's and Nico's suggestions, also implementing some of the logic inside
This sounds like a good idea to me.
Feel free to do refactorings! This current PR should not block other work. If this monolithic version is used to show this shape works and we're wanting to work towards it, I'm happy to redo work. It'll be nice to have a cleaner starting state then. I'm also happy to split stuff up when I'm back at work (perhaps less churn that way, but can't get to it for one or two weeks!) |
taj-p
left a comment
There was a problem hiding this comment.
This is very good! Kudos! My read is that it's the right shape barring the fontique dependency.
I've done a first preliminary pass. I am comfortable continuing with large reviews, but I realize that not everyone is happy to review a large PR. As I asked for another reviewer, I think I agree we should split it up (and my bad to suggest that).
I think I would suggest the following PR train:
parley_data: Simply add in the vertical orientation property. This has no cost to consumers. - should be a 5 min review.- Migrate analysis (just keep the internal fields public for now with a TODO to make them
pub(crate))- Create
parley_core/.../analyzer.rs - Migrate
parleyto use it
- Create
- Migrate itemization
- Create
parley_core/.../itemize.rs - I think we might need an adapter to feed the current shaper with item items
- Keep out vertical shaping
- Create
- Migrate shaping
- Copy in
parley_core/.../shaping/* - Avoid the reshaping after breaking methods (i.e., don't include
apply_break,apply_concat, etc)
- Add in vertical writing modes to parley core
- Add in reshaping after break functionality
- (optional) Documentation if it makes sense to one shot rather than incrementally add to
lib.rs/ README.md.
My hope with the above proposal is that each PR can migrate one unit of Parley atomically while keeping LOC change relatively small that feasibly it only requires one reviewer - given the mitigated risk.
I think that this huge PR had to happen anyway. To be able to "see into the future" of a large change hugely reduces the risk of us incrementally building towards a bad destination.
An ask for this work is to copy paste old code to new locations rather than relying on AI to do the copy through memory (or prompt the AI to use copy paste semantics via CLI or similar).
| for ch in grapheme_text.chars() { | ||
| let info = run_char_infos[*char_cursor]; | ||
| *char_cursor += 1; | ||
| self.force_normalize |= info.force_normalize(); | ||
| let contributes_to_shaping = info.contributes_to_shaping(); | ||
| if contributes_to_shaping { | ||
| self.map_len += 1; | ||
| } | ||
| self.chars.push(Char { | ||
| ch, | ||
| is_control_character: info.is_control(), | ||
| contributes_to_shaping, | ||
| }); | ||
| } |
There was a problem hiding this comment.
We've lost a lot of behaviour here from the original code. In particular, variation selector exclusion:
parley/parley/src/shape/mod.rs
Lines 243 to 271 in 5f75f53
There was a problem hiding this comment.
Have we lost emoji fallback chaining too? Is that on purpose? If it's intentionally supposed to not be in Parley Core, then we should pass it from Parley (assuming we're pushing responsibility of Query to caller via prior comments about fontique dependency)
parley/parley/src/shape/mod.rs
Lines 588 to 592 in 5f75f53
| query.matches_with(|font| { | ||
| candidates.push(font.clone()); | ||
| QueryStatus::Continue | ||
| }); |
There was a problem hiding this comment.
This iterates every font in the query. We previously stopped on finding an appropriate candidate. I think this is probably better, but it's worthwhile double checking with a benchmark (against consumers with long font chains where most of the time the first font is chosen).
| let mut charmaps: Vec<Option<Charmap<'_>>> = core::iter::repeat_with(|| None) | ||
| .take(candidates.len()) | ||
| .collect(); |
There was a problem hiding this comment.
Why not re-use this scratch space by putting it in ShapeContext?
It's possible to re-use vectors that contain items with lifetimes via something like:
/// Used to reinterpret the lifetimes of a vector.
// For how this works, see:
// https://davidlattimore.github.io/posts/2025/09/02/rustforge-wild-performance-tricks.html
fn reuse_vec<T, U>(mut v: Vec<T>) -> Vec<U> {
const {
assert!(size_of::<T>() == size_of::<U>());
assert!(align_of::<T>() == align_of::<U>());
}
v.clear();
v.into_iter().map(|_x| unreachable!()).collect()
}| let fragment_char_start = text[..text_range.start].chars().count(); | ||
| let fragment_char_count = fragment_text.chars().count(); |
There was a problem hiding this comment.
This could be called many times. I wonder whether it would be better for Analysis to provide a byte -> char index vector mapping
| if vo == VerticalOrientation::Upright || vo == VerticalOrientation::TransformedUpright { | ||
| RunOrientation::Upright | ||
| } else { | ||
| RunOrientation::Sideways |
There was a problem hiding this comment.
This means that Mixed becomes Sideways, but I think Mixed is supposed to be locale/context dependent rather than forced one way.
I think we either pass that context here so that we're correct or document the gap in a comment
| // Clusters tile the run's source characters one-to-one in logical order (a ligature is a | ||
| // start cluster plus one zero-glyph component per extra character), so the per-character | ||
| // style index advances by one cluster at a time. | ||
| for (char_index, cluster) in (char_start..).zip(run.clusters()) { |
There was a problem hiding this comment.
This is causing extra work to occur in Parley. Previously, as part of the loop, we were pushing glyphs and clusters into their final destinations but now we push that same data to scratch space and then copied into ShapedText and then re-iterated on the parley side to copy into the final destination. Thus, we pay for intermediate copies and an additional loop.
This kind of made me skeptical of the benchmark numbers showing neutral performance change. On my machine, I get the below for styled text.
My feeling is that this might be warranted because it's impacting styled text more than default (which have more runs to push and passes through harfrust) because previously the glyphs/cluster data flow was:
- Collect glyph infos in HarfRust buffer
- Iterate harfrust buffer, push each glyph / cluster into final destination
Whereas the proposal does:
- Collect glyph infos in HarfRust buffer
- Iterate harfrust buffer, push each glyph / cluster into
ShapeContext::[glyphs|clusters] - In
ShapedText::append_run, copy internal glyph / cluster buffers toShapeContext::[glyphs|clusters] - In
push_shaped_run, copyShapedTextglyph / cluster buffers intoLayoutData::[glyphs|clusters](most expensive copy because it re-walks every cluster and re-pushes every glyph instead of bulk copying).
I think there are some simple ways to avoid this:
- At step 2 / 3, push directly into
ShapedTextbuffers from harfrust instead of requiring the additional scratch space - Step 4 can be mitigated by
parleyusingparley_coretypes directly and not re-interpreting glyphs / clusters into new structs (and store any relevant "parley" only data in parallel data types). Then we either do a bulk copy or "take" the allocations fromShapedText
There was a problem hiding this comment.
This suggestion actually makes some of the code nicer, too (esp. using parley_core's Glyph in parley, see d222b7c). I have some work locally that does the same for ClusterData and parley can then extend_from_slice the cluster and glyph slices. That seems to improve the benchmark delta by a few percentage points, but...
... the benchmarking itself turned out to be a bit of a rabbit hole. I kept getting unexpected results, and at some point decided to run a paired benchmark without code changes; on current main, if I run cargo export target/benchmarks -- bench --bench=main followed by cargo bench -q --bench=main -- compare target/benchmarks/main -p -t 4., I get the following!
Default Style - arabic 20 characters [ 8.9 us ... 8.9 us ] +0.35%
Default Style - latin 20 characters [ 4.3 us ... 4.1 us ] -3.50%*
Default Style - japanese 20 characters [ 8.2 us ... 8.1 us ] -0.44%
Default Style - arabic 1 paragraph [ 48.5 us ... 48.1 us ] -0.66%
Default Style - latin 1 paragraph [ 17.2 us ... 16.1 us ] -6.52%*
Default Style - japanese 1 paragraph [ 71.3 us ... 69.6 us ] -2.50%*
Default Style - arabic 4 paragraph [ 214.5 us ... 212.7 us ] -0.86%
Default Style - latin 4 paragraph [ 66.0 us ... 62.0 us ] -5.96%*
Default Style - japanese 4 paragraph [ 100.7 us ... 98.5 us ] -2.18%*
Styled - arabic 20 characters [ 9.9 us ... 9.9 us ] +0.41%
Styled - latin 20 characters [ 5.3 us ... 5.3 us ] -1.01%*
Styled - japanese 20 characters [ 8.8 us ... 8.7 us ] -0.60%*
Styled - arabic 1 paragraph [ 51.3 us ... 51.0 us ] -0.60%
Styled - latin 1 paragraph [ 21.4 us ... 20.9 us ] -2.52%*
Styled - japanese 1 paragraph [ 78.7 us ... 77.5 us ] -1.52%*
Styled - arabic 4 paragraph [ 232.7 us ... 232.1 us ] -0.27%
Styled - latin 4 paragraph [ 83.2 us ... 80.6 us ] -3.18%*
Styled - japanese 4 paragraph [ 109.7 us ... 108.3 us ] -1.34%*
I.e., there are quite a few significant "improvements." This is without any code changes and isn't measurement noise. A possible explanation is there's a difference between the in-process code and the dlopen'd code performance that shows up at least on my machine. I don't think it's a P- vs E-core thing or something like that, as when I pin the threads to equally-powerful cores (same features, same max freq), the results stay like this.
I agree with this
LoC is only part of the concern for me. My other concern is that this PR mixes "moving code around" with "functional changes". I personally don't have the context to review the new pipeline standalone, so my review of that code would be in comparison to the old code. And that's really difficult to do when you don't have a proper diff. I would ideally like to see the existing pipeline migrated to a I would be able to submit PRs for mechanical work to split out the crate if that is helpful. |
The `style_index` is redundant as it's constant for all glyphs in a cluster.
This introduces `Analyzer` and `Analysis` as scratch, moves over `analyze_text` from Parley into Parley Core, as well some types like `CharInfo`, `Boundary`. Parley now calls into Parley Core for analysis. Analysis is unchanged, except Parley Core is given word break spans by Parley, where Parley previously reached into the style runs during analysis. Quite a bit is `pub` so Parley can reach into Core's fields. `CharInfo` has different fields in linebender#634 (the big PR where all of shaping was migrated to `parley_core`), and we may move to something like that later. For now, this should be fine. Benches neutral as expected: <details> <summary>Benchmark results</summary> ``` $ cargo export target/benchmarks -- bench --bench=main $ cargo bench -q --bench=main -- compare ../target/benchmarks/main -t 8. Default Style - arabic 20 characters [ 8.7 us ... 8.7 us ] -0.26% Default Style - latin 20 characters [ 4.1 us ... 4.2 us ] +1.69%* Default Style - japanese 20 characters [ 8.1 us ... 8.2 us ] +0.49% Default Style - arabic 1 paragraph [ 47.0 us ... 46.7 us ] -0.66% Default Style - latin 1 paragraph [ 16.0 us ... 16.5 us ] +2.78%* Default Style - japanese 1 paragraph [ 69.1 us ... 68.7 us ] -0.55% Default Style - arabic 4 paragraph [ 203.1 us ... 196.7 us ] -3.13%* Default Style - latin 4 paragraph [ 61.5 us ... 61.7 us ] +0.39% Default Style - japanese 4 paragraph [ 98.1 us ... 97.0 us ] -1.14%* Styled - arabic 20 characters [ 9.7 us ... 9.7 us ] +0.04% Styled - latin 20 characters [ 5.3 us ... 5.3 us ] +1.55%* Styled - japanese 20 characters [ 8.6 us ... 8.6 us ] -0.46% Styled - arabic 1 paragraph [ 49.3 us ... 49.0 us ] -0.76% Styled - latin 1 paragraph [ 20.4 us ... 20.6 us ] +0.93% Styled - japanese 1 paragraph [ 75.0 us ... 74.4 us ] -0.90% Styled - arabic 4 paragraph [ 220.1 us ... 215.6 us ] -2.02%* Styled - latin 4 paragraph [ 79.5 us ... 80.0 us ] +0.59% Styled - japanese 4 paragraph [ 106.2 us ... 105.3 us ] -0.85% ``` </details>
This introduces `Analyzer` and `Analysis` as scratch, moves over `analyze_text` from Parley into Parley Core, as well some types like `CharInfo`, `Boundary`. Parley now calls into Parley Core for analysis. Analysis is unchanged, except Parley Core is given word break spans by Parley, where Parley previously reached into the style runs during analysis. Quite a bit is `pub` so Parley can reach into Core's fields. `CharInfo` has different fields in linebender#634 (the big PR where all of shaping was migrated to `parley_core`), and we may move to something like that later. For now, this should be fine. Benches neutral as expected: <details> <summary>Benchmark results</summary> ``` $ cargo export target/benchmarks -- bench --bench=main $ cargo bench -q --bench=main -- compare ../target/benchmarks/main -t 8. Default Style - arabic 20 characters [ 8.7 us ... 8.7 us ] -0.26% Default Style - latin 20 characters [ 4.1 us ... 4.2 us ] +1.69%* Default Style - japanese 20 characters [ 8.1 us ... 8.2 us ] +0.49% Default Style - arabic 1 paragraph [ 47.0 us ... 46.7 us ] -0.66% Default Style - latin 1 paragraph [ 16.0 us ... 16.5 us ] +2.78%* Default Style - japanese 1 paragraph [ 69.1 us ... 68.7 us ] -0.55% Default Style - arabic 4 paragraph [ 203.1 us ... 196.7 us ] -3.13%* Default Style - latin 4 paragraph [ 61.5 us ... 61.7 us ] +0.39% Default Style - japanese 4 paragraph [ 98.1 us ... 97.0 us ] -1.14%* Styled - arabic 20 characters [ 9.7 us ... 9.7 us ] +0.04% Styled - latin 20 characters [ 5.3 us ... 5.3 us ] +1.55%* Styled - japanese 20 characters [ 8.6 us ... 8.6 us ] -0.46% Styled - arabic 1 paragraph [ 49.3 us ... 49.0 us ] -0.76% Styled - latin 1 paragraph [ 20.4 us ... 20.6 us ] +0.93% Styled - japanese 1 paragraph [ 75.0 us ... 74.4 us ] -0.90% Styled - arabic 4 paragraph [ 220.1 us ... 215.6 us ] -2.02%* Styled - latin 4 paragraph [ 79.5 us ... 80.0 us ] +0.59% Styled - japanese 4 paragraph [ 106.2 us ... 105.3 us ] -0.85% ``` </details>
This introduces `Analyzer` and `Analysis` as scratch, moves over `analyze_text` from Parley into Parley Core, as well some types like `CharInfo`, `Boundary`. Parley now calls into Parley Core for analysis. Analysis is unchanged, except Parley Core is given word break spans by Parley, where Parley previously reached into the style runs during analysis. Quite a bit is `pub` so Parley can reach into Core's fields. `CharInfo` has different fields in linebender#634 (the big PR where all of shaping was migrated to `parley_core`), and we may move to something like that later. For now, this should be fine. Benches neutral as expected: <details> <summary>Benchmark results</summary> ``` $ cargo export target/benchmarks -- bench --bench=main $ cargo bench -q --bench=main -- compare ../target/benchmarks/main -t 8. Default Style - arabic 20 characters [ 8.7 us ... 8.7 us ] -0.26% Default Style - latin 20 characters [ 4.1 us ... 4.2 us ] +1.69%* Default Style - japanese 20 characters [ 8.1 us ... 8.2 us ] +0.49% Default Style - arabic 1 paragraph [ 47.0 us ... 46.7 us ] -0.66% Default Style - latin 1 paragraph [ 16.0 us ... 16.5 us ] +2.78%* Default Style - japanese 1 paragraph [ 69.1 us ... 68.7 us ] -0.55% Default Style - arabic 4 paragraph [ 203.1 us ... 196.7 us ] -3.13%* Default Style - latin 4 paragraph [ 61.5 us ... 61.7 us ] +0.39% Default Style - japanese 4 paragraph [ 98.1 us ... 97.0 us ] -1.14%* Styled - arabic 20 characters [ 9.7 us ... 9.7 us ] +0.04% Styled - latin 20 characters [ 5.3 us ... 5.3 us ] +1.55%* Styled - japanese 20 characters [ 8.6 us ... 8.6 us ] -0.46% Styled - arabic 1 paragraph [ 49.3 us ... 49.0 us ] -0.76% Styled - latin 1 paragraph [ 20.4 us ... 20.6 us ] +0.93% Styled - japanese 1 paragraph [ 75.0 us ... 74.4 us ] -0.90% Styled - arabic 4 paragraph [ 220.1 us ... 215.6 us ] -2.02%* Styled - latin 4 paragraph [ 79.5 us ... 80.0 us ] +0.59% Styled - japanese 4 paragraph [ 106.2 us ... 105.3 us ] -0.85% ``` </details>
This introduces `Analyzer` and `Analysis` as scratch, moves over `analyze_text` from Parley into Parley Core, as well some types like `CharInfo`, `Boundary`. Parley now calls into Parley Core for analysis. Analysis is unchanged, except Parley Core is given word break spans by Parley, where Parley previously reached into the style runs during analysis. Quite a bit is `pub` so Parley can reach into Core's fields. `CharInfo` has different fields in linebender#634 (the big PR where all of shaping was migrated to `parley_core`), and we may move to something like that later. For now, this should be fine. Benches neutral as expected: <details> <summary>Benchmark results</summary> ``` $ cargo export target/benchmarks -- bench --bench=main $ cargo bench -q --bench=main -- compare ../target/benchmarks/main -t 8. Default Style - arabic 20 characters [ 8.7 us ... 8.7 us ] -0.26% Default Style - latin 20 characters [ 4.1 us ... 4.2 us ] +1.69%* Default Style - japanese 20 characters [ 8.1 us ... 8.2 us ] +0.49% Default Style - arabic 1 paragraph [ 47.0 us ... 46.7 us ] -0.66% Default Style - latin 1 paragraph [ 16.0 us ... 16.5 us ] +2.78%* Default Style - japanese 1 paragraph [ 69.1 us ... 68.7 us ] -0.55% Default Style - arabic 4 paragraph [ 203.1 us ... 196.7 us ] -3.13%* Default Style - latin 4 paragraph [ 61.5 us ... 61.7 us ] +0.39% Default Style - japanese 4 paragraph [ 98.1 us ... 97.0 us ] -1.14%* Styled - arabic 20 characters [ 9.7 us ... 9.7 us ] +0.04% Styled - latin 20 characters [ 5.3 us ... 5.3 us ] +1.55%* Styled - japanese 20 characters [ 8.6 us ... 8.6 us ] -0.46% Styled - arabic 1 paragraph [ 49.3 us ... 49.0 us ] -0.76% Styled - latin 1 paragraph [ 20.4 us ... 20.6 us ] +0.93% Styled - japanese 1 paragraph [ 75.0 us ... 74.4 us ] -0.90% Styled - arabic 4 paragraph [ 220.1 us ... 215.6 us ] -2.02%* Styled - latin 4 paragraph [ 79.5 us ... 80.0 us ] +0.59% Styled - japanese 4 paragraph [ 106.2 us ... 105.3 us ] -0.85% ``` </details>
This introduces `Analyzer` and `Analysis` as scratch, moves over `analyze_text` from Parley into Parley Core, as well some types like `CharInfo`, `Boundary`. Parley now calls into Parley Core for analysis. Analysis is unchanged, except Parley Core is given word break spans by Parley, where Parley previously reached into the style runs during analysis. Quite a bit is `pub` so Parley can reach into Core's fields. `CharInfo` has different fields in linebender#634 (the big PR where all of shaping was migrated to `parley_core`), and we may move to something like that later. For now, this should be fine. Benches neutral as expected: <details> <summary>Benchmark results</summary> ``` $ cargo export target/benchmarks -- bench --bench=main $ cargo bench -q --bench=main -- compare ../target/benchmarks/main -t 8. Default Style - arabic 20 characters [ 8.7 us ... 8.7 us ] -0.26% Default Style - latin 20 characters [ 4.1 us ... 4.2 us ] +1.69%* Default Style - japanese 20 characters [ 8.1 us ... 8.2 us ] +0.49% Default Style - arabic 1 paragraph [ 47.0 us ... 46.7 us ] -0.66% Default Style - latin 1 paragraph [ 16.0 us ... 16.5 us ] +2.78%* Default Style - japanese 1 paragraph [ 69.1 us ... 68.7 us ] -0.55% Default Style - arabic 4 paragraph [ 203.1 us ... 196.7 us ] -3.13%* Default Style - latin 4 paragraph [ 61.5 us ... 61.7 us ] +0.39% Default Style - japanese 4 paragraph [ 98.1 us ... 97.0 us ] -1.14%* Styled - arabic 20 characters [ 9.7 us ... 9.7 us ] +0.04% Styled - latin 20 characters [ 5.3 us ... 5.3 us ] +1.55%* Styled - japanese 20 characters [ 8.6 us ... 8.6 us ] -0.46% Styled - arabic 1 paragraph [ 49.3 us ... 49.0 us ] -0.76% Styled - latin 1 paragraph [ 20.4 us ... 20.6 us ] +0.93% Styled - japanese 1 paragraph [ 75.0 us ... 74.4 us ] -0.90% Styled - arabic 4 paragraph [ 220.1 us ... 215.6 us ] -2.02%* Styled - latin 4 paragraph [ 79.5 us ... 80.0 us ] +0.59% Styled - japanese 4 paragraph [ 106.2 us ... 105.3 us ] -0.85% ``` </details>
|
I've continued work locally to address some of the feedback, but that'd trigger some new discussions (things like how we encode graphemes), so I think it's best to tackle those things as we do the landable sequence. I've started off the migration based on the suggestions. Do we want to land these PRs as we go, or do we perhaps want to review-but-leave-open until we have a full stack of PRs that we can land such that the repo is immediately in a cleanly releasable state? The latter would sort of block some types of work while the bulk of the migration is happening (which will likely take at least a few days, and probably more). The alternative is to potentially have to release alpha versions of |
|
@tomcur What do you think of the idea of first moving things to a (the idea being that this would allow us to keep |
Hmm, that's possible and probably better than keeping a stack of PRs open. I suppose the main downside is it's slightly easier to miss visibility issues. If we're OK with potentially releasing 0.0.x or even alpha version of (Because of time zones and wanting to run CI, I've pushed the next two PRs that I had ready. Happy to change them if we decide to go with a |
This introduces `Analyzer` and `Analysis` as scratch, moves over `analyze_text` from Parley into Parley Core, as well some types like `CharInfo`, `Boundary`. Parley now calls into Parley Core for analysis. Analysis is unchanged, except Parley Core is given word break spans by Parley, where Parley previously reached into the style runs during analysis. Quite a bit is `pub` so Parley can reach into Core's fields. `CharInfo` has different fields in linebender#634 (the big PR where all of shaping was migrated to `parley_core`), and we may move to something like that later. For now, this should be fine. Benches neutral as expected: <details> <summary>Benchmark results</summary> ``` $ cargo export target/benchmarks -- bench --bench=main $ cargo bench -q --bench=main -- compare ../target/benchmarks/main -t 8. Default Style - arabic 20 characters [ 8.7 us ... 8.7 us ] -0.26% Default Style - latin 20 characters [ 4.1 us ... 4.2 us ] +1.69%* Default Style - japanese 20 characters [ 8.1 us ... 8.2 us ] +0.49% Default Style - arabic 1 paragraph [ 47.0 us ... 46.7 us ] -0.66% Default Style - latin 1 paragraph [ 16.0 us ... 16.5 us ] +2.78%* Default Style - japanese 1 paragraph [ 69.1 us ... 68.7 us ] -0.55% Default Style - arabic 4 paragraph [ 203.1 us ... 196.7 us ] -3.13%* Default Style - latin 4 paragraph [ 61.5 us ... 61.7 us ] +0.39% Default Style - japanese 4 paragraph [ 98.1 us ... 97.0 us ] -1.14%* Styled - arabic 20 characters [ 9.7 us ... 9.7 us ] +0.04% Styled - latin 20 characters [ 5.3 us ... 5.3 us ] +1.55%* Styled - japanese 20 characters [ 8.6 us ... 8.6 us ] -0.46% Styled - arabic 1 paragraph [ 49.3 us ... 49.0 us ] -0.76% Styled - latin 1 paragraph [ 20.4 us ... 20.6 us ] +0.93% Styled - japanese 1 paragraph [ 75.0 us ... 74.4 us ] -0.90% Styled - arabic 4 paragraph [ 220.1 us ... 215.6 us ] -2.02%* Styled - latin 4 paragraph [ 79.5 us ... 80.0 us ] +0.59% Styled - japanese 4 paragraph [ 106.2 us ... 105.3 us ] -0.85% ``` </details>
This introduces `Analyzer` and `Analysis` as scratch, moves over `analyze_text` from Parley into Parley Core, as well some types like `CharInfo`, `Boundary`. Parley now calls into Parley Core for analysis. Analysis is unchanged, except Parley Core is given word break spans by Parley, where Parley previously reached into the style runs during analysis. Quite a bit is `pub` so Parley can reach into Core's fields. `CharInfo` has different fields in linebender#634 (the big PR where all of shaping was migrated to `parley_core`), and we may move to something like that later. For now, this should be fine. Benches neutral as expected: <details> <summary>Benchmark results</summary> ``` $ cargo export target/benchmarks -- bench --bench=main $ cargo bench -q --bench=main -- compare ../target/benchmarks/main -t 8. Default Style - arabic 20 characters [ 8.7 us ... 8.7 us ] -0.26% Default Style - latin 20 characters [ 4.1 us ... 4.2 us ] +1.69%* Default Style - japanese 20 characters [ 8.1 us ... 8.2 us ] +0.49% Default Style - arabic 1 paragraph [ 47.0 us ... 46.7 us ] -0.66% Default Style - latin 1 paragraph [ 16.0 us ... 16.5 us ] +2.78%* Default Style - japanese 1 paragraph [ 69.1 us ... 68.7 us ] -0.55% Default Style - arabic 4 paragraph [ 203.1 us ... 196.7 us ] -3.13%* Default Style - latin 4 paragraph [ 61.5 us ... 61.7 us ] +0.39% Default Style - japanese 4 paragraph [ 98.1 us ... 97.0 us ] -1.14%* Styled - arabic 20 characters [ 9.7 us ... 9.7 us ] +0.04% Styled - latin 20 characters [ 5.3 us ... 5.3 us ] +1.55%* Styled - japanese 20 characters [ 8.6 us ... 8.6 us ] -0.46% Styled - arabic 1 paragraph [ 49.3 us ... 49.0 us ] -0.76% Styled - latin 1 paragraph [ 20.4 us ... 20.6 us ] +0.93% Styled - japanese 1 paragraph [ 75.0 us ... 74.4 us ] -0.90% Styled - arabic 4 paragraph [ 220.1 us ... 215.6 us ] -2.02%* Styled - latin 4 paragraph [ 79.5 us ... 80.0 us ] +0.59% Styled - japanese 4 paragraph [ 106.2 us ... 105.3 us ] -0.85% ``` </details>
I think the migration needs to be land-able. Keeping PRs open invites churn because they can conflict with other changes. My preference is to merge each PR as its approved and not keep the whole train open until everything's ready.
There are quite a few
The purpose of I think we also want to encourage development in As I understand versioning, this means that whenever we bump i.e.: The next version of ^^^: All this said, if we find that we can't arrive at a solution where |
|
We can technically release whenever so long as we're happy to release |
|
Quick response to part of what you said before I turn in for the night.
I believe that's right in the likely event we expose |
I'm in agreement with this. If it ever gets to a point where |
|
Daniel suggested to move the discussion to Zulip (#647 (review)). Wrote here: #parley > parley_core PR sprint? @ 💬.. |
`tango` recently switched to running the paired comparison benchmarks in separate processes (bazhenov/tango#78). This is fortunate, as it turns out to also fix an issue I encountered in linebender#634 (comment). On current `main`, if I run `cargo export target/benchmarks -- bench --bench=main` followed by `cargo bench -q --bench=main -- compare target/benchmarks/main -p -t 4.`, I get the following! ``` Default Style - arabic 20 characters [ 8.9 us ... 8.9 us ] +0.35% Default Style - latin 20 characters [ 4.3 us ... 4.1 us ] -3.50%* Default Style - japanese 20 characters [ 8.2 us ... 8.1 us ] -0.44% Default Style - arabic 1 paragraph [ 48.5 us ... 48.1 us ] -0.66% Default Style - latin 1 paragraph [ 17.2 us ... 16.1 us ] -6.52%* Default Style - japanese 1 paragraph [ 71.3 us ... 69.6 us ] -2.50%* Default Style - arabic 4 paragraph [ 214.5 us ... 212.7 us ] -0.86% Default Style - latin 4 paragraph [ 66.0 us ... 62.0 us ] -5.96%* Default Style - japanese 4 paragraph [ 100.7 us ... 98.5 us ] -2.18%* Styled - arabic 20 characters [ 9.9 us ... 9.9 us ] +0.41% Styled - latin 20 characters [ 5.3 us ... 5.3 us ] -1.01%* Styled - japanese 20 characters [ 8.8 us ... 8.7 us ] -0.60%* Styled - arabic 1 paragraph [ 51.3 us ... 51.0 us ] -0.60% Styled - latin 1 paragraph [ 21.4 us ... 20.9 us ] -2.52%* Styled - japanese 1 paragraph [ 78.7 us ... 77.5 us ] -1.52%* Styled - arabic 4 paragraph [ 232.7 us ... 232.1 us ] -0.27% Styled - latin 4 paragraph [ 83.2 us ... 80.6 us ] -3.18%* Styled - japanese 4 paragraph [ 109.7 us ... 108.3 us ] -1.34%* ``` I.e., there are quite a few significant "improvements." This is without any code changes and isn't measurement noise. A possible explanation is there's a difference between the in-process code and the `dlopen`'d code performance that shows up at least on my machine. I don't think it's a P- vs E-core thing or something like that, as when I pin the threads to equally-powerful cores (same features, same max freq), the results stay like this. Measured using the git revision of `tango`, the results are more reasonable. ``` $ cargo export target/benchmarks -- bench --bench=main $ cargo bench -q --bench=main -- compare ../target/benchmarks/main -t 4. Default Style - arabic 20 characters [ 8.7 us ... 8.7 us ] -0.04% Default Style - latin 20 characters [ 4.1 us ... 4.1 us ] +1.76%* Default Style - japanese 20 characters [ 7.9 us ... 8.0 us ] +0.24% Default Style - arabic 1 paragraph [ 46.7 us ... 46.9 us ] +0.47% Default Style - latin 1 paragraph [ 16.0 us ... 16.0 us ] -0.08% Default Style - japanese 1 paragraph [ 68.4 us ... 68.3 us ] -0.10% Default Style - arabic 4 paragraph [ 196.5 us ... 196.9 us ] +0.23% Default Style - latin 4 paragraph [ 61.1 us ... 61.1 us ] +0.10% Default Style - japanese 4 paragraph [ 96.7 us ... 96.4 us ] -0.32% Styled - arabic 20 characters [ 9.6 us ... 9.6 us ] +0.19% Styled - latin 20 characters [ 5.2 us ... 5.3 us ] +0.32% Styled - japanese 20 characters [ 8.5 us ... 8.4 us ] -0.10% Styled - arabic 1 paragraph [ 49.1 us ... 49.0 us ] -0.08% Styled - latin 1 paragraph [ 20.4 us ... 20.4 us ] -0.05% Styled - japanese 1 paragraph [ 74.2 us ... 74.4 us ] +0.22% Styled - arabic 4 paragraph [ 216.0 us ... 215.8 us ] -0.10% Styled - latin 4 paragraph [ 79.1 us ... 79.2 us ] +0.19% Styled - japanese 4 paragraph [ 105.2 us ... 105.4 us ] +0.17% ```
Kicking off the sequence to end up at (roughly) the shape of linebender#634. Instead of starting with `parley_data`, I figured to first move the bidi algorithm as it wires up the `parley` and `parley_core` crates plus handles some lints that `parley` suppressed and `parley_core` doesn't. Two more PRs are ready: first, simply moving the line break opportunity overrides introduced in linebender#640 (similarly moving files and handling lints), second and more interestingly, migrating `parley`'s analysis without any real changes into `parley_core` and introducing `parley_core::{Analysis, Analyzer}`.
This introduces `Analyzer` and `Analysis` as scratch, moves over `analyze_text` from Parley into Parley Core, as well some types like `CharInfo`, `Boundary`. Parley now calls into Parley Core for analysis. Analysis is unchanged, except Parley Core is given word break spans by Parley, where Parley previously reached into the style runs during analysis. Quite a bit is `pub` so Parley can reach into Core's fields. `CharInfo` has different fields in linebender#634 (the big PR where all of shaping was migrated to `parley_core`), and we may move to something like that later. For now, this should be fine. Benches neutral as expected: <details> <summary>Benchmark results</summary> ``` $ cargo export target/benchmarks -- bench --bench=main $ cargo bench -q --bench=main -- compare ../target/benchmarks/main -t 8. Default Style - arabic 20 characters [ 8.7 us ... 8.7 us ] -0.26% Default Style - latin 20 characters [ 4.1 us ... 4.2 us ] +1.69%* Default Style - japanese 20 characters [ 8.1 us ... 8.2 us ] +0.49% Default Style - arabic 1 paragraph [ 47.0 us ... 46.7 us ] -0.66% Default Style - latin 1 paragraph [ 16.0 us ... 16.5 us ] +2.78%* Default Style - japanese 1 paragraph [ 69.1 us ... 68.7 us ] -0.55% Default Style - arabic 4 paragraph [ 203.1 us ... 196.7 us ] -3.13%* Default Style - latin 4 paragraph [ 61.5 us ... 61.7 us ] +0.39% Default Style - japanese 4 paragraph [ 98.1 us ... 97.0 us ] -1.14%* Styled - arabic 20 characters [ 9.7 us ... 9.7 us ] +0.04% Styled - latin 20 characters [ 5.3 us ... 5.3 us ] +1.55%* Styled - japanese 20 characters [ 8.6 us ... 8.6 us ] -0.46% Styled - arabic 1 paragraph [ 49.3 us ... 49.0 us ] -0.76% Styled - latin 1 paragraph [ 20.4 us ... 20.6 us ] +0.93% Styled - japanese 1 paragraph [ 75.0 us ... 74.4 us ] -0.90% Styled - arabic 4 paragraph [ 220.1 us ... 215.6 us ] -2.02%* Styled - latin 4 paragraph [ 79.5 us ... 80.0 us ] +0.59% Styled - japanese 4 paragraph [ 106.2 us ... 105.3 us ] -0.85% ``` </details>
This introduces `Analyzer` and `Analysis` as scratch, moves over `analyze_text` from Parley into Parley Core, as well some types like `CharInfo`, `Boundary`. Parley now calls into Parley Core for analysis. Analysis is unchanged, except Parley Core is given word break spans by Parley, where Parley previously reached into the style runs during analysis. Quite a bit is `pub` so Parley can reach into Core's fields. `CharInfo` has different fields in linebender#634 (the big PR where all of shaping was migrated to `parley_core`), and we may move to something like that later. For now, this should be fine. Benches neutral as expected: <details> <summary>Benchmark results</summary> ``` $ cargo export target/benchmarks -- bench --bench=main $ cargo bench -q --bench=main -- compare ../target/benchmarks/main -t 8. Default Style - arabic 20 characters [ 8.7 us ... 8.7 us ] -0.26% Default Style - latin 20 characters [ 4.1 us ... 4.2 us ] +1.69%* Default Style - japanese 20 characters [ 8.1 us ... 8.2 us ] +0.49% Default Style - arabic 1 paragraph [ 47.0 us ... 46.7 us ] -0.66% Default Style - latin 1 paragraph [ 16.0 us ... 16.5 us ] +2.78%* Default Style - japanese 1 paragraph [ 69.1 us ... 68.7 us ] -0.55% Default Style - arabic 4 paragraph [ 203.1 us ... 196.7 us ] -3.13%* Default Style - latin 4 paragraph [ 61.5 us ... 61.7 us ] +0.39% Default Style - japanese 4 paragraph [ 98.1 us ... 97.0 us ] -1.14%* Styled - arabic 20 characters [ 9.7 us ... 9.7 us ] +0.04% Styled - latin 20 characters [ 5.3 us ... 5.3 us ] +1.55%* Styled - japanese 20 characters [ 8.6 us ... 8.6 us ] -0.46% Styled - arabic 1 paragraph [ 49.3 us ... 49.0 us ] -0.76% Styled - latin 1 paragraph [ 20.4 us ... 20.6 us ] +0.93% Styled - japanese 1 paragraph [ 75.0 us ... 74.4 us ] -0.90% Styled - arabic 4 paragraph [ 220.1 us ... 215.6 us ] -2.02%* Styled - latin 4 paragraph [ 79.5 us ... 80.0 us ] +0.59% Styled - japanese 4 paragraph [ 106.2 us ... 105.3 us ] -0.85% ``` </details>
This implements shaping in `parley_core`, taking the result of the new itemization that just landed, some additional shaping options, plus an API that keeps specifics of font selection up to the caller. Taking this form, we should be able to drop the `fontique` dependency from `parley_core`, but I've not done so here as it inflates the diff and would benefit from separate discussion. The next step after landing this would be the introduction of a `ShapedText`. I've left some notes in `parley_core/src/shaped_text.rs` about what that may look like. A `ShapedText` is going to be useful for reshaping; I have something in mind that I think is better than what I proposed in linebender#634, but more on that later/elsewhere! The `ShaperContext::shape_item` method is somewhat callback-heavy. I wonder if we should start introducing traits instead. (Though the `|shaped_run|` callback is going to become a `&mut ShapedText` soon enough I suppose.) <details> <summary>This benches neutral to ~+1.5% on my machine.</summary> ``` $ cargo bench -q --bench=main -- compare ../target/benchmarks/main -t 8. Default Style - arabic 20 characters [ 8.7 us ... 8.9 us ] +1.50%* Default Style - latin 20 characters [ 4.2 us ... 4.2 us ] +0.29% Default Style - japanese 20 characters [ 8.1 us ... 8.1 us ] +0.01% Default Style - arabic 1 paragraph [ 47.6 us ... 47.9 us ] +0.53% Default Style - latin 1 paragraph [ 16.2 us ... 16.5 us ] +1.37%* Default Style - japanese 1 paragraph [ 69.4 us ... 69.0 us ] -0.50% Default Style - arabic 4 paragraph [ 203.4 us ... 205.8 us ] +1.16%* Default Style - latin 4 paragraph [ 61.7 us ... 62.2 us ] +0.87% Default Style - japanese 4 paragraph [ 97.9 us ... 97.7 us ] -0.25% Styled - arabic 20 characters [ 9.7 us ... 9.9 us ] +1.28%* Styled - latin 20 characters [ 5.3 us ... 5.4 us ] +0.49% Styled - japanese 20 characters [ 8.6 us ... 8.6 us ] -0.73% Styled - arabic 1 paragraph [ 50.2 us ... 50.5 us ] +0.68% Styled - latin 1 paragraph [ 20.9 us ... 20.9 us ] +0.09% Styled - japanese 1 paragraph [ 75.0 us ... 75.1 us ] +0.11% Styled - arabic 4 paragraph [ 219.9 us ... 223.9 us ] +1.80%* Styled - latin 4 paragraph [ 81.1 us ... 81.5 us ] +0.50% Styled - japanese 4 paragraph [ 106.7 us ... 106.3 us ] -0.31% ``` </details> Slight changes are getting compilation and the benches to fall somewhat differently, e.g., taking a trait for font selection seems to have a very slight positive effect, but it seems mostly up to the whims of the compiler. (As a performance aside, the whole font fallback code is quite hot as we're calling it for each character, and we could optimize, e.g., under the assumption that the preferred font is likely to match.) Small review guide: `parley`'s behavior should be unchanged, and I copied the shaping code verbatim. The first commit just moves blocks around (it compiles and passes tests). The second commit moves some blocks around, with some changes to get stuff to compile, but does not wire up `parley` to use the shaping that's been moved out. The third commit wires everything up and passes tests, and the commits after that are clean up, more docs, etc.

The
parley_coreproposed here provides primitives for shaping text, including reshaping across breaks and undoing breaks. In this form, everything relevant to shaping should be inparley_core(including features like vertical writing modes), but it does not provide layout (stacking of lines, wrapping around floated boxes, alignment, etc.).Having correct shaping and efficient reshaping across breaks probably allows most layout engines to be built on top, and as such that may be the correct seam for
parley_core(and experimenting with it is what led to this PR). Whether that conceptually really is the correct seam forparley_coreis an important question!Overview
The main new functionality here is reshaping, along with vertical writing modes and improved handling of inline objects. (This PR probably needs to be split up into multiple.)
The current API looks like the following (and I'm not too attached to the exact form!).
The pipeline in this PR is such that later stages can be redone without redoing earlier stages, though I'm also not too attached at exactly where those seams are. First you use the
Analyzerto create anAnalysis. This does bidi resolution, finds segmentation boundaries, and records the per-character Unicode properties.Next, you itemize using
Analysis::itemize_withinto individually shapeable runs of constant bidi, script, language, orientation. Callers likeparleythat style text provide their own additional predicate for splitting (e.g., if the font size changes).These itemized runs are shaped using a
ShapeContextand appended to aShapedText. The shaped runs can be rendered.If you want to apply a break, you use
ShapeContext::apply_break. This reshapes around the break, severing cursive joins/ligatures. Undo a break usingShapeContext::apply_concat. Fancy line breaking algorithms (something like Knuth-Plass) can apply the break, remeasure the runs after reshaping, and backtrack if needed.ShapedTextallows getting/iterating the shaped runs that have the information needed by the renderer (e.g., glyphs, advances, whether the glyph needs to be rotated for vertical text, etc.).This operates on a
&str, and the unit of correct analysis and shaping is effectively an entire paragraph. If a paragraph is huge, the allocations required are correspondingly huge. I'm not sure whether this really matters in practice (the user can decide to cut into parts and forego strictly correct analysis/shaping). It might be possible to expose something like the "pre- and post-context" like whatharfrusthas, which I believe would at least give correct contextual substitutions across seams, but not ligatures (and also not correct bidi resolving).Some notes
This PR implements vertical writing modes in
parley_core, includingtext-orientation.Inline objects are supported first-class using the
u+fffcobject replacement character, which gets correct analysis, bidi, etc. around the object (parleystill uses its out-of-band encoding (for now?)).parleyis refactored to be on top ofparley_corefor its analysis and shaping. It doesn't use the reshaping in this PR yet.parley_coredoes not do things like text transforms, whitespace collapsing, etc., the idea being that for clean separation of these kinds of concerns, the layer above, likeparley, has aStringthat it builds the transformed text into (including control characters likeu+fffcfor inline objects and bidi overrides).The diff is rather large, and there is new functionality here (and a bit of experimentation to get here), but I've attempted to port some of the core things over from
parleywithout too many gratuitous changes. That also means this is punting on some clean up, but I think the core that's been brought over is mostly in an OK state.I have some follow-ups to this if we land this, like better emoji presentation classification, which correctly chooses between color emoji or pictographs.
This benches on my machines as follows.
Desktop
Laptop
Disclosure: done with some LLM assistance, especially porting. I've scrutinized this and iterated on it quite a bit, but if we want to land this general shape, I'll scrutinize it some more.