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).
384 lines
15 KiB
Rust
384 lines
15 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
|
|
|
|
use alloc::boxed::Box;
|
|
use alloc::rc::Rc;
|
|
use core::pin::Pin;
|
|
|
|
use crate::api::PlatformError;
|
|
#[cfg(feature = "std")]
|
|
use crate::graphics::{Rgba8Pixel, SharedPixelBuffer};
|
|
use crate::item_tree::ItemTreeRef;
|
|
use crate::items::{ItemRc, TextWrap};
|
|
use crate::lengths::{LogicalLength, LogicalPoint, LogicalRect, LogicalSize, ScaleFactor};
|
|
use crate::window::WindowAdapter;
|
|
|
|
/// Result of a single rendering attempt. WGPU-backed renderers can report `Occluded`,
|
|
/// `Timeout` or `Skipped` instead of rendering a frame; in those cases the caller should
|
|
/// re-arm a redraw rather than wait for the next external event.
|
|
#[must_use]
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum DrawOutcome {
|
|
Success,
|
|
Occluded,
|
|
Timeout,
|
|
/// The renderer wasn't ready to draw yet (e.g. the WGPU surface is still being set up
|
|
/// asynchronously). No frame was produced; the caller should re-arm a redraw.
|
|
Skipped,
|
|
}
|
|
|
|
/// The content widths of a text, as used for its layout constraints.
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub struct ContentWidths {
|
|
/// The width of the widest chunk that cannot be broken up: the longest word for
|
|
/// word-wrap. A wrapping text cannot be laid out narrower than this without breaking
|
|
/// words apart.
|
|
pub min: LogicalLength,
|
|
/// The width the text takes without wrapping.
|
|
pub max: LogicalLength,
|
|
}
|
|
|
|
/// This trait represents a Renderer that can render a slint scene.
|
|
///
|
|
/// This trait is [sealed](https://rust-lang.github.io/api-guidelines/future-proofing.html#sealed-traits-protect-against-downstream-implementations-c-sealed),
|
|
/// meaning that you are not expected to implement this trait
|
|
/// yourself, but you should use the provided ones from Slint.
|
|
pub trait Renderer: RendererSealed {}
|
|
impl<T: RendererSealed> Renderer for T {}
|
|
|
|
/// Implementation details behind [`Renderer`], but since this
|
|
/// trait is not exported in the public API, it is not possible for the
|
|
/// users to re-implement these functions.
|
|
pub trait RendererSealed {
|
|
/// The cache used by the default implementations of the text related trait functions,
|
|
/// which lay text out through [`crate::textlayout::sharedparley`]. Renderers using
|
|
/// those default implementations return their cache here; with the default `None`,
|
|
/// text layouts are computed without caching.
|
|
#[cfg(feature = "shared-parley")]
|
|
fn text_layout_cache(&self) -> Option<&crate::textlayout::sharedparley::TextLayoutCache> {
|
|
None
|
|
}
|
|
|
|
/// Returns the size of the given text in logical pixels.
|
|
/// When set, `max_width` means that one need to wrap the text, so it does not go further than that,
|
|
/// using the wrapping type passed by `text_wrap`.
|
|
#[cfg(not(feature = "shared-parley"))]
|
|
fn text_size(
|
|
&self,
|
|
text_item: Pin<&dyn crate::item_rendering::RenderString>,
|
|
item_rc: &crate::item_tree::ItemRc,
|
|
max_width: Option<LogicalLength>,
|
|
text_wrap: TextWrap,
|
|
) -> LogicalSize;
|
|
|
|
/// Returns the size of the given text in logical pixels.
|
|
/// When set, `max_width` means that one need to wrap the text, so it does not go further than that,
|
|
/// using the wrapping type passed by `text_wrap`.
|
|
///
|
|
/// The default implementation uses the shared parley text layout with [`Self::text_layout_cache`].
|
|
#[cfg(feature = "shared-parley")]
|
|
fn text_size(
|
|
&self,
|
|
text_item: Pin<&dyn crate::item_rendering::RenderString>,
|
|
item_rc: &crate::item_tree::ItemRc,
|
|
max_width: Option<LogicalLength>,
|
|
text_wrap: TextWrap,
|
|
) -> LogicalSize {
|
|
crate::textlayout::sharedparley::text_size(
|
|
self,
|
|
text_item,
|
|
item_rc,
|
|
max_width,
|
|
text_wrap,
|
|
self.text_layout_cache(),
|
|
)
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Returns the content widths of the text, or None if the renderer can't measure them,
|
|
/// in which case the caller falls back to `text_size` without a lower bound.
|
|
///
|
|
/// These are intrinsic to the text and don't depend on its `wrap` mode: `min` is the
|
|
/// longest word, `max` is the unwrapped width.
|
|
fn text_content_widths(
|
|
&self,
|
|
text_item: Pin<&dyn crate::item_rendering::RenderString>,
|
|
item_rc: &crate::item_tree::ItemRc,
|
|
) -> Option<ContentWidths> {
|
|
#[cfg(feature = "shared-parley")]
|
|
{
|
|
crate::textlayout::sharedparley::text_content_widths(
|
|
self,
|
|
text_item,
|
|
item_rc,
|
|
self.text_layout_cache(),
|
|
)
|
|
}
|
|
#[cfg(not(feature = "shared-parley"))]
|
|
{
|
|
let _ = (text_item, item_rc);
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Returns the size of the individual character in logical pixels.
|
|
#[cfg(not(feature = "shared-parley"))]
|
|
fn char_size(
|
|
&self,
|
|
text_item: Pin<&dyn crate::item_rendering::HasFont>,
|
|
item_rc: &crate::item_tree::ItemRc,
|
|
ch: char,
|
|
) -> LogicalSize;
|
|
|
|
/// Returns the size of the individual character in logical pixels.
|
|
///
|
|
/// The default implementation measures the character with the shared parley layout.
|
|
#[cfg(feature = "shared-parley")]
|
|
fn char_size(
|
|
&self,
|
|
text_item: Pin<&dyn crate::item_rendering::HasFont>,
|
|
item_rc: &crate::item_tree::ItemRc,
|
|
ch: char,
|
|
) -> LogicalSize {
|
|
self.slint_context()
|
|
.and_then(|ctx| {
|
|
let mut font_ctx = ctx.font_context().borrow_mut();
|
|
crate::textlayout::sharedparley::char_size(&mut font_ctx, text_item, item_rc, ch)
|
|
})
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Returns the metrics of the given font.
|
|
#[cfg(not(feature = "shared-parley"))]
|
|
fn font_metrics(&self, font_request: crate::graphics::FontRequest)
|
|
-> crate::items::FontMetrics;
|
|
|
|
/// Returns the metrics of the given font.
|
|
///
|
|
/// The default implementation queries the font through the shared fontique collection.
|
|
#[cfg(feature = "shared-parley")]
|
|
fn font_metrics(
|
|
&self,
|
|
font_request: crate::graphics::FontRequest,
|
|
) -> crate::items::FontMetrics {
|
|
self.slint_context()
|
|
.map(|ctx| {
|
|
let mut font_ctx = ctx.font_context().borrow_mut();
|
|
crate::textlayout::sharedparley::font_metrics(&mut font_ctx, font_request)
|
|
})
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// The height of one line of text: what a shaped single-line layout reports, without
|
|
/// shaping. `None` means the caller must measure through [`Self::text_size`].
|
|
fn text_line_height(
|
|
&self,
|
|
font_request: crate::graphics::FontRequest,
|
|
) -> Option<LogicalLength> {
|
|
#[cfg(feature = "shared-parley")]
|
|
{
|
|
let ctx = self.slint_context()?;
|
|
let mut font_ctx = ctx.font_context().borrow_mut();
|
|
crate::textlayout::sharedparley::text_line_height(&mut font_ctx, &font_request)
|
|
}
|
|
#[cfg(not(feature = "shared-parley"))]
|
|
{
|
|
let _ = font_request;
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Returns the (UTF-8) byte offset in the text property that refers to the character that contributed to
|
|
/// the glyph cluster that's visually nearest to the given coordinate. This is used for hit-testing,
|
|
/// for example when receiving a mouse click into a text field. Then this function returns the "cursor"
|
|
/// position. The affinity says which visual position was hit (differs only at a soft break).
|
|
#[cfg(not(feature = "shared-parley"))]
|
|
fn text_input_byte_offset_for_position(
|
|
&self,
|
|
text_input: Pin<&crate::items::TextInput>,
|
|
item_rc: &ItemRc,
|
|
pos: LogicalPoint,
|
|
) -> (usize, crate::items::TextCursorAffinity);
|
|
|
|
/// Returns the (UTF-8) byte offset in the text property that refers to the character that contributed to
|
|
/// the glyph cluster that's visually nearest to the given coordinate. This is used for hit-testing,
|
|
/// for example when receiving a mouse click into a text field. Then this function returns the "cursor"
|
|
/// position. The affinity says which visual position was hit (differs only at a soft break).
|
|
///
|
|
/// The default implementation uses the shared parley text layout with [`Self::text_layout_cache`].
|
|
#[cfg(feature = "shared-parley")]
|
|
fn text_input_byte_offset_for_position(
|
|
&self,
|
|
text_input: Pin<&crate::items::TextInput>,
|
|
item_rc: &ItemRc,
|
|
pos: LogicalPoint,
|
|
) -> (usize, crate::items::TextCursorAffinity) {
|
|
crate::textlayout::sharedparley::text_input_byte_offset_for_position(
|
|
self,
|
|
text_input,
|
|
item_rc,
|
|
pos,
|
|
self.text_layout_cache(),
|
|
)
|
|
}
|
|
|
|
/// That's the opposite of [`Self::text_input_byte_offset_for_position`]
|
|
/// It takes a (UTF-8) byte offset in the text property, and returns a Rectangle
|
|
/// left to the char. It is one logical pixel wide and ends at the baseline.
|
|
/// An offset at a soft line break has one rectangle per line; `affinity` picks between them.
|
|
#[cfg(not(feature = "shared-parley"))]
|
|
fn text_input_cursor_rect_for_byte_offset(
|
|
&self,
|
|
text_input: Pin<&crate::items::TextInput>,
|
|
item_rc: &ItemRc,
|
|
byte_offset: usize,
|
|
affinity: crate::items::TextCursorAffinity,
|
|
) -> LogicalRect;
|
|
|
|
/// That's the opposite of [`Self::text_input_byte_offset_for_position`]
|
|
/// It takes a (UTF-8) byte offset in the text property, and returns a Rectangle
|
|
/// left to the char. It is one logical pixel wide and ends at the baseline.
|
|
/// An offset at a soft line break has one rectangle per line; `affinity` picks between them.
|
|
///
|
|
/// The default implementation uses the shared parley text layout with [`Self::text_layout_cache`].
|
|
#[cfg(feature = "shared-parley")]
|
|
fn text_input_cursor_rect_for_byte_offset(
|
|
&self,
|
|
text_input: Pin<&crate::items::TextInput>,
|
|
item_rc: &ItemRc,
|
|
byte_offset: usize,
|
|
affinity: crate::items::TextCursorAffinity,
|
|
) -> LogicalRect {
|
|
crate::textlayout::sharedparley::text_input_cursor_rect_for_byte_offset(
|
|
self,
|
|
text_input,
|
|
item_rc,
|
|
byte_offset,
|
|
affinity,
|
|
self.text_layout_cache(),
|
|
)
|
|
}
|
|
|
|
/// Whether this renderer lays `text_input`'s text out through parley.
|
|
#[cfg(feature = "shared-parley")]
|
|
fn text_input_has_parley_layout(
|
|
&self,
|
|
_text_input: Pin<&crate::items::TextInput>,
|
|
_item_rc: &ItemRc,
|
|
) -> bool {
|
|
true
|
|
}
|
|
|
|
/// Clear the caches for the items that are being removed
|
|
fn free_graphics_resources(
|
|
&self,
|
|
_component: ItemTreeRef,
|
|
_items: &mut dyn Iterator<Item = Pin<crate::items::ItemRef<'_>>>,
|
|
) -> Result<(), crate::platform::PlatformError> {
|
|
Ok(())
|
|
}
|
|
|
|
/// Mark a given region as dirty regardless whether the items actually are dirty.
|
|
///
|
|
/// Example: when a PopupWindow disappears, the region under the popup needs to be redrawn
|
|
fn mark_dirty_region(&self, _region: crate::partial_renderer::DirtyRegion) {}
|
|
|
|
#[cfg(all(feature = "std", not(feature = "shared-parley")))] // FIXME: just because of the Error
|
|
/// This function can be used to register a custom TrueType font with Slint,
|
|
/// for use with the `font-family` property. The provided slice must be a valid TrueType
|
|
/// font.
|
|
fn register_font_from_memory(
|
|
&self,
|
|
_data: &'static [u8],
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
Err("This renderer does not support registering custom fonts.".into())
|
|
}
|
|
|
|
#[cfg(all(feature = "std", feature = "shared-parley"))]
|
|
/// This function can be used to register a custom TrueType font with Slint,
|
|
/// for use with the `font-family` property. The provided slice must be a valid TrueType
|
|
/// font.
|
|
///
|
|
/// The default implementation registers the font with the shared fontique collection.
|
|
fn register_font_from_memory(
|
|
&self,
|
|
data: &'static [u8],
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let ctx = self.slint_context().ok_or("slint platform not initialized")?;
|
|
ctx.font_context().borrow_mut().register_static_font(data);
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(all(feature = "std", not(feature = "shared-parley")))]
|
|
/// This function can be used to register a custom TrueType font with Slint,
|
|
/// for use with the `font-family` property. The provided path must refer to a valid TrueType
|
|
/// font.
|
|
fn register_font_from_path(
|
|
&self,
|
|
_path: &std::path::Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
Err("This renderer does not support registering custom fonts.".into())
|
|
}
|
|
|
|
#[cfg(all(feature = "std", feature = "shared-parley"))]
|
|
/// This function can be used to register a custom TrueType font with Slint,
|
|
/// for use with the `font-family` property. The provided path must refer to a valid TrueType
|
|
/// font.
|
|
///
|
|
/// The default implementation registers the font with the shared fontique collection.
|
|
fn register_font_from_path(
|
|
&self,
|
|
path: &std::path::Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let requested_path = path.canonicalize().unwrap_or_else(|_| path.into());
|
|
let contents = std::fs::read(requested_path)?;
|
|
let ctx = self.slint_context().ok_or("slint platform not initialized")?;
|
|
ctx.font_context().borrow_mut().collection.register_fonts(contents.into(), None);
|
|
Ok(())
|
|
}
|
|
|
|
fn register_bitmap_font(&self, _font_data: &'static crate::graphics::BitmapFont) {
|
|
crate::debug_log!(
|
|
"Internal error: The current renderer cannot load fonts build with the `EmbedForSoftwareRenderer` option. Please use the software Renderer, or disable that option when building your slint files"
|
|
);
|
|
}
|
|
|
|
/// This function is called through the public API to register a callback that the backend needs to invoke during
|
|
/// different phases of rendering.
|
|
fn set_rendering_notifier(
|
|
&self,
|
|
_callback: Box<dyn crate::api::RenderingNotifier>,
|
|
) -> Result<(), crate::api::SetRenderingNotifierError> {
|
|
Err(crate::api::SetRenderingNotifierError::Unsupported)
|
|
}
|
|
|
|
fn set_window_adapter(&self, _window_adapter: &Rc<dyn WindowAdapter>);
|
|
|
|
fn window_adapter(&self) -> Option<Rc<dyn WindowAdapter>>;
|
|
|
|
fn scale_factor(&self) -> Option<ScaleFactor> {
|
|
self.window_adapter()
|
|
.map(|window_adapter| ScaleFactor::new(window_adapter.window().scale_factor()))
|
|
}
|
|
|
|
#[cfg(feature = "shared-parley")]
|
|
fn slint_context(&self) -> Option<crate::SlintContext> {
|
|
self.window_adapter()
|
|
.map(|wa| crate::window::WindowInner::from_pub(wa.window()).context().clone())
|
|
}
|
|
|
|
fn resize(&self, _size: crate::api::PhysicalSize) -> Result<(), PlatformError> {
|
|
Ok(())
|
|
}
|
|
|
|
/// Re-implement this function to support Window::take_snapshot(), i.e. return
|
|
/// the contents of the window in an image buffer.
|
|
#[cfg(feature = "std")]
|
|
fn take_snapshot(&self) -> Result<SharedPixelBuffer<Rgba8Pixel>, PlatformError> {
|
|
Err("WindowAdapter::take_snapshot is not implemented by the platform".into())
|
|
}
|
|
|
|
/// Whether the renderer supports transformations such as rotations and scaling or not.
|
|
fn supports_transformations(&self) -> bool;
|
|
}
|