gallery/vendor/i-slint-core/translations.rs
Greg Pomerantz 2f4768f354 Fix 120Hz scroll jank end-to-end: render-ahead present + full-cache eviction off the UI thread
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).
2026-10-30 02:49:23 -04:00

610 lines
22 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 cntr demangle indice
use crate::SharedString;
use core::fmt::Display;
pub use formatter::FormatArgs;
use i_slint_common::TranslationsBundled;
#[cfg(feature = "tr")]
pub use tr::Translator;
mod formatter {
use core::fmt::{Display, Formatter, Result};
pub trait FormatArgs {
type Output<'a>: Display
where
Self: 'a;
#[allow(clippy::wrong_self_convention)]
fn from_index(&self, index: usize) -> Option<Self::Output<'_>>;
#[allow(clippy::wrong_self_convention)]
fn from_name(&self, _name: &str) -> Option<Self::Output<'_>> {
None
}
}
impl<T: Display> FormatArgs for [T] {
type Output<'a>
= &'a T
where
T: 'a;
fn from_index(&self, index: usize) -> Option<&T> {
self.get(index)
}
}
impl<const N: usize, T: Display> FormatArgs for [T; N] {
type Output<'a>
= &'a T
where
T: 'a;
fn from_index(&self, index: usize) -> Option<&T> {
self.get(index)
}
}
pub fn format<'a>(
format_str: &'a str,
args: &'a (impl FormatArgs + ?Sized),
) -> impl Display + 'a {
FormatResult { format_str, args }
}
struct FormatResult<'a, T: ?Sized> {
format_str: &'a str,
args: &'a T,
}
impl<T: FormatArgs + ?Sized> Display for FormatResult<'_, T> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
let mut arg_idx = 0;
let mut pos = 0;
while let Some(mut p) = self.format_str[pos..].find(['{', '}']) {
if self.format_str.len() - pos < p + 1 {
break;
}
p += pos;
// Skip escaped }
if self.format_str.get(p..=p) == Some("}") {
self.format_str[pos..=p].fmt(f)?;
if self.format_str.get(p + 1..=p + 1) == Some("}") {
pos = p + 2;
} else {
// FIXME! this is an error, it should be reported ('}' must be escaped)
pos = p + 1;
}
continue;
}
// Skip escaped {
if self.format_str.get(p + 1..=p + 1) == Some("{") {
self.format_str[pos..=p].fmt(f)?;
pos = p + 2;
continue;
}
// Find the argument
let end = if let Some(end) = self.format_str[p..].find('}') {
end + p
} else {
// FIXME! this is an error, it should be reported
self.format_str[pos..=p].fmt(f)?;
pos = p + 1;
continue;
};
let argument = self.format_str[p + 1..end].trim();
let pa = if p == end - 1 {
arg_idx += 1;
self.args.from_index(arg_idx - 1)
} else if let Ok(n) = argument.parse::<usize>() {
self.args.from_index(n)
} else {
self.args.from_name(argument)
};
// format the part before the '{'
self.format_str[pos..p].fmt(f)?;
if let Some(a) = pa {
a.fmt(f)?;
} else {
// FIXME! this is an error, it should be reported
self.format_str[p..=end].fmt(f)?;
}
pos = end + 1;
}
self.format_str[pos..].fmt(f)
}
}
#[cfg(test)]
mod tests {
use super::format;
use core::fmt::Display;
use std::string::{String, ToString};
#[test]
fn test_format() {
assert_eq!(format("Hello", (&[]) as &[String]).to_string(), "Hello");
assert_eq!(format("Hello {}!", &["world"]).to_string(), "Hello world!");
assert_eq!(format("Hello {0}!", &["world"]).to_string(), "Hello world!");
assert_eq!(
format("Hello -{1}- -{0}-", &[&(40 + 5) as &dyn Display, &"World"]).to_string(),
"Hello -World- -45-"
);
assert_eq!(
format(
format("Hello {{}}!", (&[]) as &[String]).to_string().as_str(),
&[format("{}", &["world"])]
)
.to_string(),
"Hello world!"
);
assert_eq!(
format("Hello -{}- -{}-", &[&(40 + 5) as &dyn Display, &"World"]).to_string(),
"Hello -45- -World-"
);
assert_eq!(format("Hello {{0}} {}", &["world"]).to_string(), "Hello {0} world");
}
}
}
struct WithPlural<'a, T: ?Sized>(&'a T, i32);
enum DisplayOrInt<T> {
Display(T),
Int(i32),
}
impl<T: Display> Display for DisplayOrInt<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
DisplayOrInt::Display(d) => d.fmt(f),
DisplayOrInt::Int(i) => i.fmt(f),
}
}
}
impl<T: FormatArgs + ?Sized> FormatArgs for WithPlural<'_, T> {
type Output<'b>
= DisplayOrInt<T::Output<'b>>
where
Self: 'b;
fn from_index(&self, index: usize) -> Option<Self::Output<'_>> {
self.0.from_index(index).map(DisplayOrInt::Display)
}
fn from_name<'b>(&'b self, name: &str) -> Option<Self::Output<'b>> {
if name == "n" {
Some(DisplayOrInt::Int(self.1))
} else {
self.0.from_name(name).map(DisplayOrInt::Display)
}
}
}
/// Do the translation and formatting
pub fn translate(
original: &str,
contextid: &str,
domain: &str,
arguments: &(impl FormatArgs + ?Sized),
n: i32,
plural: &str,
) -> SharedString {
#![allow(unused)]
let mut output = SharedString::default();
// Register a dependency so that language changes trigger a re-evaluation of all relevant bindings
// and this function is called again.
#[cfg(any(feature = "tr", all(target_family = "unix", feature = "gettext-rs")))]
global_translation_property();
let mut translated: Option<alloc::borrow::Cow<'_, str>> = None;
#[cfg(feature = "tr")]
{
translated = crate::context::GLOBAL_CONTEXT.with(|ctx| {
let ctx = ctx.get()?;
let external_translator = ctx.external_translator()?;
let context = if !contextid.is_empty() { Some(contextid) } else { None };
Some(
if plural.is_empty() {
external_translator.translate(original, context)
} else {
external_translator.ntranslate(n.try_into().ok()?, original, plural, context)
}
.into_owned()
.into(),
)
});
}
#[cfg(all(target_family = "unix", feature = "gettext-rs"))]
if translated.is_none() {
translated = Some(alloc::borrow::Cow::Owned(translate_gettext(
original, contextid, domain, n, plural,
)));
}
let translated = translated
.unwrap_or_else(|| if plural.is_empty() || n == 1 { original } else { plural }.into());
use core::fmt::Write;
write!(output, "{}", formatter::format(&translated, &WithPlural(arguments, n))).unwrap();
output
}
#[cfg(all(target_family = "unix", feature = "gettext-rs"))]
fn translate_gettext(
string: &str,
ctx: &str,
domain: &str,
n: i32,
plural: &str,
) -> std::string::String {
use std::string::String;
fn mangle_context(ctx: &str, s: &str) -> String {
std::format!("{ctx}\u{4}{s}")
}
fn demangle_context(r: String) -> String {
if let Some(x) = r.split('\u{4}').next_back() {
return x.into();
}
r
}
if plural.is_empty() {
if !ctx.is_empty() {
demangle_context(gettextrs::dgettext(domain, mangle_context(ctx, string)))
} else {
gettextrs::dgettext(domain, string)
}
} else if !ctx.is_empty() {
demangle_context(gettextrs::dngettext(
domain,
mangle_context(ctx, string),
mangle_context(ctx, plural),
n as u32,
))
} else {
gettextrs::dngettext(domain, string, plural, n as u32)
}
}
/// Returns the language index and make sure to register a dependency
fn global_translation_property() -> usize {
crate::context::GLOBAL_CONTEXT.with(|ctx| {
let Some(ctx) = ctx.get() else { return 0 };
ctx.0.as_ref().project_ref().translations_dirty.get()
})
}
pub fn mark_all_translations_dirty() {
// _nl_msg_cat_cntr is defined by glibc and the standalone GNU libintl, but not by musl,
// which provides its own gettext implementation without that cache counter.
#[cfg(all(feature = "gettext-rs", target_family = "unix", not(target_env = "musl")))]
{
// SAFETY: This trick from https://www.gnu.org/software/gettext/manual/html_node/gettext-grok.html
// is merely incrementing a generational counter that will invalidate gettext's internal cache for translations.
// If in the worst case it won't invalidate, then old translations are shown.
#[allow(unsafe_code)]
unsafe {
unsafe extern "C" {
static mut _nl_msg_cat_cntr: std::ffi::c_int;
}
_nl_msg_cat_cntr += 1;
}
}
crate::context::GLOBAL_CONTEXT.with(|ctx| {
let Some(ctx) = ctx.get() else { return };
let pinned = ctx.0.as_ref().project_ref();
pinned.translations_dirty.mark_dirty();
// Update the decimal separator
#[cfg(all(feature = "gettext-rs", target_family = "unix"))]
if let Some(locale) = sys_locale::get_locale() {
pinned
.locale_decimal_separator
.set(i_slint_common::decimal_separator_for_locale(&locale))
}
})
}
#[cfg(feature = "gettext-rs")]
/// Initialize the translation by calling the [`bindtextdomain`](https://man7.org/linux/man-pages/man3/bindtextdomain.3.html) function from gettext
pub fn gettext_bindtextdomain(_domain: &str, _dirname: std::path::PathBuf) -> std::io::Result<()> {
#[cfg(target_family = "unix")]
{
gettextrs::bindtextdomain(_domain, _dirname)?;
static START: std::sync::Once = std::sync::Once::new();
START.call_once(|| {
gettextrs::setlocale(gettextrs::LocaleCategory::LcAll, "");
});
mark_all_translations_dirty();
}
Ok(())
}
/// Translate the strings bundled into the applications. If the desired language is not available, use the default
///
/// `strs` - the string which should be translated. The slice contains the string in multiple languages
/// `arguments` - arguments for the translation
pub fn translate_from_bundle(
strs: &[Option<&str>],
arguments: &(impl FormatArgs + ?Sized),
) -> SharedString {
let idx = global_translation_property();
let mut output = SharedString::default();
let Some(translated) = strs.get(idx).and_then(|x| *x).or_else(|| strs.first().and_then(|x| *x))
else {
return output;
};
use core::fmt::Write;
write!(output, "{}", formatter::format(translated, arguments)).unwrap();
output
}
/// Translate the strings bundled into the applications with plurals. If the desired language is not available, use the default
///
/// `strs` - the string which should be translated
/// `plural_rules` - the rules to create the plurals
/// `arguments` - arguments for the translation
pub fn translate_from_bundle_with_plural(
strs: &[Option<&[&str]>],
plural_rules: &[Option<fn(i32) -> usize>],
arguments: &(impl FormatArgs + ?Sized),
n: i32,
) -> SharedString {
let idx = global_translation_property();
let mut output = SharedString::default();
let en = |n| (n != 1) as usize;
let (translations, rule) = match strs.get(idx) {
Some(Some(x)) => (x, plural_rules.get(idx).and_then(|x| *x).unwrap_or(en)),
_ => match strs.first() {
Some(Some(x)) => (x, plural_rules.first().and_then(|x| *x).unwrap_or(en)),
_ => return output,
},
};
let Some(translated) = translations.get(rule(n)).or_else(|| translations.first()).cloned()
else {
return output;
};
use core::fmt::Write;
write!(output, "{}", formatter::format(translated, &WithPlural(arguments, n))).unwrap();
output
}
/// This function is called by the generated code to assign the list of bundled languages
/// and decimal separators.
/// Do nothing if the list is already assigned.
/// It selects also the language based on the system locale as default
pub fn set_bundled_languages(translations: &[TranslationsBundled]) {
crate::context::GLOBAL_CONTEXT.with(|ctx| {
let Some(ctx) = ctx.get() else { return };
if ctx.0.translations_bundle.borrow().is_none() {
ctx.0.translations_bundle.replace(Some(translations.to_vec()));
#[cfg(feature = "std")]
if let Some(idx) = language_index_from_sys_locale(translations) {
ctx.0.as_ref().project_ref().translations_dirty.set(idx);
}
}
});
}
/// attempt to select the right bundled translation based on the current system locale
#[cfg(feature = "std")]
fn language_index_from_sys_locale(languages: &[TranslationsBundled]) -> Option<usize> {
let locale = sys_locale::get_locale()?;
// first, try an exact match
let idx = languages.iter().position(|x| *x.language == locale);
// else, only match the language part
fn base(l: &str) -> &str {
l.find(['-', '_', '@']).map_or(l, |i| &l[..i])
}
idx.or_else(|| {
let locale = base(&locale);
languages.iter().position(|x| base(x.language) == locale)
})
}
#[i_slint_core_macros::slint_doc]
/// Select the current translation language when using bundled translations.
///
/// This function requires that the application's `.slint` file was compiled with bundled translations..
/// It must be called after creating the first component.
///
/// The language string is the locale, which matches the name of the folder that contains the `LC_MESSAGES` folder.
/// An empty string or `"en"` will select the default language.
///
/// Returns `Ok` if the language was selected; [`SelectBundledTranslationError`] otherwise.
///
/// See also the [Translation documentation](slint:translations).
pub fn select_bundled_translation(language: &str) -> Result<(), SelectBundledTranslationError> {
crate::context::GLOBAL_CONTEXT.with(|ctx| {
let Some(ctx) = ctx.get() else {
return Err(SelectBundledTranslationError::NoTranslationsBundled);
};
let translations = ctx.0.translations_bundle.borrow();
let Some(translations) = &*translations else {
return Err(SelectBundledTranslationError::NoTranslationsBundled);
};
let pinned = ctx.0.as_ref().project_ref();
if let Some((idx, translation_bundle)) =
translations.iter().enumerate().find(|(_i, x)| x.language == language)
{
pinned.translations_dirty.as_ref().set(idx);
// Update the decimal separator
pinned.locale_decimal_separator.as_ref().set(translation_bundle.decimal_separator);
Ok(())
} else if language.is_empty() || language == "en" {
pinned.translations_dirty.as_ref().set(0);
pinned.locale_decimal_separator.as_ref().set(i_slint_common::DEFAULT_DECIMAL_SEPARATOR);
Ok(())
} else {
Err(SelectBundledTranslationError::LanguageNotFound {
available_languages: translations.iter().map(|x| (*x.language).into()).collect(),
})
}
})
}
/// Error type returned from the [`select_bundled_translation`] function.
#[derive(Debug)]
pub enum SelectBundledTranslationError {
/// The language was not found. The list of available languages is included in this error variant.
LanguageNotFound { available_languages: crate::SharedVector<SharedString> },
/// There are no bundled translations. Either [`select_bundled_translation`] was called before creating a component,
/// or the application's `.slint` file was compiled without the bundle translation option.
NoTranslationsBundled,
}
impl core::fmt::Display for SelectBundledTranslationError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
SelectBundledTranslationError::LanguageNotFound { available_languages } => {
write!(
f,
"The specified language was not found. Available languages are: {available_languages:?}"
)
}
SelectBundledTranslationError::NoTranslationsBundled => {
write!(
f,
"There are no bundled translations. Either select_bundled_translation was called before creating a component, or the application's `.slint` file was compiled without the bundle translation option"
)
}
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for SelectBundledTranslationError {}
#[cfg(feature = "ffi")]
mod ffi {
#![allow(unsafe_code)]
use super::*;
use crate::slice::Slice;
/// return the current decimal-separator for the `Platform.decimal-separator` property
#[unsafe(no_mangle)]
pub extern "C" fn slint_decimal_separator(out: &mut SharedString) {
crate::context::GLOBAL_CONTEXT.with(|ctx| {
let separator = if let Some(ctx) = ctx.get() {
ctx.0.as_ref().project_ref().locale_decimal_separator.get()
} else {
i_slint_common::DEFAULT_DECIMAL_SEPARATOR
};
*out = crate::SharedString::from(separator)
})
}
/// Perform the translation and formatting.
#[unsafe(no_mangle)]
pub extern "C" fn slint_translate(
to_translate: &mut SharedString,
context: &SharedString,
domain: &SharedString,
arguments: Slice<SharedString>,
n: i32,
plural: &SharedString,
) {
*to_translate =
translate(to_translate.as_str(), context, domain, arguments.as_slice(), n, plural)
}
/// Mark all translated string as dirty to perform re-translation in case the language change
#[unsafe(no_mangle)]
pub extern "C" fn slint_translations_mark_dirty() {
mark_all_translations_dirty();
}
/// Safety: The slice must contain valid null-terminated utf-8 strings
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_translate_from_bundle(
strs: Slice<*const core::ffi::c_char>,
arguments: Slice<SharedString>,
output: &mut SharedString,
) {
*output = SharedString::default();
let idx = global_translation_property();
let Some(translated) = strs
.get(idx)
.filter(|x| !x.is_null())
.or_else(|| strs.first())
.map(|x| unsafe { core::ffi::CStr::from_ptr(*x) }.to_str().unwrap())
else {
return;
};
use core::fmt::Write;
write!(output, "{}", formatter::format(translated, arguments.as_slice())).unwrap();
}
/// strs is all the strings variant of all languages.
/// indices is the array of indices such that for each language, the corresponding indice is one past the last index of the string for that language.
/// So to get the string array for that language, one would do `strs[indices[lang-1]..indices[lang]]`
/// (where indices[-1] is 0)
///
/// Safety; the strs must be pointer to valid null-terminated utf-8 strings
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_translate_from_bundle_with_plural(
strs: Slice<*const core::ffi::c_char>,
indices: Slice<u32>,
plural_rules: Slice<Option<fn(i32) -> usize>>,
arguments: Slice<SharedString>,
n: i32,
output: &mut SharedString,
) {
*output = SharedString::default();
let idx = global_translation_property();
let en = |n| (n != 1) as usize;
let begin = *indices.get(idx.wrapping_sub(1)).unwrap_or(&0);
let (translations, rule) = match indices.get(idx) {
Some(end) if *end != begin => (
&strs.as_slice()[begin as usize..*end as usize],
plural_rules.get(idx).and_then(|x| *x).unwrap_or(en),
),
_ => (
&strs.as_slice()[..*indices.first().unwrap_or(&0) as usize],
plural_rules.first().and_then(|x| *x).unwrap_or(en),
),
};
let Some(translated) = translations
.get(rule(n))
.or_else(|| translations.first())
.map(|x| unsafe { core::ffi::CStr::from_ptr(*x) }.to_str().unwrap())
else {
return;
};
use core::fmt::Write;
write!(output, "{}", formatter::format(translated, &WithPlural(arguments.as_slice(), n)))
.unwrap();
}
#[unsafe(no_mangle)]
pub extern "C" fn slint_translate_set_bundled_languages(
languages: Slice<Slice<'static, u8>>,
separators: Slice<u32>,
) {
let translations = languages
.iter()
.zip(separators.as_slice().iter())
.map(|(language, separator)| TranslationsBundled {
language: core::str::from_utf8(language.as_slice()).unwrap(),
decimal_separator: core::char::from_u32(*separator)
.unwrap_or(i_slint_common::DEFAULT_DECIMAL_SEPARATOR),
})
.collect::<alloc::vec::Vec<_>>();
set_bundled_languages(&translations);
}
#[unsafe(no_mangle)]
pub extern "C" fn slint_translate_select_bundled_translation(language: Slice<u8>) -> bool {
let language = core::str::from_utf8(&language).unwrap();
select_bundled_translation(language).is_ok()
}
}