Three root causes, all fixed and verified on the Pixel 9 Pro: 1. Present phase (backend + renderer): VSYNC_LOCKED_RENDER defers the render to the vsync wake; RENDER_AHEAD presents the previous frame at the start of render() (phase = wake + ~1.1 ms) and holds the new frame one slot. Crash fix: a held wgpu frame must be PRESENTED, never merely dropped, before the next acquire (AlreadyAcquired panic at launch otherwise); discard_pending_frame presents too. 2. Full-res prefetch churn (src/lib.rs): the 8-slot full cache cycled through a ~20-cell viewport forever (137 decodes/20 s) and each eviction free()'d a 50 MB Vec on the UI thread (20-52 ms CPU spikes, ~7/s = the residual SLOWEV NonMain jank). Fix: 60 s tombstone cooldown on evicted paths (idle prefetch skips, urgent tap bypasses) + off-thread reaper for the freed blobs (insert: 21 ms -> 4 us). 3. (earlier) Skia final flush + Mailbox present mode (vendor, M3). Instrumentation (all behind --features perf): OTHR/INVK/INVK_TAG/FLCK lines for cross-thread closure timing and push-site tagging. Warm measured swipe, 8580-file grid: cadence median 8.33 ms (exact 120 Hz), p99 15.5 ms, 5/552 gaps >16.7 ms (0.9%, max 28.3 ms, zero >33 ms); PRES phase median ~994 us, zero late frames. Docs updated (docs/BUGS/scroll-jank-120hz.md, full scorecard + line reference). Also: vendored i-slint-core + wgpu-hal (vkshim interposition patch), vkshim profiling tooling, backend made a non-optional dep, debuggable flag for simpleperf (REMOVE before shipping).
196 lines
9.0 KiB
Rust
196 lines
9.0 KiB
Rust
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
|
|
|
// cSpell: ignore bidi
|
|
|
|
//! Selection geometry, resolved once per draw into per-line horizontal spans.
|
|
//!
|
|
//! The same rounded span edges fill the highlight and clip the glyph runs, so the two can't
|
|
//! disagree -- see [`SelectionSpan`].
|
|
|
|
use super::*;
|
|
|
|
/// One contiguous run of selected text on a single line.
|
|
///
|
|
/// Selection is deliberately *not* expressed as a text style. A style can only ever recolor a
|
|
/// whole glyph, but a selection boundary may fall in the middle of one: with an `fi` ligature,
|
|
/// selecting just the `i` leaves parley with a single glyph whose style comes from the cluster's
|
|
/// first character (`Glyph::style_index` is `char_infos[cluster_id]`), so the whole ligature would
|
|
/// be painted unselected while the highlight covers only its right half. Instead the spans below
|
|
/// are used twice — to fill the highlight background, and to clip the glyph runs that straddle a
|
|
/// boundary so each half is drawn in its own color.
|
|
#[derive(Clone, Debug)]
|
|
pub(super) struct SelectionSpan {
|
|
/// Index into `Layout::paragraphs`.
|
|
pub(super) paragraph: usize,
|
|
/// Line within that paragraph.
|
|
pub(super) line: usize,
|
|
/// Highlight rectangle in item coordinates, ready to fill. Its horizontal edges are snapped to
|
|
/// whole device pixels where they are computed, and [`Self::x`] hands the very same edges to
|
|
/// the glyph clip -- so the highlight edge and the clip edge cannot disagree and leave a sliver
|
|
/// of wrongly-colored glyph on top of the highlight.
|
|
background: PhysicalRect,
|
|
}
|
|
|
|
impl SelectionSpan {
|
|
/// Horizontal extent of the highlight, in the same coordinate space as `GlyphRun::offset()`.
|
|
pub(super) fn x(&self) -> Range<f32> {
|
|
self.background.min_x()..self.background.max_x()
|
|
}
|
|
}
|
|
|
|
/// Sorted by `(paragraph, line, x.start)`: the spans belonging to one line form a contiguous slice,
|
|
/// and within that slice they run left to right. Both halves are load-bearing -- see
|
|
/// [`Self::for_line`] and the segment walk in `draw_glyph_run_with_selection`.
|
|
#[derive(Clone, Debug, Default)]
|
|
pub(super) struct SelectionSpans(pub(super) Vec<SelectionSpan>);
|
|
|
|
impl SelectionSpans {
|
|
pub(super) fn is_empty(&self) -> bool {
|
|
self.0.is_empty()
|
|
}
|
|
|
|
pub(super) fn backgrounds(&self) -> impl Iterator<Item = PhysicalRect> + '_ {
|
|
self.0.iter().map(|span| span.background)
|
|
}
|
|
|
|
/// The spans covering one line. Both the stored spans and the draw loop walk paragraphs and
|
|
/// lines in order, but a binary search keeps this independent of that ordering.
|
|
pub(super) fn for_line(&self, paragraph: usize, line: usize) -> &[SelectionSpan] {
|
|
let key = (paragraph, line);
|
|
let start = self.0.partition_point(|span| (span.paragraph, span.line) < key);
|
|
let len = self.0[start..].partition_point(|span| (span.paragraph, span.line) == key);
|
|
&self.0[start..start + len]
|
|
}
|
|
}
|
|
|
|
/// How selected glyphs are painted, resolved once per draw call.
|
|
pub(super) struct SelectionRendering<'a, R: GlyphRenderer> {
|
|
pub(super) spans: &'a SelectionSpans,
|
|
/// Forced fill for selected glyphs. It wins over `Brush::override_fill_color` and
|
|
/// `Brush::link_color`, so a colored span or a link inside the selection still reads as
|
|
/// selected.
|
|
pub(super) foreground: <R as GlyphRenderer>::PlatformBrush,
|
|
}
|
|
|
|
/// How much of one glyph run a selection covers.
|
|
pub(super) enum RunCoverage {
|
|
/// No selected pixels: draw once, in the run's own brush.
|
|
Unselected,
|
|
/// Fully selected: draw once, in the selection foreground. No clip needed.
|
|
Full,
|
|
/// A boundary falls inside the run — possibly inside a ligature. The line's spans have to be
|
|
/// drawn separately, each clipped to its own horizontal band.
|
|
Partial,
|
|
}
|
|
|
|
/// Classifies `run_x` against the selection spans of the line it sits on.
|
|
pub(super) fn run_coverage(run_x: &Range<f32>, spans: &[SelectionSpan]) -> RunCoverage {
|
|
// Empty runs (and the degenerate zero-advance runs parley emits for ligature tails) can't
|
|
// show a boundary.
|
|
if spans.is_empty() || run_x.end <= run_x.start {
|
|
return RunCoverage::Unselected;
|
|
}
|
|
let mut overlapping = false;
|
|
for span in spans {
|
|
let span_x = span.x();
|
|
if span_x.start <= run_x.start && span_x.end >= run_x.end {
|
|
return RunCoverage::Full;
|
|
}
|
|
overlapping |= span_x.start < run_x.end && span_x.end > run_x.start;
|
|
}
|
|
if overlapping { RunCoverage::Partial } else { RunCoverage::Unselected }
|
|
}
|
|
|
|
impl Layout {
|
|
/// Resolves `selection_range` into per-line horizontal spans, for the lines whose boxes may
|
|
/// intersect `visible_band` (a physical y range in item coordinates; spans feed drawing only,
|
|
/// so off-screen lines need none). Pass an unbounded band to resolve everything.
|
|
///
|
|
/// Parley already splits a ligature into one cluster per character and apportions the
|
|
/// advance between them, so the geometry it reports is accurate to sub-glyph precision --
|
|
/// selecting the `i` of an `fi` ligature yields exactly the ligature's right half. That
|
|
/// precision is what makes clip-based selection drawing possible; see [`SelectionSpan`].
|
|
pub(super) fn selection_geometry(
|
|
&self,
|
|
selection_range: Range<usize>,
|
|
visible_band: &Range<PhysicalLength>,
|
|
) -> SelectionSpans {
|
|
let mut spans = Vec::new();
|
|
|
|
for (paragraph_index, paragraph) in self.visible_paragraphs().iter().enumerate() {
|
|
// Like the draw cull, padded by an (average) line height for ink that overhangs its
|
|
// line box: such a line still draws, so it still needs its spans.
|
|
let paragraph_top = self.y_offset + paragraph.y;
|
|
let paragraph_height = PhysicalLength::new(paragraph.layout.height());
|
|
let line_pad = paragraph_height / paragraph.layout.lines().len().max(1) as f32;
|
|
if paragraph_top + paragraph_height + line_pad < visible_band.start
|
|
|| paragraph_top - line_pad > visible_band.end
|
|
{
|
|
continue;
|
|
}
|
|
|
|
let selection_start = selection_range.start.max(paragraph.range.start);
|
|
let selection_end = selection_range.end.min(paragraph.range.end);
|
|
|
|
if selection_start >= selection_end {
|
|
continue;
|
|
}
|
|
|
|
let local_start = selection_start - paragraph.range.start;
|
|
let local_end = selection_end - paragraph.range.start;
|
|
|
|
let selection = parley::editing::Selection::new(
|
|
parley::editing::Cursor::from_byte_index(
|
|
¶graph.layout,
|
|
local_start,
|
|
Default::default(),
|
|
),
|
|
parley::editing::Cursor::from_byte_index(
|
|
¶graph.layout,
|
|
local_end,
|
|
Default::default(),
|
|
),
|
|
);
|
|
|
|
selection.geometry_with(¶graph.layout, |rect, line| {
|
|
// Snap the horizontal edges to device pixels once, here, so that the highlight
|
|
// rectangle and the glyph clip derived from the same span are pixel-identical.
|
|
let x = (rect.x0 as f32).round()..(rect.x1 as f32).round();
|
|
if x.end <= x.start {
|
|
return;
|
|
}
|
|
// A giant wrapped paragraph passes the paragraph test above with all its lines;
|
|
// keep only the ones that can reach the band.
|
|
let top = PhysicalLength::new(rect.y0 as _) + paragraph_top;
|
|
let bottom = PhysicalLength::new(rect.y1 as _) + paragraph_top;
|
|
if bottom + line_pad < visible_band.start || top - line_pad > visible_band.end {
|
|
return;
|
|
}
|
|
let background = PhysicalRect::new(
|
|
PhysicalPoint::from_lengths(
|
|
PhysicalLength::new(x.start),
|
|
PhysicalLength::new(rect.y0 as _) + paragraph_top,
|
|
),
|
|
PhysicalSize::new(x.end - x.start, rect.height() as _),
|
|
);
|
|
spans.push(SelectionSpan { paragraph: paragraph_index, line, background });
|
|
});
|
|
}
|
|
|
|
// Already in this order: paragraphs are visited in order, `geometry_with` walks a
|
|
// paragraph's lines in order, and within a line it accumulates x left to right over
|
|
// visually reordered items -- so even a bidi line yields ascending spans. Sort defensively
|
|
// anyway, since both consumers depend on it and neither would fail loudly: `for_line`
|
|
// needs the `(paragraph, line)` grouping, and the segment walk in
|
|
// `draw_glyph_run_with_selection` needs ascending x within a line.
|
|
spans.sort_by(|a, b| {
|
|
(a.paragraph, a.line)
|
|
.cmp(&(b.paragraph, b.line))
|
|
.then_with(|| a.x().start.total_cmp(&b.x().start))
|
|
});
|
|
|
|
SelectionSpans(spans)
|
|
}
|
|
}
|