Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions crates/render-foundation/src/font/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,16 @@ impl FontLoader {
// monospace
let mono_id = self.resolve_generic_family(&mono_names).unwrap_or(default_id);
resolver.insert("monospace".to_string(), mono_id);
// P9-D2(R5072,CSS Fonts 4 §5.2 weight 匹配在族内 face 集上进行):mono 族
// 常仅一个 face(Noto Sans Mono CJK SC),w700 须族内回退到唯一 face。:700
// 语义跟随解析结果族:mono_id 命中专属 mono 族时取该族 Bold(无 Bold 则自
// 身);generic 关键字全落空(mono_id = default)时取 default 族自身谱系的
// Bold/自身——不套用 R3993 sans 链的「任意族第二 face」兜底(那才会把不相干
// 的 sans bold 塞给 monospace)。
resolver.insert(
"monospace:700".to_string(),
self.bold_face_of_same_family(mono_id).unwrap_or(mono_id),
);

// cursive / fantasy / system-ui → 暂映射到 sans-serif
if std::env::var("ZW_RESOLVER_TRACE").is_ok() {
Expand All @@ -367,6 +377,12 @@ impl FontLoader {
resolver.insert("cursive".to_string(), sans_id);
resolver.insert("fantasy".to_string(), sans_id);
resolver.insert("system-ui".to_string(), sans_id);
// P9-D2(R5072):三族与 sans-serif 同族,w700 共用 sans-serif:700 值(R3993
// 链)——缺 :700 键时 resolve_font_face 的 [":700",""] 查找直接落 regular face
//(probe2 B1:system-ui w700 渲染 regular;GitHub system-ui 栈粗体标题细体)。
resolver.insert("cursive:700".to_string(), sans_bold_id);
resolver.insert("fantasy:700".to_string(), sans_bold_id);
resolver.insert("system-ui:700".to_string(), sans_bold_id);

resolver
}
Expand Down Expand Up @@ -1223,6 +1239,61 @@ mod tests {
assert!(resolver.contains_key("serif:700") && serif != u32::MAX);
}

/// P9-D2(R5072):system-ui/cursive/fantasy 与 sans-serif 共用 :700 face;
/// monospace:700 族内回退(无同族 Bold 时 = 自身,不得跨族取 sans bold)。
/// 手工加载 Liberation R+B 构造确定性环境(无字体时跳过,同 R3993 口径)。
#[test]
fn p9d2_generic_and_system_ui_bold_aliases_registered() {
let Some(data) = std::fs::read("/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf").ok() else {
eprintln!("p9d2: Liberation Sans not found, skip");
return;
};
let Some(bold) = std::fs::read("/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf").ok() else {
eprintln!("p9d2: Liberation Sans Bold not found, skip");
return;
};
let mut loader = FontLoader::new();
loader.load_font(&data).expect("load Liberation Sans");
loader.load_font(&bold).expect("load Liberation Sans Bold");
let resolver = loader.build_font_resolver();

let sans_bold = resolver
.get("sans-serif:700")
.copied()
.expect("sans-serif:700 registered");
for family in ["system-ui", "cursive", "fantasy"] {
let key = format!("{family}:700");
assert_eq!(
resolver.get(&key),
Some(&sans_bold),
"P9-D2: {key} must share the sans-serif:700 face (same family), got {:?}",
resolver.get(&key)
);
}

// 单 face mono 族(DejaVu Sans Mono,不载其 Bold):monospace:700 =
// monospace 自身——族内回退,禁止跨族取「任意族第二 face」(sans bold)。
// 注:mono 关键字解析不到任何清单族时落 default face,此时「解析族」的
// bold face 即正确答案(族语义跟随解析结果,不跟随关键字)。
let Some(mono_data) = std::fs::read("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf").ok() else {
eprintln!("p9d2: DejaVu Sans Mono not found, skip mono assertion");
return;
};
loader.load_font(&mono_data).expect("load DejaVu Sans Mono");
let resolver = loader.build_font_resolver();
let mono = resolver.get("DejaVu Sans Mono").copied().expect("mono face registered");
assert_eq!(
resolver.get("monospace"),
Some(&mono),
"P9-D2: monospace must resolve to the loaded mono face"
);
assert_eq!(
resolver.get("monospace:700"),
Some(&mono),
"P9-D2: monospace:700 must stay within the mono family when no mono Bold face exists"
);
}

#[test]
fn test_font_loader_empty() {
let loader = FontLoader::new();
Expand Down
195 changes: 192 additions & 3 deletions crates/render-foundation/src/font/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ pub struct PlatformFonts {
/// 按跨进程稳定顺序加载 primary、bold 与 fallback 字体。
pub fn load_platform_fonts() -> PlatformFonts {
let mut loader = FontLoader::new();
let primary = load_first(&mut loader, primary_font_paths(), 0, "primary");
// P9-D2 DF-1(R5072 返修):跨角色共享已载路径集合——极简环境(如仅
// fonts-dejavu-core)primary 与 bold-regular 会命中同一 DejaVuSans.ttf,二次
// 载入生成重复 face 使 family_map=[Reg,RegDup,Bold]、自动注册的 {family}:700
// 指向 RegDup(w700 落 regular,对 base 回归)。同路径只载一次,族谱由真实
// Bold face 补齐。
let mut loaded_paths = std::collections::HashSet::new();
let primary = load_first(&mut loader, primary_font_paths(), 0, "primary", &mut loaded_paths);
let Some(primary_id) = primary else {
tracing::warn!("No platform primary font found; text rendering will be limited");
return PlatformFonts {
Expand All @@ -24,7 +30,30 @@ pub fn load_platform_fonts() -> PlatformFonts {
};
};

let _ = load_first(&mut loader, bold_font_paths(), 0, "bold");
// P9-D2(R5072):primary 族同样须 [Regular, Bold] 成对——GitHub 栈含
// "Noto Sans"(先于 sans-serif),只载 NotoSans-Regular 时该族单 face,w700
// 标题命中 regular 停在栈中途(真机复验实证:补齐 sans-serif:700 后首页标题
// 仍细体)。
let _ = load_first(
&mut loader,
primary_bold_font_paths(),
0,
"primary-bold",
&mut loaded_paths,
);
// P9-D2(R5072):bold 角色须与同族 Regular 成对注册——family_map 约定首 face
// Regular、第二 face Bold(build_font_resolver 据此注册 {family} 与 {family}:700,
// CSS Fonts 4 §5.2)。只载 Bold 时该族单 face:任意字重(含 w400)都命中 Bold,
// 且族内无 :700 键(probe2 A3:"DejaVu Sans" w400 渲染粗体)。两清单同族序一致,
// 保证 Regular 先于 Bold 进 family_map。
let _ = load_first(
&mut loader,
bold_regular_font_paths(),
0,
"bold-regular",
&mut loaded_paths,
);
let _ = load_first(&mut loader, bold_font_paths(), 0, "bold", &mut loaded_paths);

// R4377:generic 族 face 与 reftest harness 对齐(R1259 serif / R1263 sans / R4373 mono
// 谱系——chromium fontconfig 默认:serif/initial = Times New Roman → Liberation Serif、
Expand All @@ -34,10 +63,17 @@ pub fn load_platform_fonts() -> PlatformFonts {
// 仅加载为 family 成员(family_map 供名匹配),**不进 fallback chain**(缺字回退行为
// 零变化);加载顺序在 primary/bold 之后、CJK chain 之前(各进程同序 → font_id 一致)。
for path in generic_family_font_paths() {
// P9-D2 DF-1:角色清单已载的同路径(如 bold-regular 兜底命中
// LiberationSans-Regular)不再二次载入——family_map 已注册该 face。
if loaded_paths.contains(*path) {
continue;
}
match std::fs::read(path) {
Ok(data) => {
if let Err(error) = loader.load_font(&data) {
tracing::debug!(path, %error, "Failed to load generic family font");
} else {
loaded_paths.insert((*path).to_string());
}
}
Err(error) => tracing::debug!(path, %error, "Generic family font not found"),
Expand Down Expand Up @@ -82,10 +118,22 @@ pub fn load_platform_fonts() -> PlatformFonts {
}
}

fn load_first(loader: &mut FontLoader, paths: &[&str], face_index: u32, role: &str) -> Option<u32> {
fn load_first(
loader: &mut FontLoader,
paths: &[&str],
face_index: u32,
role: &str,
loaded_paths: &mut std::collections::HashSet<String>,
) -> Option<u32> {
paths.iter().find_map(|path| {
// P9-D2 DF-1:同路径已被更早角色载入时跳过(重复 face 会占据 family_map
// 第二位,使自动注册的 {family}:700 指向重复 Regular 而非 Bold)。
if loaded_paths.contains(*path) {
return None;
}
let data = std::fs::read(path).ok()?;
let id = loader.load_font_at_index(&data, face_index).ok()?;
loaded_paths.insert((*path).to_string());
tracing::info!(path, font_id = id, "Loaded platform {role} font");
Some(id)
})
Expand Down Expand Up @@ -175,6 +223,53 @@ fn bold_font_paths() -> &'static [&'static str] {
}
}

/// P9-D2(R5072):bold 角色的同族 Regular face,与 `bold_font_paths` 同族序配对,
/// 先于 Bold 加载。仅覆盖 Linux(本切片证据环境);macOS/Windows 的 bold 角色同样
/// 只载 Bold——macOS 的 Arial Regular 由 `generic_family_font_paths` 在其后补载,
/// family_map 序为 [Bold, Regular],resolver["Arial"] 仍取 Bold。FIXME: 各平台
/// 族谱成对加载待清单核对后统一(对齐 harness reftest_fonts 的成对加载口径)。
fn bold_regular_font_paths() -> &'static [&'static str] {
#[cfg(target_os = "macos")]
{
&[] as &[&str]
}
#[cfg(target_os = "windows")]
{
&[] as &[&str]
}
#[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
{
&[
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/TTF/DejaVuSans.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
]
}
}

/// P9-D2(R5072):primary 族(`primary_font_paths` 前两项 NotoSans-Regular)的
/// 同族 Bold face,紧随 primary 加载,保证 family_map 序 [Regular, Bold]。仅覆盖
/// Linux(本切片证据环境);清单首项须与 primary 实际命中的族对应。macOS/Windows
/// 同病 FIXME:各平台族谱成对加载待清单核对后统一(对齐 harness reftest_fonts
/// 的成对加载口径)。
fn primary_bold_font_paths() -> &'static [&'static str] {
#[cfg(target_os = "macos")]
{
&[] as &[&str]
}
#[cfg(target_os = "windows")]
{
&[] as &[&str]
}
#[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
{
&[
"/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf",
"/usr/share/fonts/opentype/noto/NotoSans-Bold.ttf",
]
}
}

fn platform_fallback_font_paths() -> &'static [&'static str] {
#[cfg(target_os = "macos")]
{
Expand Down Expand Up @@ -306,4 +401,98 @@ mod tests {
);
}
}

/// P9-D2(R5072):bold 角色族须有完整 [Regular, Bold] 谱——w400 命中 Regular、
/// w700 命中 Bold。此前只载 DejaVuSans-Bold:family_map 单 face,"DejaVu Sans"
/// 任意字重都命中 Bold(probe2 A3)。system-ui/cursive/fantasy 的 :700 与
/// sans-serif:700 同值;monospace:700 族内回退(生产清单下 mono 族单 face)。
/// DejaVu Regular 缺失(极简环境)时跳过族谱断言。
#[test]
fn p9d2_bold_role_family_pairs_regular_with_bold() {
let platform = load_platform_fonts();
let resolver = platform.loader.build_font_resolver();

if std::path::Path::new("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf").exists() {
let regular = resolver
.get("DejaVu Sans")
.copied()
.expect("DejaVu Sans registered when its Regular face is loaded");
let bold = resolver
.get("DejaVu Sans:700")
.copied()
.expect("DejaVu Sans:700 registered (family_map second face)");
assert_ne!(
regular, bold,
"P9-D2: 'DejaVu Sans' w400 must resolve to the Regular face, not the Bold face"
);
} else {
eprintln!("p9d2: DejaVu Sans Regular not found, skip family-pair assertion");
}

// primary 族(Noto Sans):GitHub 栈含 "Noto Sans"(先于 sans-serif),
// w700 须能命中 Bold face(R5072 首页标题细体残口的回归锚)。
if std::path::Path::new("/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf").exists() {
let regular = resolver
.get("Noto Sans")
.copied()
.expect("Noto Sans registered (primary role)");
let bold = resolver
.get("Noto Sans:700")
.copied()
.expect("Noto Sans:700 registered (primary-bold companion)");
assert_ne!(
regular, bold,
"P9-D2: 'Noto Sans' w700 must reach the Bold face, not stop at Regular"
);
}

assert_eq!(
resolver.get("system-ui:700"),
resolver.get("sans-serif:700"),
"P9-D2: system-ui:700 must share the sans-serif:700 face (same family)"
);
assert_eq!(
resolver.get("cursive:700"),
resolver.get("sans-serif:700"),
"P9-D2: cursive:700 must share the sans-serif:700 face (same family)"
);
assert_eq!(
resolver.get("fantasy:700"),
resolver.get("sans-serif:700"),
"P9-D2: fantasy:700 must share the sans-serif:700 face (same family)"
);
assert_eq!(
resolver.get("monospace:700"),
resolver.get("monospace"),
"P9-D2: monospace:700 must stay within the mono family (single-face family falls back to itself)"
);
}

/// P9-D2 DF-1(R5072 返修):load_first 跨角色路径去重——同路径二次调用被跳过
/// (返回 None 且不载入)。缺陷形态:极简环境 primary 与 bold-regular 命中同一
/// DejaVuSans.ttf,重复 face 占据 family_map 第二位,自动注册的 {family}:700
/// 指向重复 Regular(w700 落细体)。本机无对应字体时跳过。
#[test]
fn p9d2_df1_load_first_dedups_already_loaded_path() {
let candidate = [
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
]
.into_iter()
.find(|p| std::path::Path::new(p).exists());
let Some(path) = candidate else {
eprintln!("p9d2 DF-1: no DejaVu/Liberation regular font on this machine, skip dedup assertion");
return;
};
let paths = [path];
let mut loader = FontLoader::new();
let mut seen = std::collections::HashSet::new();
let first = load_first(&mut loader, &paths, 0, "dedup-probe-a", &mut seen);
assert!(first.is_some(), "first load of an existing font must succeed");
let second = load_first(&mut loader, &paths, 0, "dedup-probe-b", &mut seen);
assert!(
second.is_none(),
"second load of the same path must be skipped (DF-1 dedup)"
);
}
}
55 changes: 55 additions & 0 deletions crates/webview/src/tests/async_drain_bound.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//! execute_script 的异步回调排水预算上界回归测试。
//!
//! 根因背景(service_worker_runtime 套件 leg 上下文 livelock):`webview` 的
//! `drain_next_async_callback_if_pending` 原先仅在通道空档(`try_recv` 返回
//! Empty)检查 20ms 预算。SW 轮询链(pollRegistration / pollClientMessages)
//! 以 `setTimeout(fn, 0)` 自续且每跳含宿主调用——解析耗时大于定时器线程
//! spawn——多链并行时供给压倒排水,通道永无空档,排水循环永不返回:测试
//! 线程在单次 `execute_script` 内 100% 单核空转(实测 101.6s),自身 60s
//! deadline 断言不可达,serial 锁被占,整个测试二进制挂死。本测试以 4 条
//! 永续慢跳自续链复现该形态,要求每次 `execute_script` 在有界时间内返回;
//! 回归时断言快速失败,修复后确定性通过。

use std::time::{Duration, Instant};

use crate::{WebView, WebViewConfig};

#[test]
fn execute_script_returns_with_eternal_zero_timeout_chain() {
let mut wv = WebView::new(WebViewConfig::default());
let setup = wv
.execute_script("typeof setTimeout")
.expect("setup execute_script should succeed");
assert_eq!(setup, "function", "setTimeout shim must exist in plain execute_script");

// 4 条永续慢跳链:每跳 ~3ms 同步 JS 计算后 setTimeout(f, 0)。单条链在每次
// 回调解析期间无新回调到达,通道必现空档、deadline 可兜住;多条慢链下,
// 任一回调解析期间其余链已积累新回调(spawn ~50µs ≪ 解析 ~3ms),队列
// 永不空档——模拟 pollRegistration / pollClientMessages 每跳含宿主调用
// (解析慢于线程 spawn)且多注册并行轮询的形态。
wv.execute_script(
"globalThis.__drainProbeBusy = function () {
var s = 0;
for (var i = 0; i < 200000; i++) s += Math.sqrt(i);
return s;
};
for (var i = 0; i < 4; i++) {
(function f() { globalThis.__drainProbeBusy(); setTimeout(f, 0); })();
} undefined;",
)
.expect("chain setup should succeed");

let overall = Instant::now() + Duration::from_secs(30);
for i in 0..50 {
let call_start = Instant::now();
let result = wv.execute_script("1+1");
assert!(result.is_ok(), "execute_script call {i} failed: {result:?}");
let elapsed = call_start.elapsed();
assert!(
elapsed < Duration::from_secs(2),
"execute_script call {i} took {elapsed:?} — async-callback drain exceeded its budget"
);
assert!(Instant::now() < overall, "probe overall budget exceeded");
std::thread::sleep(Duration::from_millis(5));
}
}
1 change: 1 addition & 0 deletions crates/webview/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! WebView 测试模块。

mod advanced;
mod async_drain_bound;
mod basic;
mod builder_nav;
mod cache_storage;
Expand Down
Loading
Loading