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).
206 lines
6.1 KiB
Rust
206 lines
6.1 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
|
|
|
|
/// Styled text that has been parsed and separated into paragraphs
|
|
#[repr(transparent)]
|
|
#[derive(Debug, PartialEq, Clone, Default)]
|
|
pub struct StyledText {
|
|
/// Paragraphs of styled text
|
|
pub(crate) paragraphs: crate::SharedVector<i_slint_common::styled_text::StyledTextParagraph>,
|
|
}
|
|
|
|
/// Error returned when [`StyledText::from_markdown`] cannot parse the provided markdown input.
|
|
#[cfg(feature = "std")]
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct StyledTextFromMarkdownError {
|
|
message: alloc::string::String,
|
|
}
|
|
|
|
#[cfg(feature = "std")]
|
|
impl StyledTextFromMarkdownError {
|
|
fn new(errors: alloc::vec::Vec<i_slint_common::styled_text::StyledTextParseError>) -> Self {
|
|
Self {
|
|
message: errors
|
|
.iter()
|
|
.map(alloc::string::ToString::to_string)
|
|
.collect::<alloc::vec::Vec<_>>()
|
|
.join("\n"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "std")]
|
|
impl core::fmt::Display for StyledTextFromMarkdownError {
|
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
|
f.write_str(&self.message)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "std")]
|
|
impl std::error::Error for StyledTextFromMarkdownError {}
|
|
|
|
#[cfg(feature = "std")]
|
|
impl StyledText {
|
|
/// Creates styled text from plain text without applying markdown parsing.
|
|
pub fn from_plain_text(text: &str) -> Self {
|
|
Self {
|
|
paragraphs: [i_slint_common::styled_text::paragraph_from_plain_text(text.into())]
|
|
.into(),
|
|
}
|
|
}
|
|
|
|
/// Parses markdown into styled text.
|
|
pub fn from_markdown(markdown: &str) -> Result<Self, StyledTextFromMarkdownError> {
|
|
let (paragraphs, errors) = i_slint_common::styled_text::parse_interpolated::<
|
|
&[i_slint_common::styled_text::StyledTextParagraph],
|
|
>(markdown, &[]);
|
|
|
|
if errors.is_empty() {
|
|
Ok(Self { paragraphs: paragraphs.as_slice().into() })
|
|
} else {
|
|
Err(StyledTextFromMarkdownError::new(errors))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn get_raw_text(styled_text: &StyledText) -> alloc::borrow::Cow<'_, str> {
|
|
match styled_text.paragraphs.as_slice() {
|
|
[] => "".into(),
|
|
[paragraph] => paragraph.text.as_str().into(),
|
|
_ => {
|
|
let mut result = alloc::string::String::new();
|
|
for paragraph in styled_text.paragraphs.iter() {
|
|
if !result.is_empty() {
|
|
result.push('\n');
|
|
}
|
|
result.push_str(paragraph.text.as_str());
|
|
}
|
|
result.into()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Bindings for cbindgen
|
|
#[cfg(feature = "ffi")]
|
|
pub mod ffi {
|
|
#![allow(unsafe_code)]
|
|
|
|
use super::*;
|
|
|
|
#[unsafe(no_mangle)]
|
|
/// Create a new default styled text
|
|
pub unsafe extern "C" fn slint_styled_text_new(out: *mut StyledText) {
|
|
unsafe {
|
|
core::ptr::write(out, Default::default());
|
|
}
|
|
}
|
|
|
|
#[unsafe(no_mangle)]
|
|
/// Destroy the shared string
|
|
pub unsafe extern "C" fn slint_styled_text_drop(text: *const StyledText) {
|
|
unsafe {
|
|
core::ptr::read(text);
|
|
}
|
|
}
|
|
|
|
#[unsafe(no_mangle)]
|
|
/// Returns true if \a a is equal to \a b; otherwise returns false.
|
|
pub extern "C" fn slint_styled_text_eq(a: &StyledText, b: &StyledText) -> bool {
|
|
a == b
|
|
}
|
|
|
|
#[unsafe(no_mangle)]
|
|
/// Clone the styled text
|
|
pub unsafe extern "C" fn slint_styled_text_clone(out: *mut StyledText, ss: &StyledText) {
|
|
unsafe { core::ptr::write(out, ss.clone()) }
|
|
}
|
|
|
|
#[cfg(feature = "std")]
|
|
#[unsafe(no_mangle)]
|
|
/// Create a styled text from plain text
|
|
pub extern "C" fn slint_styled_text_from_plain_text(
|
|
text: crate::slice::Slice<u8>,
|
|
out: &mut StyledText,
|
|
) {
|
|
let text = unsafe { core::str::from_utf8_unchecked(text.as_slice()) };
|
|
*out = StyledText::from_plain_text(text);
|
|
}
|
|
|
|
#[cfg(feature = "std")]
|
|
#[unsafe(no_mangle)]
|
|
/// Parse markdown into styled text. Returns true on success.
|
|
/// On failure, resets `out` to default.
|
|
pub extern "C" fn slint_styled_text_from_markdown(
|
|
markdown: crate::slice::Slice<u8>,
|
|
out: &mut StyledText,
|
|
) -> bool {
|
|
let markdown = unsafe { core::str::from_utf8_unchecked(markdown.as_slice()) };
|
|
match StyledText::from_markdown(markdown) {
|
|
Ok(styled) => {
|
|
*out = styled;
|
|
true
|
|
}
|
|
Err(_) => false,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn parse_markdown(_format_string: &str, _args: &[StyledText]) -> StyledText {
|
|
#[cfg(feature = "std")]
|
|
{
|
|
let paragraph_slices = _args
|
|
.iter()
|
|
.map(|styled_text| styled_text.paragraphs.as_slice())
|
|
.collect::<alloc::vec::Vec<_>>();
|
|
|
|
let (paragraphs, errors) =
|
|
i_slint_common::styled_text::parse_interpolated(_format_string, ¶graph_slices);
|
|
|
|
for e in &errors {
|
|
crate::debug_log!("@markdown: {e}");
|
|
}
|
|
|
|
StyledText { paragraphs: paragraphs.as_slice().into() }
|
|
}
|
|
#[cfg(not(feature = "std"))]
|
|
Default::default()
|
|
}
|
|
|
|
pub fn color_to_styled_text(_color: crate::Color) -> StyledText {
|
|
#[cfg(feature = "std")]
|
|
{
|
|
let hex = alloc::format!(
|
|
"#{:02x}{:02x}{:02x}{:02x}",
|
|
_color.red(),
|
|
_color.green(),
|
|
_color.blue(),
|
|
_color.alpha()
|
|
);
|
|
StyledText::from_plain_text(&hex)
|
|
}
|
|
#[cfg(not(feature = "std"))]
|
|
Default::default()
|
|
}
|
|
|
|
pub fn string_to_styled_text(_string: alloc::string::String) -> StyledText {
|
|
#[cfg(feature = "std")]
|
|
{
|
|
if _string.is_empty() {
|
|
return Default::default();
|
|
}
|
|
StyledText::from_plain_text(&_string)
|
|
}
|
|
#[cfg(not(feature = "std"))]
|
|
Default::default()
|
|
}
|
|
|
|
#[cfg(all(test, feature = "std"))]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn string_to_styled_text_returns_default_for_empty_string() {
|
|
assert_eq!(super::string_to_styled_text(Default::default()), StyledText::default());
|
|
}
|
|
}
|