diff --git a/apps/benchmarks/src/generated/package-sizes.json b/apps/benchmarks/src/generated/package-sizes.json index c2363cdb..36e55e50 100644 --- a/apps/benchmarks/src/generated/package-sizes.json +++ b/apps/benchmarks/src/generated/package-sizes.json @@ -43,11 +43,11 @@ "label": "Shaper Wasm", "status": "measured", "format": "wasm", - "sha256": "f798cae5df8aaf379b59b1d81cdd19068143611532dd1c37f4b8ec19c9fc7d11", - "rawBytes": 1106070, - "minifiedBytes": 1106070, - "gzipBytes": 428166, - "brotliBytes": 338258 + "sha256": "05a12365942118a52b1682dfeffa9659b8e13a2dafe239f3ac4e345d668f9a93", + "rawBytes": 1106204, + "minifiedBytes": 1106204, + "gzipBytes": 428282, + "brotliBytes": 338809 }, { "id": "three-runtime-js", diff --git a/docs/log.md b/docs/log.md index f1540744..0f57c273 100644 --- a/docs/log.md +++ b/docs/log.md @@ -13,6 +13,15 @@ ## 2026-08-12 +- **SIMD kernels reach their consumers (11.14, layer 4 / D-245)** — Two lab-admitted kernels graduated into + `engine/line_kernels.rs` production consumers: transition masks now drive the bidi run scan (sixteen levels per + step instead of a per-unit compare) and flag masks drive the justification space scan over the new + `CLUSTER_SPACE` flag, which also replaced per-cluster text lookups everywhere justification detects spaces. Each + kernel is one scalar function with a `cfg(simd128)` sixteen-lane prefix and a scalar tail, proven equal by + boundary-sweeping parity tests natively and exercised on the SIMD path by every layout integration test against + the shipped simd128 Wasm. Chunk-64 advance summaries stay lab-admitted with the reason recorded in D-245: exact + parity for whole-chunk width arithmetic needs a fixed-point cluster-advance representation first. + - **Justification controls (11.14, layer 3)** — Justify grows professional bounds. Word spaces expand uniformly up to the declared maximum ratio of their natural advance sum; the remaining deficit spills into inter-cluster letter gaps bounded per gap, and any residue reads as an under-full line. A declared minimum ratio makes spaces diff --git a/docs/packages/benchmarks.md b/docs/packages/benchmarks.md index e1013155..b375e9a8 100644 --- a/docs/packages/benchmarks.md +++ b/docs/packages/benchmarks.md @@ -5,7 +5,7 @@ description: Provides the shared interactive and automated benchmark product sur resource: ../../apps/benchmarks workspace_package: '@pmndrs/glyph-benchmarks' documentation_type: reference -source_digest: 'sha256:b771d35e801acf08e10cd2b7e7da0452c9ec1a1845bfa90e65b1aa295ff4675b' +source_digest: 'sha256:defe604551e49dcd3d34cdf17e31c1b448a5b4b20efd62ee42c71cfc38c7afac' tags: [package, benchmarks, react, vite, product-e2e] sources: - id: manifest diff --git a/docs/packages/glyph.md b/docs/packages/glyph.md index 850bc03e..0a9744a3 100644 --- a/docs/packages/glyph.md +++ b/docs/packages/glyph.md @@ -5,7 +5,7 @@ description: Implements portable font loading, retained Rust shaping and layout, resource: ../../packages/glyph workspace_package: '@pmndrs/glyph' documentation_type: reference -source_digest: 'sha256:af3db3822c6edecccdb336677a1067b1435035549b45712e6be4b0caabb95b1f' +source_digest: 'sha256:31d96bf7e6de7586b3fd2f7ece097c90ac759d3c992b4ff329660a403d0fcc1d' tags: [package, public-api, rust, wasm, threejs, typography] sources: - id: manifest diff --git a/docs/planning/decision-register.md b/docs/planning/decision-register.md index 08cda3ad..e0390dfc 100644 --- a/docs/planning/decision-register.md +++ b/docs/planning/decision-register.md @@ -333,7 +333,7 @@ The [architecture](architecture.md) owns loading behavior and dependency rules. | D-248 | Text decoration rendering lands as the first 11.18 slice, pulled forward for visual proof. Spans declare `decoration` (underline, overline, line-through; solid only — other line styles are rejected at the boundary rather than silently rendered solid). The engine cascade stamps the CSS decorating box: the declaring span's resolved font size rides the resolved decoration group, positioning derives one continuous line per decorating box from the baked `post`/`OS/2` metrics (top-of-stroke semantics), and adjacent runs with one decoration identity merge across nested font-size changes. Decoration records flow as a reserved resource-free `pmndrs.decoration` technique: plan programs declare a primitive kind in the former reserved wire field, planners admit resource-free rows and emit `PRIMITIVE_DECORATION` with zero-resource draws, underline/overline rows append before the paragraph's glyphs and line-through after so draw order matches CSS paint order, and Three realizes one shared flat-quad TSL material with no texture, decoding the packed sRGB paint through the sRGB EOTF so a text-colored line is byte-identical to its glyph ink at the framebuffer. Decorated sessions rebuild their gather output; the undecorated fast path is unchanged, verified by a same-window interleaved A/B against the pre-decoration checkpoint (−0.3%/+0.6%/−0.7%/+1.6% on cold/font-size/suffix-edit/splice; earlier apparent regressions reproduced on the checkpoint under ambient load). Retained decoration diffing and patterned line styles remain 11.18 work. | Accepted | | D-247 | The shaping and layout contract represents a break-inserted hyphen glyph without a source cluster, proven by an exact positioning test rather than a contract change. Any flow fragment — not only an ellipsized final line — may reference a boundary-shape record whose source span is empty and whose inserted span holds independently shaped glyphs: the inserted glyph positions after the fragment's retained clusters, carries its own stable glyph identity from the boundary arena, publishes the boundary text position as its semantic cluster (`SemanticGlyph.cluster = text_end`, the "no source cluster" representation), inherits style and paint from the neighbor cluster, participates in alignment through the composed line advance, and receives a content revision; the following line is unaffected. Nothing in the path is ellipsis-specific, so 11.14's hyphenation-adjacent work composes onto this record shape. Language patterns, break selection, and justification quality controls remain later work. | Accepted | | D-246 | Baked font artifacts carry text-decoration metrics from bake time: `metrics` gains required `underlinePosition`/`underlineThickness` from `post` and `strikeoutPosition`/`strikeoutSize` from `OS/2`, with a conservative derived fallback (thickness em/14, position −em/10) when a source font omits `post`. The canonical and packaged extension schemas stay byte-identical, the loader decodes the four values into public `FontMetrics`, and the rich-text conformance lane probes every font it loads for finite, positive-thickness values. Because the fields are required, every checked-in baked fixture, contract font, benchmark asset, and pinned artifact identity regenerated in the same change — the pre-v1 re-bake this decision exists to avoid after publication; shaping payload hashes are byte-identical throughout, so only the JSON metadata grew (for Inter, 172,144 → 172,240 bytes). Decoration rendering remains an additive renderer feature and is not part of this change. | Accepted | -| D-245 | Explicit `simd128` expansion is admitted kernel by kernel from recorded lab evidence, not speculatively. The complete kernel-lab run over real paragraph arrays ([evidence](../../packages/glyph/rust/shaper/evidence/kernel-lab-v0.json)) measures explicit break-opportunity masks at 7.6×, bidi transition masks at 4.8×, and chunk-64 advance summaries at 2.2× the auto-vectorized builds, while confirming two prior decisions: the shared scalar pack loop matches the rejected hand shuffle, and the production policy interpreter's explicit kernel is 2.9× its auto-vectorized form. No production path consumes mask or chunk lanes today, so these kernels remain lab-admitted with named consumers: break masks and chunk summaries wire into the 11.14 justification/line-planner tier, and bidi transition masks into analysis run segmentation, each behind `cfg(simd128)` with exact scalar-parity tests and end-to-end lane gates. | Accepted | +| D-245 | Explicit `simd128` expansion is admitted kernel by kernel from recorded lab evidence, not speculatively. The complete kernel-lab run over real paragraph arrays ([evidence](../../packages/glyph/rust/shaper/evidence/kernel-lab-v0.json)) measures explicit break-opportunity masks at 7.6×, bidi transition masks at 4.8×, and chunk-64 advance summaries at 2.2× the auto-vectorized builds, while confirming two prior decisions: the shared scalar pack loop matches the rejected hand shuffle, and the production policy interpreter's explicit kernel is 2.9× its auto-vectorized form. The 11.14 tier graduated two kernels into `engine/line_kernels.rs` production consumers: transition masks drive the bidi run scan and flag masks drive the justification space scan, each a scalar function with a `cfg(simd128)` sixteen-lane prefix, exact scalar-parity tests, and the shipped simd128 Wasm exercising the SIMD path through every layout integration test. Chunk-64 advance summaries remain lab-admitted: their line-planner consumer needs a fixed-point cluster-advance representation before whole-chunk arithmetic can match sequential f64 rounding bit-for-bit, and that representation is scheduled with the editorial performance pass. | Accepted | | D-244 | The Wasm optimizer pipeline for every distributed artifact is `--merge-similar-functions -Oz --merge-similar-functions -Oz`. The merge pass finds no candidates after a completed `-Oz` pipeline, but running it before each `-Oz` pass removes 8,248 raw / 4,106 gzip bytes from the shaper and 4,396 / 16,838 / 4,127 / 3,853 raw bytes from the Bitmap, font, MTSDF, and Slug bakers, with the 22k-glyph benchmark lanes unchanged within noise and byte-identical bake goldens. Explicit `#[inline(never)]` seams on the fourteen update-path stage functions measured size-neutral (+241 raw, −741 gzip) and are rejected: the large `text_update` export body is stage aggregation under single-caller inlining, not duplicated code, so outlining only adds call overhead. | Accepted | | D-243 | Engine orderings sort through one shared kernel. Each call site lowers its key into a `u64` — packed integer fields via `pack2`, or the order-preserving bit image of an `f64` under `total_cmp` — and sorts retained `(key, source index)` pairs through a single `sort_unstable` instantiation, applying the permutation by in-place cycle walking; raw `f64` buffers sort through one `u64`-slice instantiation viewed in place, and the four-field style-cascade key runs as two stable passes over the same pair kernel. The index tiebreak makes equal-key order total, deterministic, and sort-algorithm independent, strictly refining the previous unspecified `sort_unstable` equal-key order; every accepting call site either forbids equal keys, merges them order-insensitively, or collapses them. Scratch pairs are retained per owner and reserved with the session envelope. Optimized Wasm falls from 1,160,223 / 442,808 / 348,415 to 1,109,644 / 428,350 / 337,447 raw/gzip/Brotli bytes; measured sort bodies fall from 115.5 KiB in 64 functions to 79.5 KiB in 45 (45.5 KiB HarfRust-internal). All 165 unit and integration tests pass unchanged, bake goldens are byte-identical, and the eight-warmup/31-sample 22k-glyph lanes are within noise. | Accepted | | D-242 | Cargo `opt-level` stays per-crate evidence-pinned: the shaper keeps default `3`, the Bitmap/MTSDF/Slug bakers keep `3`, and the font baker keeps `z`. A four-variant shaper matrix (whole-`z` 890,381, dependency-`z` 1,025,289, HarfRust-family-`s` 1,076,427, whole-`s` 977,753 raw bytes vs the 1,160,223 baseline) regressed shaping-bound lanes beyond acceptance on the 22k-glyph eight-warmup/31-sample benchmark: whole-`z` roughly doubled all five lanes, dependency-`z` cost +33% cold / +40% suffix-edit, and HarfRust-`s` still cost +22% cold / +27% suffix-edit, because the HarfRust bytes that dominate size are the shaping hot path; warm planner lanes stayed within noise whenever the engine crate kept `3`. The inverse holds for parser-generic bakers: `z`/`s` inflate Bitmap +26/+53 KB, MTSDF +99/+123 KB, and Slug +76/+104 KB over `-O3` because `-O3` inlining enables constant-folding elimination that size levels forgo, and the font baker at `3`/`s` inflates +192/+96 KB over its current `z`. Wasm size reduction therefore proceeds through code-shape changes (shared sort kernel, update outlining seams) rather than optimizer flags; `wasm-opt -Oz` remains per the recorded Binaryen evidence. | Accepted | diff --git a/packages/glyph/rust/shaper/src/bidi.rs b/packages/glyph/rust/shaper/src/bidi.rs index 6f9b0610..0b22daa0 100644 --- a/packages/glyph/rust/shaper/src/bidi.rs +++ b/packages/glyph/rust/shaper/src/bidi.rs @@ -83,10 +83,9 @@ pub fn analyze_into( let mut start = 0usize; while start < output.levels.len() { let level = output.levels[start]; - let mut end = start + 1; - while end < output.levels.len() && output.levels[end] == level { - end += 1; - } + // Level runs end at the next transition; the D-245 transition-mask + // kernel scans sixteen levels per step on simd128 builds. + let end = crate::engine::line_kernels::next_transition(&output.levels, start); output.runs.push(BidiRun { text_start: u32::try_from(start).map_err(|_| BidiError::ResultTooLarge)?, text_end: u32::try_from(end).map_err(|_| BidiError::ResultTooLarge)?, diff --git a/packages/glyph/rust/shaper/src/engine/layout_query.rs b/packages/glyph/rust/shaper/src/engine/layout_query.rs index 30a40fae..8150e4d0 100644 --- a/packages/glyph/rust/shaper/src/engine/layout_query.rs +++ b/packages/glyph/rust/shaper/src/engine/layout_query.rs @@ -39,7 +39,6 @@ pub(crate) fn append_measurement( semantic_line_glyph_starts: &[u32], semantic_line_glyph_counts: &[u32], semantic_line_inline_extents: Option<&[f64]>, - text: &[u16], clusters: &ClusterArena, intrinsic_extents: Option, include_glyphs: bool, @@ -110,7 +109,6 @@ pub(crate) fn append_measurement( flow, line, index, - text, clusters, constraint_typography(constraint), )? @@ -218,7 +216,6 @@ pub(crate) fn append_measurement( pub(crate) fn flow_extents( flow_thread_id: u32, flow: &FlowLayoutArena, - text: &[u16], clusters: &ClusterArena, typography: ThreadTypography, ) -> Result { @@ -234,9 +231,9 @@ pub(crate) fn flow_extents( let Some(last) = fragments.last() else { continue; }; - extents.width = extents.width.max(line_inline_extent( - flow, line, index, text, clusters, typography, - )?); + extents.width = extents + .width + .max(line_inline_extent(flow, line, index, clusters, typography)?); extents.height = extents.height.max(line.block_start + line.height); extents.consumed_clusters = extents .consumed_clusters @@ -249,7 +246,6 @@ fn line_inline_extent( flow: &FlowLayoutArena, line: FlowLine, index: usize, - text: &[u16], clusters: &ClusterArena, typography: ThreadTypography, ) -> Result { @@ -278,7 +274,6 @@ fn line_inline_extent( line, fragment, final_line, - text, clusters, indent, typography.justify, @@ -389,7 +384,6 @@ mod tests { &[0], &[2], None, - &[], &ClusterArena::default(), None, false, @@ -449,7 +443,6 @@ mod tests { &[0], &[2], Some(&[7.0]), - &[], &ClusterArena::default(), None, false, @@ -485,7 +478,6 @@ mod tests { &[0], &[2], Some(&[7.0]), - &[], &ClusterArena::default(), None, true, @@ -546,7 +538,6 @@ mod tests { &[0], &[0], Some(&[7.0]), - &[], &ClusterArena::default(), Some(LayoutExtents { width: 9.0, @@ -610,7 +601,6 @@ mod tests { &[0], &[0], Some(&[140.64]), - &[], &ClusterArena::default(), None, false, diff --git a/packages/glyph/rust/shaper/src/engine/line_kernels.rs b/packages/glyph/rust/shaper/src/engine/line_kernels.rs new file mode 100644 index 00000000..c92cf390 --- /dev/null +++ b/packages/glyph/rust/shaper/src/engine/line_kernels.rs @@ -0,0 +1,138 @@ +//! Production SIMD lanes for segmentation and line planning (D-245). +//! +//! Each entry point is a scalar function with an explicit `simd128` prefix that +//! computes the identical integer result sixteen lanes at a time; the scalar +//! tail finishes every call, so native builds and Wasm builds agree exactly. +//! Consumers: the bidi run scan (`transition masks`) and the justification +//! space scan (`flag masks`). Chunk-64 advance summaries remain lab-admitted +//! until cluster advances gain a fixed-point representation (see D-245). + +/// The first index after `start` whose level differs from `levels[start]`, +/// or `levels.len()` when the run extends to the end. +pub(crate) fn next_transition(levels: &[u8], start: usize) -> usize { + let level = levels[start]; + #[allow(unused_mut)] + let mut index = start + 1; + #[cfg(all(target_arch = "wasm32", feature = "simd128"))] + { + index = next_transition_simd(levels, index, level); + } + while index < levels.len() && levels[index] == level { + index += 1; + } + index +} + +#[cfg(all(target_arch = "wasm32", feature = "simd128"))] +fn next_transition_simd(levels: &[u8], mut index: usize, level: u8) -> usize { + use core::arch::wasm32::{i8x16_bitmask, i8x16_ne, u8x16_splat, v128, v128_load}; + + let expected = u8x16_splat(level); + while index + 16 <= levels.len() { + // SAFETY: the loop bound keeps every sixteen-byte load inside `levels`. + let block = unsafe { v128_load(levels.as_ptr().add(index).cast::()) }; + let mask = i8x16_bitmask(i8x16_ne(block, expected)); + if mask != 0 { + return index + mask.trailing_zeros() as usize; + } + index += 16; + } + index +} + +/// Invoke `visit` for every index in `[start, end)` whose flags carry `flag`. +pub(crate) fn for_each_flagged( + flags: &[u8], + start: usize, + end: usize, + flag: u8, + mut visit: impl FnMut(usize), +) { + #[allow(unused_mut)] + let mut index = start; + #[cfg(all(target_arch = "wasm32", feature = "simd128"))] + { + use core::arch::wasm32::{i8x16_bitmask, i8x16_ne, u8x16_splat, v128, v128_and, v128_load}; + + let selected = u8x16_splat(flag); + let zero = u8x16_splat(0); + while index + 16 <= end { + // SAFETY: the loop bound keeps every sixteen-byte load inside `flags`. + let block = unsafe { v128_load(flags.as_ptr().add(index).cast::()) }; + let mut mask = i8x16_bitmask(i8x16_ne(v128_and(block, selected), zero)); + while mask != 0 { + let lane = mask.trailing_zeros() as usize; + visit(index + lane); + mask &= mask - 1; + } + index += 16; + } + } + while index < end { + if flags[index] & flag != 0 { + visit(index); + } + index += 1; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec::Vec; + + #[test] + fn transitions_match_the_naive_scan_across_block_boundaries() { + let mut levels = [0_u8; 40]; + for (index, level) in levels.iter_mut().enumerate() { + *level = match index { + 0..=14 => 1, + 15..=16 => 2, + 17..=32 => 1, + _ => 3, + }; + } + let naive = |start: usize| { + let mut end = start + 1; + while end < levels.len() && levels[end] == levels[start] { + end += 1; + } + end + }; + for start in 0..levels.len() { + assert_eq!( + next_transition(&levels, start), + naive(start), + "start {start}" + ); + } + let uniform = [7_u8; 33]; + assert_eq!(next_transition(&uniform, 0), 33); + } + + #[test] + fn flagged_visits_match_the_naive_filter_for_every_alignment() { + let mut flags = [0_u8; 50]; + for (index, flag) in flags.iter_mut().enumerate() { + if index % 3 == 0 { + *flag |= 0b1_0000; + } + if index % 7 == 0 { + *flag |= 0b0_1000; + } + } + for start in [0, 1, 15, 16, 17] { + for end in [start, 31, 32, 33, 50] { + if end < start { + continue; + } + let mut visited = Vec::new(); + for_each_flagged(&flags, start, end, 0b1_0000, |index| visited.push(index)); + let naive: Vec = (start..end) + .filter(|index| flags[*index] & 0b1_0000 != 0) + .collect(); + assert_eq!(visited, naive, "range {start}..{end}"); + } + } + } +} diff --git a/packages/glyph/rust/shaper/src/engine/mod.rs b/packages/glyph/rust/shaper/src/engine/mod.rs index cee1b2b1..e595393b 100644 --- a/packages/glyph/rust/shaper/src/engine/mod.rs +++ b/packages/glyph/rust/shaper/src/engine/mod.rs @@ -22,6 +22,7 @@ pub(crate) mod kernel_lab; pub(crate) mod layout_query; #[cfg_attr(not(test), allow(dead_code))] mod line_composition; +pub(crate) mod line_kernels; #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] mod state; #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] diff --git a/packages/glyph/rust/shaper/src/engine/positioning.rs b/packages/glyph/rust/shaper/src/engine/positioning.rs index bef24d4c..87a344f6 100644 --- a/packages/glyph/rust/shaper/src/engine/positioning.rs +++ b/packages/glyph/rust/shaper/src/engine/positioning.rs @@ -6,7 +6,7 @@ use crate::{FontGlyphExtents, FontMetrics, bidi::BidiAnalysis}; use super::{ EngineError, - cluster_state::{CLUSTER_HARD_BREAK, ClusterArena}, + cluster_state::{CLUSTER_HARD_BREAK, CLUSTER_SPACE, ClusterArena}, flow_composition::{FlowFragment, FlowLayoutArena, FlowLine}, frame::{ALIGN_CENTER, ALIGN_END, ALIGN_JUSTIFY, ALIGN_START}, identity_index::{IdentityIndex, IdentityIndexError}, @@ -477,7 +477,6 @@ impl PositionedGlyphArena { line, fragment, final_line, - text, clusters, cluster_start, cluster_end, @@ -629,7 +628,7 @@ impl PositionedGlyphArena { cursor += x_advance; } cursor = cluster_origin + clusters.advances[cluster]; - if justify.per_space != 0.0 && cluster_is_space(text, clusters, cluster) { + if justify.per_space != 0.0 && clusters.flags[cluster] & CLUSTER_SPACE != 0 { cursor += justify.per_space; } if justify.per_gap != 0.0 && cluster + 1 < justify.gap_end { @@ -1381,23 +1380,18 @@ struct JustifiableSpan { trimmed_end: usize, } -fn justifiable_span( - text: &[u16], - clusters: &ClusterArena, - start: usize, - mut end: usize, -) -> JustifiableSpan { - while end > start && cluster_is_space(text, clusters, end - 1) { +fn justifiable_span(clusters: &ClusterArena, start: usize, mut end: usize) -> JustifiableSpan { + while end > start && clusters.flags[end - 1] & CLUSTER_SPACE != 0 { end -= 1; } let mut spaces = 0_u32; let mut space_advance_sum = 0.0_f64; - for cluster in start..end { - if cluster_is_space(text, clusters, cluster) { - spaces = spaces.saturating_add(1); - space_advance_sum += clusters.advances[cluster]; - } - } + // The D-245 flag-mask kernel scans sixteen cluster flags per step on + // simd128 builds; the visit order matches the scalar loop exactly. + super::line_kernels::for_each_flagged(&clusters.flags, start, end, CLUSTER_SPACE, |cluster| { + spaces = spaces.saturating_add(1); + space_advance_sum += clusters.advances[cluster]; + }); JustifiableSpan { spaces, space_advance_sum, @@ -1410,7 +1404,6 @@ fn justification_adjustment( line: FlowLine, fragment: FlowFragment, final_line: bool, - text: &[u16], clusters: &ClusterArena, cluster_start: usize, cluster_end: usize, @@ -1422,7 +1415,7 @@ fn justification_adjustment( if !justified { return JustifyDistribution::default(); } - let span = justifiable_span(text, clusters, cluster_start, cluster_end); + let span = justifiable_span(clusters, cluster_start, cluster_end); if span.spaces == 0 { return JustifyDistribution::default(); } @@ -1478,7 +1471,6 @@ pub(crate) fn positioned_fragment_advance( line: FlowLine, fragment: FlowFragment, final_line: bool, - text: &[u16], clusters: &ClusterArena, indent: f64, controls: JustifyControls, @@ -1491,7 +1483,6 @@ pub(crate) fn positioned_fragment_advance( line, fragment, final_line, - text, clusters, cluster_start, cluster_end, @@ -1512,15 +1503,6 @@ fn justification_space_advance(available: f64, space_count: u32) -> f64 { } } -fn cluster_is_space(text: &[u16], clusters: &ClusterArena, cluster: usize) -> bool { - clusters - .starts - .get(cluster) - .and_then(|offset| usize::try_from(*offset).ok()) - .and_then(|offset| text.get(offset)) - == Some(&0x20) -} - fn finite_f32(value: f64) -> Result { let value = value as f32; value @@ -1607,11 +1589,14 @@ mod tests { fn justify_fixture() -> (Vec, ClusterArena, FlowLine, FlowFragment) { // "ab cd f" — seven 1.0-advance clusters with spaces at 2 and 5. let text: Vec = "ab cd f".encode_utf16().collect(); + let mut flags = vec![0_u8; 7]; + flags[2] = CLUSTER_SPACE; + flags[5] = CLUSTER_SPACE; let clusters = ClusterArena { starts: (0..7).collect(), ends: (1..=7).collect(), advances: vec![1.0; 7], - flags: vec![0; 7], + flags, style_indexes: vec![0; 7], source_runs: vec![0; 7], font_handles: vec![1; 7], @@ -1648,7 +1633,7 @@ mod tests { #[test] fn word_space_caps_spill_into_bounded_letter_expansion() { - let (text, clusters, line, fragment) = justify_fixture(); + let (_text, clusters, line, fragment) = justify_fixture(); // Deficit 10 over 2 spaces (natural sum 2.0): a 3x cap allows 4.0 of // word-space growth; 6.0 spills into six inter-cluster gaps bounded to // 0.75 each; the final 1.5 stays unfilled. @@ -1659,7 +1644,7 @@ mod tests { last_line_justify: false, }; let distribution = - justification_adjustment(line, fragment, false, &text, &clusters, 0, 7, 0.0, controls); + justification_adjustment(line, fragment, false, &clusters, 0, 7, 0.0, controls); assert_eq!(distribution.spaces, 2); assert_eq!(distribution.per_space, 2.0); assert_eq!(distribution.gaps, 6); @@ -1667,32 +1652,30 @@ mod tests { // Unbounded controls reproduce the pre-tier distribution exactly. let unbounded = JustifyControls::default(); - let plain = justification_adjustment( - line, fragment, false, &text, &clusters, 0, 7, 0.0, unbounded, - ); + let plain = + justification_adjustment(line, fragment, false, &clusters, 0, 7, 0.0, unbounded); assert_eq!(plain.per_space, 5.0); assert_eq!(plain.per_gap, 0.0); } #[test] fn last_line_policy_justifies_final_and_hard_broken_lines() { - let (text, clusters, line, fragment) = justify_fixture(); + let (_text, clusters, line, fragment) = justify_fixture(); let auto = JustifyControls::default(); - let final_auto = - justification_adjustment(line, fragment, true, &text, &clusters, 0, 7, 0.0, auto); + let final_auto = justification_adjustment(line, fragment, true, &clusters, 0, 7, 0.0, auto); assert_eq!(final_auto.per_space, 0.0); let policy = JustifyControls { last_line_justify: true, ..JustifyControls::default() }; let final_justified = - justification_adjustment(line, fragment, true, &text, &clusters, 0, 7, 0.0, policy); + justification_adjustment(line, fragment, true, &clusters, 0, 7, 0.0, policy); assert_eq!(final_justified.per_space, 5.0); } #[test] fn word_spaces_shrink_only_to_the_declared_minimum() { - let (text, clusters, line, mut fragment) = justify_fixture(); + let (_text, clusters, line, mut fragment) = justify_fixture(); // Overfull by 1.0: a 0.75 minimum permits 0.25 shrink per space (0.5 // total), so shrink clamps at -0.25 and the line stays 0.5 overfull. fragment.slot_end = 6.0; @@ -1703,7 +1686,7 @@ mod tests { last_line_justify: false, }; let shrunk = - justification_adjustment(line, fragment, false, &text, &clusters, 0, 7, 0.0, controls); + justification_adjustment(line, fragment, false, &clusters, 0, 7, 0.0, controls); assert_eq!(shrunk.per_space, -0.25); assert_eq!(shrunk.per_gap, 0.0); // Without a declared minimum an overfull line never shrinks. @@ -1711,7 +1694,6 @@ mod tests { line, fragment, false, - &text, &clusters, 0, 7, diff --git a/packages/glyph/rust/shaper/src/engine/state.rs b/packages/glyph/rust/shaper/src/engine/state.rs index 0176d8bb..689d92fa 100644 --- a/packages/glyph/rust/shaper/src/engine/state.rs +++ b/packages/glyph/rust/shaper/src/engine/state.rs @@ -770,11 +770,6 @@ impl TextEngine { .ok_or(EngineError::InvalidRequest)?; let state = &mut paragraph.state; let visible_extents = { - let text = if state.text_prepared { - &state.pending_text - } else { - &state.text - }; let clusters = if state.clusters_prepared { &state.pending_clusters } else { @@ -798,7 +793,6 @@ impl TextEngine { super::layout_query::flow_extents( flow_thread_id, flow, - text, clusters, thread_typography(geometry, flow_thread_id), )? @@ -889,7 +883,6 @@ impl TextEngine { Some(super::layout_query::flow_extents( flow_thread_id, &state.intrinsic_flow_layout_scratch, - text, clusters, thread_typography(geometry, flow_thread_id), )?) @@ -909,7 +902,6 @@ impl TextEngine { line_glyph_starts, line_glyph_counts, Some(positioned.semantic_line_inline_extents()), - text, clusters, intrinsic_extents, include_layout_inspection,