From a4d877c702d5c7ec2aca1aca3f50c6c1ad32172d Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 02:34:41 +0200 Subject: [PATCH 01/33] LibWeb: Count crossings of the style system's FFI boundary The style port needs to measure which C++/Rust seams dominate before removing them. Add a per-operation counter table to the CSS Rust crate, bumped at every entry point into the style core and at every callback the core makes back into C++: the longhand driver stages, cascade origin stages, declaration application, shorthand expansion, selector DOM callbacks, calc operations, style value shell retain/release, and style group clone/free. The counters are always-on relaxed atomics; one increment per crossing is negligible next to the crossing itself. The counters are exposed to tests as internals.styleFfiCounters() and internals.resetStyleFfiCounters(), and a new test covers that surface. Baseline measurements on deterministic workloads show the per-longhand computation loop dominating every other boundary by two orders of magnitude: roughly 1330 crossings per recomputed element, of which 333 are compute_and_store callbacks, ~330 are initial and inherited value fetches, and ~980 are per-longhand style value queries made from C++. --- Libraries/LibWeb/CSS/Rust/build.rs | 1 + Libraries/LibWeb/CSS/Rust/src/calc.rs | 30 +++++ .../CSS/Rust/src/cascaded_properties.rs | 18 ++- .../LibWeb/CSS/Rust/src/computed_values.rs | 2 + Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs | 121 ++++++++++++++++++ Libraries/LibWeb/CSS/Rust/src/lib.rs | 1 + .../LibWeb/CSS/Rust/src/selector_engine.rs | 58 +++++++-- .../LibWeb/CSS/Rust/src/style_compute.rs | 46 ++++++- Libraries/LibWeb/CSS/Rust/src/style_value.rs | 7 + .../CSS/StyleValues/RustStyleValueHandle.h | 1 + Libraries/LibWeb/Internals/Internals.cpp | 21 +++ Libraries/LibWeb/Internals/Internals.h | 2 + Libraries/LibWeb/Internals/Internals.idl | 4 + .../Text/expected/css/style-ffi-counters.txt | 4 + .../Text/input/css/style-ffi-counters.html | 23 ++++ 15 files changed, 323 insertions(+), 16 deletions(-) create mode 100644 Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs create mode 100644 Tests/LibWeb/Text/expected/css/style-ffi-counters.txt create mode 100644 Tests/LibWeb/Text/input/css/style-ffi-counters.html diff --git a/Libraries/LibWeb/CSS/Rust/build.rs b/Libraries/LibWeb/CSS/Rust/build.rs index 7c5dc786c8cf0..e1f928161dcd6 100644 --- a/Libraries/LibWeb/CSS/Rust/build.rs +++ b/Libraries/LibWeb/CSS/Rust/build.rs @@ -590,6 +590,7 @@ fn main() -> Result<(), Box> { &[ manifest_dir.join("src/style_value.rs"), manifest_dir.join("src/calc.rs"), + manifest_dir.join("src/ffi_stats.rs"), ], &out_dir, &ffi_out_dir, diff --git a/Libraries/LibWeb/CSS/Rust/src/calc.rs b/Libraries/LibWeb/CSS/Rust/src/calc.rs index c92b46c94d914..44574f3a5492e 100644 --- a/Libraries/LibWeb/CSS/Rust/src/calc.rs +++ b/Libraries/LibWeb/CSS/Rust/src/calc.rs @@ -454,6 +454,7 @@ pub unsafe extern "C" fn rust_numeric_type_operate( first: *const FfiNumericType, second: *const FfiNumericType, ) -> FfiNumericType { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcOperationEntry); crate::abort_on_panic(|| { let first = unsafe { &*first }.to_calc(); let second = unsafe { &*second }.to_calc(); @@ -856,6 +857,7 @@ unsafe fn children_from_raw(children: *const *const CalcNode, count: usize) -> V /// order number, angle, flex, frequency, length, percentage, resolution, time. #[unsafe(no_mangle)] pub extern "C" fn rust_calc_node_create_numeric_dimension(kind: u8, value: f64, unit: u8) -> *const CalcNode { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcNodeBuildEntry); crate::abort_on_panic(|| { let numeric = match kind { 0 => CalcNumericValue::Number { @@ -877,6 +879,7 @@ pub extern "C" fn rust_calc_node_create_numeric_dimension(kind: u8, value: f64, #[unsafe(no_mangle)] pub extern "C" fn rust_calc_node_create_channel_keyword(channel: u8) -> *const CalcNode { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcNodeBuildEntry); crate::abort_on_panic(|| handle(CalcNode::ChannelKeyword(channel))) } @@ -891,6 +894,7 @@ pub unsafe extern "C" fn rust_calc_node_create_variadic( children: *const *const CalcNode, count: usize, ) -> *const CalcNode { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcNodeBuildEntry); crate::abort_on_panic(|| { let children = unsafe { children_from_raw(children, count) }; let node = match kind { @@ -913,6 +917,7 @@ pub unsafe extern "C" fn rust_calc_node_create_variadic( /// `child` must be a valid transferred handle. #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_calc_node_create_unary(kind: u8, child: *const CalcNode) -> *const CalcNode { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcNodeBuildEntry); crate::abort_on_panic(|| { let child = unsafe { Arc::from_raw(child) }; let node = match kind { @@ -945,6 +950,7 @@ pub unsafe extern "C" fn rust_calc_node_create_binary( first: *const CalcNode, second: *const CalcNode, ) -> *const CalcNode { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcNodeBuildEntry); crate::abort_on_panic(|| { let first = unsafe { Arc::from_raw(first) }; let second = unsafe { Arc::from_raw(second) }; @@ -980,6 +986,7 @@ pub unsafe extern "C" fn rust_calc_node_create_clamp( center: *const CalcNode, max: *const CalcNode, ) -> *const CalcNode { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcNodeBuildEntry); crate::abort_on_panic(|| { handle(CalcNode::Clamp { min: unsafe { Arc::from_raw(min) }, @@ -998,6 +1005,7 @@ pub unsafe extern "C" fn rust_calc_node_create_progress( from: *const CalcNode, to: *const CalcNode, ) -> *const CalcNode { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcNodeBuildEntry); crate::abort_on_panic(|| { handle(CalcNode::Progress { no_clamp, @@ -1016,6 +1024,7 @@ pub unsafe extern "C" fn rust_calc_node_create_round( value: *const CalcNode, interval: *const CalcNode, ) -> *const CalcNode { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcNodeBuildEntry); crate::abort_on_panic(|| { handle(CalcNode::Round { strategy, @@ -1035,6 +1044,7 @@ pub unsafe extern "C" fn rust_calc_node_create_random( step: *const CalcNode, sharing: *const std::ffi::c_void, ) -> *const CalcNode { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcNodeBuildEntry); crate::abort_on_panic(|| { handle(CalcNode::Random { min: unsafe { Arc::from_raw(min) }, @@ -1056,6 +1066,7 @@ pub unsafe extern "C" fn rust_calc_node_create_non_math_function( value: *const std::ffi::c_void, numeric_type: *const FfiNumericType, ) -> *const CalcNode { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcNodeBuildEntry); crate::abort_on_panic(|| { handle(CalcNode::NonMathFunction { value: unsafe { RetainedStyleValue::from_shell_pointer(value) }, @@ -1068,6 +1079,7 @@ pub unsafe extern "C" fn rust_calc_node_create_non_math_function( /// `node` must be a valid transferred handle; this releases it. #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_calc_node_release(node: *const CalcNode) { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcNodeRetainReleaseEntry); crate::abort_on_panic(|| drop(unsafe { Arc::from_raw(node) })); } @@ -1078,6 +1090,7 @@ pub unsafe extern "C" fn rust_calc_node_release(node: *const CalcNode) { /// `node` must be a valid calculation node pointer. #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_calc_node_retain(node: *const CalcNode) { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcNodeRetainReleaseEntry); crate::abort_on_panic(|| unsafe { Arc::increment_strong_count(node) }); } @@ -1095,6 +1108,7 @@ pub unsafe extern "C" fn rust_calc_node_determine_type( resolve_as_is_number: bool, resolve_as_base: u8, ) -> FfiNumericType { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcNodeQueryEntry); crate::abort_on_panic(|| { let resolve_as = resolve_as_from_fields(has_percentages_resolve_as, resolve_as_is_number, resolve_as_base); let percentage_leaf_type = percentage_leaf_type_for(resolve_as); @@ -1108,6 +1122,7 @@ pub unsafe extern "C" fn rust_calc_node_determine_type( /// `node` must be a valid calculation node pointer. #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_calc_node_contains_percentage(node: *const CalcNode) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcNodeQueryEntry); crate::abort_on_panic(|| unsafe { &*node }.contains_percentage()) } @@ -2720,6 +2735,7 @@ pub unsafe extern "C" fn rust_calc_resolve( context: *const FfiCalcResolutionContext, apply_censoring_and_clamping: bool, ) -> FfiResolvedCalc { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcOperationEntry); use crate::style_value::StyleValueData; crate::abort_on_panic(|| { let StyleValueData::Calculated { @@ -2824,11 +2840,13 @@ struct CalcSerializer<'a> { #[allow(dead_code)] impl CalcSerializer<'_> { fn literal(&self, text: &str) { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcSerializationCallback); unsafe { (self.callbacks.append_literal)(self.callbacks.context, text.as_ptr(), text.len()) }; } fn leaf(&self, value: CalcNumericValue) { let (kind, raw, unit) = value.leaf_parts(); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcSerializationCallback); unsafe { (self.callbacks.append_numeric_leaf)(self.callbacks.context, kind, raw, unit, self.resolved_mode) }; } @@ -2960,6 +2978,7 @@ impl CalcSerializer<'_> { } = &**node { self.literal("random("); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcSerializationCallback); let appended = unsafe { (self.callbacks.append_style_value)(self.callbacks.context, sharing.shell_pointer()) }; if appended { @@ -3103,11 +3122,13 @@ impl CalcSerializer<'_> { // rules for it and return the result. CalcNode::Numeric(value) => self.leaf(*value), CalcNode::NonMathFunction { value, .. } => { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcSerializationCallback); unsafe { (self.callbacks.append_style_value)(self.callbacks.context, value.shell_pointer()) }; } // AD-HOC: ChannelKeyword nodes, used for relative-color syntax, serialize directly as // the keyword name. CalcNode::ChannelKeyword(channel) => unsafe { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcSerializationCallback); (self.callbacks.append_channel_name)(self.callbacks.context, *channel); }, // 4. If root is a Negate node, let s be a string initially containing "(-1 * ". @@ -3223,6 +3244,7 @@ pub unsafe extern "C" fn rust_calc_serialize( callbacks: *const FfiCalcSerializationCallbacks, resolved_mode: bool, ) { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcOperationEntry); use crate::style_value::StyleValueData; crate::abort_on_panic(|| { let StyleValueData::Calculated { @@ -3446,6 +3468,7 @@ pub unsafe extern "C" fn rust_calc_equals( b: *const std::ffi::c_void, ) -> bool, ) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcOperationEntry); use crate::style_value::StyleValueData; crate::abort_on_panic(|| { let tree_of = |data: *const std::ffi::c_void| { @@ -3504,6 +3527,7 @@ fn node_kind_code(node: &CalcNode) -> u8 { /// `node` must be a valid calculation node pointer for all read functions. #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_calc_node_kind(node: *const CalcNode) -> u8 { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcNodeQueryEntry); crate::abort_on_panic(|| node_kind_code(unsafe { &*node })) } @@ -3518,6 +3542,7 @@ pub unsafe extern "C" fn rust_calc_node_children( out_children: *mut *const CalcNode, capacity: usize, ) -> usize { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcNodeQueryEntry); crate::abort_on_panic(|| { let mut count = 0; unsafe { &*node }.for_each_child(&mut |child| { @@ -3541,6 +3566,7 @@ pub unsafe extern "C" fn rust_calc_node_numeric_leaf( out_value: *mut f64, out_unit: *mut u8, ) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcNodeQueryEntry); crate::abort_on_panic(|| { let CalcNode::Numeric(value) = (unsafe { &*node }) else { return false; @@ -3562,6 +3588,7 @@ pub unsafe extern "C" fn rust_calc_node_numeric_leaf( /// `node` must be a valid calculation node pointer. #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_calc_node_style_value(node: *const CalcNode) -> *const std::ffi::c_void { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcNodeQueryEntry); crate::abort_on_panic(|| match unsafe { &*node } { CalcNode::Random { sharing, .. } => sharing.shell_pointer(), CalcNode::NonMathFunction { value, .. } => value.shell_pointer(), @@ -3579,6 +3606,7 @@ pub unsafe extern "C" fn rust_calc_node_numeric_type( calculated: *const std::ffi::c_void, node: *const CalcNode, ) -> FfiNumericType { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcNodeQueryEntry); use crate::style_value::StyleValueData; crate::abort_on_panic(|| { let StyleValueData::Calculated { @@ -3619,6 +3647,7 @@ pub unsafe extern "C" fn rust_calc_simplify_tree( resolve_as_is_number: bool, resolve_as_base: u8, ) -> *const CalcNode { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcOperationEntry); crate::abort_on_panic(|| { let context = unsafe { &*context }; unsafe { Arc::increment_strong_count(root) }; @@ -3652,6 +3681,7 @@ pub unsafe extern "C" fn rust_calc_absolutize( calculated: *const std::ffi::c_void, context: *const FfiCalcResolutionContext, ) -> FfiAbsolutizedCalc { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CalcOperationEntry); use crate::style_value::StyleValueData; crate::abort_on_panic(|| { let StyleValueData::Calculated { diff --git a/Libraries/LibWeb/CSS/Rust/src/cascaded_properties.rs b/Libraries/LibWeb/CSS/Rust/src/cascaded_properties.rs index e265dd6af7079..028ffe4a0f465 100644 --- a/Libraries/LibWeb/CSS/Rust/src/cascaded_properties.rs +++ b/Libraries/LibWeb/CSS/Rust/src/cascaded_properties.rs @@ -294,6 +294,7 @@ pub unsafe extern "C" fn rust_cascaded_properties_property( store: *const CascadedPropertyStore, property_id: u16, ) -> *const c_void { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CascadedStoreQueryEntry); abort_on_panic(|| match unsafe { &*store }.last_entry(property_id) { Some(entry) => entry.value.shell_pointer(), None => std::ptr::null(), @@ -309,6 +310,7 @@ pub unsafe extern "C" fn rust_cascaded_properties_source_slot( store: *const CascadedPropertyStore, property_id: u16, ) -> i64 { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CascadedStoreQueryEntry); abort_on_panic(|| match unsafe { &*store }.last_entry(property_id) { Some(entry) => entry.source_slot as i64, None => -1, @@ -367,6 +369,7 @@ pub unsafe extern "C" fn rust_cascaded_properties_apply_property_list( unset_data: *const c_void, callbacks: *const FfiCascadeApplicationCallbacks, ) { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CascadeApplyDeclarationListEntry); abort_on_panic(|| { let store = unsafe { &mut *store }; let callbacks = unsafe { &*callbacks }; @@ -387,6 +390,7 @@ pub unsafe extern "C" fn rust_cascaded_properties_apply_property_list( let declared_value = unsafe { &*(declaration.data as *const StyleValueData) }; let declared_is_unresolved = matches!(declared_value, StyleValueData::Unresolved { .. }); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CascadePropertyDisallowedCallback); if unsafe { (callbacks.is_property_disallowed)(context, declaration.property_id) } && !declared_is_unresolved { @@ -401,7 +405,9 @@ pub unsafe extern "C" fn rust_cascaded_properties_apply_property_list( let mut data = declaration.data; if declared_is_unresolved { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CascadeResolveUnresolvedCallback); shell = unsafe { (callbacks.resolve_unresolved)(context, declaration.property_id, shell) }; + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CascadeDataOfCallback); data = unsafe { (callbacks.data_of)(context, shell) }; } @@ -432,12 +438,19 @@ pub unsafe extern "C" fn rust_cascaded_properties_apply_property_list( ); expand_shorthands_with( - &|shell| unsafe { (callbacks.data_of)(context, shell) }, - &|shell| unsafe { (callbacks.create_pending_substitution)(context, shell) }, + &|shell| { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CascadeDataOfCallback); + unsafe { (callbacks.data_of)(context, shell) } + }, + &|shell| { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CascadePendingSubstitutionCallback); + unsafe { (callbacks.create_pending_substitution)(context, shell) } + }, declaration.property_id, shell, data, &mut |longhand_id, longhand_shell, longhand_data| { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CascadePropertyDisallowedCallback); if unsafe { (callbacks.is_property_disallowed)(context, longhand_id) } { return; } @@ -486,6 +499,7 @@ pub unsafe extern "C" fn rust_cascaded_properties_apply_property_list( source_shadow_root_identity, ); if slot >= 0 { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CascadeSourceSlotCallback); unsafe { (callbacks.assign_source_slot)(context, slot as u32) }; } } diff --git a/Libraries/LibWeb/CSS/Rust/src/computed_values.rs b/Libraries/LibWeb/CSS/Rust/src/computed_values.rs index 02b964e7c7607..0bacd694a553f 100644 --- a/Libraries/LibWeb/CSS/Rust/src/computed_values.rs +++ b/Libraries/LibWeb/CSS/Rust/src/computed_values.rs @@ -138,6 +138,7 @@ pub unsafe extern "C" fn rust_style_group_registry_register( /// `source` must be a valid payload of the same group type. #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_style_group_clone(group_index: usize, source: *const c_void) -> *mut c_void { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::StyleGroupCloneEntry); abort_on_panic(|| unsafe { let table = vtable(group_index); let payload = allocate_payload(table, 1); @@ -153,6 +154,7 @@ pub unsafe extern "C" fn rust_style_group_clone(group_index: usize, source: *con /// references, and must not be a static default payload. #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_style_group_free(group_index: usize, payload: *mut c_void) { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::StyleGroupFreeEntry); abort_on_panic(|| unsafe { let table = vtable(group_index); debug_assert!(refcount_of(payload, table.align).load(Ordering::Relaxed) == 0); diff --git a/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs b/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs new file mode 100644 index 0000000000000..a9657222c1fcc --- /dev/null +++ b/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +//! Counters for the style system's FFI boundary crossings. +//! +//! Every entry point into the Rust style core and every callback it makes into +//! C++ bumps one counter, grouped by operation rather than by symbol. The +//! counters measure how coarse the boundary is (calls per element, per +//! declaration, per longhand) so that boundary-removal work can compare +//! before/after counts on deterministic workloads. Reading and resetting is +//! exposed to C++ for the `internals.styleFfiCounters()` test surface. +//! +//! The counters are always compiled in: one relaxed atomic increment per +//! crossing is negligible next to the crossing itself. + +use std::sync::atomic::{AtomicU64, Ordering}; + +macro_rules! define_ffi_ops { + ($($variant:ident => $name:literal,)+) => { + /// One countable boundary operation. Entries are C++ calls into the + /// Rust core; callbacks are calls the core makes back into C++. + #[derive(Clone, Copy)] + #[repr(usize)] + pub(crate) enum FfiOp { + $($variant,)+ + } + + const FFI_OP_COUNT: usize = 0 $(+ { let _ = FfiOp::$variant; 1 })+; + + /// Nul-terminated so the name can cross the FFI as a C string. + static FFI_OP_NAMES: [&str; FFI_OP_COUNT] = [$(concat!($name, "\0"),)+]; + }; +} + +define_ffi_ops! { + // Entries: C++ -> Rust. + SelectorMatchEntry => "selectorMatchEntries", + CascadeOriginDriverEntry => "cascadeOriginDriverEntries", + CascadeApplyDeclarationListEntry => "cascadeApplyDeclarationListEntries", + CascadedStoreQueryEntry => "cascadedStoreQueryEntries", + LonghandDriverEntry => "longhandDriverEntries", + ShorthandExpansionEntry => "shorthandExpansionEntries", + NestedPropertyComputeEntry => "nestedPropertyComputeEntries", + CalcOperationEntry => "calcOperationEntries", + CalcNodeBuildEntry => "calcNodeBuildEntries", + CalcNodeQueryEntry => "calcNodeQueryEntries", + CalcNodeRetainReleaseEntry => "calcNodeRetainReleaseEntries", + StyleValueCreateEntry => "styleValueCreateEntries", + StyleValueDestroyEntry => "styleValueDestroyEntries", + StyleValueQueryEntry => "styleValueQueryEntries", + StyleGroupCloneEntry => "styleGroupCloneEntries", + StyleGroupFreeEntry => "styleGroupFreeEntries", + // Callbacks: Rust -> C++. + SelectorSimpleSelectorCallback => "selectorSimpleSelectorCallbacks", + SelectorTreeNavigationCallback => "selectorTreeNavigationCallbacks", + SelectorMetadataCallback => "selectorMetadataCallbacks", + CascadeStageCallback => "cascadeStageCallbacks", + CascadePropertyDisallowedCallback => "cascadePropertyDisallowedCallbacks", + CascadeResolveUnresolvedCallback => "cascadeResolveUnresolvedCallbacks", + CascadeDataOfCallback => "cascadeDataOfCallbacks", + CascadePendingSubstitutionCallback => "cascadePendingSubstitutionCallbacks", + CascadeSourceSlotCallback => "cascadeSourceSlotCallbacks", + ShorthandSetLonghandCallback => "shorthandSetLonghandCallbacks", + LonghandCascadedValueCallback => "longhandCascadedValueCallbacks", + LonghandInheritedValueCallback => "longhandInheritedValueCallbacks", + LonghandInitialValueCallback => "longhandInitialValueCallbacks", + LonghandComputeAndStoreCallback => "longhandComputeAndStoreCallbacks", + LonghandWritingModeCallback => "longhandWritingModeCallbacks", + CalcSerializationCallback => "calcSerializationCallbacks", + StyleValueShellRetainCallback => "styleValueShellRetainCallbacks", + StyleValueShellReleaseCallback => "styleValueShellReleaseCallbacks", + StringRetainReleaseCallback => "stringRetainReleaseCallbacks", +} + +static COUNTERS: [AtomicU64; FFI_OP_COUNT] = [const { AtomicU64::new(0) }; FFI_OP_COUNT]; + +#[inline] +pub(crate) fn bump(op: FfiOp) { + COUNTERS[op as usize].fetch_add(1, Ordering::Relaxed); +} + +#[inline] +pub(crate) fn bump_by(op: FfiOp, count: u64) { + COUNTERS[op as usize].fetch_add(count, Ordering::Relaxed); +} + +/// Returns the number of FFI boundary counters. +#[unsafe(no_mangle)] +pub extern "C" fn rust_style_ffi_counter_count() -> usize { + FFI_OP_COUNT +} + +/// Returns the nul-terminated name of the counter at `index`. +#[unsafe(no_mangle)] +pub extern "C" fn rust_style_ffi_counter_name(index: usize) -> *const u8 { + FFI_OP_NAMES[index].as_ptr() +} + +/// Returns the current value of the counter at `index`. +#[unsafe(no_mangle)] +pub extern "C" fn rust_style_ffi_counter_value(index: usize) -> u64 { + COUNTERS[index].load(Ordering::Relaxed) +} + +/// Resets every counter to zero. +#[unsafe(no_mangle)] +pub extern "C" fn rust_style_ffi_counters_reset() { + for counter in &COUNTERS { + counter.store(0, Ordering::Relaxed); + } +} + +/// Notes the adoption of a Rust style value allocation by a C++ shell; called +/// from the C++ side where shell construction funnels through one place. +#[unsafe(no_mangle)] +pub extern "C" fn rust_style_ffi_note_style_value_created() { + bump(FfiOp::StyleValueCreateEntry); +} diff --git a/Libraries/LibWeb/CSS/Rust/src/lib.rs b/Libraries/LibWeb/CSS/Rust/src/lib.rs index 6c0eebe2fd8ce..917830c86a37e 100644 --- a/Libraries/LibWeb/CSS/Rust/src/lib.rs +++ b/Libraries/LibWeb/CSS/Rust/src/lib.rs @@ -13,6 +13,7 @@ pub mod cascaded_properties; pub mod computed_values; pub mod css_pixels; mod css_tokenizer; +pub mod ffi_stats; pub mod property_metadata; mod selector_engine; pub mod style_compute; diff --git a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs index 9c457cedf87e7..d9a862055022e 100644 --- a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs +++ b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs @@ -1731,6 +1731,7 @@ impl<'a> SelectorDom for FfiDom<'a> { type Element = FfiNode<'a>; fn matches_universal_selector(&mut self, element: FfiNode<'a>, name: &QualifiedName) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); // SAFETY: `FfiDom` guarantees that the context, element, and retained simple selector // remain valid for the duration of matching. unsafe { @@ -1748,6 +1749,7 @@ impl<'a> SelectorDom for FfiDom<'a> { name: &QualifiedName, mode: TagNameMatchingMode, ) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); // SAFETY: `FfiDom` guarantees that the context, element, and retained simple selector // remain valid for the duration of matching. unsafe { @@ -1761,18 +1763,21 @@ impl<'a> SelectorDom for FfiDom<'a> { } fn matches_id_selector(&mut self, element: FfiNode<'a>, id: &NameSelector) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); // SAFETY: `FfiDom` guarantees that the element and retained simple selector remain valid // for the duration of matching. unsafe { selector_ffi_matches_id(element.as_element_pointer(), id.cxx_simple_selector.as_ptr()) } } fn matches_class_selector(&mut self, element: FfiNode<'a>, class_name: &NameSelector) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); // SAFETY: `FfiDom` guarantees that the element and retained simple selector remain valid // for the duration of matching. unsafe { selector_ffi_matches_class(element.as_element_pointer(), class_name.cxx_simple_selector.as_ptr()) } } fn matches_attribute_selector(&mut self, element: FfiNode<'a>, attribute: &AttributeSelector) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); // SAFETY: `FfiDom` guarantees that the context, element, and retained simple selector // remain valid for the duration of matching. unsafe { @@ -1787,33 +1792,39 @@ impl<'a> SelectorDom for FfiDom<'a> { fn matches_pseudo_class_state(&mut self, element: FfiNode<'a>, pseudo_class: &PseudoClassSelector) -> bool { match pseudo_class.pseudo_class { PseudoClassType::Lang => pseudo_class.languages.iter().any(|language| { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); // SAFETY: `FfiDom` guarantees that the element remains valid, and the string // view is borrowed from `language` for this callback only. unsafe { selector_ffi_matches_language(element.as_element_pointer(), ffi_string_view(language)) } }), PseudoClassType::Dir => match pseudo_class.direction { - // SAFETY: `FfiDom` guarantees that the element remains valid for matching. - Some(Direction::LeftToRight) => unsafe { - selector_ffi_matches_direction(element.as_element_pointer(), FfiDirection::LeftToRight) - }, - // SAFETY: `FfiDom` guarantees that the element remains valid for matching. - Some(Direction::RightToLeft) => unsafe { - selector_ffi_matches_direction(element.as_element_pointer(), FfiDirection::RightToLeft) - }, + Some(Direction::LeftToRight) => { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); + // SAFETY: `FfiDom` guarantees that the element remains valid for matching. + unsafe { selector_ffi_matches_direction(element.as_element_pointer(), FfiDirection::LeftToRight) } + } + Some(Direction::RightToLeft) => { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); + // SAFETY: `FfiDom` guarantees that the element remains valid for matching. + unsafe { selector_ffi_matches_direction(element.as_element_pointer(), FfiDirection::RightToLeft) } + } _ => false, }, PseudoClassType::State => { - pseudo_class.identifier.is_some() + pseudo_class.identifier.is_some() && { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); // SAFETY: `FfiDom` guarantees that the element and retained simple selector // remain valid for the duration of matching. - && unsafe { + unsafe { selector_ffi_matches_state( element.as_element_pointer(), pseudo_class.cxx_simple_selector.as_ptr(), ) } + } } PseudoClassType::Heading => { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); // SAFETY: `FfiDom` guarantees that the element remains valid, and the levels array // remains valid for this callback. unsafe { @@ -1825,6 +1836,7 @@ impl<'a> SelectorDom for FfiDom<'a> { } } _ => { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); // SAFETY: `FfiDom` guarantees that the element remains valid for matching. unsafe { selector_ffi_matches_pseudo_class(element.as_element_pointer(), pseudo_class.pseudo_class as u8) @@ -1834,6 +1846,7 @@ impl<'a> SelectorDom for FfiDom<'a> { } fn parent_element(&mut self, element: FfiNode<'a>, shadow_host: Option>) -> Option> { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); // SAFETY: `FfiDom` guarantees that the input handles remain valid. The callback returns // either null or another live element borrowed for the same lifetime. unsafe { @@ -1845,36 +1858,42 @@ impl<'a> SelectorDom for FfiDom<'a> { } fn parent_element_in_light_tree(&mut self, element: FfiNode<'a>) -> Option> { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); // SAFETY: `FfiDom` guarantees that the input remains valid. The callback returns either // null or another live element borrowed for the same lifetime. unsafe { self.element(selector_ffi_parent_element_in_light_tree(element.as_element_pointer())) } } fn previous_element_sibling(&mut self, element: FfiNode<'a>) -> Option> { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); // SAFETY: `FfiDom` guarantees that the input remains valid. The callback returns either // null or another live element borrowed for the same lifetime. unsafe { self.element(selector_ffi_previous_element_sibling(element.as_element_pointer())) } } fn next_element_sibling(&mut self, element: FfiNode<'a>) -> Option> { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); // SAFETY: `FfiDom` guarantees that the input remains valid. The callback returns either // null or another live element borrowed for the same lifetime. unsafe { self.element(selector_ffi_next_element_sibling(element.as_element_pointer())) } } fn first_element_child(&mut self, element: FfiNode<'a>) -> Option> { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); // SAFETY: `FfiDom` guarantees that the input remains valid. The callback returns either // null or another live element borrowed for the same lifetime. unsafe { self.element(selector_ffi_first_element_child(element.as_element_pointer())) } } fn first_element_descendant(&mut self, element: FfiNode<'a>) -> Option> { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); // SAFETY: `FfiDom` guarantees that the input remains valid. The callback returns either // null or another live element borrowed for the same lifetime. unsafe { self.element(selector_ffi_first_element_descendant(element.as_element_pointer())) } } fn next_element_descendant(&mut self, element: FfiNode<'a>, root: FfiNode<'a>) -> Option> { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); // SAFETY: `FfiDom` guarantees that both element handles remain valid for this call. The // callback returns either null or another live element borrowed for the same lifetime. unsafe { @@ -1886,26 +1905,31 @@ impl<'a> SelectorDom for FfiDom<'a> { } fn has_no_element_or_nonempty_text_children(&mut self, element: FfiNode<'a>) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); // SAFETY: `FfiDom` guarantees that the element remains valid for matching. unsafe { selector_ffi_has_no_element_or_nonempty_text_children(element.as_element_pointer()) } } fn has_same_type(&mut self, first: FfiNode<'a>, second: FfiNode<'a>) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); // SAFETY: `FfiDom` guarantees that both elements remain valid for matching. unsafe { selector_ffi_has_same_type(first.as_element_pointer(), second.as_element_pointer()) } } fn is_document_root(&mut self, element: FfiNode<'a>) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); // SAFETY: `FfiDom` guarantees that the element remains valid for matching. unsafe { selector_ffi_is_document_root(element.as_element_pointer()) } } fn is_shadow_tree_slot(&mut self, element: FfiNode<'a>) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); // SAFETY: `FfiDom` guarantees that the element remains valid for matching. unsafe { selector_ffi_is_shadow_tree_slot(element.as_element_pointer()) } } fn slotted_parent(&mut self, element: FfiNode<'a>) -> Option<(FfiNode<'a>, Option>)> { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); // SAFETY: `FfiDom` guarantees that the context and element remain valid. The callback // returns null or live elements borrowed for the same lifetime. unsafe { self.element_and_shadow_host(selector_ffi_slotted_parent(self.context, element.as_element_pointer())) } @@ -1918,6 +1942,7 @@ impl<'a> SelectorDom for FfiDom<'a> { allow_same_shadow_root_scope: bool, shadow_host: Option>, ) -> Option<(FfiNode<'a>, Option>)> { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); let identifiers = identifiers .iter() .map(|identifier| ffi_string_view(identifier)) @@ -1941,6 +1966,7 @@ impl<'a> SelectorDom for FfiDom<'a> { if !self.collects_selector_involvement_metadata { return; } + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorMetadataCallback); // SAFETY: `FfiDom` guarantees that the context and element remain valid for matching. unsafe { selector_ffi_note_structural_pseudo_class(self.context, element.as_element_pointer(), pseudo_class as u8); @@ -1951,6 +1977,7 @@ impl<'a> SelectorDom for FfiDom<'a> { if !self.collects_selector_involvement_metadata { return; } + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorMetadataCallback); // SAFETY: `FfiDom` guarantees that the context and element remain valid for matching. unsafe { selector_ffi_note_has_pseudo_class(self.context, element.as_element_pointer()) } } @@ -1964,6 +1991,7 @@ impl<'a> SelectorDom for FfiDom<'a> { if !self.collects_selector_involvement_metadata { return; } + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorMetadataCallback); // SAFETY: `FfiDom` guarantees that the context and element remain valid for matching. unsafe { selector_ffi_note_sibling_combinator( @@ -1979,6 +2007,7 @@ impl<'a> SelectorDom for FfiDom<'a> { if !self.collects_selector_involvement_metadata { return; } + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorMetadataCallback); // SAFETY: `FfiDom` guarantees that the context and anchor remain valid for matching. unsafe { selector_ffi_note_has_sibling_combinator_anchor(self.context, anchor.as_element_pointer()) } } @@ -1987,6 +2016,7 @@ impl<'a> SelectorDom for FfiDom<'a> { if !self.collects_selector_involvement_metadata { return; } + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorMetadataCallback); // SAFETY: `FfiDom` guarantees that the context and element remain valid for matching. unsafe { selector_ffi_note_has_sibling_combinator_element(self.context, element.as_element_pointer()) } } @@ -1995,6 +2025,7 @@ impl<'a> SelectorDom for FfiDom<'a> { if !self.collects_selector_involvement_metadata || !self.inside_has_argument { return; } + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorMetadataCallback); // SAFETY: `FfiDom` guarantees that the context and element remain valid for matching. unsafe { selector_ffi_note_has_scope_element(self.context, element.as_element_pointer()) } } @@ -2004,6 +2035,7 @@ impl<'a> SelectorDom for FfiDom<'a> { } fn enter_has_argument_matching(&mut self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorMetadataCallback); let previous_value = self.inside_has_argument; self.inside_has_argument = true; // SAFETY: `FfiDom` guarantees that the context remains valid for matching. @@ -2012,12 +2044,14 @@ impl<'a> SelectorDom for FfiDom<'a> { } fn leave_has_argument_matching(&mut self, previous_value: bool) { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorMetadataCallback); self.inside_has_argument = previous_value; // SAFETY: `FfiDom` guarantees that the context remains valid for matching. unsafe { selector_ffi_set_inside_has_argument(self.context, previous_value) } } fn has_cache_get(&mut self, selector_id: u64, anchor: FfiNode<'a>) -> Option { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorMetadataCallback); // SAFETY: `FfiDom` guarantees that the context and anchor remain valid for matching. match unsafe { selector_ffi_has_cache_get(self.context, selector_id, anchor.as_element_pointer()) } { HasCacheResult::NotCached => None, @@ -2027,11 +2061,13 @@ impl<'a> SelectorDom for FfiDom<'a> { } fn has_cache_set(&mut self, selector_id: u64, anchor: FfiNode<'a>, result: bool) { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorMetadataCallback); // SAFETY: `FfiDom` guarantees that the context and anchor remain valid for matching. unsafe { selector_ffi_has_cache_set(self.context, selector_id, anchor.as_element_pointer(), result) } } fn should_reject_has_argument(&mut self, selector: &CompiledSelector, anchor: FfiNode<'a>) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorMetadataCallback); // SAFETY: `FfiDom` guarantees that the context, retained selector, and anchor remain valid // for matching. unsafe { @@ -2308,6 +2344,7 @@ pub unsafe extern "C" fn rust_selector_matches( collects_selector_involvement_metadata: bool, inside_has_argument: bool, ) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorMatchEntry); abort_on_panic(|| { assert!(!selector.is_null()); assert!(!element.is_null()); @@ -2355,6 +2392,7 @@ pub unsafe extern "C" fn rust_selector_matches_originating_element( collects_selector_involvement_metadata: bool, inside_has_argument: bool, ) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorMatchEntry); abort_on_panic(|| { assert!(!selector.is_null()); assert!(!element.is_null()); diff --git a/Libraries/LibWeb/CSS/Rust/src/style_compute.rs b/Libraries/LibWeb/CSS/Rust/src/style_compute.rs index abb7209bb9a39..c08cf3c18f34e 100644 --- a/Libraries/LibWeb/CSS/Rust/src/style_compute.rs +++ b/Libraries/LibWeb/CSS/Rust/src/style_compute.rs @@ -264,6 +264,7 @@ pub unsafe extern "C" fn rust_absolutize_length( unit: u8, context: *const FfiLengthResolutionContext, ) -> FfiAbsolutizedLength { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::NestedPropertyComputeEntry); abort_on_panic(|| absolutize_length(value, unit as usize, unsafe { &*context })) } @@ -427,6 +428,7 @@ fn compute_font_width(value: &StyleValueData) -> FfiComputedNumber { /// `absolutized_value` must point at a valid StyleValueData. #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_compute_font_width(absolutized_value: *const c_void) -> FfiComputedNumber { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::NestedPropertyComputeEntry); abort_on_panic(|| { let value = unsafe { &*(absolutized_value as *const StyleValueData) }; compute_font_width(value) @@ -578,6 +580,7 @@ pub unsafe extern "C" fn rust_compute_font_size( inherited_math_depth: i32, default_font_size_raw: i32, ) -> FfiComputedNumber { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::NestedPropertyComputeEntry); abort_on_panic(|| { let value = unsafe { &*(absolutized_value as *const StyleValueData) }; compute_font_size( @@ -634,6 +637,7 @@ pub unsafe extern "C" fn rust_recascade_font_size_step( default_size_raw: i32, length_resolution_context: *const FfiLengthResolutionContext, ) -> FfiFontSizeRecascadeStep { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::NestedPropertyComputeEntry); abort_on_panic(|| { let value = unsafe { &*(value as *const StyleValueData) }; let current_size = CssPixels::from_raw(current_size_raw); @@ -707,6 +711,7 @@ pub unsafe extern "C" fn rust_recascade_font_size_step( /// styles must be computed even when no rules matched. #[unsafe(no_mangle)] pub extern "C" fn rust_pseudo_element_has_implicit_style(pseudo_element: u8) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::NestedPropertyComputeEntry); use crate::selector_engine::PseudoElementType; abort_on_panic(|| { matches!( @@ -729,6 +734,7 @@ pub extern "C" fn rust_pseudo_element_has_implicit_style(pseudo_element: u8) -> /// `content_value` must be null or point at a valid StyleValueData. #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_pseudo_element_content_bails(content_value: *const c_void, pseudo_element: u8) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::NestedPropertyComputeEntry); use crate::selector_engine::PseudoElementType; abort_on_panic(|| { let content_is_normal = if content_value.is_null() { @@ -1061,6 +1067,7 @@ pub unsafe extern "C" fn rust_style_value_is_computationally_independent( data_of: unsafe extern "C" fn(shell: *const c_void) -> *const c_void, decide_fallback: unsafe extern "C" fn(shell: *const c_void) -> bool, ) -> FfiIndependenceDecision { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::StyleValueQueryEntry); abort_on_panic(|| { match value_is_computationally_independent( unsafe { &*(data as *const StyleValueData) }, @@ -1123,6 +1130,7 @@ pub unsafe extern "C" fn rust_compute_math_depth( inherited_math_depth: i32, inherited_math_style_is_compact: bool, ) -> FfiComputedNumber { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::NestedPropertyComputeEntry); abort_on_panic(|| { let value = unsafe { &*(absolutized_value as *const StyleValueData) }; compute_math_depth(value, inherited_math_depth, inherited_math_style_is_compact) @@ -1188,6 +1196,7 @@ pub unsafe extern "C" fn rust_compute_line_height( absolutized_value: *const c_void, computed_font_size_raw: i32, ) -> FfiComputedLineHeight { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::NestedPropertyComputeEntry); abort_on_panic(|| { let value = unsafe { &*(absolutized_value as *const StyleValueData) }; compute_line_height(value, CssPixels::from_raw(computed_font_size_raw)) @@ -1257,6 +1266,7 @@ pub unsafe extern "C" fn rust_compute_border_or_outline_width( absolutized_value: *const c_void, device_pixels_per_css_pixel: f64, ) -> FfiComputedNumber { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::NestedPropertyComputeEntry); abort_on_panic(|| { let value = unsafe { &*(absolutized_value as *const StyleValueData) }; compute_border_or_outline_width(value, device_pixels_per_css_pixel) @@ -1305,6 +1315,7 @@ fn compute_corner_shape_parameter(value: &StyleValueData) -> FfiComputedNumber { /// `absolutized_value` must point at a valid StyleValueData. #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_compute_corner_shape_parameter(absolutized_value: *const c_void) -> FfiComputedNumber { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::NestedPropertyComputeEntry); abort_on_panic(|| { let value = unsafe { &*(absolutized_value as *const StyleValueData) }; compute_corner_shape_parameter(value) @@ -1323,6 +1334,7 @@ pub unsafe extern "C" fn rust_font_family_is_monospace( data: *const c_void, data_of: unsafe extern "C" fn(shell: *const c_void) -> *const c_void, ) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::NestedPropertyComputeEntry); abort_on_panic(|| { let StyleValueData::ValueList { values, .. } = (unsafe { &*(data as *const StyleValueData) }) else { return false; @@ -1358,6 +1370,7 @@ pub unsafe extern "C" fn rust_font_feature_settings_computed_order( tag_less: unsafe extern "C" fn(*mut c_void, usize, usize) -> bool, out_indices: *mut u32, ) -> usize { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::NestedPropertyComputeEntry); abort_on_panic(|| { // Keep the last occurrence of each tag; later declarations take precedence. let mut survivors: Vec = (0..count) @@ -1405,6 +1418,7 @@ pub unsafe extern "C" fn rust_value_depends_on_inherited_info_for_property( value: *const c_void, property_id: u16, ) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::StyleValueQueryEntry); use crate::property_metadata::property_id as prop; abort_on_panic(|| { let value = unsafe { &*(value as *const StyleValueData) }; @@ -1441,6 +1455,7 @@ pub struct FfiFontStyleComputation { /// `absolutized_value` must point at a valid StyleValueData. #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_compute_font_style(absolutized_value: *const c_void) -> FfiFontStyleComputation { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::NestedPropertyComputeEntry); abort_on_panic(|| { if let StyleValueData::Keyword { keyword } = (unsafe { &*(absolutized_value as *const StyleValueData) }) && let Some(font_style_keyword) = keyword_to_font_style_keyword(*keyword) @@ -1464,6 +1479,7 @@ pub unsafe extern "C" fn rust_compute_font_style(absolutized_value: *const c_voi /// `absolutized_value` must point at a valid StyleValueData. #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_compute_letter_or_word_spacing(absolutized_value: *const c_void) -> FfiComputedNumber { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::NestedPropertyComputeEntry); abort_on_panic(|| match unsafe { &*(absolutized_value as *const StyleValueData) } { StyleValueData::Keyword { keyword } if *keyword == keyword::NORMAL => FfiComputedNumber { handled: true, @@ -1484,6 +1500,7 @@ pub unsafe extern "C" fn rust_compute_letter_or_word_spacing(absolutized_value: // as equivalent. It serializes with the logical keywords in their short forms. #[unsafe(no_mangle)] pub extern "C" fn rust_position_area_short_keyword(keyword: u16) -> u16 { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::NestedPropertyComputeEntry); match keyword { keyword::BLOCK_START | keyword::INLINE_START => keyword::START, keyword::BLOCK_END | keyword::INLINE_END => keyword::END, @@ -1512,6 +1529,7 @@ pub struct FfiPositionAreaRemap { /// https://drafts.csswg.org/css-anchor-position/#position-area-computed #[unsafe(no_mangle)] pub extern "C" fn rust_position_area_span_all_remap(block_keyword: u16, inline_keyword: u16) -> FfiPositionAreaRemap { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::NestedPropertyComputeEntry); let remapped = |keyword| FfiPositionAreaRemap { remapped: true, keyword, @@ -1745,6 +1763,7 @@ pub unsafe extern "C" fn rust_drive_property_computation( has_inheritance_parent: bool, has_new_font_size: bool, ) { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandDriverEntry); abort_on_panic(|| { let callbacks = unsafe { &*callbacks }; let context = callbacks.context; @@ -1764,6 +1783,7 @@ pub unsafe extern "C" fn rust_drive_property_computation( let is_logical_alias = table_row_maps(&LOGICAL_ALIAS_TABLE, property_id); if is_logical_alias || table_row_maps(&PHYSICAL_TO_LOGICAL_TABLE, property_id) { let (writing_mode, direction) = *cached_writing_mode_and_direction.get_or_insert_with(|| { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandWritingModeCallback); let packed = unsafe { (callbacks.writing_mode_and_direction)(context) }; ((packed & 0xff) as u8, (packed >> 8) as u8) }); @@ -1783,6 +1803,7 @@ pub unsafe extern "C" fn rust_drive_property_computation( let mut value: *const c_void = std::ptr::null(); if let Some((value_shell, value_data, important)) = store.winning_declaration(cascaded_property_id) { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandCascadedValueCallback); unsafe { (callbacks.on_cascaded_value)(context, property_id, value_shell, important) }; value = value_data; } else if property_id == crate::property_metadata::property_id::FONT_SIZE && has_new_font_size { @@ -1801,6 +1822,7 @@ pub unsafe extern "C" fn rust_drive_property_computation( let inherit_fetch_attempted = decision.should_inherit && has_inheritance_parent; if inherit_fetch_attempted { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandInheritedValueCallback); value = unsafe { (callbacks.fetch_inherited_value)( context, @@ -1817,9 +1839,11 @@ pub unsafe extern "C" fn rust_drive_property_computation( decision.use_initial_without_inherit }; if use_initial { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandInitialValueCallback); unsafe { (callbacks.use_initial_value)(context, property_id) }; } + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandComputeAndStoreCallback); unsafe { (callbacks.compute_and_store)(context, property_id, inherited_property_id) }; } }); @@ -1848,9 +1872,11 @@ pub struct FfiCascadeStageCallbacks { /// `callbacks` must point at a valid callback table. #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_drive_cascade_origins(callbacks: *const FfiCascadeStageCallbacks) { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CascadeOriginDriverEntry); abort_on_panic(|| { let callbacks = unsafe { &*callbacks }; let context = callbacks.context; + crate::ffi_stats::bump_by(crate::ffi_stats::FfiOp::CascadeStageCallback, 7); unsafe { (callbacks.cascade_user_agent_rules)(context, false); (callbacks.cascade_user_rules)(context, false); @@ -1976,13 +2002,20 @@ fn expand_shorthands( ) { let context = callbacks.context; expand_shorthands_with( - &|shell| unsafe { (callbacks.data_of)(context, shell) }, - &|shell| unsafe { (callbacks.create_pending_substitution)(context, shell) }, + &|shell| { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CascadeDataOfCallback); + unsafe { (callbacks.data_of)(context, shell) } + }, + &|shell| { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::CascadePendingSubstitutionCallback); + unsafe { (callbacks.create_pending_substitution)(context, shell) } + }, property_id, shell, data, - &mut |longhand_id, longhand_shell, _longhand_data| unsafe { - (callbacks.set_longhand_property)(context, longhand_id, longhand_shell); + &mut |longhand_id, longhand_shell, _longhand_data| { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::ShorthandSetLonghandCallback); + unsafe { (callbacks.set_longhand_property)(context, longhand_id, longhand_shell) }; }, ); } @@ -2000,6 +2033,7 @@ pub unsafe extern "C" fn rust_for_each_property_expanding_shorthands( shell: *const c_void, data: *const c_void, ) { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::ShorthandExpansionEntry); abort_on_panic(|| expand_shorthands(unsafe { &*callbacks }, property_id, shell, data)); } @@ -2158,6 +2192,7 @@ fn required_box_type_transformation(input: &FfiBoxTypeTransformationInput) -> Bo pub unsafe extern "C" fn rust_transform_box_type( input: *const FfiBoxTypeTransformationInput, ) -> FfiBoxTypeTransformation { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::NestedPropertyComputeEntry); abort_on_panic(|| { let input = unsafe { &*input }; let display = input.display; @@ -2262,6 +2297,7 @@ pub struct FfiEffectiveOverflow { /// overflow-y is neither visible nor clip. #[unsafe(no_mangle)] pub extern "C" fn rust_resolve_effective_overflow_keywords(overflow_x: u16, overflow_y: u16) -> FfiEffectiveOverflow { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::NestedPropertyComputeEntry); abort_on_panic(|| { let is_visible_or_clip = |keyword: u16| keyword == keyword::VISIBLE || keyword == keyword::CLIP; let mut result = FfiEffectiveOverflow { @@ -2311,6 +2347,7 @@ pub extern "C" fn rust_compute_text_align( parent_text_align: u16, parent_direction_is_ltr: bool, ) -> FfiTextAlignAdjustment { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::NestedPropertyComputeEntry); abort_on_panic(|| { let unchanged = FfiTextAlignAdjustment { changed: false, @@ -2367,6 +2404,7 @@ pub unsafe extern "C" fn rust_compute_font_weight( absolutized_value: *const c_void, inherited_font_weight: f64, ) -> FfiComputedNumber { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::NestedPropertyComputeEntry); abort_on_panic(|| { let value = unsafe { &*(absolutized_value as *const StyleValueData) }; compute_font_weight(value, inherited_font_weight) diff --git a/Libraries/LibWeb/CSS/Rust/src/style_value.rs b/Libraries/LibWeb/CSS/Rust/src/style_value.rs index fcffcbf3f4adb..073c5bf7454c3 100644 --- a/Libraries/LibWeb/CSS/Rust/src/style_value.rs +++ b/Libraries/LibWeb/CSS/Rust/src/style_value.rs @@ -51,6 +51,7 @@ impl RetainedStyleValue { /// # Safety /// `pointer` must point at a live StyleValue shell. pub(crate) unsafe fn from_borrowed_shell_pointer(pointer: *const c_void) -> Self { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::StyleValueShellRetainCallback); unsafe { ladybird_style_value_ref(pointer) }; Self { pointer } } @@ -65,6 +66,7 @@ impl Drop for RetainedStyleValue { fn drop(&mut self) { // A null pointer represents an absent optional reference. if !self.pointer.is_null() { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::StyleValueShellReleaseCallback); unsafe { ladybird_style_value_unref(self.pointer) }; } } @@ -90,6 +92,7 @@ impl RetainedUtf16FlyString { /// # Safety /// `raw` must be the raw representation of a live fly string. pub(crate) unsafe fn from_borrowed_raw(raw: usize) -> Self { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::StringRetainReleaseCallback); unsafe { ladybird_utf16_fly_string_ref(raw) }; Self { raw } } @@ -97,6 +100,7 @@ impl RetainedUtf16FlyString { impl Drop for RetainedUtf16FlyString { fn drop(&mut self) { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::StringRetainReleaseCallback); unsafe { ladybird_utf16_fly_string_unref(self.raw) }; } } @@ -199,6 +203,7 @@ pub struct RetainedString { impl Drop for RetainedString { fn drop(&mut self) { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::StringRetainReleaseCallback); unsafe { ladybird_string_unref(self.raw) }; } } @@ -2204,6 +2209,7 @@ pub unsafe extern "C" fn rust_style_value_create_image( #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_style_value_destroy(value: *mut StyleValueData) { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::StyleValueDestroyEntry); abort_on_panic(|| { if value.is_null() { return; @@ -2249,5 +2255,6 @@ pub unsafe extern "C" fn rust_style_value_depends_on_current_color( data: *const c_void, data_of: unsafe extern "C" fn(shell: *const c_void) -> *const c_void, ) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::StyleValueQueryEntry); crate::abort_on_panic(|| value_depends_on_current_color(unsafe { &*(data as *const StyleValueData) }, data_of)) } diff --git a/Libraries/LibWeb/CSS/StyleValues/RustStyleValueHandle.h b/Libraries/LibWeb/CSS/StyleValues/RustStyleValueHandle.h index d2029ed9dcb5e..2ff5b17bc0d5a 100644 --- a/Libraries/LibWeb/CSS/StyleValues/RustStyleValueHandle.h +++ b/Libraries/LibWeb/CSS/StyleValues/RustStyleValueHandle.h @@ -25,6 +25,7 @@ class RustStyleValueHandle { : m_value(value) { VERIFY(m_value); + StyleValueFFI::rust_style_ffi_note_style_value_created(); } RustStyleValueHandle(RustStyleValueHandle&& other) diff --git a/Libraries/LibWeb/Internals/Internals.cpp b/Libraries/LibWeb/Internals/Internals.cpp index 12a9b86bbb281..0ae20edc346d9 100644 --- a/Libraries/LibWeb/Internals/Internals.cpp +++ b/Libraries/LibWeb/Internals/Internals.cpp @@ -63,6 +63,7 @@ #include #include #include +#include #include #include @@ -947,6 +948,26 @@ void Internals::reset_style_invalidation_counters() window().associated_document().reset_style_invalidation_counters(); } +JS::Object* Internals::style_ffi_counters() +{ + auto object = JS::Object::create(realm(), nullptr); + auto const counter_count = CSS::StyleValueFFI::rust_style_ffi_counter_count(); + for (size_t index = 0; index < counter_count; ++index) { + auto const* name = reinterpret_cast(CSS::StyleValueFFI::rust_style_ffi_counter_name(index)); + auto const value = CSS::StyleValueFFI::rust_style_ffi_counter_value(index); + object->define_direct_property( + Utf16FlyString::from_utf8(StringView { name, strlen(name) }), + JS::Value(static_cast(value)), + JS::default_attributes); + } + return object; +} + +void Internals::reset_style_ffi_counters() +{ + CSS::StyleValueFFI::rust_style_ffi_counters_reset(); +} + JS::Object* Internals::style_group_sharing_info(DOM::Element& element) { auto object = JS::Object::create(realm(), nullptr); diff --git a/Libraries/LibWeb/Internals/Internals.h b/Libraries/LibWeb/Internals/Internals.h index 86f8945b02cbc..2d8a4d317c12a 100644 --- a/Libraries/LibWeb/Internals/Internals.h +++ b/Libraries/LibWeb/Internals/Internals.h @@ -147,6 +147,8 @@ class WEB_API Internals final : public InternalsBase { JS::Object* get_style_invalidation_counters(); void reset_style_invalidation_counters(); JS::Object* computed_values_stats(); + JS::Object* style_ffi_counters(); + void reset_style_ffi_counters(); JS::Object* style_group_sharing_info(DOM::Element&); void update_style(); void set_preferred_color_scheme(Utf16String const& color_scheme); diff --git a/Libraries/LibWeb/Internals/Internals.idl b/Libraries/LibWeb/Internals/Internals.idl index b6cbede2de74a..e87d4b33fdc34 100644 --- a/Libraries/LibWeb/Internals/Internals.idl +++ b/Libraries/LibWeb/Internals/Internals.idl @@ -142,6 +142,10 @@ interface Internals { // Returns process-wide ComputedValues instance statistics. // Keys: liveComputedValues, totalComputedValuesCreated. object computedValuesStats(); + // Returns a snapshot of the process-wide style FFI boundary counters: one key per boundary + // operation, counting calls into the Rust style core and callbacks it makes into C++. + object styleFfiCounters(); + undefined resetStyleFfiCounters(); // Returns, for each ComputedValues style value group of the given element, whether the // group payload is shared with the parent element and whether it is the default payload. object styleGroupSharingInfo(Element element); diff --git a/Tests/LibWeb/Text/expected/css/style-ffi-counters.txt b/Tests/LibWeb/Text/expected/css/style-ffi-counters.txt new file mode 100644 index 0000000000000..b5b17a0d3d829 --- /dev/null +++ b/Tests/LibWeb/Text/expected/css/style-ffi-counters.txt @@ -0,0 +1,4 @@ +has counters: true +all zero after reset: true +longhand driver ran: true +longhand loop called back into C++: true diff --git a/Tests/LibWeb/Text/input/css/style-ffi-counters.html b/Tests/LibWeb/Text/input/css/style-ffi-counters.html new file mode 100644 index 0000000000000..775e464b5a0aa --- /dev/null +++ b/Tests/LibWeb/Text/input/css/style-ffi-counters.html @@ -0,0 +1,23 @@ + + + From e631ebeb074669993582a132b901873df0efe055 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 02:35:21 +0200 Subject: [PATCH 02/33] Meta: Add style FFI baseline workloads Deterministic pages measuring the style FFI boundary counters over representative DOM shapes: a small document, a stylesheet-heavy document, an inheritance-heavy chain, custom properties, shadow DOM with slots and parts, animations and transitions, and a targeted single-element restyle. collect.py copies them into the LibWeb test tree, runs them through test-web, prints each workload's counters, and cleans up again. The output is a measurement of the current boundary, not a regression test, so the pages stay out of the committed suite. --- .../animation-transition.html | 38 ++++++++++++++++ Meta/StyleFfiBaseline/collect.py | 43 +++++++++++++++++++ .../custom-property-heavy.html | 27 ++++++++++++ Meta/StyleFfiBaseline/harness.js | 11 +++++ Meta/StyleFfiBaseline/inheritance-heavy.html | 21 +++++++++ Meta/StyleFfiBaseline/shadow-slots-parts.html | 32 ++++++++++++++ .../single-element-restyle.html | 31 +++++++++++++ Meta/StyleFfiBaseline/small-document.html | 15 +++++++ Meta/StyleFfiBaseline/stylesheet-heavy.html | 23 ++++++++++ 9 files changed, 241 insertions(+) create mode 100644 Meta/StyleFfiBaseline/animation-transition.html create mode 100755 Meta/StyleFfiBaseline/collect.py create mode 100644 Meta/StyleFfiBaseline/custom-property-heavy.html create mode 100644 Meta/StyleFfiBaseline/harness.js create mode 100644 Meta/StyleFfiBaseline/inheritance-heavy.html create mode 100644 Meta/StyleFfiBaseline/shadow-slots-parts.html create mode 100644 Meta/StyleFfiBaseline/single-element-restyle.html create mode 100644 Meta/StyleFfiBaseline/small-document.html create mode 100644 Meta/StyleFfiBaseline/stylesheet-heavy.html diff --git a/Meta/StyleFfiBaseline/animation-transition.html b/Meta/StyleFfiBaseline/animation-transition.html new file mode 100644 index 0000000000000..2dbd811b5881a --- /dev/null +++ b/Meta/StyleFfiBaseline/animation-transition.html @@ -0,0 +1,38 @@ + + + + diff --git a/Meta/StyleFfiBaseline/collect.py b/Meta/StyleFfiBaseline/collect.py new file mode 100755 index 0000000000000..0a15c9dd9f5ae --- /dev/null +++ b/Meta/StyleFfiBaseline/collect.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +"""Runs the style FFI baseline workloads and prints the per-workload FFI +boundary counters (see internals.styleFfiCounters()). + +The workloads are deterministic pages that build a DOM shape, force a style +update, and print every counter. They are copied into the LibWeb test tree +for the run so test-web can drive them, then removed again; the printed +counts are a measurement of the current boundary, not a regression test. + +Usage: Meta/StyleFfiBaseline/collect.py [build-dir] +""" + +import pathlib +import shutil +import subprocess +import sys + +script_dir = pathlib.Path(__file__).resolve().parent +repo_root = script_dir.parent.parent +build_dir = pathlib.Path(sys.argv[1]) if len(sys.argv) > 1 else repo_root / "Build" / "release" + +input_dir = repo_root / "Tests" / "LibWeb" / "Text" / "input" / "css" / "ffi-baseline" +expected_dir = repo_root / "Tests" / "LibWeb" / "Text" / "expected" / "css" / "ffi-baseline" + +try: + input_dir.mkdir(parents=True) + for page in sorted(script_dir.glob("*.html")): + shutil.copy(page, input_dir) + shutil.copy(script_dir / "harness.js", input_dir) + + subprocess.run( + ["./bin/test-web", "--rebaseline", "-f", "Text/input/css/ffi-baseline"], + cwd=build_dir, + check=True, + stdout=subprocess.DEVNULL, + ) + + for expected in sorted(expected_dir.glob("*.txt")): + print(f"=== {expected.stem}") + print(expected.read_text(), end="") +finally: + shutil.rmtree(input_dir, ignore_errors=True) + shutil.rmtree(expected_dir, ignore_errors=True) diff --git a/Meta/StyleFfiBaseline/custom-property-heavy.html b/Meta/StyleFfiBaseline/custom-property-heavy.html new file mode 100644 index 0000000000000..aac85401f43b7 --- /dev/null +++ b/Meta/StyleFfiBaseline/custom-property-heavy.html @@ -0,0 +1,27 @@ + + + + diff --git a/Meta/StyleFfiBaseline/harness.js b/Meta/StyleFfiBaseline/harness.js new file mode 100644 index 0000000000000..a48582c6f02ef --- /dev/null +++ b/Meta/StyleFfiBaseline/harness.js @@ -0,0 +1,11 @@ +// Shared harness for the (temporary) style FFI baseline workloads: runs the +// workload with counters reset, then prints every counter. +function runFfiBaselineWorkload(workload) { + test(() => { + internals.resetStyleFfiCounters(); + workload(); + internals.updateStyle(); + const counters = internals.styleFfiCounters(); + for (const key of Object.keys(counters)) println(`${key}: ${counters[key]}`); + }); +} diff --git a/Meta/StyleFfiBaseline/inheritance-heavy.html b/Meta/StyleFfiBaseline/inheritance-heavy.html new file mode 100644 index 0000000000000..27d001d5c2d0a --- /dev/null +++ b/Meta/StyleFfiBaseline/inheritance-heavy.html @@ -0,0 +1,21 @@ + + + + diff --git a/Meta/StyleFfiBaseline/shadow-slots-parts.html b/Meta/StyleFfiBaseline/shadow-slots-parts.html new file mode 100644 index 0000000000000..ceae2c9eda903 --- /dev/null +++ b/Meta/StyleFfiBaseline/shadow-slots-parts.html @@ -0,0 +1,32 @@ + + + + diff --git a/Meta/StyleFfiBaseline/single-element-restyle.html b/Meta/StyleFfiBaseline/single-element-restyle.html new file mode 100644 index 0000000000000..1c1afd83eeea5 --- /dev/null +++ b/Meta/StyleFfiBaseline/single-element-restyle.html @@ -0,0 +1,31 @@ + + + + diff --git a/Meta/StyleFfiBaseline/small-document.html b/Meta/StyleFfiBaseline/small-document.html new file mode 100644 index 0000000000000..d9f618821376a --- /dev/null +++ b/Meta/StyleFfiBaseline/small-document.html @@ -0,0 +1,15 @@ + + + + diff --git a/Meta/StyleFfiBaseline/stylesheet-heavy.html b/Meta/StyleFfiBaseline/stylesheet-heavy.html new file mode 100644 index 0000000000000..204de2b8c1172 --- /dev/null +++ b/Meta/StyleFfiBaseline/stylesheet-heavy.html @@ -0,0 +1,23 @@ + + + + From 4b59dfce93bd644a79bf884007d7f0be208733e1 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 03:02:33 +0200 Subject: [PATCH 03/33] LibWeb: Install a Rust-readable initial value table The longhand computation loop calls back into C++ once per property that falls back to its initial value, only to have C++ look the value up in a process-wide table. Pin every longhand's initial value for the process lifetime and install the (shell, data) pointer pairs into the Rust style computation core alongside the other style metadata tables, so the core can select initial values without crossing the FFI. A parity test compares every table entry against property_initial_value on the C++ side, which is now exported from LibWeb for that purpose. The table is installed but not yet consumed; the driver switches over when longhand value selection moves into the core. --- .../LibWeb/CSS/Rust/src/property_metadata.rs | 3 + .../LibWeb/CSS/Rust/src/style_compute.rs | 64 +++++++++++++++++++ Libraries/LibWeb/CSS/RustStyleBridge.cpp | 5 ++ Libraries/LibWeb/CSS/RustStyleBridge.h | 1 + Libraries/LibWeb/CSS/StyleComputer.cpp | 13 ++++ .../generate_libweb_css_property_id.py | 2 +- .../TestStylePropertyMetadataParity.cpp | 11 ++++ 7 files changed, 98 insertions(+), 1 deletion(-) diff --git a/Libraries/LibWeb/CSS/Rust/src/property_metadata.rs b/Libraries/LibWeb/CSS/Rust/src/property_metadata.rs index f120b34f95976..0fd657357fb03 100644 --- a/Libraries/LibWeb/CSS/Rust/src/property_metadata.rs +++ b/Libraries/LibWeb/CSS/Rust/src/property_metadata.rs @@ -16,6 +16,9 @@ include!(concat!(env!("OUT_DIR"), "/property_metadata_generated.rs")); +pub(crate) const NUMBER_OF_LONGHAND_PROPERTIES: usize = + (LAST_LONGHAND_PROPERTY_ID - FIRST_LONGHAND_PROPERTY_ID + 1) as usize; + /// How much of the computation a property needs, mirroring the C++ /// requires-computation levels: 0 = never, 1 = with the cascaded value, /// 2 = with any non-inherited value, 3 = always. diff --git a/Libraries/LibWeb/CSS/Rust/src/style_compute.rs b/Libraries/LibWeb/CSS/Rust/src/style_compute.rs index c08cf3c18f34e..cc33dbdbd9c7d 100644 --- a/Libraries/LibWeb/CSS/Rust/src/style_compute.rs +++ b/Libraries/LibWeb/CSS/Rust/src/style_compute.rs @@ -1567,6 +1567,70 @@ pub extern "C" fn rust_position_area_span_all_remap(block_keyword: u16, inline_k not_remapped } +/// A style value crossing the FFI as its C++ shell pointer paired with its +/// Rust-owned data pointer. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FfiShellAndData { + pub shell: *const c_void, + pub data: *const c_void, +} + +impl FfiShellAndData { + pub const fn null() -> Self { + Self { + shell: std::ptr::null(), + data: std::ptr::null(), + } + } +} + +/// The per-longhand initial values. The C++ side pins every entry for the +/// process lifetime before installing the table, so lookups never cross the +/// FFI and the pointers never dangle. +struct InitialValueTable(Vec); + +// SAFETY: The entries reference immortal, immutable style values. +unsafe impl Send for InitialValueTable {} +unsafe impl Sync for InitialValueTable {} + +static INITIAL_VALUE_TABLE: std::sync::OnceLock = std::sync::OnceLock::new(); + +/// Installs the initial value table, one entry per longhand in property id +/// order. +/// +/// # Safety +/// `entries` must point at `length` valid entries whose shells and data stay +/// alive for the process lifetime. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rust_style_metadata_set_initial_value_table(entries: *const FfiShellAndData, length: usize) { + abort_on_panic(|| { + let entries = unsafe { std::slice::from_raw_parts(entries, length) }.to_vec(); + assert_eq!( + length, + crate::property_metadata::NUMBER_OF_LONGHAND_PROPERTIES, + "initial value table has one entry per longhand" + ); + assert!( + INITIAL_VALUE_TABLE.set(InitialValueTable(entries)).is_ok(), + "initial value table installed twice" + ); + }); +} + +/// Returns the initial value of a longhand property. +pub(crate) fn initial_value(property_id: u16) -> FfiShellAndData { + use crate::property_metadata::FIRST_LONGHAND_PROPERTY_ID; + let table = INITIAL_VALUE_TABLE.get().expect("initial value table not installed"); + table.0[(property_id - FIRST_LONGHAND_PROPERTY_ID) as usize] +} + +/// FFI accessor for the parity test on the C++ side. +#[unsafe(no_mangle)] +pub extern "C" fn rust_style_metadata_initial_value(property_id: u16) -> FfiShellAndData { + abort_on_panic(|| initial_value(property_id)) +} + /// The inherit-or-initial decision for one longhand in the property /// computation loop. #[repr(C)] diff --git a/Libraries/LibWeb/CSS/RustStyleBridge.cpp b/Libraries/LibWeb/CSS/RustStyleBridge.cpp index 3801bdf1e84c8..b397cd4afe9b6 100644 --- a/Libraries/LibWeb/CSS/RustStyleBridge.cpp +++ b/Libraries/LibWeb/CSS/RustStyleBridge.cpp @@ -59,6 +59,11 @@ u8 invoke_rust_property_metadata_requires_computation_level(u16 property_id) return ComputedValuesFFI::rust_property_metadata_requires_computation_level(property_id); } +ComputedValuesFFI::FfiShellAndData invoke_rust_style_metadata_initial_value(u16 property_id) +{ + return ComputedValuesFFI::rust_style_metadata_initial_value(property_id); +} + ComputedValuesFFI::FfiAbsolutizedLength invoke_rust_absolutize_length(double value, u8 unit, ComputedValuesFFI::FfiLengthResolutionContext const* context) { return ComputedValuesFFI::rust_absolutize_length(value, unit, context); diff --git a/Libraries/LibWeb/CSS/RustStyleBridge.h b/Libraries/LibWeb/CSS/RustStyleBridge.h index f0579eafc69f6..5e1aebbee60fc 100644 --- a/Libraries/LibWeb/CSS/RustStyleBridge.h +++ b/Libraries/LibWeb/CSS/RustStyleBridge.h @@ -24,6 +24,7 @@ WEB_API u16 invoke_rust_map_physical_to_logical_alias(u16 property_id, u8 writin WEB_API bool invoke_rust_property_metadata_is_shorthand(u16 property_id); WEB_API u16 const* invoke_rust_property_metadata_longhands_for_shorthand(u16 property_id, size_t* length); WEB_API u8 invoke_rust_property_metadata_requires_computation_level(u16 property_id); +WEB_API ComputedValuesFFI::FfiShellAndData invoke_rust_style_metadata_initial_value(u16 property_id); WEB_API ComputedValuesFFI::FfiAbsolutizedLength invoke_rust_absolutize_length(double value, u8 unit, ComputedValuesFFI::FfiLengthResolutionContext const* context); WEB_API i32 rust_css_pixels_multiply(i32 left, i32 right); diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index 3c49344837340..4aa2c01866b87 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -3275,6 +3275,19 @@ void StyleComputer::ensure_style_metadata_tables_installed() } } ComputedValuesFFI::rust_style_metadata_set_physical_to_logical_table(reverse_table.data(), reverse_table.size()); + + // Pin every longhand's initial value for the process lifetime and hand the + // (shell, data) pointer pairs to the core, so initial-value selection never + // crosses the FFI. + static NeverDestroyed>> initial_value_pins; + Vector initial_value_entries; + initial_value_entries.ensure_capacity(number_of_longhand_properties); + for (auto i = to_underlying(first_longhand_property_id); i <= to_underlying(last_longhand_property_id); ++i) { + auto initial_value = property_initial_value(static_cast(i)); + initial_value_entries.unchecked_append({ initial_value.ptr(), initial_value->rust_style_value_data() }); + initial_value_pins->append(move(initial_value)); + } + ComputedValuesFFI::rust_style_metadata_set_initial_value_table(initial_value_entries.data(), initial_value_entries.size()); return true; }(); (void)installed; diff --git a/Meta/Generators/generate_libweb_css_property_id.py b/Meta/Generators/generate_libweb_css_property_id.py index b4556eb3a547a..c142f4c5b148b 100644 --- a/Meta/Generators/generate_libweb_css_property_id.py +++ b/Meta/Generators/generate_libweb_css_property_id.py @@ -249,7 +249,7 @@ def write_header_file(out: TextIO, properties: dict, logical_property_groups: di [[nodiscard]] WEB_API Utf16FlyString const& string_from_property_id(PropertyID); [[nodiscard]] Utf16FlyString const& camel_case_string_from_property_id(PropertyID); WEB_API bool is_inherited_property(PropertyID); -NonnullRefPtr property_initial_value(PropertyID); +WEB_API NonnullRefPtr property_initial_value(PropertyID); enum class PropertyMultiplicity {{ Single, diff --git a/Tests/LibWeb/TestStylePropertyMetadataParity.cpp b/Tests/LibWeb/TestStylePropertyMetadataParity.cpp index b83821a444374..cf71d9545f40f 100644 --- a/Tests/LibWeb/TestStylePropertyMetadataParity.cpp +++ b/Tests/LibWeb/TestStylePropertyMetadataParity.cpp @@ -93,6 +93,17 @@ TEST_CASE(shorthand_expansions_match) } } +TEST_CASE(initial_value_table_matches) +{ + StyleComputer::ensure_style_metadata_tables_installed(); + for (auto i = to_underlying(first_longhand_property_id); i <= to_underlying(last_longhand_property_id); ++i) { + auto entry = invoke_rust_style_metadata_initial_value(i); + auto initial_value = property_initial_value(static_cast(i)); + EXPECT_EQ(entry.shell, static_cast(initial_value.ptr())); + EXPECT_EQ(entry.data, static_cast(initial_value->rust_style_value_data())); + } +} + TEST_CASE(requires_computation_levels_match) { for (auto i = to_underlying(first_longhand_property_id); i <= to_underlying(last_longhand_property_id); ++i) { From f90a1a8fb66ea76c1d4ebb9eae5c7269f4135dc3 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 03:31:15 +0200 Subject: [PATCH 04/33] LibWeb: Select longhand values natively in the Rust style core The property computation driver called back into C++ up to four times per longhand: to pin the winning cascaded value, to fetch the inherited value, to select the initial value, and to compute and store the result, with C++ then re-entering Rust three more times per longhand to decide whether the specified value must be kept for re-resolution when an ancestor changes. Move longhand value selection into the driver itself. The winning cascaded value comes straight from the cascaded property store, initial values come from the process-wide table installed by the previous commit, and inherited values come from a per-element parent snapshot of the inheritable computed values that C++ prepares in bulk before the drive. The importance and inheritance bitmaps, the raw cascaded font-size, and the viewport font-metric and shadow-root inheritance side effects accumulate in a results block that C++ applies once after the loop, replacing the C++ LonghandFlowState protocol entirely. The inheritance-dependence decision runs natively over the Rust value graph, with C++ consulted only for value kinds whose computational independence rule still lives with their shells. Explicit inherit of a non-inherited property fetches the parent value through a separately counted rare callback, since the snapshot only carries the inherited-by-default longhands. The one remaining per-longhand callback computes and stores the selected value. It also copies the parent's animated value for inherited properties at the same point in the flow as before, so that computation contexts built later in the loop still see animated inherited values; a new test pins that ordering by checking that a child's percentage line-height resolves against the parent's animated font-size. On the inheritance-heavy baseline workload this removes every per-longhand selection and query crossing: longhand callbacks drop from about 665 to about 334 per element and style value query entries from about 980 to about 4 per element, while the parent snapshot adds no style value allocations because inheritable keyword values are shared singletons. --- Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs | 5 +- .../LibWeb/CSS/Rust/src/style_compute.rs | 233 ++++++++++++++---- Libraries/LibWeb/CSS/Rust/src/style_value.rs | 2 +- Libraries/LibWeb/CSS/StyleComputer.cpp | 188 +++++++------- ...imated-inherited-font-size-line-height.txt | 3 + ...mated-inherited-font-size-line-height.html | 32 +++ 6 files changed, 311 insertions(+), 152 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/css/animated-inherited-font-size-line-height.txt create mode 100644 Tests/LibWeb/Text/input/css/animated-inherited-font-size-line-height.html diff --git a/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs b/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs index a9657222c1fcc..ababb153a3c44 100644 --- a/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs +++ b/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs @@ -64,10 +64,9 @@ define_ffi_ops! { CascadePendingSubstitutionCallback => "cascadePendingSubstitutionCallbacks", CascadeSourceSlotCallback => "cascadeSourceSlotCallbacks", ShorthandSetLonghandCallback => "shorthandSetLonghandCallbacks", - LonghandCascadedValueCallback => "longhandCascadedValueCallbacks", - LonghandInheritedValueCallback => "longhandInheritedValueCallbacks", - LonghandInitialValueCallback => "longhandInitialValueCallbacks", LonghandComputeAndStoreCallback => "longhandComputeAndStoreCallbacks", + LonghandParentValueFetchCallback => "longhandParentValueFetchCallbacks", + LonghandIndependenceFallbackCallback => "longhandIndependenceFallbackCallbacks", LonghandWritingModeCallback => "longhandWritingModeCallbacks", CalcSerializationCallback => "calcSerializationCallbacks", StyleValueShellRetainCallback => "styleValueShellRetainCallbacks", diff --git a/Libraries/LibWeb/CSS/Rust/src/style_compute.rs b/Libraries/LibWeb/CSS/Rust/src/style_compute.rs index cc33dbdbd9c7d..1bc613fc569a0 100644 --- a/Libraries/LibWeb/CSS/Rust/src/style_compute.rs +++ b/Libraries/LibWeb/CSS/Rust/src/style_compute.rs @@ -1411,6 +1411,24 @@ fn value_contains_percentage(value: &StyleValueData) -> bool { /// (depends-on-current-color and computational independence) stay with the /// value's own operations. /// +/// # Safety +/// `value` must point at a valid StyleValueData. +pub(crate) fn value_depends_on_inherited_info_for_property(value: &StyleValueData, property_id: u16) -> bool { + use crate::property_metadata::property_id as prop; + match property_id { + prop::FONT_WEIGHT => { + matches!(value, StyleValueData::Keyword { keyword } if matches!(*keyword, keyword::BOLDER | keyword::LIGHTER)) + } + prop::FONT_SIZE => { + value_contains_percentage(value) + || matches!(value, StyleValueData::Keyword { keyword } + if matches!(*keyword, keyword::LARGER | keyword::SMALLER | keyword::MATH)) + } + prop::LINE_HEIGHT => value_contains_percentage(value), + _ => false, + } +} + /// # Safety /// `value` must point at a valid StyleValueData. #[unsafe(no_mangle)] @@ -1419,21 +1437,8 @@ pub unsafe extern "C" fn rust_value_depends_on_inherited_info_for_property( property_id: u16, ) -> bool { crate::ffi_stats::bump(crate::ffi_stats::FfiOp::StyleValueQueryEntry); - use crate::property_metadata::property_id as prop; abort_on_panic(|| { - let value = unsafe { &*(value as *const StyleValueData) }; - match property_id { - prop::FONT_WEIGHT => { - matches!(value, StyleValueData::Keyword { keyword } if matches!(*keyword, keyword::BOLDER | keyword::LIGHTER)) - } - prop::FONT_SIZE => { - value_contains_percentage(value) - || matches!(value, StyleValueData::Keyword { keyword } - if matches!(*keyword, keyword::LARGER | keyword::SMALLER | keyword::MATH)) - } - prop::LINE_HEIGHT => value_contains_percentage(value), - _ => false, - } + value_depends_on_inherited_info_for_property(unsafe { &*(value as *const StyleValueData) }, property_id) }) } @@ -1763,32 +1768,69 @@ pub extern "C" fn rust_map_physical_to_logical_alias(property_id: u16, writing_m map_physical_to_logical_alias(property_id, writing_mode, direction) } -/// The per-longhand leaf callbacks the C++ side provides to the property -/// computation driver. Every value pointer returned by a callback is pinned by -/// the C++ flow state until the next callback for the same longhand. +/// The leaf callbacks the C++ side provides to the property computation +/// driver. The driver selects each longhand's cascaded, inherited or initial +/// value natively and calls back only to compute and store the result. #[repr(C)] pub struct FfiLonghandCallbacks { pub context: *mut c_void, - /// Pins the winning cascaded value into the flow state and applies its - /// side effects. Only called when a winning declaration exists; the - /// driver reads the cascaded property store natively. - pub on_cascaded_value: - unsafe extern "C" fn(context: *mut c_void, property_id: u16, value_shell: *const c_void, important: bool), - /// Fetches the inherited value, applying its side effects; returns the - /// fetched data or null. - pub fetch_inherited_value: unsafe extern "C" fn( + /// Computes the selected value if the property requires computation and + /// stores the result. `value_shell` stays alive for the duration of the + /// call: cascaded values are retained by the store, initial values are + /// immortal, and parent values are pinned by the snapshot or the fetch + /// below. + pub compute_and_store: unsafe extern "C" fn( context: *mut c_void, property_id: u16, inherited_property_id: u16, - explicitly_inherits_non_inherited_property: bool, - ) -> *const c_void, - pub use_initial_value: unsafe extern "C" fn(context: *mut c_void, property_id: u16), - pub compute_and_store: unsafe extern "C" fn(context: *mut c_void, property_id: u16, inherited_property_id: u16), + value_shell: *const c_void, + requires_computation: bool, + inheritance_dependent: bool, + inherited: bool, + ), + /// Rare: fetches the parent's computed value for an explicit `inherit` of + /// a non-inherited property, which the parent snapshot does not carry. + /// The C++ side pins the returned shell until the next fetch or the end + /// of the drive. + pub fetch_non_inherited_parent_value: + unsafe extern "C" fn(context: *mut c_void, inherited_property_id: u16) -> FfiShellAndData, + /// Maps a nested value's shell pointer to its Rust-owned data while the + /// driver decides inheritance dependence. + pub data_of: unsafe extern "C" fn(shell: *const c_void) -> *const c_void, + /// Decides computational independence for value kinds whose rule still + /// lives with their C++ shells. + pub computational_independence_fallback: unsafe extern "C" fn(shell: *const c_void) -> bool, /// Returns the element's computed writing mode and direction, packed as /// writing_mode | direction << 8. pub writing_mode_and_direction: unsafe extern "C" fn(context: *mut c_void) -> u16, } +/// The parent's inheritable computed values, prepared once per element: one +/// (shell, data) entry per inherited-by-default longhand in property id +/// order. Null entries mark values the parent could not provide. The C++ side +/// pins every entry for the duration of the drive. +#[repr(C)] +pub struct FfiParentSnapshot { + pub entries: *const FfiShellAndData, + pub entry_count: usize, + pub font_metrics_depend_on_viewport_metrics: bool, +} + +/// Bulk results of one longhand drive, applied by C++ after the loop instead +/// of once per longhand. The bitmap storage is provided by the caller, one +/// bit per longhand in property id order. +#[repr(C)] +pub struct FfiLonghandDriverResults { + pub important_words: *mut u64, + pub inherited_words: *mut u64, + pub word_count: usize, + /// The raw winning cascaded font-size value, or null; borrowed from the + /// cascaded property store. + pub raw_cascaded_font_size_shell: *const c_void, + pub font_metrics_depend_on_viewport_metrics: bool, + pub explicitly_inherited_non_inherited_property: bool, +} + fn table_row_maps(table: &std::sync::OnceLock>, property_id: u16) -> bool { use crate::property_metadata::FIRST_LONGHAND_PROPERTY_ID; let Some(table) = table.get() else { @@ -1810,28 +1852,55 @@ fn value_is_initial_or_unset(value: *const c_void) -> bool { } } +fn set_longhand_bit(words: &mut [u64], property_id: u16) { + use crate::property_metadata::FIRST_LONGHAND_PROPERTY_ID; + let index = (property_id - FIRST_LONGHAND_PROPERTY_ID) as usize; + words[index / 64] |= 1 << (index % 64); +} + /// Drives the property computation loop: iterates every longhand in /// computation order, resolves logical pairing, reads the winning cascaded -/// declarations straight from the store, decides between the cascaded, -/// inherited and initial values, and calls back into C++ for the flow stages -/// that have not moved into the core yet. +/// declarations straight from the store, selects between the cascaded, +/// inherited and initial values, decides inheritance dependence, and calls +/// back into C++ once per longhand to compute and store the result. The +/// importance and inheritance flags and the other per-element side effects +/// accumulate in `results` for bulk application after the loop. /// /// # Safety -/// `callbacks` must point at a valid callback table and `store` at a valid -/// cascaded property store; the callbacks must not mutate the store for the -/// duration of the call. +/// `callbacks` must point at a valid callback table, `store` at a valid +/// cascaded property store, `parent_snapshot` at a valid snapshot or null, +/// and `results` at a results block whose bitmap storage covers every +/// longhand; the callbacks must not mutate the store for the duration of the +/// call. #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_drive_property_computation( callbacks: *const FfiLonghandCallbacks, store: *const CascadedPropertyStore, - has_inheritance_parent: bool, + parent_snapshot: *const FfiParentSnapshot, has_new_font_size: bool, + results: *mut FfiLonghandDriverResults, ) { crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandDriverEntry); abort_on_panic(|| { + use crate::property_metadata::{ + FIRST_INHERITED_PROPERTY_ID, NUMBER_OF_LONGHAND_PROPERTIES, REQUIRES_COMPUTATION_ALWAYS, + REQUIRES_COMPUTATION_CASCADED, REQUIRES_COMPUTATION_NON_INHERITED, property_is_inherited, + property_requires_computation_level, + }; + let callbacks = unsafe { &*callbacks }; let context = callbacks.context; let store = unsafe { &*store }; + let snapshot = if parent_snapshot.is_null() { + None + } else { + Some(unsafe { &*parent_snapshot }) + }; + let has_inheritance_parent = snapshot.is_some(); + let results = unsafe { &mut *results }; + assert!(results.word_count * 64 >= NUMBER_OF_LONGHAND_PROPERTIES); + let important_words = unsafe { std::slice::from_raw_parts_mut(results.important_words, results.word_count) }; + let inherited_words = unsafe { std::slice::from_raw_parts_mut(results.inherited_words, results.word_count) }; let mut cached_writing_mode_and_direction: Option<(u8, u8)> = None; for &property_id in crate::property_metadata::property_computation_order() { @@ -1865,54 +1934,110 @@ pub unsafe extern "C" fn rust_drive_property_computation( cascaded_property_id = store.property_with_higher_priority(property_id, counterpart_property_id); } - let mut value: *const c_void = std::ptr::null(); + let mut value = FfiShellAndData::null(); if let Some((value_shell, value_data, important)) = store.winning_declaration(cascaded_property_id) { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandCascadedValueCallback); - unsafe { (callbacks.on_cascaded_value)(context, property_id, value_shell, important) }; - value = value_data; + value = FfiShellAndData { + shell: value_shell, + data: value_data, + }; + if important { + set_longhand_bit(important_words, property_id); + } + // Keep the raw winning cascaded font-size for the monospace font-size + // recascade (see recascade_font_size_if_needed on the C++ side). + if property_id == crate::property_metadata::property_id::FONT_SIZE { + results.raw_cascaded_font_size_shell = value_shell; + } } else if property_id == crate::property_metadata::property_id::FONT_SIZE && has_new_font_size { // NOTE: The recascaded font-size has already been stored before the loop. continue; } let decision = longhand_decision( - if value.is_null() { + if value.data.is_null() { None } else { - Some(unsafe { &*(value as *const StyleValueData) }) + Some(unsafe { &*(value.data as *const StyleValueData) }) }, property_id, ); + // The computation-need level to compare against depends on which source wins; + // cascaded is the baseline and is overridden by the inherit and initial paths. + let mut required_level = REQUIRES_COMPUTATION_CASCADED; + let inherit_fetch_attempted = decision.should_inherit && has_inheritance_parent; if inherit_fetch_attempted { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandInheritedValueCallback); - value = unsafe { - (callbacks.fetch_inherited_value)( - context, - property_id, - inherited_property_id, - decision.explicitly_inherits_non_inherited_property, - ) + let snapshot = snapshot.unwrap(); + set_longhand_bit(inherited_words, property_id); + if decision.explicitly_inherits_non_inherited_property { + results.explicitly_inherited_non_inherited_property = true; + } + value = if property_is_inherited(inherited_property_id) { + let index = (inherited_property_id - FIRST_INHERITED_PROPERTY_ID) as usize; + assert!(index < snapshot.entry_count); + unsafe { *snapshot.entries.add(index) } + } else { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandParentValueFetchCallback); + unsafe { (callbacks.fetch_non_inherited_parent_value)(context, inherited_property_id) } }; + if property_affects_font_metrics(inherited_property_id) + && snapshot.font_metrics_depend_on_viewport_metrics + { + results.font_metrics_depend_on_viewport_metrics = true; + } + required_level = REQUIRES_COMPUTATION_ALWAYS; } let use_initial = if inherit_fetch_attempted { - value.is_null() || value_is_initial_or_unset(value) + value.data.is_null() || value_is_initial_or_unset(value.data) } else { decision.use_initial_without_inherit }; if use_initial { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandInitialValueCallback); - unsafe { (callbacks.use_initial_value)(context, property_id) }; + value = initial_value(property_id); + required_level = REQUIRES_COMPUTATION_NON_INHERITED; } + let requires_computation = property_requires_computation_level(property_id) >= required_level; + + // Whether the computed value depends on inherited information, so the specified + // value must be kept for re-resolution when an ancestor changes. + let value_data = unsafe { &*(value.data as *const StyleValueData) }; + let inheritance_dependent = + crate::style_value::value_depends_on_current_color(value_data, callbacks.data_of) + || !value_is_computationally_independent( + value_data, + callbacks.data_of, + callbacks.computational_independence_fallback, + ) + .unwrap_or_else(|| { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandIndependenceFallbackCallback); + unsafe { (callbacks.computational_independence_fallback)(value.shell) } + }) + || value_depends_on_inherited_info_for_property(value_data, property_id); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandComputeAndStoreCallback); - unsafe { (callbacks.compute_and_store)(context, property_id, inherited_property_id) }; + unsafe { + (callbacks.compute_and_store)( + context, + property_id, + inherited_property_id, + value.shell, + requires_computation, + inheritance_dependent, + inherit_fetch_attempted, + ); + } } }); } +fn property_affects_font_metrics(property_id: u16) -> bool { + property_id == crate::property_metadata::property_id::FONT_SIZE + || property_id == crate::property_metadata::property_id::LINE_HEIGHT +} + /// Stage callbacks for the cascade origin sequence. #[repr(C)] pub struct FfiCascadeStageCallbacks { diff --git a/Libraries/LibWeb/CSS/Rust/src/style_value.rs b/Libraries/LibWeb/CSS/Rust/src/style_value.rs index 073c5bf7454c3..262f303205fbc 100644 --- a/Libraries/LibWeb/CSS/Rust/src/style_value.rs +++ b/Libraries/LibWeb/CSS/Rust/src/style_value.rs @@ -2222,7 +2222,7 @@ pub unsafe extern "C" fn rust_style_value_destroy(value: *mut StyleValueData) { /// currentcolor keyword itself, or a color function, color-mix(), contrast-color() or /// light-dark() whose nested colors do. `data_of` maps a nested value's shell pointer to /// its Rust-owned data. -fn value_depends_on_current_color( +pub(crate) fn value_depends_on_current_color( value: &StyleValueData, data_of: unsafe extern "C" fn(*const c_void) -> *const c_void, ) -> bool { diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index 4aa2c01866b87..140f763c266e1 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -7,6 +7,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include @@ -3337,89 +3338,58 @@ NonnullRefPtr StyleComputer::compute_properties(DOM::Abstrac return *logical_alias_mapping_context; }; - struct LonghandFlowState { - RefPtr value; - bool requires_computation { false }; - // The longhand the other fields were filled for, guarding against a stage being skipped. - PropertyID value_for_property { PropertyID::Custom }; - }; - - // Pins the winning cascaded value for the (logically paired) property into the flow state. - // The driver only calls this when a winning declaration exists. - auto on_cascaded_value = [&](PropertyID property_id, StyleValue const& value, bool important, LonghandFlowState& state) { - state = {}; - state.value_for_property = property_id; - if (important) - builder.set_property_important(property_id, Important::Yes); - state.value = value; - state.requires_computation = property_requires_computation_with_cascaded_value(property_id); - - // Store the raw winning cascaded font-size. This is needed to implement the time-traveling inheritance for - // font-size when font-family is monospace. - // See the recascade_font_size_if_needed() function for further details. - if (property_id == PropertyID::FontSize) - builder.set_raw_cascaded_font_size(value); - }; - - auto fetch_inherited_value = [&](PropertyID property_id, PropertyID inherited_property_id, bool explicitly_inherits_non_inherited_property, LonghandFlowState& state) { - if (state.value_for_property != property_id) { - state = {}; - state.value_for_property = property_id; - } - if (explicitly_inherits_non_inherited_property) { - if (auto* parent = abstract_element.element().parent(); parent && is(*parent)) - parent->set_children_may_depend_on_non_inherited_property_inheritance(); - } - builder.set_property_inherited(property_id, ComputedProperties::Inherited::Yes); - state.value = get_non_animated_inherit_value(inherited_property_id, abstract_element); - state.requires_computation = property_requires_computation_with_inherited_value(property_id); - if (property_affects_font_metrics(inherited_property_id)) { - if (computed_values_to_inherit_from->font_metrics_depend_on_viewport_metrics()) - builder.set_font_metrics_depend_on_viewport_metrics(); + // The parent's inheritable computed values, prepared once so the driver's inherit + // path never crosses the FFI. Every entry is pinned for the duration of the drive. + constexpr size_t inherited_longhand_count = to_underlying(last_inherited_property_id) - to_underlying(first_inherited_property_id) + 1; + Array, inherited_longhand_count> parent_snapshot_pins; + Array parent_snapshot_entries {}; + Optional parent_snapshot; + if (computed_values_to_inherit_from) { + for (size_t index = 0; index < inherited_longhand_count; ++index) { + auto property_id = static_cast(to_underlying(first_inherited_property_id) + index); + auto value = computed_values_to_inherit_from->computed_style_value_for_inheritance(property_id, ComputedValues::WithAnimationsApplied::No); + VERIFY(value); + parent_snapshot_entries[index] = { value.ptr(), value->rust_style_value_data() }; + parent_snapshot_pins[index] = move(value); } + parent_snapshot = ComputedValuesFFI::FfiParentSnapshot { + .entries = parent_snapshot_entries.data(), + .entry_count = inherited_longhand_count, + .font_metrics_depend_on_viewport_metrics = computed_values_to_inherit_from->font_metrics_depend_on_viewport_metrics(), + }; + } + // Computes the value the driver selected for the longhand, when computation is + // needed, and stores the result. The driver selects, pins and flags values + // natively; this is the only per-longhand callback left. + auto compute_and_store = [&](PropertyID property_id, PropertyID inherited_property_id, StyleValue const& value, bool requires_computation, bool inheritance_dependent, bool inherited) { // FIXME: Do we need to recompute animated inherited values? - if (auto const* animated_properties = computed_values_to_inherit_from->animated_properties(); animated_properties && animated_properties->has_property(inherited_property_id)) { - auto animated_value = animated_properties->values().get(inherited_property_id); - VERIFY(animated_value.has_value()); - computed_style.set_animated_property( - Badge {}, - property_id, - *animated_value.value(), - animated_properties->is_property_result_of_transition(inherited_property_id) - ? AnimatedPropertyResultOfTransition::Yes - : AnimatedPropertyResultOfTransition::No, - ComputedProperties::Inherited::Yes); - } - }; - - auto use_initial_value = [&](PropertyID property_id, LonghandFlowState& state) { - if (state.value_for_property != property_id) { - state = {}; - state.value_for_property = property_id; + if (inherited) { + if (auto const* animated_properties = computed_values_to_inherit_from->animated_properties(); animated_properties && animated_properties->has_property(inherited_property_id)) { + auto animated_value = animated_properties->values().get(inherited_property_id); + VERIFY(animated_value.has_value()); + computed_style.set_animated_property( + Badge {}, + property_id, + *animated_value.value(), + animated_properties->is_property_result_of_transition(inherited_property_id) + ? AnimatedPropertyResultOfTransition::Yes + : AnimatedPropertyResultOfTransition::No, + ComputedProperties::Inherited::Yes); + } } - state.value = property_initial_value(property_id); - state.requires_computation = property_requires_computation_with_initial_value(property_id); - }; - - auto compute_and_store = [&](PropertyID property_id, PropertyID inherited_property_id, LonghandFlowState& state) { - VERIFY(state.value_for_property == property_id); - auto value = state.value.release_nonnull(); // Store the resolved specified value for properties whose computation depends on inherited info, so they can // be re-resolved when an ancestor changes without keeping CascadedProperties alive on the element. - bool depends_on_inherited_info = value->depends_on_current_color() - || !value->is_computationally_independent() - || ComputedValuesFFI::rust_value_depends_on_inherited_info_for_property(value->rust_style_value_data(), to_underlying(property_id)); - if (depends_on_inherited_info) - builder.add_inheritance_dependent_specified_value(property_id, *value); + if (inheritance_dependent) + builder.add_inheritance_dependent_specified_value(property_id, value); // NB: We compute using the inherited (physical) property to avoid having to add cases for all the logical // alias properties in `compute_value_of_property` bool depends_on_viewport_metrics = false; - auto computed_value = state.requires_computation - ? compute_property(inherited_property_id, move(value), depends_on_viewport_metrics) - : move(value); + auto computed_value = requires_computation + ? compute_property(inherited_property_id, value, depends_on_viewport_metrics) + : NonnullRefPtr(value); if (depends_on_viewport_metrics) { builder.set_depends_on_viewport_metrics(); if (property_affects_font_metrics(inherited_property_id)) @@ -3430,47 +3400,77 @@ NonnullRefPtr StyleComputer::compute_properties(DOM::Abstrac // The property computation flow is driven from the Rust style computation core: it // iterates the longhands in computation order, resolves logical pairing through its - // mapping tables, and decides between the cascaded, inherited and initial values. The - // flow stages above are the leaf callbacks; the flow state pins every value the - // callbacks hand out until the next stage runs. + // mapping tables, and selects the cascaded, inherited or initial value natively. struct LonghandLoopContext { - decltype(on_cascaded_value)& on_cascaded_value_callback; - decltype(fetch_inherited_value)& fetch_inherited_value_callback; - decltype(use_initial_value)& use_initial_value_callback; decltype(compute_and_store)& compute_and_store_callback; decltype(get_logical_alias_mapping_context)& get_logical_alias_mapping_context_callback; - LonghandFlowState state {}; + DOM::AbstractElement abstract_element; + // Pins the parent value handed out by the explicit-inherit fetch until the next + // fetch or the end of the drive. + RefPtr pinned_parent_value; } loop_context { - .on_cascaded_value_callback = on_cascaded_value, - .fetch_inherited_value_callback = fetch_inherited_value, - .use_initial_value_callback = use_initial_value, .compute_and_store_callback = compute_and_store, .get_logical_alias_mapping_context_callback = get_logical_alias_mapping_context, + .abstract_element = abstract_element, + .pinned_parent_value = nullptr, }; ComputedValuesFFI::FfiLonghandCallbacks const callbacks { .context = &loop_context, - .on_cascaded_value = [](void* context, u16 property_id, void const* value_shell, bool important) { + .compute_and_store = [](void* context, u16 property_id, u16 inherited_property_id, void const* value_shell, bool requires_computation, bool inheritance_dependent, bool inherited) { auto& loop_context = *static_cast(context); - loop_context.on_cascaded_value_callback(static_cast(property_id), *static_cast(value_shell), important, loop_context.state); }, - .fetch_inherited_value = [](void* context, u16 property_id, u16 inherited_property_id, bool explicitly_inherits_non_inherited_property) -> void const* { + loop_context.compute_and_store_callback(static_cast(property_id), static_cast(inherited_property_id), *static_cast(value_shell), requires_computation, inheritance_dependent, inherited); }, + .fetch_non_inherited_parent_value = [](void* context, u16 inherited_property_id) -> ComputedValuesFFI::FfiShellAndData { auto& loop_context = *static_cast(context); - loop_context.fetch_inherited_value_callback(static_cast(property_id), static_cast(inherited_property_id), explicitly_inherits_non_inherited_property, loop_context.state); - return loop_context.state.value ? loop_context.state.value->rust_style_value_data() : nullptr; + auto value = get_non_animated_inherit_value(static_cast(inherited_property_id), loop_context.abstract_element); + ComputedValuesFFI::FfiShellAndData entry { value.ptr(), value->rust_style_value_data() }; + loop_context.pinned_parent_value = move(value); + return entry; }, - .use_initial_value = [](void* context, u16 property_id) { - auto& loop_context = *static_cast(context); - loop_context.use_initial_value_callback(static_cast(property_id), loop_context.state); }, - .compute_and_store = [](void* context, u16 property_id, u16 inherited_property_id) { - auto& loop_context = *static_cast(context); - loop_context.compute_and_store_callback(static_cast(property_id), static_cast(inherited_property_id), loop_context.state); }, + .data_of = [](void const* shell) -> void const* { return static_cast(shell)->rust_style_value_data(); }, + .computational_independence_fallback = [](void const* shell) -> bool { return static_cast(shell)->decide_computational_independence_fallback(); }, .writing_mode_and_direction = [](void* context) -> u16 { auto& loop_context = *static_cast(context); auto mapping_context = loop_context.get_logical_alias_mapping_context_callback(); return static_cast(to_underlying(mapping_context.writing_mode)) | static_cast(to_underlying(mapping_context.direction)) << 8; }, }; - ComputedValuesFFI::rust_drive_property_computation(&callbacks, cascaded_properties.rust_store(), computed_values_to_inherit_from != nullptr, new_font_size != nullptr); + + constexpr size_t longhand_bitmap_words = (number_of_longhand_properties + 63) / 64; + Array important_words {}; + Array inherited_words {}; + ComputedValuesFFI::FfiLonghandDriverResults driver_results { + .important_words = important_words.data(), + .inherited_words = inherited_words.data(), + .word_count = longhand_bitmap_words, + .raw_cascaded_font_size_shell = nullptr, + .font_metrics_depend_on_viewport_metrics = false, + .explicitly_inherited_non_inherited_property = false, + }; + ComputedValuesFFI::rust_drive_property_computation(&callbacks, cascaded_properties.rust_store(), parent_snapshot.has_value() ? &*parent_snapshot : nullptr, new_font_size != nullptr, &driver_results); + + // Apply the driver's bulk results. + auto longhand_bit_is_set = [](Array const& words, size_t index) { + return (words[index / 64] & (1ull << (index % 64))) != 0; + }; + for (size_t index = 0; index < number_of_longhand_properties; ++index) { + auto property_id = static_cast(to_underlying(first_longhand_property_id) + index); + if (longhand_bit_is_set(important_words, index)) + builder.set_property_important(property_id, Important::Yes); + if (longhand_bit_is_set(inherited_words, index)) + builder.set_property_inherited(property_id, ComputedProperties::Inherited::Yes); + } + // Store the raw winning cascaded font-size. This is needed to implement the time-traveling inheritance for + // font-size when font-family is monospace. + // See the recascade_font_size_if_needed() function for further details. + if (driver_results.raw_cascaded_font_size_shell) + builder.set_raw_cascaded_font_size(*static_cast(driver_results.raw_cascaded_font_size_shell)); + if (driver_results.font_metrics_depend_on_viewport_metrics) + builder.set_font_metrics_depend_on_viewport_metrics(); + if (driver_results.explicitly_inherited_non_inherited_property) { + if (auto* parent = abstract_element.element().parent(); parent && is(*parent)) + parent->set_children_may_depend_on_non_inherited_property_inheritance(); + } if (is(abstract_element.element())) { m_root_element_font_metrics = calculate_root_element_font_metrics(computed_style); diff --git a/Tests/LibWeb/Text/expected/css/animated-inherited-font-size-line-height.txt b/Tests/LibWeb/Text/expected/css/animated-inherited-font-size-line-height.txt new file mode 100644 index 0000000000000..02d093e6a29ff --- /dev/null +++ b/Tests/LibWeb/Text/expected/css/animated-inherited-font-size-line-height.txt @@ -0,0 +1,3 @@ +parent font-size: 20px +child font-size: 20px +child line-height: 40px diff --git a/Tests/LibWeb/Text/input/css/animated-inherited-font-size-line-height.html b/Tests/LibWeb/Text/input/css/animated-inherited-font-size-line-height.html new file mode 100644 index 0000000000000..7b9d4407ece07 --- /dev/null +++ b/Tests/LibWeb/Text/input/css/animated-inherited-font-size-line-height.html @@ -0,0 +1,32 @@ + + + +
x
+ From 9de1a4a5d0354ad0c1fbc5d0d1d5c8e2621da3f5 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 11:52:25 +0200 Subject: [PATCH 05/33] LibWeb: Batch longhand stores that need no computation Roughly 270 of the 333 longhands per element crossed the FFI only to store their selected value and run its bookkeeping, because the value itself requires no computation. Queue those store operations natively in the driver and flush the queue in one crossing before any callback that may read the stored values: the compute callback, the writing-mode query, and the end of the drive. The C++ side thus observes exactly the same store, animated-inheritance and bookkeeping sequence that one call per property used to produce, and the compute callback now only runs for properties that actually require computation. Batching extends how long the driver holds value shells: the fetched parent value of an explicitly inherited non-inherited property used to be consumed immediately, but can now sit in the queue until the next flush. The fetch pin therefore keeps every fetched value alive for the whole drive instead of only until the next fetch; with the shorter pin, the WPT ref tests that explicitly inherit freshly built grid track lists and overflow-clip-margin values crashed on released shells. A new test covers a batch of explicitly inherited non-inherited properties, including freshly built grid track lists. On the inheritance-heavy baseline workload, per-element longhand crossings drop from about 334 to about 42: 28 compute callbacks plus 14 batch flushes. The counters test now checks the compute and batch crossings together. --- Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs | 1 + .../LibWeb/CSS/Rust/src/style_compute.rs | 83 +++++++++++++++---- Libraries/LibWeb/CSS/StyleComputer.cpp | 72 ++++++++++------ ...licit-inherit-non-inherited-properties.txt | 7 ++ ...icit-inherit-non-inherited-properties.html | 40 +++++++++ .../Text/input/css/style-ffi-counters.html | 4 +- 6 files changed, 165 insertions(+), 42 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/css/explicit-inherit-non-inherited-properties.txt create mode 100644 Tests/LibWeb/Text/input/css/explicit-inherit-non-inherited-properties.html diff --git a/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs b/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs index ababb153a3c44..bcafcc90e2b99 100644 --- a/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs +++ b/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs @@ -65,6 +65,7 @@ define_ffi_ops! { CascadeSourceSlotCallback => "cascadeSourceSlotCallbacks", ShorthandSetLonghandCallback => "shorthandSetLonghandCallbacks", LonghandComputeAndStoreCallback => "longhandComputeAndStoreCallbacks", + LonghandStoreBatchCallback => "longhandStoreBatchCallbacks", LonghandParentValueFetchCallback => "longhandParentValueFetchCallbacks", LonghandIndependenceFallbackCallback => "longhandIndependenceFallbackCallbacks", LonghandWritingModeCallback => "longhandWritingModeCallbacks", diff --git a/Libraries/LibWeb/CSS/Rust/src/style_compute.rs b/Libraries/LibWeb/CSS/Rust/src/style_compute.rs index 1bc613fc569a0..e82986f6ac6b1 100644 --- a/Libraries/LibWeb/CSS/Rust/src/style_compute.rs +++ b/Libraries/LibWeb/CSS/Rust/src/style_compute.rs @@ -1768,30 +1768,48 @@ pub extern "C" fn rust_map_physical_to_logical_alias(property_id: u16, writing_m map_physical_to_logical_alias(property_id, writing_mode, direction) } +/// One deferred store operation for a longhand whose selected value needs no +/// computation: the value shell and the flags driving the C++ side effects +/// (animated-inheritance copy and inheritance-dependent bookkeeping). +#[repr(C)] +pub struct FfiComputedStoreEntry { + pub property_id: u16, + pub inherited_property_id: u16, + pub shell: *const c_void, + pub inheritance_dependent: bool, + pub inherited: bool, +} + /// The leaf callbacks the C++ side provides to the property computation /// driver. The driver selects each longhand's cascaded, inherited or initial /// value natively and calls back only to compute and store the result. #[repr(C)] pub struct FfiLonghandCallbacks { pub context: *mut c_void, - /// Computes the selected value if the property requires computation and - /// stores the result. `value_shell` stays alive for the duration of the - /// call: cascaded values are retained by the store, initial values are - /// immortal, and parent values are pinned by the snapshot or the fetch - /// below. + /// Computes the selected value and stores the result; only called for + /// properties that require computation. `value_shell` stays alive for + /// the duration of the call: cascaded values are retained by the store, + /// initial values are immortal, and parent values are pinned by the + /// snapshot or the fetch below. pub compute_and_store: unsafe extern "C" fn( context: *mut c_void, property_id: u16, inherited_property_id: u16, value_shell: *const c_void, - requires_computation: bool, inheritance_dependent: bool, inherited: bool, ), + /// Stores a batch of selected values that need no computation, applying + /// each entry's side effects in property order. The driver flushes the + /// batch before any callback that may read the stored values, so the + /// C++ side always observes the same store sequence as one call per + /// property would produce. + pub store_computed_batch: + unsafe extern "C" fn(context: *mut c_void, entries: *const FfiComputedStoreEntry, count: usize), /// Rare: fetches the parent's computed value for an explicit `inherit` of /// a non-inherited property, which the parent snapshot does not carry. - /// The C++ side pins the returned shell until the next fetch or the end - /// of the drive. + /// The C++ side pins the returned shell until the end of the drive, so + /// deferred store batches may hold it. pub fetch_non_inherited_parent_value: unsafe extern "C" fn(context: *mut c_void, inherited_property_id: u16) -> FfiShellAndData, /// Maps a nested value's shell pointer to its Rust-owned data while the @@ -1903,6 +1921,24 @@ pub unsafe extern "C" fn rust_drive_property_computation( let inherited_words = unsafe { std::slice::from_raw_parts_mut(results.inherited_words, results.word_count) }; let mut cached_writing_mode_and_direction: Option<(u8, u8)> = None; + // Store operations queued for properties that need no computation, flushed in one + // crossing before any callback that may read the stored values. + let mut pending_stores: Vec = Vec::new(); + fn flush_pending_stores( + callbacks: &FfiLonghandCallbacks, + context: *mut c_void, + pending_stores: &mut Vec, + ) { + if pending_stores.is_empty() { + return; + } + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandStoreBatchCallback); + // SAFETY: The entries and their shells stay alive for the call; the callback + // table outlives the drive. + unsafe { (callbacks.store_computed_batch)(context, pending_stores.as_ptr(), pending_stores.len()) }; + pending_stores.clear(); + } + for &property_id in crate::property_metadata::property_computation_order() { let mut cascaded_property_id = property_id; let mut inherited_property_id = property_id; @@ -1915,6 +1951,9 @@ pub unsafe extern "C" fn rust_drive_property_computation( // logical property group exactly when either mapping table maps it. let is_logical_alias = table_row_maps(&LOGICAL_ALIAS_TABLE, property_id); if is_logical_alias || table_row_maps(&PHYSICAL_TO_LOGICAL_TABLE, property_id) { + if cached_writing_mode_and_direction.is_none() { + flush_pending_stores(callbacks, context, &mut pending_stores); + } let (writing_mode, direction) = *cached_writing_mode_and_direction.get_or_insert_with(|| { crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandWritingModeCallback); let packed = unsafe { (callbacks.writing_mode_and_direction)(context) }; @@ -2017,19 +2056,31 @@ pub unsafe extern "C" fn rust_drive_property_computation( }) || value_depends_on_inherited_info_for_property(value_data, property_id); - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandComputeAndStoreCallback); - unsafe { - (callbacks.compute_and_store)( - context, + if requires_computation { + flush_pending_stores(callbacks, context, &mut pending_stores); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandComputeAndStoreCallback); + unsafe { + (callbacks.compute_and_store)( + context, + property_id, + inherited_property_id, + value.shell, + inheritance_dependent, + inherit_fetch_attempted, + ); + } + } else { + pending_stores.push(FfiComputedStoreEntry { property_id, inherited_property_id, - value.shell, - requires_computation, + shell: value.shell, inheritance_dependent, - inherit_fetch_attempted, - ); + inherited: inherit_fetch_attempted, + }); } } + + flush_pending_stores(callbacks, context, &mut pending_stores); }); } diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index 140f763c266e1..a3dc661c8de61 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -3362,22 +3362,25 @@ NonnullRefPtr StyleComputer::compute_properties(DOM::Abstrac // Computes the value the driver selected for the longhand, when computation is // needed, and stores the result. The driver selects, pins and flags values // natively; this is the only per-longhand callback left. - auto compute_and_store = [&](PropertyID property_id, PropertyID inherited_property_id, StyleValue const& value, bool requires_computation, bool inheritance_dependent, bool inherited) { - // FIXME: Do we need to recompute animated inherited values? - if (inherited) { - if (auto const* animated_properties = computed_values_to_inherit_from->animated_properties(); animated_properties && animated_properties->has_property(inherited_property_id)) { - auto animated_value = animated_properties->values().get(inherited_property_id); - VERIFY(animated_value.has_value()); - computed_style.set_animated_property( - Badge {}, - property_id, - *animated_value.value(), - animated_properties->is_property_result_of_transition(inherited_property_id) - ? AnimatedPropertyResultOfTransition::Yes - : AnimatedPropertyResultOfTransition::No, - ComputedProperties::Inherited::Yes); - } + // FIXME: Do we need to recompute animated inherited values? + auto copy_animated_inherited_value = [&](PropertyID property_id, PropertyID inherited_property_id) { + if (auto const* animated_properties = computed_values_to_inherit_from->animated_properties(); animated_properties && animated_properties->has_property(inherited_property_id)) { + auto animated_value = animated_properties->values().get(inherited_property_id); + VERIFY(animated_value.has_value()); + computed_style.set_animated_property( + Badge {}, + property_id, + *animated_value.value(), + animated_properties->is_property_result_of_transition(inherited_property_id) + ? AnimatedPropertyResultOfTransition::Yes + : AnimatedPropertyResultOfTransition::No, + ComputedProperties::Inherited::Yes); } + }; + + auto compute_and_store = [&](PropertyID property_id, PropertyID inherited_property_id, StyleValue const& value, bool inheritance_dependent, bool inherited) { + if (inherited) + copy_animated_inherited_value(property_id, inherited_property_id); // Store the resolved specified value for properties whose computation depends on inherited info, so they can // be re-resolved when an ancestor changes without keeping CascadedProperties alive on the element. @@ -3387,9 +3390,7 @@ NonnullRefPtr StyleComputer::compute_properties(DOM::Abstrac // NB: We compute using the inherited (physical) property to avoid having to add cases for all the logical // alias properties in `compute_value_of_property` bool depends_on_viewport_metrics = false; - auto computed_value = requires_computation - ? compute_property(inherited_property_id, value, depends_on_viewport_metrics) - : NonnullRefPtr(value); + auto computed_value = compute_property(inherited_property_id, value, depends_on_viewport_metrics); if (depends_on_viewport_metrics) { builder.set_depends_on_viewport_metrics(); if (property_affects_font_metrics(inherited_property_id)) @@ -3398,33 +3399,54 @@ NonnullRefPtr StyleComputer::compute_properties(DOM::Abstrac builder.set_property_without_modifying_flags(property_id, move(computed_value)); }; + // Applies a batch of store operations the driver queued for properties that need no + // computation, in property order, replicating the per-property side effects. + auto store_computed_batch = [&](ComputedValuesFFI::FfiComputedStoreEntry const* entries, size_t count) { + for (size_t i = 0; i < count; ++i) { + auto const& entry = entries[i]; + auto property_id = static_cast(entry.property_id); + auto inherited_property_id = static_cast(entry.inherited_property_id); + auto const& value = *static_cast(entry.shell); + if (entry.inherited) + copy_animated_inherited_value(property_id, inherited_property_id); + if (entry.inheritance_dependent) + builder.add_inheritance_dependent_specified_value(property_id, value); + builder.set_property_without_modifying_flags(property_id, value); + } + }; + // The property computation flow is driven from the Rust style computation core: it // iterates the longhands in computation order, resolves logical pairing through its // mapping tables, and selects the cascaded, inherited or initial value natively. struct LonghandLoopContext { decltype(compute_and_store)& compute_and_store_callback; + decltype(store_computed_batch)& store_computed_batch_callback; decltype(get_logical_alias_mapping_context)& get_logical_alias_mapping_context_callback; DOM::AbstractElement abstract_element; - // Pins the parent value handed out by the explicit-inherit fetch until the next - // fetch or the end of the drive. - RefPtr pinned_parent_value; + // Pins every parent value handed out by the explicit-inherit fetch until the end + // of the drive; the driver may queue the shells in deferred store batches. + Vector> pinned_parent_values; } loop_context { .compute_and_store_callback = compute_and_store, + .store_computed_batch_callback = store_computed_batch, .get_logical_alias_mapping_context_callback = get_logical_alias_mapping_context, .abstract_element = abstract_element, - .pinned_parent_value = nullptr, + .pinned_parent_values = {}, }; ComputedValuesFFI::FfiLonghandCallbacks const callbacks { .context = &loop_context, - .compute_and_store = [](void* context, u16 property_id, u16 inherited_property_id, void const* value_shell, bool requires_computation, bool inheritance_dependent, bool inherited) { + .compute_and_store = [](void* context, u16 property_id, u16 inherited_property_id, void const* value_shell, bool inheritance_dependent, bool inherited) { + auto& loop_context = *static_cast(context); + loop_context.compute_and_store_callback(static_cast(property_id), static_cast(inherited_property_id), *static_cast(value_shell), inheritance_dependent, inherited); }, + .store_computed_batch = [](void* context, ComputedValuesFFI::FfiComputedStoreEntry const* entries, size_t count) { auto& loop_context = *static_cast(context); - loop_context.compute_and_store_callback(static_cast(property_id), static_cast(inherited_property_id), *static_cast(value_shell), requires_computation, inheritance_dependent, inherited); }, + loop_context.store_computed_batch_callback(entries, count); }, .fetch_non_inherited_parent_value = [](void* context, u16 inherited_property_id) -> ComputedValuesFFI::FfiShellAndData { auto& loop_context = *static_cast(context); auto value = get_non_animated_inherit_value(static_cast(inherited_property_id), loop_context.abstract_element); ComputedValuesFFI::FfiShellAndData entry { value.ptr(), value->rust_style_value_data() }; - loop_context.pinned_parent_value = move(value); + loop_context.pinned_parent_values.append(move(value)); return entry; }, .data_of = [](void const* shell) -> void const* { return static_cast(shell)->rust_style_value_data(); }, diff --git a/Tests/LibWeb/Text/expected/css/explicit-inherit-non-inherited-properties.txt b/Tests/LibWeb/Text/expected/css/explicit-inherit-non-inherited-properties.txt new file mode 100644 index 0000000000000..99d0d979c51e6 --- /dev/null +++ b/Tests/LibWeb/Text/expected/css/explicit-inherit-non-inherited-properties.txt @@ -0,0 +1,7 @@ +position: absolute +float: none +clear: both +overflow-x: hidden +text-decoration-line: underline +grid-template-columns: [a] 100px [b] 200px +grid-template-rows: [c] 50px diff --git a/Tests/LibWeb/Text/input/css/explicit-inherit-non-inherited-properties.html b/Tests/LibWeb/Text/input/css/explicit-inherit-non-inherited-properties.html new file mode 100644 index 0000000000000..0fee043934a90 --- /dev/null +++ b/Tests/LibWeb/Text/input/css/explicit-inherit-non-inherited-properties.html @@ -0,0 +1,40 @@ + + + +
x
+ diff --git a/Tests/LibWeb/Text/input/css/style-ffi-counters.html b/Tests/LibWeb/Text/input/css/style-ffi-counters.html index 775e464b5a0aa..a629a6a921bcd 100644 --- a/Tests/LibWeb/Text/input/css/style-ffi-counters.html +++ b/Tests/LibWeb/Text/input/css/style-ffi-counters.html @@ -18,6 +18,8 @@ internals.updateStyle(); const afterOneElement = internals.styleFfiCounters(); println(`longhand driver ran: ${afterOneElement.longhandDriverEntries >= 1}`); - println(`longhand loop called back into C++: ${afterOneElement.longhandComputeAndStoreCallbacks >= 100}`); + const longhandCallbacks = + afterOneElement.longhandComputeAndStoreCallbacks + afterOneElement.longhandStoreBatchCallbacks; + println(`longhand loop called back into C++: ${longhandCallbacks >= 1}`); }); From 1ca5610503208fd3fdbc8cfbab7c8a9b9c271b51 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 12:19:08 +0200 Subject: [PATCH 06/33] LibWeb: Absolutize plain longhand values natively in the driver For properties without a dedicated computed-value rule, computation is absolutization of the specified value. Handle the two most common value shapes natively in the driver: value types whose absolutization is the identity (keywords that are not resolvable colors, numbers, integers, strings, custom identifiers, percentages, flex values, unicode ranges and URLs) queue directly into the store batch, and plain length values absolutize through the core's existing length resolution math against a per-kind cached resolution context fetched once from C++, entering the batch either unchanged or as a computed pixel length that the flush materializes. The C++ side installs a color keyword bitmap alongside the other style metadata tables so the core can tell which keywords resolve to something else at computed-value time, and hands out length resolution contexts through a new callback that mirrors the per-element context caching, flushing queued stores first since context construction reads stored values. Viewport dependency flags recorded during native absolutization travel through the driver results block. The remaining per-longhand compute crossings on the baseline workloads are almost exactly the properties with dedicated computed-value rules; cascaded keyword, percentage and length values on other properties now compute without crossing. A precision test pins that font-relative lengths absolutize to unquantized pixel values: the batch flush constructs the computed pixel length from the raw double exactly like the C++ absolutization path, without rounding through the CSSPixels fixed-point grid, observable through outline-offset which serializes its computed value directly. --- Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs | 1 + .../LibWeb/CSS/Rust/src/style_compute.rs | 208 +++++++++++++++++- Libraries/LibWeb/CSS/StyleComputer.cpp | 31 ++- .../em-length-computed-value-precision.txt | 4 + .../em-length-computed-value-precision.html | 24 ++ 5 files changed, 259 insertions(+), 9 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/css/em-length-computed-value-precision.txt create mode 100644 Tests/LibWeb/Text/input/css/em-length-computed-value-precision.html diff --git a/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs b/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs index bcafcc90e2b99..933ceaa3f912f 100644 --- a/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs +++ b/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs @@ -66,6 +66,7 @@ define_ffi_ops! { ShorthandSetLonghandCallback => "shorthandSetLonghandCallbacks", LonghandComputeAndStoreCallback => "longhandComputeAndStoreCallbacks", LonghandStoreBatchCallback => "longhandStoreBatchCallbacks", + LonghandContextFetchCallback => "longhandContextFetchCallbacks", LonghandParentValueFetchCallback => "longhandParentValueFetchCallbacks", LonghandIndependenceFallbackCallback => "longhandIndependenceFallbackCallbacks", LonghandWritingModeCallback => "longhandWritingModeCallbacks", diff --git a/Libraries/LibWeb/CSS/Rust/src/style_compute.rs b/Libraries/LibWeb/CSS/Rust/src/style_compute.rs index e82986f6ac6b1..cc3b04fdb3e05 100644 --- a/Libraries/LibWeb/CSS/Rust/src/style_compute.rs +++ b/Libraries/LibWeb/CSS/Rust/src/style_compute.rs @@ -1636,6 +1636,35 @@ pub extern "C" fn rust_style_metadata_initial_value(property_id: u16) -> FfiShel abort_on_panic(|| initial_value(property_id)) } +/// One bit per keyword marking the color keywords, installed once from the +/// C++ side's KeywordStyleValue::is_color classification. +static COLOR_KEYWORD_BITMAP: std::sync::OnceLock> = std::sync::OnceLock::new(); + +/// Installs the color keyword bitmap. +/// +/// # Safety +/// `words` must point at `length` valid words. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rust_style_metadata_set_color_keyword_bitmap(words: *const u64, length: usize) { + abort_on_panic(|| { + let words = unsafe { std::slice::from_raw_parts(words, length) }.to_vec(); + assert!( + COLOR_KEYWORD_BITMAP.set(words).is_ok(), + "color keyword bitmap installed twice" + ); + }); +} + +pub(crate) fn keyword_is_color(keyword: u16) -> bool { + let Some(bitmap) = COLOR_KEYWORD_BITMAP.get() else { + return false; + }; + let index = keyword as usize; + bitmap + .get(index / 64) + .is_some_and(|word| word & (1 << (index % 64)) != 0) +} + /// The inherit-or-initial decision for one longhand in the property /// computation loop. #[repr(C)] @@ -1775,9 +1804,16 @@ pub extern "C" fn rust_map_physical_to_logical_alias(property_id: u16, writing_m pub struct FfiComputedStoreEntry { pub property_id: u16, pub inherited_property_id: u16, + /// The selected specified value; also the stored value unless a computed + /// pixel length replaces it. pub shell: *const c_void, pub inheritance_dependent: bool, pub inherited: bool, + /// When set, the driver computed the value natively: the stored value is + /// a pixel length of `px` while `shell` remains the specified value for + /// the inheritance-dependence bookkeeping. + pub has_computed_px: bool, + pub px: f64, } /// The leaf callbacks the C++ side provides to the property computation @@ -1821,6 +1857,107 @@ pub struct FfiLonghandCallbacks { /// Returns the element's computed writing mode and direction, packed as /// writing_mode | direction << 8. pub writing_mode_and_direction: unsafe extern "C" fn(context: *mut c_void) -> u16, + /// Fetches the length resolution context the property's computation would + /// use; the driver caches one per context kind and flushes pending stores + /// first, since building a context reads stored values. + pub length_resolution_context: + unsafe extern "C" fn(context: *mut c_void, property_id: u16, out: *mut FfiLengthResolutionContext), +} + +/// The computation-context kind a property's lengths resolve against, +/// mirroring StyleComputer::get_computation_context_for_property until the +/// context construction moves into the core. +#[derive(Clone, Copy, PartialEq)] +enum ComputationContextKind { + Font, + LineHeight, + Generic, +} + +fn computation_context_kind(property_id: u16) -> ComputationContextKind { + use crate::property_metadata::property_id as prop; + match property_id { + prop::COLOR_SCHEME + | prop::FONT_FAMILY + | prop::FONT_FEATURE_SETTINGS + | prop::FONT_KERNING + | prop::FONT_OPTICAL_SIZING + | prop::FONT_SIZE + | prop::FONT_STYLE + | prop::FONT_VARIANT_ALTERNATES + | prop::FONT_VARIANT_CAPS + | prop::FONT_VARIANT_EAST_ASIAN + | prop::FONT_VARIANT_EMOJI + | prop::FONT_VARIANT_LIGATURES + | prop::FONT_VARIANT_NUMERIC + | prop::FONT_VARIANT_POSITION + | prop::FONT_VARIATION_SETTINGS + | prop::FONT_WEIGHT + | prop::FONT_WIDTH + | prop::MATH_DEPTH + | prop::TEXT_RENDERING => ComputationContextKind::Font, + prop::LINE_HEIGHT => ComputationContextKind::LineHeight, + _ => ComputationContextKind::Generic, + } +} + +/// Whether a value's absolutization is the identity, so the specified value +/// is already the computed value. Mirrors the value types that fall through +/// to the default arm of StyleValue::absolutized, plus keywords that resolve +/// to themselves: the currentcolor keyword computes to itself, and only +/// color keywords resolve to something else at computed-value time. +fn absolutization_is_identity(value: &StyleValueData) -> bool { + match value { + StyleValueData::Keyword { keyword } => *keyword == keyword::CURRENTCOLOR || !keyword_is_color(*keyword), + StyleValueData::Number { .. } + | StyleValueData::Integer { .. } + | StyleValueData::String { .. } + | StyleValueData::CustomIdent { .. } + | StyleValueData::Percentage { .. } + | StyleValueData::Flex { .. } + | StyleValueData::UnicodeRange { .. } + | StyleValueData::Url { .. } => true, + _ => false, + } +} + +/// Properties with a dedicated computed-value rule in the C++ dispatcher +/// (StyleComputer::compute_value_of_property); everything else computes as +/// plain absolutization. Mirrors the C++ switch until the dispatch moves +/// into the core. +fn property_has_dedicated_compute_rule(property_id: u16) -> bool { + use crate::property_metadata::property_id as prop; + matches!( + property_id, + prop::ANIMATION_NAME + | prop::BACKGROUND_ATTACHMENT + | prop::BACKGROUND_CLIP + | prop::BACKGROUND_ORIGIN + | prop::BACKGROUND_POSITION_X + | prop::BACKGROUND_POSITION_Y + | prop::BACKGROUND_REPEAT + | prop::BACKGROUND_SIZE + | prop::BORDER_BOTTOM_WIDTH + | prop::BORDER_LEFT_WIDTH + | prop::BORDER_RIGHT_WIDTH + | prop::BORDER_TOP_WIDTH + | prop::OUTLINE_WIDTH + | prop::CORNER_BOTTOM_LEFT_SHAPE + | prop::CORNER_BOTTOM_RIGHT_SHAPE + | prop::CORNER_TOP_LEFT_SHAPE + | prop::CORNER_TOP_RIGHT_SHAPE + | prop::FONT_SIZE + | prop::FONT_STYLE + | prop::FONT_WEIGHT + | prop::FONT_WIDTH + | prop::FONT_FEATURE_SETTINGS + | prop::FONT_VARIATION_SETTINGS + | prop::LETTER_SPACING + | prop::WORD_SPACING + | prop::LINE_HEIGHT + | prop::MATH_DEPTH + | prop::POSITION_AREA + ) } /// The parent's inheritable computed values, prepared once per element: one @@ -1845,6 +1982,7 @@ pub struct FfiLonghandDriverResults { /// The raw winning cascaded font-size value, or null; borrowed from the /// cascaded property store. pub raw_cascaded_font_size_shell: *const c_void, + pub depends_on_viewport_metrics: bool, pub font_metrics_depend_on_viewport_metrics: bool, pub explicitly_inherited_non_inherited_property: bool, } @@ -1924,6 +2062,9 @@ pub unsafe extern "C" fn rust_drive_property_computation( // Store operations queued for properties that need no computation, flushed in one // crossing before any callback that may read the stored values. let mut pending_stores: Vec = Vec::new(); + // Length resolution contexts fetched from C++ on first use, one per kind, like + // the C++ side's per-element computation context caches. + let mut cached_length_resolution_contexts: [Option; 3] = [None; 3]; fn flush_pending_stores( callbacks: &FfiLonghandCallbacks, context: *mut c_void, @@ -2057,17 +2198,66 @@ pub unsafe extern "C" fn rust_drive_property_computation( || value_depends_on_inherited_info_for_property(value_data, property_id); if requires_computation { - flush_pending_stores(callbacks, context, &mut pending_stores); - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandComputeAndStoreCallback); - unsafe { - (callbacks.compute_and_store)( - context, + // Plain length values of properties without a dedicated computed-value rule + // absolutize natively; everything else still computes through C++. + let mut computed_px = None; + let mut handled_natively = !property_has_dedicated_compute_rule(inherited_property_id) + && absolutization_is_identity(value_data); + if !handled_natively + && !property_has_dedicated_compute_rule(inherited_property_id) + && let StyleValueData::Length { + value: length_value, + unit, + } = value_data + { + let kind = computation_context_kind(inherited_property_id) as usize; + let resolution_context = cached_length_resolution_contexts[kind].get_or_insert_with(|| { + // Building a context on the C++ side reads stored values. + flush_pending_stores(callbacks, context, &mut pending_stores); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandContextFetchCallback); + let mut fetched = std::mem::MaybeUninit::::uninit(); + // SAFETY: The callback fills the context before returning. + unsafe { + (callbacks.length_resolution_context)(context, inherited_property_id, fetched.as_mut_ptr()); + fetched.assume_init() + } + }); + let result = absolutize_length(*length_value, *unit as usize, resolution_context); + if result.handled { + if result.resolved_viewport_relative_length { + results.depends_on_viewport_metrics = true; + if property_affects_font_metrics(inherited_property_id) { + results.font_metrics_depend_on_viewport_metrics = true; + } + } + computed_px = result.changed.then_some(result.px); + handled_natively = true; + } + } + + if handled_natively { + pending_stores.push(FfiComputedStoreEntry { property_id, inherited_property_id, - value.shell, + shell: value.shell, inheritance_dependent, - inherit_fetch_attempted, - ); + inherited: inherit_fetch_attempted, + has_computed_px: computed_px.is_some(), + px: computed_px.unwrap_or(0.0), + }); + } else { + flush_pending_stores(callbacks, context, &mut pending_stores); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandComputeAndStoreCallback); + unsafe { + (callbacks.compute_and_store)( + context, + property_id, + inherited_property_id, + value.shell, + inheritance_dependent, + inherit_fetch_attempted, + ); + } } } else { pending_stores.push(FfiComputedStoreEntry { @@ -2076,6 +2266,8 @@ pub unsafe extern "C" fn rust_drive_property_computation( shell: value.shell, inheritance_dependent, inherited: inherit_fetch_attempted, + has_computed_px: false, + px: 0.0, }); } } diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index a3dc661c8de61..e7980f3793263 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -3289,6 +3289,16 @@ void StyleComputer::ensure_style_metadata_tables_installed() initial_value_pins->append(move(initial_value)); } ComputedValuesFFI::rust_style_metadata_set_initial_value_table(initial_value_entries.data(), initial_value_entries.size()); + + // Mark the color keywords, so the core knows which keyword values resolve to + // something other than themselves at computed-value time. + Vector color_keyword_words; + color_keyword_words.resize((number_of_keywords + 63) / 64); + for (size_t keyword = 0; keyword < number_of_keywords; ++keyword) { + if (KeywordStyleValue::is_color(static_cast(keyword))) + color_keyword_words[keyword / 64] |= 1ull << (keyword % 64); + } + ComputedValuesFFI::rust_style_metadata_set_color_keyword_bitmap(color_keyword_words.data(), color_keyword_words.size()); return true; }(); (void)installed; @@ -3399,6 +3409,14 @@ NonnullRefPtr StyleComputer::compute_properties(DOM::Abstrac builder.set_property_without_modifying_flags(property_id, move(computed_value)); }; + // Hands the driver the length resolution context a property's computation would + // use, so plain lengths can absolutize natively; the driver caches one per + // context kind, like get_computation_context_for_property does here. + auto fetch_length_resolution_context = [&](PropertyID property_id) { + auto const& computation_context = get_computation_context_for_property(property_id, computed_style, abstract_element); + return to_ffi_length_resolution_context(computation_context.length_resolution_context); + }; + // Applies a batch of store operations the driver queued for properties that need no // computation, in property order, replicating the per-property side effects. auto store_computed_batch = [&](ComputedValuesFFI::FfiComputedStoreEntry const* entries, size_t count) { @@ -3411,7 +3429,10 @@ NonnullRefPtr StyleComputer::compute_properties(DOM::Abstrac copy_animated_inherited_value(property_id, inherited_property_id); if (entry.inheritance_dependent) builder.add_inheritance_dependent_specified_value(property_id, value); - builder.set_property_without_modifying_flags(property_id, value); + if (entry.has_computed_px) + builder.set_property_without_modifying_flags(property_id, LengthStyleValue::create(Length::make_px(entry.px))); + else + builder.set_property_without_modifying_flags(property_id, value); } }; @@ -3421,6 +3442,7 @@ NonnullRefPtr StyleComputer::compute_properties(DOM::Abstrac struct LonghandLoopContext { decltype(compute_and_store)& compute_and_store_callback; decltype(store_computed_batch)& store_computed_batch_callback; + decltype(fetch_length_resolution_context)& fetch_length_resolution_context_callback; decltype(get_logical_alias_mapping_context)& get_logical_alias_mapping_context_callback; DOM::AbstractElement abstract_element; // Pins every parent value handed out by the explicit-inherit fetch until the end @@ -3429,6 +3451,7 @@ NonnullRefPtr StyleComputer::compute_properties(DOM::Abstrac } loop_context { .compute_and_store_callback = compute_and_store, .store_computed_batch_callback = store_computed_batch, + .fetch_length_resolution_context_callback = fetch_length_resolution_context, .get_logical_alias_mapping_context_callback = get_logical_alias_mapping_context, .abstract_element = abstract_element, .pinned_parent_values = {}, @@ -3456,6 +3479,9 @@ NonnullRefPtr StyleComputer::compute_properties(DOM::Abstrac auto mapping_context = loop_context.get_logical_alias_mapping_context_callback(); return static_cast(to_underlying(mapping_context.writing_mode)) | static_cast(to_underlying(mapping_context.direction)) << 8; }, + .length_resolution_context = [](void* context, u16 property_id, ComputedValuesFFI::FfiLengthResolutionContext* out) { + auto& loop_context = *static_cast(context); + *out = loop_context.fetch_length_resolution_context_callback(static_cast(property_id)); }, }; constexpr size_t longhand_bitmap_words = (number_of_longhand_properties + 63) / 64; @@ -3466,6 +3492,7 @@ NonnullRefPtr StyleComputer::compute_properties(DOM::Abstrac .inherited_words = inherited_words.data(), .word_count = longhand_bitmap_words, .raw_cascaded_font_size_shell = nullptr, + .depends_on_viewport_metrics = false, .font_metrics_depend_on_viewport_metrics = false, .explicitly_inherited_non_inherited_property = false, }; @@ -3487,6 +3514,8 @@ NonnullRefPtr StyleComputer::compute_properties(DOM::Abstrac // See the recascade_font_size_if_needed() function for further details. if (driver_results.raw_cascaded_font_size_shell) builder.set_raw_cascaded_font_size(*static_cast(driver_results.raw_cascaded_font_size_shell)); + if (driver_results.depends_on_viewport_metrics) + builder.set_depends_on_viewport_metrics(); if (driver_results.font_metrics_depend_on_viewport_metrics) builder.set_font_metrics_depend_on_viewport_metrics(); if (driver_results.explicitly_inherited_non_inherited_property) { diff --git a/Tests/LibWeb/Text/expected/css/em-length-computed-value-precision.txt b/Tests/LibWeb/Text/expected/css/em-length-computed-value-precision.txt new file mode 100644 index 0000000000000..abae7561c1395 --- /dev/null +++ b/Tests/LibWeb/Text/expected/css/em-length-computed-value-precision.txt @@ -0,0 +1,4 @@ +margin-left: 1.59375px +padding-left: 4.796875px +outline-offset: 1.6px +letter-spacing: 1.6px diff --git a/Tests/LibWeb/Text/input/css/em-length-computed-value-precision.html b/Tests/LibWeb/Text/input/css/em-length-computed-value-precision.html new file mode 100644 index 0000000000000..bf392d24b3551 --- /dev/null +++ b/Tests/LibWeb/Text/input/css/em-length-computed-value-precision.html @@ -0,0 +1,24 @@ + + + +
x
+ From db2a10c6572bdf913c4aba297927df09eb70c993 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 12:34:03 +0200 Subject: [PATCH 07/33] LibWeb: Compute border widths and spacing longhands in the driver Border and outline widths and letter- and word-spacing have dedicated computed-value rules whose logic already lives in the core as leaf functions the C++ dispatcher calls per value. Run them natively in the driver instead: the specified value absolutizes natively first (the identity for the line-width and normal keywords, the length resolution math for plain lengths), then the border-width snapping or the normal-to-zero spacing rule produces a pixel result that joins the store batch. Values the core cannot absolutize, such as calc, still fall back to the compute callback. The driver takes the device pixel ratio as an argument since the border-width snapping needs it, and the letter-or-word-spacing rule is now callable natively instead of only through its FFI wrapper. On the inheritance-heavy baseline workload this drops per-element longhand crossings from about 42 to about 24: 19 compute callbacks plus 5 batch flushes, with the remaining computes concentrated in the font cluster, line-height, and the list-valued rules. --- .../LibWeb/CSS/Rust/src/style_compute.rs | 105 +++++++++++++++--- Libraries/LibWeb/CSS/StyleComputer.cpp | 2 +- 2 files changed, 92 insertions(+), 15 deletions(-) diff --git a/Libraries/LibWeb/CSS/Rust/src/style_compute.rs b/Libraries/LibWeb/CSS/Rust/src/style_compute.rs index cc3b04fdb3e05..166d8803ecfa5 100644 --- a/Libraries/LibWeb/CSS/Rust/src/style_compute.rs +++ b/Libraries/LibWeb/CSS/Rust/src/style_compute.rs @@ -94,6 +94,11 @@ enum ViewportAxis { Max, } +pub(crate) fn px_length_unit() -> u8 { + static PX: OnceLock = OnceLock::new(); + *PX.get_or_init(|| LENGTH_UNIT_NAMES.iter().position(|&name| name == "px").unwrap() as u8) +} + fn length_unit_kinds() -> &'static [LengthUnitKind] { static KINDS: OnceLock> = OnceLock::new(); KINDS.get_or_init(|| { @@ -1485,7 +1490,11 @@ pub unsafe extern "C" fn rust_compute_font_style(absolutized_value: *const c_voi #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_compute_letter_or_word_spacing(absolutized_value: *const c_void) -> FfiComputedNumber { crate::ffi_stats::bump(crate::ffi_stats::FfiOp::NestedPropertyComputeEntry); - abort_on_panic(|| match unsafe { &*(absolutized_value as *const StyleValueData) } { + abort_on_panic(|| compute_letter_or_word_spacing_value(unsafe { &*(absolutized_value as *const StyleValueData) })) +} + +fn compute_letter_or_word_spacing_value(absolutized_value: &StyleValueData) -> FfiComputedNumber { + match absolutized_value { StyleValueData::Keyword { keyword } if *keyword == keyword::NORMAL => FfiComputedNumber { handled: true, unchanged: false, @@ -1496,7 +1505,7 @@ pub unsafe extern "C" fn rust_compute_letter_or_word_spacing(absolutized_value: unchanged: true, value: 0.0, }, - }) + } } // https://drafts.csswg.org/css-anchor-position/#position-area-computed @@ -2034,6 +2043,7 @@ pub unsafe extern "C" fn rust_drive_property_computation( store: *const CascadedPropertyStore, parent_snapshot: *const FfiParentSnapshot, has_new_font_size: bool, + device_pixels_per_css_pixel: f64, results: *mut FfiLonghandDriverResults, ) { crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandDriverEntry); @@ -2200,15 +2210,15 @@ pub unsafe extern "C" fn rust_drive_property_computation( if requires_computation { // Plain length values of properties without a dedicated computed-value rule // absolutize natively; everything else still computes through C++. - let mut computed_px = None; - let mut handled_natively = !property_has_dedicated_compute_rule(inherited_property_id) - && absolutization_is_identity(value_data); - if !handled_natively - && !property_has_dedicated_compute_rule(inherited_property_id) - && let StyleValueData::Length { - value: length_value, - unit, - } = value_data + // The specified value absolutized natively when the core can: + // Some(None) leaves the value unchanged, Some(Some(px)) resolves it to + // a pixel length, and None means C++ must handle it. + let absolutized: Option> = if absolutization_is_identity(value_data) { + Some(None) + } else if let StyleValueData::Length { + value: length_value, + unit, + } = value_data { let kind = computation_context_kind(inherited_property_id) as usize; let resolution_context = cached_length_resolution_contexts[kind].get_or_insert_with(|| { @@ -2230,12 +2240,79 @@ pub unsafe extern "C" fn rust_drive_property_computation( results.font_metrics_depend_on_viewport_metrics = true; } } - computed_px = result.changed.then_some(result.px); - handled_natively = true; + Some(result.changed.then_some(result.px)) + } else { + None } + } else { + None + }; + + // The computed value: for properties without a dedicated rule the + // absolutized value is the computed value; the dedicated rules that + // have moved into the core run over the absolutized value here. + enum NativeValue { + Unsupported, + Unchanged, + Px(f64), } + use crate::property_metadata::property_id as prop; + let synthesized_px_length = |absolutized: Option| { + absolutized.map(|px| StyleValueData::Length { + value: px, + unit: px_length_unit(), + }) + }; + let native = match (absolutized, inherited_property_id) { + ( + Some(absolutized), + prop::BORDER_BOTTOM_WIDTH + | prop::BORDER_LEFT_WIDTH + | prop::BORDER_RIGHT_WIDTH + | prop::BORDER_TOP_WIDTH + | prop::OUTLINE_WIDTH, + ) => { + let synthesized = synthesized_px_length(absolutized); + let result = compute_border_or_outline_width( + synthesized.as_ref().unwrap_or(value_data), + device_pixels_per_css_pixel, + ); + if result.handled { + NativeValue::Px(result.value) + } else { + NativeValue::Unsupported + } + } + (Some(absolutized), prop::LETTER_SPACING | prop::WORD_SPACING) => { + let synthesized = synthesized_px_length(absolutized); + let result = compute_letter_or_word_spacing_value(synthesized.as_ref().unwrap_or(value_data)); + if result.handled { + if result.unchanged { + match absolutized { + Some(px) => NativeValue::Px(px), + None => NativeValue::Unchanged, + } + } else { + NativeValue::Px(result.value) + } + } else { + NativeValue::Unsupported + } + } + (Some(absolutized), _) if !property_has_dedicated_compute_rule(inherited_property_id) => { + match absolutized { + Some(px) => NativeValue::Px(px), + None => NativeValue::Unchanged, + } + } + _ => NativeValue::Unsupported, + }; - if handled_natively { + if !matches!(native, NativeValue::Unsupported) { + let computed_px = match native { + NativeValue::Px(px) => Some(px), + _ => None, + }; pending_stores.push(FfiComputedStoreEntry { property_id, inherited_property_id, diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index e7980f3793263..59c69b75df7e8 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -3496,7 +3496,7 @@ NonnullRefPtr StyleComputer::compute_properties(DOM::Abstrac .font_metrics_depend_on_viewport_metrics = false, .explicitly_inherited_non_inherited_property = false, }; - ComputedValuesFFI::rust_drive_property_computation(&callbacks, cascaded_properties.rust_store(), parent_snapshot.has_value() ? &*parent_snapshot : nullptr, new_font_size != nullptr, &driver_results); + ComputedValuesFFI::rust_drive_property_computation(&callbacks, cascaded_properties.rust_store(), parent_snapshot.has_value() ? &*parent_snapshot : nullptr, new_font_size != nullptr, device_pixels_per_css_pixel, &driver_results); // Apply the driver's bulk results. auto longhand_bit_is_set = [](Array const& words, size_t index) { From b90d9f9e22961e9eb7fe8cb236134b861b6fcf14 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 12:46:32 +0200 Subject: [PATCH 08/33] LibWeb: Compute corner shapes and math-depth in the driver Both rules already live in the core as leaf functions; run them natively in the driver. The store batch entry's pixel-length flag becomes a computed-value kind so the flush can materialize pixel lengths, integers, or superellipse corner shapes, keeping the C++ round-value cache for the common corner case. The inherited math-depth and math-style that the math-depth rule needs come straight from the parent snapshot as an integer and the compact keyword check, with the initial values applying when there is no inheritance parent. Superellipse-valued corner shapes and calc-valued math-depth still fall back to the compute callback, since their absolutization and resolution stay with C++ for now. Per-element longhand crossings on the inheritance-heavy baseline workload drop from about 24 to about 15: 11 compute callbacks plus 4 batch flushes, leaving the font cluster, line-height, and the list-valued rules. --- .../LibWeb/CSS/Rust/src/style_compute.rs | 87 ++++++++++++++++--- Libraries/LibWeb/CSS/StyleComputer.cpp | 23 ++++- 2 files changed, 95 insertions(+), 15 deletions(-) diff --git a/Libraries/LibWeb/CSS/Rust/src/style_compute.rs b/Libraries/LibWeb/CSS/Rust/src/style_compute.rs index 166d8803ecfa5..7f40609fbabc8 100644 --- a/Libraries/LibWeb/CSS/Rust/src/style_compute.rs +++ b/Libraries/LibWeb/CSS/Rust/src/style_compute.rs @@ -1818,13 +1818,22 @@ pub struct FfiComputedStoreEntry { pub shell: *const c_void, pub inheritance_dependent: bool, pub inherited: bool, - /// When set, the driver computed the value natively: the stored value is - /// a pixel length of `px` while `shell` remains the specified value for - /// the inheritance-dependence bookkeeping. - pub has_computed_px: bool, - pub px: f64, + /// How the natively computed value crosses: with COMPUTED_KIND_SHELL the + /// stored value is `shell` itself; the other kinds carry a replacement in + /// `value` while `shell` remains the specified value for the + /// inheritance-dependence bookkeeping. + pub computed_kind: u8, + pub value: f64, } +pub const COMPUTED_KIND_SHELL: u8 = 0; +/// A pixel length of `value`. +pub const COMPUTED_KIND_PX_LENGTH: u8 = 1; +/// An integer of `value`. +pub const COMPUTED_KIND_INTEGER: u8 = 2; +/// A superellipse with parameter `value`. +pub const COMPUTED_KIND_SUPERELLIPSE: u8 = 3; + /// The leaf callbacks the C++ side provides to the property computation /// driver. The driver selects each longhand's cascaded, inherited or initial /// value natively and calls back only to compute and store the result. @@ -2255,6 +2264,8 @@ pub unsafe extern "C" fn rust_drive_property_computation( Unsupported, Unchanged, Px(f64), + Integer(i32), + Superellipse(f64), } use crate::property_metadata::property_id as prop; let synthesized_px_length = |absolutized: Option| { @@ -2283,6 +2294,56 @@ pub unsafe extern "C" fn rust_drive_property_computation( NativeValue::Unsupported } } + ( + Some(_), + prop::CORNER_BOTTOM_LEFT_SHAPE + | prop::CORNER_BOTTOM_RIGHT_SHAPE + | prop::CORNER_TOP_LEFT_SHAPE + | prop::CORNER_TOP_RIGHT_SHAPE, + ) => { + // Corner shape values are keywords or superellipses; only keywords + // reach here since superellipse absolutization stays with C++. + let result = compute_corner_shape_parameter(value_data); + if result.handled && !result.unchanged { + NativeValue::Superellipse(result.value) + } else if result.handled { + NativeValue::Unchanged + } else { + NativeValue::Unsupported + } + } + (Some(_), prop::MATH_DEPTH) => { + // The inherited math-depth and math-style come from the parent + // snapshot; without an inheritance parent the initial values apply + // (math-depth 0, math-style normal). + let (inherited_math_depth, inherited_math_style_is_compact) = match snapshot { + Some(snapshot) => { + let entry_data = |property_id: u16| { + let index = (property_id - FIRST_INHERITED_PROPERTY_ID) as usize; + assert!(index < snapshot.entry_count); + // SAFETY: Snapshot entries are valid for the drive. + unsafe { (*snapshot.entries.add(index)).data as *const StyleValueData } + }; + let math_depth = match unsafe { entry_data(prop::MATH_DEPTH).as_ref() } { + Some(StyleValueData::Integer { value }) => *value, + _ => 0, + }; + let compact = matches!( + unsafe { entry_data(prop::MATH_STYLE).as_ref() }, + Some(StyleValueData::Keyword { keyword }) if *keyword == keyword::COMPACT + ); + (math_depth, compact) + } + None => (0, false), + }; + let result = + compute_math_depth(value_data, inherited_math_depth, inherited_math_style_is_compact); + if result.handled { + NativeValue::Integer(result.value as i32) + } else { + NativeValue::Unsupported + } + } (Some(absolutized), prop::LETTER_SPACING | prop::WORD_SPACING) => { let synthesized = synthesized_px_length(absolutized); let result = compute_letter_or_word_spacing_value(synthesized.as_ref().unwrap_or(value_data)); @@ -2309,9 +2370,11 @@ pub unsafe extern "C" fn rust_drive_property_computation( }; if !matches!(native, NativeValue::Unsupported) { - let computed_px = match native { - NativeValue::Px(px) => Some(px), - _ => None, + let (computed_kind, computed_value) = match native { + NativeValue::Px(px) => (COMPUTED_KIND_PX_LENGTH, px), + NativeValue::Integer(integer) => (COMPUTED_KIND_INTEGER, integer as f64), + NativeValue::Superellipse(parameter) => (COMPUTED_KIND_SUPERELLIPSE, parameter), + _ => (COMPUTED_KIND_SHELL, 0.0), }; pending_stores.push(FfiComputedStoreEntry { property_id, @@ -2319,8 +2382,8 @@ pub unsafe extern "C" fn rust_drive_property_computation( shell: value.shell, inheritance_dependent, inherited: inherit_fetch_attempted, - has_computed_px: computed_px.is_some(), - px: computed_px.unwrap_or(0.0), + computed_kind, + value: computed_value, }); } else { flush_pending_stores(callbacks, context, &mut pending_stores); @@ -2343,8 +2406,8 @@ pub unsafe extern "C" fn rust_drive_property_computation( shell: value.shell, inheritance_dependent, inherited: inherit_fetch_attempted, - has_computed_px: false, - px: 0.0, + computed_kind: COMPUTED_KIND_SHELL, + value: 0.0, }); } } diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index 59c69b75df7e8..5651453081a91 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -3429,10 +3429,27 @@ NonnullRefPtr StyleComputer::compute_properties(DOM::Abstrac copy_animated_inherited_value(property_id, inherited_property_id); if (entry.inheritance_dependent) builder.add_inheritance_dependent_specified_value(property_id, value); - if (entry.has_computed_px) - builder.set_property_without_modifying_flags(property_id, LengthStyleValue::create(Length::make_px(entry.px))); - else + switch (entry.computed_kind) { + case ComputedValuesFFI::COMPUTED_KIND_PX_LENGTH: + builder.set_property_without_modifying_flags(property_id, LengthStyleValue::create(Length::make_px(entry.value))); + break; + case ComputedValuesFFI::COMPUTED_KIND_INTEGER: + builder.set_property_without_modifying_flags(property_id, IntegerStyleValue::create(static_cast(entry.value))); + break; + case ComputedValuesFFI::COMPUTED_KIND_SUPERELLIPSE: { + // NB: The round value is cached since it is the initial value of the corner-*-shape properties. + if (entry.value == 1) { + static auto const& cached_round_value = SuperellipseStyleValue::create(NumberStyleValue::create(1)).leak_ref(); + builder.set_property_without_modifying_flags(property_id, cached_round_value); + } else { + builder.set_property_without_modifying_flags(property_id, SuperellipseStyleValue::create(NumberStyleValue::create(entry.value))); + } + break; + } + default: builder.set_property_without_modifying_flags(property_id, value); + break; + } } }; From e9894f7e2b95b90bba876dbe06c210634eba0914 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 13:18:46 +0200 Subject: [PATCH 09/33] LibWeb: Compute the font cluster natively and defer the rest to batches Two moves take the last per-longhand crossings out of the property computation loop. First, the rules whose leaf logic already lives in the core now run natively in the driver: font-size, with the inherited size and math-depth read from the parent snapshot and the computed math-depth tracked across the loop; font-weight; font-style; font-width; line-height, resolving percentages against the font size carried by the line-height resolution context; the keyword forms of the font feature and variation settings; and single-name animation-name values. Font-family lists and the keyword-valued coordinated background lists are recognized as unchanged by computation, using the background-image layer count the driver records in passing. The computed writing-mode and direction are tracked natively as well, so logical alias pairing no longer queries C++. Second, values whose computation still lives in C++ no longer cross individually: the driver queues them in the store batch as compute-in-C++ entries, and the flush handler runs the dispatcher in entry order. Deferring computation into the batch preserves the previous semantics exactly, because C++ only ever reads stored values from inside callbacks the driver invokes after flushing. The per-longhand compute callback is deleted from the driver's callback table. On the inheritance-heavy baseline workload the longhand loop now crosses the FFI about once per element, a single batch flush, with per-value slow paths remaining only for the computational-independence decision of the deliberately C++-backed grid track lists. The remaining C++-computed entries, about seven per element, are counted separately as the porting backlog. --- Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs | 2 +- .../LibWeb/CSS/Rust/src/style_compute.rs | 350 ++++++++++++++---- Libraries/LibWeb/CSS/StyleComputer.cpp | 63 ++-- .../Text/input/css/style-ffi-counters.html | 4 +- 4 files changed, 315 insertions(+), 104 deletions(-) diff --git a/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs b/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs index 933ceaa3f912f..f9bcade6b87a7 100644 --- a/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs +++ b/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs @@ -64,8 +64,8 @@ define_ffi_ops! { CascadePendingSubstitutionCallback => "cascadePendingSubstitutionCallbacks", CascadeSourceSlotCallback => "cascadeSourceSlotCallbacks", ShorthandSetLonghandCallback => "shorthandSetLonghandCallbacks", - LonghandComputeAndStoreCallback => "longhandComputeAndStoreCallbacks", LonghandStoreBatchCallback => "longhandStoreBatchCallbacks", + LonghandCppComputeFallback => "longhandCppComputeFallbacks", LonghandContextFetchCallback => "longhandContextFetchCallbacks", LonghandParentValueFetchCallback => "longhandParentValueFetchCallbacks", LonghandIndependenceFallbackCallback => "longhandIndependenceFallbackCallbacks", diff --git a/Libraries/LibWeb/CSS/Rust/src/style_compute.rs b/Libraries/LibWeb/CSS/Rust/src/style_compute.rs index 7f40609fbabc8..32ec1771e706d 100644 --- a/Libraries/LibWeb/CSS/Rust/src/style_compute.rs +++ b/Libraries/LibWeb/CSS/Rust/src/style_compute.rs @@ -1833,31 +1833,33 @@ pub const COMPUTED_KIND_PX_LENGTH: u8 = 1; pub const COMPUTED_KIND_INTEGER: u8 = 2; /// A superellipse with parameter `value`. pub const COMPUTED_KIND_SUPERELLIPSE: u8 = 3; +/// A number of `value`. +pub const COMPUTED_KIND_NUMBER: u8 = 4; +/// A percentage of `value`. +pub const COMPUTED_KIND_PERCENTAGE: u8 = 5; +/// A font-style value of the font-style keyword code in `value`. +pub const COMPUTED_KIND_FONT_STYLE: u8 = 6; +/// The value still needs computation, which the C++ flush handler performs in +/// entry order; deferring it into the batch preserves the store sequence and +/// the reads later computations make, since those reads only happen inside +/// callbacks the driver invokes after flushing. +pub const COMPUTED_KIND_COMPUTE_IN_CPP: u8 = 7; /// The leaf callbacks the C++ side provides to the property computation /// driver. The driver selects each longhand's cascaded, inherited or initial -/// value natively and calls back only to compute and store the result. +/// value natively and calls back only to flush store batches and to fetch +/// the rare context a batch entry needs. #[repr(C)] pub struct FfiLonghandCallbacks { pub context: *mut c_void, - /// Computes the selected value and stores the result; only called for - /// properties that require computation. `value_shell` stays alive for - /// the duration of the call: cascaded values are retained by the store, - /// initial values are immortal, and parent values are pinned by the - /// snapshot or the fetch below. - pub compute_and_store: unsafe extern "C" fn( - context: *mut c_void, - property_id: u16, - inherited_property_id: u16, - value_shell: *const c_void, - inheritance_dependent: bool, - inherited: bool, - ), - /// Stores a batch of selected values that need no computation, applying - /// each entry's side effects in property order. The driver flushes the - /// batch before any callback that may read the stored values, so the - /// C++ side always observes the same store sequence as one call per - /// property would produce. + /// Stores a batch of selected values, applying each entry's side effects + /// and any remaining C++ computation in property order. The driver + /// flushes the batch before any callback that may read the stored + /// values, so the C++ side always observes the same compute and store + /// sequence as one call per property would produce. Every entry's shell + /// stays alive for the duration of the drive: cascaded values are + /// retained by the store, initial values are immortal, and parent values + /// are pinned by the snapshot or the fetch below. pub store_computed_batch: unsafe extern "C" fn(context: *mut c_void, entries: *const FfiComputedStoreEntry, count: usize), /// Rare: fetches the parent's computed value for an explicit `inherit` of @@ -2053,6 +2055,8 @@ pub unsafe extern "C" fn rust_drive_property_computation( parent_snapshot: *const FfiParentSnapshot, has_new_font_size: bool, device_pixels_per_css_pixel: f64, + initial_font_size_raw: i32, + default_font_size_raw: i32, results: *mut FfiLonghandDriverResults, ) { crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandDriverEntry); @@ -2099,6 +2103,47 @@ pub unsafe extern "C" fn rust_drive_property_computation( pending_stores.clear(); } + fn fetch_length_resolution_context<'a>( + caches: &'a mut [Option; 3], + callbacks: &FfiLonghandCallbacks, + context: *mut c_void, + pending_stores: &mut Vec, + kind: usize, + property_id: u16, + ) -> &'a FfiLengthResolutionContext { + if caches[kind].is_none() { + // Building a context on the C++ side reads stored values. + flush_pending_stores(callbacks, context, pending_stores); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandContextFetchCallback); + let mut fetched = std::mem::MaybeUninit::::uninit(); + // SAFETY: The callback fills the context before returning. + caches[kind] = Some(unsafe { + (callbacks.length_resolution_context)(context, property_id, fetched.as_mut_ptr()); + fetched.assume_init() + }); + } + caches[kind].as_ref().unwrap() + } + + /// The Rust-owned data of a parent snapshot entry. + fn snapshot_entry_data(snapshot: &FfiParentSnapshot, property_id: u16) -> Option<&StyleValueData> { + use crate::property_metadata::FIRST_INHERITED_PROPERTY_ID; + let index = (property_id - FIRST_INHERITED_PROPERTY_ID) as usize; + assert!(index < snapshot.entry_count); + // SAFETY: Snapshot entries are valid for the drive. + unsafe { ((*snapshot.entries.add(index)).data as *const StyleValueData).as_ref() } + } + + // The computed math-depth, remembered for the font-size rule; None when C++ + // computed it, in which case font-size falls back as well. + let mut computed_math_depth: Option = None; + // The background-image list length, for the coordinated background properties. + let mut background_image_list_length: Option = None; + // The computed writing-mode and direction, tracked for logical alias pairing; + // both properties only take keywords, whose computed value is the specified one. + let mut computed_writing_mode: Option = None; + let mut computed_direction: Option = None; + for &property_id in crate::property_metadata::property_computation_order() { let mut cascaded_property_id = property_id; let mut inherited_property_id = property_id; @@ -2112,7 +2157,11 @@ pub unsafe extern "C" fn rust_drive_property_computation( let is_logical_alias = table_row_maps(&LOGICAL_ALIAS_TABLE, property_id); if is_logical_alias || table_row_maps(&PHYSICAL_TO_LOGICAL_TABLE, property_id) { if cached_writing_mode_and_direction.is_none() { - flush_pending_stores(callbacks, context, &mut pending_stores); + if let (Some(writing_mode), Some(direction)) = (computed_writing_mode, computed_direction) { + cached_writing_mode_and_direction = Some((writing_mode, direction)); + } else { + flush_pending_stores(callbacks, context, &mut pending_stores); + } } let (writing_mode, direction) = *cached_writing_mode_and_direction.get_or_insert_with(|| { crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandWritingModeCallback); @@ -2203,6 +2252,19 @@ pub unsafe extern "C" fn rust_drive_property_computation( // Whether the computed value depends on inherited information, so the specified // value must be kept for re-resolution when an ancestor changes. let value_data = unsafe { &*(value.data as *const StyleValueData) }; + + if inherited_property_id == crate::property_metadata::property_id::BACKGROUND_IMAGE + && let StyleValueData::ValueList { values, .. } = value_data + { + background_image_list_length = Some(values.as_slice().len()); + } + if let StyleValueData::Keyword { keyword } = value_data { + if property_id == crate::property_metadata::property_id::WRITING_MODE { + computed_writing_mode = keyword_to_writing_mode(*keyword); + } else if property_id == crate::property_metadata::property_id::DIRECTION { + computed_direction = keyword_to_direction(*keyword); + } + } let inheritance_dependent = crate::style_value::value_depends_on_current_color(value_data, callbacks.data_of) || !value_is_computationally_independent( @@ -2230,17 +2292,14 @@ pub unsafe extern "C" fn rust_drive_property_computation( } = value_data { let kind = computation_context_kind(inherited_property_id) as usize; - let resolution_context = cached_length_resolution_contexts[kind].get_or_insert_with(|| { - // Building a context on the C++ side reads stored values. - flush_pending_stores(callbacks, context, &mut pending_stores); - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandContextFetchCallback); - let mut fetched = std::mem::MaybeUninit::::uninit(); - // SAFETY: The callback fills the context before returning. - unsafe { - (callbacks.length_resolution_context)(context, inherited_property_id, fetched.as_mut_ptr()); - fetched.assume_init() - } - }); + let resolution_context = fetch_length_resolution_context( + &mut cached_length_resolution_contexts, + callbacks, + context, + &mut pending_stores, + kind, + inherited_property_id, + ); let result = absolutize_length(*length_value, *unit as usize, resolution_context); if result.handled { if result.resolved_viewport_relative_length { @@ -2266,6 +2325,9 @@ pub unsafe extern "C" fn rust_drive_property_computation( Px(f64), Integer(i32), Superellipse(f64), + Number(f64), + Percentage(f64), + FontStyle(u8), } use crate::property_metadata::property_id as prop; let synthesized_px_length = |absolutized: Option| { @@ -2318,18 +2380,12 @@ pub unsafe extern "C" fn rust_drive_property_computation( // (math-depth 0, math-style normal). let (inherited_math_depth, inherited_math_style_is_compact) = match snapshot { Some(snapshot) => { - let entry_data = |property_id: u16| { - let index = (property_id - FIRST_INHERITED_PROPERTY_ID) as usize; - assert!(index < snapshot.entry_count); - // SAFETY: Snapshot entries are valid for the drive. - unsafe { (*snapshot.entries.add(index)).data as *const StyleValueData } - }; - let math_depth = match unsafe { entry_data(prop::MATH_DEPTH).as_ref() } { + let math_depth = match snapshot_entry_data(snapshot, prop::MATH_DEPTH) { Some(StyleValueData::Integer { value }) => *value, _ => 0, }; let compact = matches!( - unsafe { entry_data(prop::MATH_STYLE).as_ref() }, + snapshot_entry_data(snapshot, prop::MATH_STYLE), Some(StyleValueData::Keyword { keyword }) if *keyword == keyword::COMPACT ); (math_depth, compact) @@ -2339,11 +2395,177 @@ pub unsafe extern "C" fn rust_drive_property_computation( let result = compute_math_depth(value_data, inherited_math_depth, inherited_math_style_is_compact); if result.handled { + computed_math_depth = Some(result.value as i32); NativeValue::Integer(result.value as i32) } else { NativeValue::Unsupported } } + (Some(absolutized), prop::FONT_SIZE) => { + if let Some(computed_math_depth) = computed_math_depth { + // A font-size relative to the inherited size also inherits the + // parent's viewport dependence of its font metrics. + if value_depends_on_inherited_info_for_property(value_data, prop::FONT_SIZE) + && snapshot.is_some_and(|snapshot| snapshot.font_metrics_depend_on_viewport_metrics) + { + results.depends_on_viewport_metrics = true; + results.font_metrics_depend_on_viewport_metrics = true; + } + let inherited = match snapshot { + Some(snapshot) => match snapshot_entry_data(snapshot, prop::FONT_SIZE) { + Some(StyleValueData::Length { value, unit }) if *unit == px_length_unit() => { + let math_depth = match snapshot_entry_data(snapshot, prop::MATH_DEPTH) { + Some(StyleValueData::Integer { value }) => *value, + _ => 0, + }; + Some((CssPixels::nearest_value_for(*value), math_depth)) + } + _ => None, + }, + None => Some((CssPixels::from_raw(initial_font_size_raw), 0)), + }; + match inherited { + Some((inherited_font_size, inherited_math_depth)) => { + let synthesized = synthesized_px_length(absolutized); + let result = compute_font_size( + synthesized.as_ref().unwrap_or(value_data), + computed_math_depth, + inherited_font_size, + inherited_math_depth, + CssPixels::from_raw(default_font_size_raw), + ); + if result.handled { + if result.unchanged { + match absolutized { + Some(px) => NativeValue::Px(px), + None => NativeValue::Unchanged, + } + } else { + NativeValue::Px(result.value) + } + } else { + NativeValue::Unsupported + } + } + None => NativeValue::Unsupported, + } + } else { + NativeValue::Unsupported + } + } + (Some(_), prop::FONT_WEIGHT) => { + let inherited_font_weight = match snapshot { + Some(snapshot) => match snapshot_entry_data(snapshot, prop::FONT_WEIGHT) { + Some(StyleValueData::Number { value }) => Some(*value), + _ => None, + }, + None => Some(400.0), + }; + match inherited_font_weight { + Some(inherited_font_weight) => { + let result = compute_font_weight(value_data, inherited_font_weight); + if result.handled { + if result.unchanged { + NativeValue::Unchanged + } else { + NativeValue::Number(result.value) + } + } else { + NativeValue::Unsupported + } + } + None => NativeValue::Unsupported, + } + } + (Some(_), prop::FONT_STYLE) => match value_data { + StyleValueData::Keyword { keyword } => match keyword_to_font_style_keyword(*keyword) { + Some(font_style_keyword) => NativeValue::FontStyle(font_style_keyword), + None => NativeValue::Unchanged, + }, + _ => NativeValue::Unchanged, + }, + (Some(_), prop::FONT_WIDTH) => { + let result = compute_font_width(value_data); + if result.handled { + if result.unchanged { + NativeValue::Unchanged + } else { + NativeValue::Percentage(result.value) + } + } else { + NativeValue::Unsupported + } + } + (Some(_), prop::FONT_FEATURE_SETTINGS | prop::FONT_VARIATION_SETTINGS) + if matches!(value_data, StyleValueData::Keyword { .. }) => + { + NativeValue::Unchanged + } + (Some(absolutized), prop::LINE_HEIGHT) => { + let result = if matches!(value_data, StyleValueData::Percentage { .. }) { + let resolution_context = fetch_length_resolution_context( + &mut cached_length_resolution_contexts, + callbacks, + context, + &mut pending_stores, + ComputationContextKind::LineHeight as usize, + prop::LINE_HEIGHT, + ); + compute_line_height( + value_data, + CssPixels::nearest_value_for(resolution_context.font_metrics.font_size), + ) + } else { + let synthesized = synthesized_px_length(absolutized); + compute_line_height(synthesized.as_ref().unwrap_or(value_data), CssPixels::from_raw(0)) + }; + if result.handled { + if result.unchanged { + match absolutized { + Some(px) => NativeValue::Px(px), + None => NativeValue::Unchanged, + } + } else if result.is_number { + NativeValue::Number(result.value) + } else { + NativeValue::Px(result.value) + } + } else { + NativeValue::Unsupported + } + } + (None, prop::FONT_FAMILY) if matches!(value_data, StyleValueData::ValueList { .. }) => { + // A font-family list only ever holds keywords, strings and custom + // identifiers, whose absolutization is the identity. + NativeValue::Unchanged + } + ( + None, + prop::BACKGROUND_ATTACHMENT + | prop::BACKGROUND_CLIP + | prop::BACKGROUND_ORIGIN + | prop::BACKGROUND_REPEAT, + ) => { + // These coordinated lists only ever hold keywords and repeat-style + // values, whose absolutization is the identity; the coordination is + // the identity too when the list already matches the layer count. + match (value_data, background_image_list_length) { + (StyleValueData::ValueList { values, .. }, Some(layer_count)) + if values.as_slice().len() == layer_count => + { + NativeValue::Unchanged + } + _ => NativeValue::Unsupported, + } + } + (Some(_), prop::ANIMATION_NAME) + if matches!( + value_data, + StyleValueData::Keyword { .. } | StyleValueData::CustomIdent { .. } + ) => + { + NativeValue::Unchanged + } (Some(absolutized), prop::LETTER_SPACING | prop::WORD_SPACING) => { let synthesized = synthesized_px_length(absolutized); let result = compute_letter_or_word_spacing_value(synthesized.as_ref().unwrap_or(value_data)); @@ -2369,36 +2591,28 @@ pub unsafe extern "C" fn rust_drive_property_computation( _ => NativeValue::Unsupported, }; - if !matches!(native, NativeValue::Unsupported) { - let (computed_kind, computed_value) = match native { - NativeValue::Px(px) => (COMPUTED_KIND_PX_LENGTH, px), - NativeValue::Integer(integer) => (COMPUTED_KIND_INTEGER, integer as f64), - NativeValue::Superellipse(parameter) => (COMPUTED_KIND_SUPERELLIPSE, parameter), - _ => (COMPUTED_KIND_SHELL, 0.0), - }; - pending_stores.push(FfiComputedStoreEntry { - property_id, - inherited_property_id, - shell: value.shell, - inheritance_dependent, - inherited: inherit_fetch_attempted, - computed_kind, - value: computed_value, - }); - } else { - flush_pending_stores(callbacks, context, &mut pending_stores); - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandComputeAndStoreCallback); - unsafe { - (callbacks.compute_and_store)( - context, - property_id, - inherited_property_id, - value.shell, - inheritance_dependent, - inherit_fetch_attempted, - ); + let (computed_kind, computed_value) = match native { + NativeValue::Px(px) => (COMPUTED_KIND_PX_LENGTH, px), + NativeValue::Integer(integer) => (COMPUTED_KIND_INTEGER, integer as f64), + NativeValue::Superellipse(parameter) => (COMPUTED_KIND_SUPERELLIPSE, parameter), + NativeValue::Number(number) => (COMPUTED_KIND_NUMBER, number), + NativeValue::Percentage(percentage) => (COMPUTED_KIND_PERCENTAGE, percentage), + NativeValue::FontStyle(font_style_keyword) => (COMPUTED_KIND_FONT_STYLE, font_style_keyword as f64), + NativeValue::Unchanged => (COMPUTED_KIND_SHELL, 0.0), + NativeValue::Unsupported => { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::LonghandCppComputeFallback); + (COMPUTED_KIND_COMPUTE_IN_CPP, 0.0) } - } + }; + pending_stores.push(FfiComputedStoreEntry { + property_id, + inherited_property_id, + shell: value.shell, + inheritance_dependent, + inherited: inherit_fetch_attempted, + computed_kind, + value: computed_value, + }); } else { pending_stores.push(FfiComputedStoreEntry { property_id, diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index 5651453081a91..1a939c6f3a024 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -3369,9 +3369,8 @@ NonnullRefPtr StyleComputer::compute_properties(DOM::Abstrac }; } - // Computes the value the driver selected for the longhand, when computation is - // needed, and stores the result. The driver selects, pins and flags values - // natively; this is the only per-longhand callback left. + // Copies the parent's animated value when a longhand inherits, as its store is + // applied, so later properties' computation contexts observe the animated value. // FIXME: Do we need to recompute animated inherited values? auto copy_animated_inherited_value = [&](PropertyID property_id, PropertyID inherited_property_id) { if (auto const* animated_properties = computed_values_to_inherit_from->animated_properties(); animated_properties && animated_properties->has_property(inherited_property_id)) { @@ -3388,27 +3387,6 @@ NonnullRefPtr StyleComputer::compute_properties(DOM::Abstrac } }; - auto compute_and_store = [&](PropertyID property_id, PropertyID inherited_property_id, StyleValue const& value, bool inheritance_dependent, bool inherited) { - if (inherited) - copy_animated_inherited_value(property_id, inherited_property_id); - - // Store the resolved specified value for properties whose computation depends on inherited info, so they can - // be re-resolved when an ancestor changes without keeping CascadedProperties alive on the element. - if (inheritance_dependent) - builder.add_inheritance_dependent_specified_value(property_id, value); - - // NB: We compute using the inherited (physical) property to avoid having to add cases for all the logical - // alias properties in `compute_value_of_property` - bool depends_on_viewport_metrics = false; - auto computed_value = compute_property(inherited_property_id, value, depends_on_viewport_metrics); - if (depends_on_viewport_metrics) { - builder.set_depends_on_viewport_metrics(); - if (property_affects_font_metrics(inherited_property_id)) - builder.set_font_metrics_depend_on_viewport_metrics(); - } - builder.set_property_without_modifying_flags(property_id, move(computed_value)); - }; - // Hands the driver the length resolution context a property's computation would // use, so plain lengths can absolutize natively; the driver caches one per // context kind, like get_computation_context_for_property does here. @@ -3417,8 +3395,9 @@ NonnullRefPtr StyleComputer::compute_properties(DOM::Abstrac return to_ffi_length_resolution_context(computation_context.length_resolution_context); }; - // Applies a batch of store operations the driver queued for properties that need no - // computation, in property order, replicating the per-property side effects. + // Applies a batch of store operations the driver queued, in property order, + // replicating the per-property side effects and performing any computation the + // driver deferred to C++. auto store_computed_batch = [&](ComputedValuesFFI::FfiComputedStoreEntry const* entries, size_t count) { for (size_t i = 0; i < count; ++i) { auto const& entry = entries[i]; @@ -3427,15 +3406,40 @@ NonnullRefPtr StyleComputer::compute_properties(DOM::Abstrac auto const& value = *static_cast(entry.shell); if (entry.inherited) copy_animated_inherited_value(property_id, inherited_property_id); + // Store the resolved specified value for properties whose computation depends on + // inherited info, so they can be re-resolved when an ancestor changes without + // keeping CascadedProperties alive on the element. if (entry.inheritance_dependent) builder.add_inheritance_dependent_specified_value(property_id, value); switch (entry.computed_kind) { + case ComputedValuesFFI::COMPUTED_KIND_COMPUTE_IN_CPP: { + // NB: We compute using the inherited (physical) property to avoid having to add cases for all the + // logical alias properties in `compute_value_of_property` + bool depends_on_viewport_metrics = false; + auto computed_value = compute_property(inherited_property_id, value, depends_on_viewport_metrics); + if (depends_on_viewport_metrics) { + builder.set_depends_on_viewport_metrics(); + if (property_affects_font_metrics(inherited_property_id)) + builder.set_font_metrics_depend_on_viewport_metrics(); + } + builder.set_property_without_modifying_flags(property_id, move(computed_value)); + break; + } case ComputedValuesFFI::COMPUTED_KIND_PX_LENGTH: builder.set_property_without_modifying_flags(property_id, LengthStyleValue::create(Length::make_px(entry.value))); break; case ComputedValuesFFI::COMPUTED_KIND_INTEGER: builder.set_property_without_modifying_flags(property_id, IntegerStyleValue::create(static_cast(entry.value))); break; + case ComputedValuesFFI::COMPUTED_KIND_NUMBER: + builder.set_property_without_modifying_flags(property_id, NumberStyleValue::create(entry.value)); + break; + case ComputedValuesFFI::COMPUTED_KIND_PERCENTAGE: + builder.set_property_without_modifying_flags(property_id, PercentageStyleValue::create(Percentage(entry.value))); + break; + case ComputedValuesFFI::COMPUTED_KIND_FONT_STYLE: + builder.set_property_without_modifying_flags(property_id, FontStyleStyleValue::create(static_cast(entry.value))); + break; case ComputedValuesFFI::COMPUTED_KIND_SUPERELLIPSE: { // NB: The round value is cached since it is the initial value of the corner-*-shape properties. if (entry.value == 1) { @@ -3457,7 +3461,6 @@ NonnullRefPtr StyleComputer::compute_properties(DOM::Abstrac // iterates the longhands in computation order, resolves logical pairing through its // mapping tables, and selects the cascaded, inherited or initial value natively. struct LonghandLoopContext { - decltype(compute_and_store)& compute_and_store_callback; decltype(store_computed_batch)& store_computed_batch_callback; decltype(fetch_length_resolution_context)& fetch_length_resolution_context_callback; decltype(get_logical_alias_mapping_context)& get_logical_alias_mapping_context_callback; @@ -3466,7 +3469,6 @@ NonnullRefPtr StyleComputer::compute_properties(DOM::Abstrac // of the drive; the driver may queue the shells in deferred store batches. Vector> pinned_parent_values; } loop_context { - .compute_and_store_callback = compute_and_store, .store_computed_batch_callback = store_computed_batch, .fetch_length_resolution_context_callback = fetch_length_resolution_context, .get_logical_alias_mapping_context_callback = get_logical_alias_mapping_context, @@ -3476,9 +3478,6 @@ NonnullRefPtr StyleComputer::compute_properties(DOM::Abstrac ComputedValuesFFI::FfiLonghandCallbacks const callbacks { .context = &loop_context, - .compute_and_store = [](void* context, u16 property_id, u16 inherited_property_id, void const* value_shell, bool inheritance_dependent, bool inherited) { - auto& loop_context = *static_cast(context); - loop_context.compute_and_store_callback(static_cast(property_id), static_cast(inherited_property_id), *static_cast(value_shell), inheritance_dependent, inherited); }, .store_computed_batch = [](void* context, ComputedValuesFFI::FfiComputedStoreEntry const* entries, size_t count) { auto& loop_context = *static_cast(context); loop_context.store_computed_batch_callback(entries, count); }, @@ -3513,7 +3512,7 @@ NonnullRefPtr StyleComputer::compute_properties(DOM::Abstrac .font_metrics_depend_on_viewport_metrics = false, .explicitly_inherited_non_inherited_property = false, }; - ComputedValuesFFI::rust_drive_property_computation(&callbacks, cascaded_properties.rust_store(), parent_snapshot.has_value() ? &*parent_snapshot : nullptr, new_font_size != nullptr, device_pixels_per_css_pixel, &driver_results); + ComputedValuesFFI::rust_drive_property_computation(&callbacks, cascaded_properties.rust_store(), parent_snapshot.has_value() ? &*parent_snapshot : nullptr, new_font_size != nullptr, device_pixels_per_css_pixel, InitialValues::font_size().raw_value(), default_user_font_size().raw_value(), &driver_results); // Apply the driver's bulk results. auto longhand_bit_is_set = [](Array const& words, size_t index) { diff --git a/Tests/LibWeb/Text/input/css/style-ffi-counters.html b/Tests/LibWeb/Text/input/css/style-ffi-counters.html index a629a6a921bcd..0b5745115ef65 100644 --- a/Tests/LibWeb/Text/input/css/style-ffi-counters.html +++ b/Tests/LibWeb/Text/input/css/style-ffi-counters.html @@ -18,8 +18,6 @@ internals.updateStyle(); const afterOneElement = internals.styleFfiCounters(); println(`longhand driver ran: ${afterOneElement.longhandDriverEntries >= 1}`); - const longhandCallbacks = - afterOneElement.longhandComputeAndStoreCallbacks + afterOneElement.longhandStoreBatchCallbacks; - println(`longhand loop called back into C++: ${longhandCallbacks >= 1}`); + println(`longhand loop called back into C++: ${afterOneElement.longhandStoreBatchCallbacks >= 1}`); }); From dbccbc8047361e8beff81c7340ef6f3342fa91d6 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 13:40:26 +0200 Subject: [PATCH 10/33] LibWeb: Build the inherited box style group in Rust Start the port of ComputedValues group population into the Rust core with the inherited box group, whose five fields are computed keyword values and whose payload layout is already defined in Rust. The group builds from the computed style value data in one call, sharing instead of allocating whenever it can: the parent's payload when every field matches it, and the immortal default payload when every field holds its initial value. A value the core cannot map falls back to the C++ setters, which remain for the other groups. The group registry keeps the default payloads it hands to C++ so builders can share them, and payload retention mirrors the C++ side's intentionally-leaked sentinel handling. The main style path passes the inheritance parent into ComputedValues::create() so the builder can share payloads at construction time rather than only through the post-construction adoption pass; sharing with a parent whose inherited box is not the default is now covered by the group sharing test, along with a child override ending the sharing. --- Libraries/LibWeb/CSS/ComputedValues.cpp | 32 +++++- Libraries/LibWeb/CSS/ComputedValues.h | 6 +- .../LibWeb/CSS/Rust/src/computed_values.rs | 103 +++++++++++++++++- Libraries/LibWeb/CSS/StyleComputer.cpp | 7 +- Libraries/LibWeb/CSS/StyleStructRef.h | 8 ++ .../css/computed-values-group-sharing.txt | 3 + .../css/computed-values-group-sharing.html | 16 +++ 7 files changed, 163 insertions(+), 12 deletions(-) diff --git a/Libraries/LibWeb/CSS/ComputedValues.cpp b/Libraries/LibWeb/CSS/ComputedValues.cpp index bde3c2e4223f3..bc9ee5e6f2502 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.cpp +++ b/Libraries/LibWeb/CSS/ComputedValues.cpp @@ -213,11 +213,26 @@ bool ComputedValues::adopt_identical_group_payloads(ComputedValues const& previo return all_shared; } -NonnullRefPtr ComputedValues::create(ComputedProperties const& computed_style, DOM::Document const& document, StyleScope const& style_scope, ColorResolutionContext color_resolution_context) +NonnullRefPtr ComputedValues::create(ComputedProperties const& computed_style, DOM::Document const& document, StyleScope const& style_scope, ColorResolutionContext color_resolution_context, ComputedValues const* inherit_parent) { Builder builder; auto& computed_values = *builder.operator->(); + // The inherited box group builds on the Rust side from the computed keyword + // values, sharing the parent's payload or the default payload when it can. A + // null result means a value the core cannot map, and the setters below apply. + auto* inherited_box_payload = ComputedValuesFFI::rust_build_inherited_box_group( + InheritedBoxValues::style_group_index, + computed_style.property(PropertyID::Visibility).rust_style_value_data(), + computed_style.property(PropertyID::Direction).rust_style_value_data(), + computed_style.property(PropertyID::WritingMode).rust_style_value_data(), + computed_style.property(PropertyID::ContentVisibility).rust_style_value_data(), + computed_style.property(PropertyID::ImageRendering).rust_style_value_data(), + inherit_parent ? static_cast(inherit_parent->m_inherited.box.operator->()) : nullptr); + bool const inherited_box_adopted = inherited_box_payload != nullptr; + if (inherited_box_adopted) + computed_values.adopt_inherited_box_group(const_cast(inherited_box_payload)); + auto custom_ident_list = [&](PropertyID property_id) { Vector names; auto append_name = [&](StyleValue const& value) { @@ -759,10 +774,12 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co computed_values.set_clear(computed_style.clear()); computed_values.set_overflow_x(computed_style.overflow_x()); computed_values.set_overflow_y(computed_style.overflow_y()); - computed_values.set_content_visibility(computed_style.content_visibility()); + if (!inherited_box_adopted) + computed_values.set_content_visibility(computed_style.content_visibility()); auto cursor = computed_style.cursor(); computed_values.set_cursor(move(cursor)); - computed_values.set_image_rendering(computed_style.image_rendering()); + if (!inherited_box_adopted) + computed_values.set_image_rendering(computed_style.image_rendering()); computed_values.set_pointer_events(computed_style.pointer_events()); computed_values.set_text_decoration_line(computed_style.text_decoration_line()); computed_values.set_text_decoration_skip_ink(computed_style.text_decoration_skip_ink()); @@ -801,7 +818,8 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co computed_values.set_z_index(computed_style.z_index()); computed_values.set_opacity(computed_style.opacity()); - computed_values.set_visibility(computed_style.visibility()); + if (!inherited_box_adopted) + computed_values.set_visibility(computed_style.visibility()); computed_values.set_width(computed_style.size_value(CSS::PropertyID::Width)); computed_values.set_min_width(computed_style.size_value(CSS::PropertyID::MinWidth)); @@ -1052,7 +1070,8 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co computed_values.set_object_fit(computed_style.object_fit()); computed_values.set_object_position(computed_style.object_position()); - computed_values.set_direction(computed_style.direction()); + if (!inherited_box_adopted) + computed_values.set_direction(computed_style.direction()); computed_values.set_unicode_bidi(computed_style.unicode_bidi()); computed_values.set_scroll_behavior(CSS::keyword_to_scroll_behavior(computed_style.property(CSS::PropertyID::ScrollBehavior).to_keyword()).release_value()); computed_values.set_scrollbar_color(computed_style.scrollbar_color(color_resolution_context)); @@ -1079,7 +1098,8 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co apply_shape_outside_item(shape_outside_value); } computed_values.set_shape_outside(move(shape_outside)); - computed_values.set_writing_mode(computed_style.writing_mode()); + if (!inherited_box_adopted) + computed_values.set_writing_mode(computed_style.writing_mode()); computed_values.set_user_select(computed_style.user_select()); computed_values.set_isolation(computed_style.isolation()); computed_values.set_mix_blend_mode(computed_style.mix_blend_mode()); diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index 86acb86d2aae1..1444f94a927a2 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -960,7 +960,7 @@ class WEB_API ComputedValues final : public RefCounted { Yes, }; - static NonnullRefPtr create(ComputedProperties const&, DOM::Document const&, StyleScope const&, ColorResolutionContext); + static NonnullRefPtr create(ComputedProperties const&, DOM::Document const&, StyleScope const&, ColorResolutionContext, ComputedValues const* inherit_parent = nullptr); RefPtr computed_style_value(PropertyID, WithAnimationsApplied = WithAnimationsApplied::Yes) const; RefPtr computed_style_value_for_inheritance(PropertyID, WithAnimationsApplied = WithAnimationsApplied::Yes) const; @@ -1892,6 +1892,10 @@ class ComputedValues::Mutator final { void set_base_values(NonnullRefPtr value) { m_values.m_base_values = move(value); } void set_animated_properties(AnimatedProperties const*); + // Adopts a Rust-built inherited box group payload, which arrives already + // carrying this reference. + void adopt_inherited_box_group(void* payload) { m_values.m_inherited.box.adopt(payload); } + void set_aspect_ratio(AspectRatio aspect_ratio) { if (m_values.m_noninherited.box->aspect_ratio == aspect_ratio) diff --git a/Libraries/LibWeb/CSS/Rust/src/computed_values.rs b/Libraries/LibWeb/CSS/Rust/src/computed_values.rs index 0bacd694a553f..303375ba970f2 100644 --- a/Libraries/LibWeb/CSS/Rust/src/computed_values.rs +++ b/Libraries/LibWeb/CSS/Rust/src/computed_values.rs @@ -68,11 +68,36 @@ pub struct StyleGroupVTable { unsafe impl Send for StyleGroupVTable {} unsafe impl Sync for StyleGroupVTable {} -static REGISTRY: OnceLock> = OnceLock::new(); +struct Registry { + vtables: Box<[StyleGroupVTable]>, + /// The intentionally leaked per-group default payloads, for building + /// groups without allocating when every field holds its initial value. + defaults: Box<[*const c_void]>, +} + +// SAFETY: The defaults are immortal, immutable payloads. +unsafe impl Send for Registry {} +unsafe impl Sync for Registry {} + +static REGISTRY: OnceLock = OnceLock::new(); fn vtable(group_index: usize) -> &'static StyleGroupVTable { let registry = REGISTRY.get().expect("style groups used before registration"); - ®istry[group_index] + ®istry.vtables[group_index] +} + +pub(crate) fn default_group_payload(group_index: usize) -> *const c_void { + REGISTRY.get().expect("style groups used before registration").defaults[group_index] +} + +/// Retains one reference to a payload, mirroring StyleStructRef::ref(): +/// intentionally leaked payloads are never counted. +pub(crate) fn retain_group_payload(group_index: usize, payload: *const c_void) { + let refcount = refcount_of(payload, vtable(group_index).align); + if refcount.load(Ordering::Relaxed) == STYLE_GROUP_STATIC_REFCOUNT { + return; + } + refcount.fetch_add(1, Ordering::Relaxed); } fn header_size(align: usize) -> usize { @@ -121,13 +146,23 @@ pub unsafe extern "C" fn rust_style_group_registry_register( ) { abort_on_panic(|| unsafe { let tables: Box<[StyleGroupVTable]> = std::slice::from_raw_parts(vtables, count).into(); + let mut defaults = Vec::with_capacity(count); for (index, table) in tables.iter().enumerate() { assert!(table.align.is_power_of_two()); let payload = allocate_payload(table, STYLE_GROUP_STATIC_REFCOUNT); (table.default_construct)(payload); *out_default_payloads.add(index) = payload; + defaults.push(payload as *const c_void); } - assert!(REGISTRY.set(tables).is_ok(), "style group registry registered twice"); + assert!( + REGISTRY + .set(Registry { + vtables: tables, + defaults: defaults.into_boxed_slice(), + }) + .is_ok(), + "style group registry registered twice" + ); }); } @@ -164,6 +199,68 @@ pub unsafe extern "C" fn rust_style_group_free(group_index: usize, payload: *mut }); } +/// Builds an inherited box group payload from the five computed keyword +/// values, sharing instead of allocating whenever it can: the parent's +/// payload when every field matches it, or the immortal default payload when +/// every field holds its initial value. Returns null when any value is not a +/// mappable keyword, in which case the C++ population path applies. +/// +/// The returned payload carries one reference for the caller; fresh payloads +/// start at one, shared payloads are retained, and default payloads are +/// intentionally leaked and never counted. +/// +/// # Safety +/// The value pointers must be valid StyleValueData or null, and +/// `parent_payload` a valid inherited box payload or null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rust_build_inherited_box_group( + group_index: usize, + visibility: *const c_void, + direction: *const c_void, + writing_mode: *const c_void, + content_visibility: *const c_void, + image_rendering: *const c_void, + parent_payload: *const c_void, +) -> *const c_void { + use crate::style_value::StyleValueData; + + abort_on_panic(|| { + let keyword_code = |data: *const c_void, map: fn(u16) -> Option| -> Option { + match unsafe { (data as *const StyleValueData).as_ref() } { + Some(StyleValueData::Keyword { keyword }) => map(*keyword), + _ => None, + } + }; + let built = InheritedBoxValues { + visibility: keyword_code(visibility, crate::style_compute::keyword_to_visibility)?, + direction: keyword_code(direction, crate::style_compute::keyword_to_direction)?, + writing_mode: keyword_code(writing_mode, crate::style_compute::keyword_to_writing_mode)?, + content_visibility: keyword_code(content_visibility, crate::style_compute::keyword_to_content_visibility)?, + image_rendering: keyword_code(image_rendering, crate::style_compute::keyword_to_image_rendering)?, + }; + + if !parent_payload.is_null() { + // SAFETY: The caller guarantees a valid inherited box payload. + if built == unsafe { *(parent_payload as *const InheritedBoxValues) } { + retain_group_payload(group_index, parent_payload); + return Some(parent_payload); + } + } + + let default_payload = default_group_payload(group_index); + // SAFETY: The default payload is a valid inherited box payload. + if built == unsafe { *(default_payload as *const InheritedBoxValues) } { + return Some(default_payload); + } + + let payload = allocate_payload(vtable(group_index), 1); + // SAFETY: The payload was allocated for this group's layout. + unsafe { *(payload as *mut InheritedBoxValues) = built }; + Some(payload as *const c_void) + }) + .unwrap_or(std::ptr::null()) +} + /// Layout of the inherited table style value group. /// /// The enum fields follow the opaque-byte convention; the border spacings are diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index 1a939c6f3a024..e39403fcc5770 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -2932,15 +2932,18 @@ NonnullRefPtr StyleComputer::build_computed_values(Compute } }; + auto const inherit_parent = abstract_element.element_to_inherit_style_from(); + auto const* inherit_parent_values = inherit_parent.has_value() ? inherit_parent->computed_values() : nullptr; + auto base_properties = computed_properties.copy_without_animations(); - auto base_values = ComputedValues::create(*base_properties, document(), style_scope, color_resolution_context); + auto base_values = ComputedValues::create(*base_properties, document(), style_scope, color_resolution_context, inherit_parent_values); auto animated_properties = computed_properties.animated_properties_snapshot(); if (!animated_properties || animated_properties->is_empty()) { adopt_group_payloads_from_parent(*base_values); return base_values; } - ComputedValues::Builder builder(*ComputedValues::create(computed_properties, document(), style_scope, move(color_resolution_context))); + ComputedValues::Builder builder(*ComputedValues::create(computed_properties, document(), style_scope, move(color_resolution_context), inherit_parent_values)); builder->set_base_values(move(base_values)); builder->set_animated_properties(animated_properties.ptr()); auto style = move(builder).build(); diff --git a/Libraries/LibWeb/CSS/StyleStructRef.h b/Libraries/LibWeb/CSS/StyleStructRef.h index 4ad58a0590517..80921d360015c 100644 --- a/Libraries/LibWeb/CSS/StyleStructRef.h +++ b/Libraries/LibWeb/CSS/StyleStructRef.h @@ -100,6 +100,14 @@ class StyleStructRef { return *m_payload; } + // Takes ownership of a payload built on the Rust side; the payload + // arrives already carrying this reference. + void adopt(void* payload) + { + deref(); + m_payload = static_cast(payload); + } + bool ptr_equals(StyleStructRef const& other) const { return m_payload == other.m_payload; } bool is_default() const { return m_payload == default_payload(); } diff --git a/Tests/LibWeb/Text/expected/css/computed-values-group-sharing.txt b/Tests/LibWeb/Text/expected/css/computed-values-group-sharing.txt index 065850cdff872..221b345310fec 100644 --- a/Tests/LibWeb/Text/expected/css/computed-values-group-sharing.txt +++ b/Tests/LibWeb/Text/expected/css/computed-values-group-sharing.txt @@ -18,3 +18,6 @@ unstyled-again child inheritedText shared with parent: true unstyled-again child font shared with parent: true unstyled-again child inheritedBox shared with parent: true unstyled-again child inheritedUI shared with parent: true +writing-mode parent: child inheritedBox at default: false +writing-mode parent: child inheritedBox shared with parent: true +visibility-overriding child inheritedBox shared with parent: false diff --git a/Tests/LibWeb/Text/input/css/computed-values-group-sharing.html b/Tests/LibWeb/Text/input/css/computed-values-group-sharing.html index f3ef4c3589df1..e5883232e7b5f 100644 --- a/Tests/LibWeb/Text/input/css/computed-values-group-sharing.html +++ b/Tests/LibWeb/Text/input/css/computed-values-group-sharing.html @@ -27,5 +27,21 @@ info = internals.styleGroupSharingInfo(child); for (const group of ["inheritedList", "inheritedText", "font", "inheritedBox", "inheritedUI"]) println(`unstyled-again child ${group} shared with parent: ${info[group].sharedWithParent}`); + + // A non-default inherited box on the parent must end up shared with the + // child, which inherits every field unchanged. + const parent = document.getElementById("parent"); + parent.style.writingMode = "vertical-rl"; + parent.style.visibility = "hidden"; + internals.updateStyle(); + info = internals.styleGroupSharingInfo(child); + println(`writing-mode parent: child inheritedBox at default: ${info.inheritedBox.isDefault}`); + println(`writing-mode parent: child inheritedBox shared with parent: ${info.inheritedBox.sharedWithParent}`); + + // A child that overrides one inherited box field must stop sharing. + child.style.visibility = "visible"; + internals.updateStyle(); + info = internals.styleGroupSharingInfo(child); + println(`visibility-overriding child inheritedBox shared with parent: ${info.inheritedBox.sharedWithParent}`); }); From 047daf709d7e4e982932eaa41e37d677151c443b Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 13:52:02 +0200 Subject: [PATCH 11/33] LibWeb: Build the inherited table style group in Rust Same recipe as the inherited box group: the three table keywords map natively and border-spacing reads a computed pixel length, with the payload shared from the parent or the defaults whenever the values allow. Two-value border-spacing lists fall back to the C++ setters, since list children are only reachable through their shells for now. --- Libraries/LibWeb/CSS/ComputedValues.cpp | 26 ++++++-- Libraries/LibWeb/CSS/ComputedValues.h | 5 +- .../LibWeb/CSS/Rust/src/computed_values.rs | 62 +++++++++++++++++++ 3 files changed, 86 insertions(+), 7 deletions(-) diff --git a/Libraries/LibWeb/CSS/ComputedValues.cpp b/Libraries/LibWeb/CSS/ComputedValues.cpp index bc9ee5e6f2502..41eb07d198e39 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.cpp +++ b/Libraries/LibWeb/CSS/ComputedValues.cpp @@ -233,6 +233,17 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co if (inherited_box_adopted) computed_values.adopt_inherited_box_group(const_cast(inherited_box_payload)); + auto* inherited_table_payload = ComputedValuesFFI::rust_build_inherited_table_group( + InheritedTableValues::style_group_index, + computed_style.property(PropertyID::BorderCollapse).rust_style_value_data(), + computed_style.property(PropertyID::CaptionSide).rust_style_value_data(), + computed_style.property(PropertyID::EmptyCells).rust_style_value_data(), + computed_style.property(PropertyID::BorderSpacing).rust_style_value_data(), + inherit_parent ? static_cast(inherit_parent->m_inherited.table.operator->()) : nullptr); + bool const inherited_table_adopted = inherited_table_payload != nullptr; + if (inherited_table_adopted) + computed_values.adopt_inherited_table_group(const_cast(inherited_table_payload)); + auto custom_ident_list = [&](PropertyID property_id) { Vector names; auto append_name = [&](StyleValue const& value) { @@ -767,10 +778,13 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co computed_values.set_float(computed_style.float_()); - computed_values.set_border_spacing_horizontal(computed_style.border_spacing_horizontal()); - computed_values.set_border_spacing_vertical(computed_style.border_spacing_vertical()); + if (!inherited_table_adopted) { + computed_values.set_border_spacing_horizontal(computed_style.border_spacing_horizontal()); + computed_values.set_border_spacing_vertical(computed_style.border_spacing_vertical()); + } - computed_values.set_caption_side(computed_style.caption_side()); + if (!inherited_table_adopted) + computed_values.set_caption_side(computed_style.caption_side()); computed_values.set_clear(computed_style.clear()); computed_values.set_overflow_x(computed_style.overflow_x()); computed_values.set_overflow_y(computed_style.overflow_y()); @@ -1023,9 +1037,11 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co computed_values.set_column_gap(computed_style.gap_value(CSS::PropertyID::ColumnGap)); computed_values.set_row_gap(computed_style.gap_value(CSS::PropertyID::RowGap)); - computed_values.set_border_collapse(computed_style.border_collapse()); + if (!inherited_table_adopted) + computed_values.set_border_collapse(computed_style.border_collapse()); - computed_values.set_empty_cells(computed_style.empty_cells()); + if (!inherited_table_adopted) + computed_values.set_empty_cells(computed_style.empty_cells()); computed_values.set_table_layout(computed_style.table_layout()); diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index 1444f94a927a2..b6fe7186793cd 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -1892,9 +1892,10 @@ class ComputedValues::Mutator final { void set_base_values(NonnullRefPtr value) { m_values.m_base_values = move(value); } void set_animated_properties(AnimatedProperties const*); - // Adopts a Rust-built inherited box group payload, which arrives already - // carrying this reference. + // Adopts Rust-built group payloads, which arrive already carrying this + // reference. void adopt_inherited_box_group(void* payload) { m_values.m_inherited.box.adopt(payload); } + void adopt_inherited_table_group(void* payload) { m_values.m_inherited.table.adopt(payload); } void set_aspect_ratio(AspectRatio aspect_ratio) { diff --git a/Libraries/LibWeb/CSS/Rust/src/computed_values.rs b/Libraries/LibWeb/CSS/Rust/src/computed_values.rs index 303375ba970f2..18cb520dd3d95 100644 --- a/Libraries/LibWeb/CSS/Rust/src/computed_values.rs +++ b/Libraries/LibWeb/CSS/Rust/src/computed_values.rs @@ -261,6 +261,68 @@ pub unsafe extern "C" fn rust_build_inherited_box_group( .unwrap_or(std::ptr::null()) } +/// Builds an inherited table group payload from the computed values, with the +/// same sharing rules as the inherited box builder. Border-spacing must be an +/// absolute pixel length; two-value spacings and anything else fall back to +/// the C++ population path by returning null. +/// +/// # Safety +/// The value pointers must be valid StyleValueData or null, and +/// `parent_payload` a valid inherited table payload or null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rust_build_inherited_table_group( + group_index: usize, + border_collapse: *const c_void, + caption_side: *const c_void, + empty_cells: *const c_void, + border_spacing: *const c_void, + parent_payload: *const c_void, +) -> *const c_void { + use crate::style_value::StyleValueData; + + abort_on_panic(|| { + let keyword_code = |data: *const c_void, map: fn(u16) -> Option| -> Option { + match unsafe { (data as *const StyleValueData).as_ref() } { + Some(StyleValueData::Keyword { keyword }) => map(*keyword), + _ => None, + } + }; + let spacing = match unsafe { (border_spacing as *const StyleValueData).as_ref() } { + Some(StyleValueData::Length { value, unit }) if *unit == crate::style_compute::px_length_unit() => { + crate::css_pixels::CssPixels::nearest_value_for(*value).raw_value() + } + _ => return None, + }; + let built = InheritedTableValues { + border_collapse: keyword_code(border_collapse, crate::style_compute::keyword_to_border_collapse)?, + caption_side: keyword_code(caption_side, crate::style_compute::keyword_to_caption_side)?, + empty_cells: keyword_code(empty_cells, crate::style_compute::keyword_to_empty_cells)?, + border_spacing_horizontal: spacing, + border_spacing_vertical: spacing, + }; + + if !parent_payload.is_null() { + // SAFETY: The caller guarantees a valid inherited table payload. + if built == unsafe { *(parent_payload as *const InheritedTableValues) } { + retain_group_payload(group_index, parent_payload); + return Some(parent_payload); + } + } + + let default_payload = default_group_payload(group_index); + // SAFETY: The default payload is a valid inherited table payload. + if built == unsafe { *(default_payload as *const InheritedTableValues) } { + return Some(default_payload); + } + + let payload = allocate_payload(vtable(group_index), 1); + // SAFETY: The payload was allocated for this group's layout. + unsafe { *(payload as *mut InheritedTableValues) = built }; + Some(payload as *const c_void) + }) + .unwrap_or(std::ptr::null()) +} + /// Layout of the inherited table style value group. /// /// The enum fields follow the opaque-byte convention; the border spacings are From d26ac366ab4737bbffb272eb1093c74c21e03d25 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 14:15:18 +0200 Subject: [PATCH 12/33] LibWeb: Add descriptor-driven style group building to the core Byte-wise Rust construction only works for the fully-plain groups whose layouts are defined in Rust. For the mixed groups, whose payloads hold C++ vectors, references and variants alongside plain fields, add a generic builder driven by per-group field descriptors that C++ registers once: each pokeable field carries its offset and kind (a keyword-mapped enum byte, a number, a pixel length, or an integer), enum fields carry a keyword-to-code table built on the C++ side from its own converters, and hard fields register as keyword constraints that let the constructor's initial value stand. The builder decodes every descriptor or returns null for the C++ population path, default-constructs a scratch payload through the group vtable, pokes the plain fields, and shares the parent or default payload when the result compares equal through the vtable's new field-wise equality callback; groups without a comparable layout report false and conservatively keep their payloads unshared. No group registers descriptors yet, so behavior is unchanged; groups switch over one table at a time. --- Libraries/LibWeb/CSS/ComputedValues.cpp | 5 + .../LibWeb/CSS/Rust/src/computed_values.rs | 206 ++++++++++++++++++ 2 files changed, 211 insertions(+) diff --git a/Libraries/LibWeb/CSS/ComputedValues.cpp b/Libraries/LibWeb/CSS/ComputedValues.cpp index 41eb07d198e39..eecf0a0f025bc 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.cpp +++ b/Libraries/LibWeb/CSS/ComputedValues.cpp @@ -56,6 +56,11 @@ static consteval ComputedValuesFFI::StyleGroupVTable make_style_group_vtable() new (payload) T(); }, .copy_construct = [](void* payload, void const* source) { new (payload) T(*static_cast(source)); }, .destruct = [](void* payload) { static_cast(payload)->~T(); }, + .equals = [](void const* a, void const* b) { + if constexpr (requires(T const& value) { value == value; }) + return *static_cast(a) == *static_cast(b); + else + return false; }, }; } diff --git a/Libraries/LibWeb/CSS/Rust/src/computed_values.rs b/Libraries/LibWeb/CSS/Rust/src/computed_values.rs index 18cb520dd3d95..76ed287568fb0 100644 --- a/Libraries/LibWeb/CSS/Rust/src/computed_values.rs +++ b/Libraries/LibWeb/CSS/Rust/src/computed_values.rs @@ -61,6 +61,9 @@ pub struct StyleGroupVTable { pub default_construct: unsafe extern "C" fn(payload: *mut c_void), pub copy_construct: unsafe extern "C" fn(payload: *mut c_void, source: *const c_void), pub destruct: unsafe extern "C" fn(payload: *mut c_void), + /// Field-wise payload equality; groups without a comparable layout + /// report false, which conservatively disables payload sharing. + pub equals: unsafe extern "C" fn(a: *const c_void, b: *const c_void) -> bool, } // SAFETY: The function pointers are stateless C++ callbacks and the plain @@ -199,6 +202,205 @@ pub unsafe extern "C" fn rust_style_group_free(group_index: usize, payload: *mut }); } +/// One field of a style group the generic builder can populate or check: a +/// pokeable simple field (an enum code mapped through a keyword table, a +/// number, or a pixel length) or a constraint requiring a hard field's value +/// to be a specific keyword so the constructor's initial value stands. +#[repr(C)] +pub struct FfiGroupFieldDescriptor { + pub group_index: u32, + pub property_id: u16, + pub offset: u32, + pub kind: u8, + /// For GROUP_FIELD_REQUIRE_KEYWORD: the required keyword. + pub keyword: u16, + /// For GROUP_FIELD_ENUM_KEYWORD: keyword code -> enum code, 255 invalid. + pub keyword_table: *const u8, + pub keyword_table_length: usize, +} + +/// An enum stored as u8, mapped through the descriptor's keyword table. +pub const GROUP_FIELD_ENUM_KEYWORD: u8 = 0; +/// A number stored as f32. +pub const GROUP_FIELD_F32: u8 = 1; +/// A number stored as f64. +pub const GROUP_FIELD_F64: u8 = 2; +/// A pixel length stored as raw CSSPixels (i32). +pub const GROUP_FIELD_CSS_PIXELS: u8 = 3; +/// An integer stored as u64. +pub const GROUP_FIELD_U64: u8 = 4; +/// A constraint: the value must be this keyword; nothing is written. +pub const GROUP_FIELD_REQUIRE_KEYWORD: u8 = 5; + +struct FieldDescriptors(Box<[FfiGroupFieldDescriptor]>); + +// SAFETY: The keyword tables are immortal C++ statics. +unsafe impl Send for FieldDescriptors {} +unsafe impl Sync for FieldDescriptors {} + +static FIELD_DESCRIPTORS: OnceLock = OnceLock::new(); + +/// Installs the pokeable-field descriptors for every group in one flat array. +/// +/// # Safety +/// `descriptors` must point at `count` valid descriptors whose keyword tables +/// stay alive for the process lifetime. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rust_style_group_register_field_descriptors( + descriptors: *const FfiGroupFieldDescriptor, + count: usize, +) { + abort_on_panic(|| { + let slice = unsafe { std::slice::from_raw_parts(descriptors, count) }; + let copied: Box<[FfiGroupFieldDescriptor]> = slice + .iter() + .map(|descriptor| FfiGroupFieldDescriptor { ..*descriptor }) + .collect(); + assert!( + FIELD_DESCRIPTORS.set(FieldDescriptors(copied)).is_ok(), + "field descriptors installed twice" + ); + }); +} + +/// Builds a style group payload generically from its registered field +/// descriptors: decodes every descriptor's value (returning null for the C++ +/// population path when any value cannot be decoded or a constraint fails), +/// default-constructs a scratch payload, pokes the simple fields, and shares +/// the parent or default payload when the result compares equal. +/// +/// # Safety +/// `values` must hold one valid (shell, data) entry per registered descriptor +/// of the group, in registration order; `parent_payload` must be a valid +/// payload of the group or null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rust_build_style_group( + group_index: usize, + values: *const crate::style_compute::FfiShellAndData, + count: usize, + parent_payload: *const c_void, +) -> *const c_void { + use crate::style_value::StyleValueData; + + abort_on_panic(|| { + let all = &FIELD_DESCRIPTORS.get()?.0; + let descriptors: Vec<&FfiGroupFieldDescriptor> = all + .iter() + .filter(|descriptor| descriptor.group_index as usize == group_index) + .collect(); + if descriptors.len() != count { + return None; + } + let values = unsafe { std::slice::from_raw_parts(values, count) }; + + enum Poke { + U8(u32, u8), + F32(u32, f32), + F64(u32, f64), + I32(u32, i32), + U64(u32, u64), + } + let mut pokes = Vec::with_capacity(count); + for (descriptor, value) in descriptors.iter().zip(values) { + let data = unsafe { (value.data as *const StyleValueData).as_ref() }?; + match descriptor.kind { + GROUP_FIELD_ENUM_KEYWORD => { + let StyleValueData::Keyword { keyword } = data else { + return None; + }; + let table = unsafe { + std::slice::from_raw_parts(descriptor.keyword_table, descriptor.keyword_table_length) + }; + let code = *table.get(*keyword as usize)?; + if code == 255 { + return None; + } + pokes.push(Poke::U8(descriptor.offset, code)); + } + GROUP_FIELD_F32 => { + let StyleValueData::Number { value } = data else { + return None; + }; + pokes.push(Poke::F32(descriptor.offset, *value as f32)); + } + GROUP_FIELD_F64 => { + let StyleValueData::Number { value } = data else { + return None; + }; + pokes.push(Poke::F64(descriptor.offset, *value)); + } + GROUP_FIELD_CSS_PIXELS => { + let StyleValueData::Length { value, unit } = data else { + return None; + }; + if *unit != crate::style_compute::px_length_unit() { + return None; + } + pokes.push(Poke::I32( + descriptor.offset, + crate::css_pixels::CssPixels::nearest_value_for(*value).raw_value(), + )); + } + GROUP_FIELD_U64 => { + let StyleValueData::Integer { value } = data else { + return None; + }; + if *value < 0 { + return None; + } + pokes.push(Poke::U64(descriptor.offset, *value as u64)); + } + GROUP_FIELD_REQUIRE_KEYWORD => { + let StyleValueData::Keyword { keyword } = data else { + return None; + }; + if *keyword != descriptor.keyword { + return None; + } + } + _ => return None, + } + } + + let table = vtable(group_index); + let scratch = allocate_payload(table, 1); + // SAFETY: The scratch payload was allocated for this group's layout, + // and every poke offset comes from offsetof on the C++ side. + unsafe { + (table.default_construct)(scratch); + for poke in &pokes { + let base = scratch as *mut u8; + match *poke { + Poke::U8(offset, value) => *base.add(offset as usize) = value, + Poke::F32(offset, value) => *(base.add(offset as usize) as *mut f32) = value, + Poke::F64(offset, value) => *(base.add(offset as usize) as *mut f64) = value, + Poke::I32(offset, value) => *(base.add(offset as usize) as *mut i32) = value, + Poke::U64(offset, value) => *(base.add(offset as usize) as *mut u64) = value, + } + } + } + + let free_scratch = || unsafe { + (table.destruct)(scratch); + let allocation = (scratch as *mut u8).sub(header_size(table.align)); + dealloc(allocation, allocation_layout(table)); + }; + + if !parent_payload.is_null() && unsafe { (table.equals)(scratch, parent_payload) } { + free_scratch(); + retain_group_payload(group_index, parent_payload); + return Some(parent_payload); + } + let default_payload = default_group_payload(group_index); + if unsafe { (table.equals)(scratch, default_payload) } { + free_scratch(); + return Some(default_payload); + } + Some(scratch as *const c_void) + }) + .unwrap_or(std::ptr::null()) +} + /// Builds an inherited box group payload from the five computed keyword /// values, sharing instead of allocating whenever it can: the parent's /// payload when every field matches it, or the immortal default payload when @@ -377,6 +579,9 @@ mod tests { unsafe extern "C" fn test_destruct(_payload: *mut c_void) { LIVE.fetch_sub(1, Ordering::Relaxed); } + unsafe extern "C" fn test_equals(a: *const c_void, b: *const c_void) -> bool { + unsafe { *(a as *const u64) == *(b as *const u64) } + } #[test] fn payload_lifecycle() { @@ -386,6 +591,7 @@ mod tests { default_construct: test_default_construct, copy_construct: test_copy_construct, destruct: test_destruct, + equals: test_equals, }]; let mut defaults = [std::ptr::null::(); 1]; unsafe { From 5d0f5d506dc325b372aeab4400bb36cd433d07fb Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 14:27:35 +0200 Subject: [PATCH 13/33] LibWeb: Build the alignment style group through the descriptors The first group on the generic builder: twelve enum and number fields poke through registered descriptors, with the keyword-code tables built from the C++ converters at registration time, and the flex-basis and gap fields registering as keyword constraints so the constructor's initial values stand for their common forms. Compound alignment values and non-initial gaps fail descriptor decoding and fall back to the C++ setters. An i32 field kind joins the builder for order, which can be negative. --- Libraries/LibWeb/CSS/ComputedValues.cpp | 134 ++++++++++++++++-- Libraries/LibWeb/CSS/ComputedValues.h | 1 + .../LibWeb/CSS/Rust/src/computed_values.rs | 8 ++ 3 files changed, 129 insertions(+), 14 deletions(-) diff --git a/Libraries/LibWeb/CSS/ComputedValues.cpp b/Libraries/LibWeb/CSS/ComputedValues.cpp index eecf0a0f025bc..4d52dea0b0a82 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.cpp +++ b/Libraries/LibWeb/CSS/ComputedValues.cpp @@ -64,6 +64,83 @@ static consteval ComputedValuesFFI::StyleGroupVTable make_style_group_vtable() }; } +// Builds the keyword-code table for one enum-typed group field: keyword code +// to enum code, 255 for keywords the converter rejects. The generic Rust +// group builder maps values through these tables. +template +static Array const& keyword_code_table() +{ + static auto const table = [] { + Array built; + built.fill(255); + for (size_t keyword = 0; keyword < number_of_keywords; ++keyword) { + if (auto value = converter(static_cast(keyword)); value.has_value()) + built[keyword] = static_cast(to_underlying(*value)); + } + return built; + }(); + return table; +} + +// The properties feeding the alignment group's descriptors, in registration +// order; create() gathers their computed values in the same order. +static constexpr Array alignment_group_properties { + PropertyID::FlexDirection, + PropertyID::FlexWrap, + PropertyID::FlexBasis, + PropertyID::FlexGrow, + PropertyID::FlexShrink, + PropertyID::Order, + PropertyID::AlignContent, + PropertyID::AlignItems, + PropertyID::AlignSelf, + PropertyID::JustifyContent, + PropertyID::JustifyItems, + PropertyID::JustifySelf, + PropertyID::ColumnGap, + PropertyID::RowGap, +}; + +static void register_style_group_field_descriptors() +{ + using namespace ComputedValuesFFI; + static_assert(sizeof(FlexDirection) == 1 && sizeof(FlexWrap) == 1 && sizeof(AlignContent) == 1 + && sizeof(AlignItems) == 1 && sizeof(AlignSelf) == 1 && sizeof(JustifyContent) == 1 + && sizeof(JustifyItems) == 1 && sizeof(JustifySelf) == 1); + + Vector descriptors; + auto add = [&](size_t group_index, PropertyID property, u32 offset, u8 kind, u16 keyword, Array const* keyword_table) { + descriptors.append({ + .group_index = static_cast(group_index), + .property_id = static_cast(to_underlying(property)), + .offset = offset, + .kind = kind, + .keyword = keyword, + .keyword_table = keyword_table ? keyword_table->data() : nullptr, + .keyword_table_length = keyword_table ? keyword_table->size() : 0, + }); + }; + + using Alignment = ComputedValues::AlignmentValues; + constexpr auto alignment = to_underlying(StyleGroupIndex::AlignmentValues); + add(alignment, PropertyID::FlexDirection, offsetof(Alignment, flex_direction), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(alignment, PropertyID::FlexWrap, offsetof(Alignment, flex_wrap), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(alignment, PropertyID::FlexBasis, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::Auto), nullptr); + add(alignment, PropertyID::FlexGrow, offsetof(Alignment, flex_grow), GROUP_FIELD_F64, 0, nullptr); + add(alignment, PropertyID::FlexShrink, offsetof(Alignment, flex_shrink), GROUP_FIELD_F64, 0, nullptr); + add(alignment, PropertyID::Order, offsetof(Alignment, order), GROUP_FIELD_I32, 0, nullptr); + add(alignment, PropertyID::AlignContent, offsetof(Alignment, align_content), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(alignment, PropertyID::AlignItems, offsetof(Alignment, align_items), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(alignment, PropertyID::AlignSelf, offsetof(Alignment, align_self), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(alignment, PropertyID::JustifyContent, offsetof(Alignment, justify_content), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(alignment, PropertyID::JustifyItems, offsetof(Alignment, justify_items), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(alignment, PropertyID::JustifySelf, offsetof(Alignment, justify_self), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(alignment, PropertyID::ColumnGap, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::Normal), nullptr); + add(alignment, PropertyID::RowGap, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::Normal), nullptr); + + rust_style_group_register_field_descriptors(descriptors.data(), descriptors.size()); +} + // Groups whose layout is defined in Rust must not change size or alignment when the C++ // side layers initial values and accessors on top of the mirrored layout. static_assert(sizeof(ComputedValues::InheritedBoxValues) == sizeof(ComputedValuesFFI::InheritedBoxValues)); @@ -81,6 +158,7 @@ void const* style_group_default_payload(size_t group_index) #undef LIBWEB_STYLE_GROUP_VTABLE Array payloads {}; ComputedValuesFFI::rust_style_group_registry_register(vtables.data(), vtables.size(), payloads.data()); + register_style_group_field_descriptors(); return payloads; }(); return default_payloads[group_index]; @@ -249,6 +327,20 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co if (inherited_table_adopted) computed_values.adopt_inherited_table_group(const_cast(inherited_table_payload)); + Array alignment_group_values; + for (size_t i = 0; i < alignment_group_properties.size(); ++i) { + auto const& value = computed_style.property(alignment_group_properties[i]); + alignment_group_values[i] = { &value, value.rust_style_value_data() }; + } + auto* alignment_payload = ComputedValuesFFI::rust_build_style_group( + AlignmentValues::style_group_index, + alignment_group_values.data(), + alignment_group_values.size(), + inherit_parent ? static_cast(inherit_parent->m_noninherited.alignment.operator->()) : nullptr); + bool const alignment_adopted = alignment_payload != nullptr; + if (alignment_adopted) + computed_values.adopt_alignment_group(const_cast(alignment_payload)); + auto custom_ident_list = [&](PropertyID property_id) { Vector names; auto append_name = [&](StyleValue const& value) { @@ -510,12 +602,18 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co computed_values.set_display(computed_style.display()); computed_values.set_display_before_box_type_transformation(computed_style.display_before_box_type_transformation()); - computed_values.set_flex_direction(computed_style.flex_direction()); - computed_values.set_flex_wrap(computed_style.flex_wrap()); - computed_values.set_flex_basis(computed_style.flex_basis()); - computed_values.set_flex_grow(computed_style.flex_grow()); - computed_values.set_flex_shrink(computed_style.flex_shrink()); - computed_values.set_order(computed_style.order()); + if (!alignment_adopted) + computed_values.set_flex_direction(computed_style.flex_direction()); + if (!alignment_adopted) + computed_values.set_flex_wrap(computed_style.flex_wrap()); + if (!alignment_adopted) + computed_values.set_flex_basis(computed_style.flex_basis()); + if (!alignment_adopted) + computed_values.set_flex_grow(computed_style.flex_grow()); + if (!alignment_adopted) + computed_values.set_flex_shrink(computed_style.flex_shrink()); + if (!alignment_adopted) + computed_values.set_order(computed_style.order()); computed_values.set_clip(computed_style.clip()); computed_values.set_backdrop_filter(computed_style.backdrop_filter()); @@ -524,13 +622,19 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co computed_values.set_flood_color(computed_style.color(CSS::PropertyID::FloodColor, color_resolution_context)); computed_values.set_flood_opacity(computed_style.flood_opacity()); - computed_values.set_justify_content(computed_style.justify_content()); - computed_values.set_justify_items(computed_style.justify_items()); - computed_values.set_justify_self(computed_style.justify_self()); + if (!alignment_adopted) + computed_values.set_justify_content(computed_style.justify_content()); + if (!alignment_adopted) + computed_values.set_justify_items(computed_style.justify_items()); + if (!alignment_adopted) + computed_values.set_justify_self(computed_style.justify_self()); - computed_values.set_align_content(computed_style.align_content()); - computed_values.set_align_items(computed_style.align_items()); - computed_values.set_align_self(computed_style.align_self()); + if (!alignment_adopted) + computed_values.set_align_content(computed_style.align_content()); + if (!alignment_adopted) + computed_values.set_align_items(computed_style.align_items()); + if (!alignment_adopted) + computed_values.set_align_self(computed_style.align_self()); computed_values.set_appearance(computed_style.appearance()); computed_values.set_computed_appearance(keyword_to_appearance(computed_style.property(PropertyID::Appearance).to_keyword()).release_value()); @@ -1039,8 +1143,10 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co computed_values.set_column_width(computed_style.size_value(CSS::PropertyID::ColumnWidth)); computed_values.set_column_height(computed_style.size_value(CSS::PropertyID::ColumnHeight)); - computed_values.set_column_gap(computed_style.gap_value(CSS::PropertyID::ColumnGap)); - computed_values.set_row_gap(computed_style.gap_value(CSS::PropertyID::RowGap)); + if (!alignment_adopted) + computed_values.set_column_gap(computed_style.gap_value(CSS::PropertyID::ColumnGap)); + if (!alignment_adopted) + computed_values.set_row_gap(computed_style.gap_value(CSS::PropertyID::RowGap)); if (!inherited_table_adopted) computed_values.set_border_collapse(computed_style.border_collapse()); diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index b6fe7186793cd..816e133948878 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -1896,6 +1896,7 @@ class ComputedValues::Mutator final { // reference. void adopt_inherited_box_group(void* payload) { m_values.m_inherited.box.adopt(payload); } void adopt_inherited_table_group(void* payload) { m_values.m_inherited.table.adopt(payload); } + void adopt_alignment_group(void* payload) { m_values.m_noninherited.alignment.adopt(payload); } void set_aspect_ratio(AspectRatio aspect_ratio) { diff --git a/Libraries/LibWeb/CSS/Rust/src/computed_values.rs b/Libraries/LibWeb/CSS/Rust/src/computed_values.rs index 76ed287568fb0..f0476df5cb943 100644 --- a/Libraries/LibWeb/CSS/Rust/src/computed_values.rs +++ b/Libraries/LibWeb/CSS/Rust/src/computed_values.rs @@ -231,6 +231,8 @@ pub const GROUP_FIELD_CSS_PIXELS: u8 = 3; pub const GROUP_FIELD_U64: u8 = 4; /// A constraint: the value must be this keyword; nothing is written. pub const GROUP_FIELD_REQUIRE_KEYWORD: u8 = 5; +/// An integer stored as i32. +pub const GROUP_FIELD_I32: u8 = 6; struct FieldDescriptors(Box<[FfiGroupFieldDescriptor]>); @@ -358,6 +360,12 @@ pub unsafe extern "C" fn rust_build_style_group( return None; } } + GROUP_FIELD_I32 => { + let StyleValueData::Integer { value } = data else { + return None; + }; + pokes.push(Poke::I32(descriptor.offset, *value)); + } _ => return None, } } From 42548010a69bddd72ac65310d36037a1681b0d7c Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 14:51:14 +0200 Subject: [PATCH 14/33] LibWeb: Build the text reset style group through the descriptors Color fields resolve against the element's own colors, which the group builder core cannot do, so the gathered value entries grow a resolved raw color that the C++ gather loop fills from its color resolution context, and a color field kind pokes it. The text reset group uses it for text-decoration-color, with the decoration line, thickness and white-space-trim registering as keyword constraints and the remaining fields as keyword-mapped enums. The constraint for text-decoration-line exposed that the group's constructor default held the none keyword's enum in a one-element vector, while the computed representation of none is the empty list; the payloads never compared equal, which also kept every element from sharing the group's default payload. The constructor default now matches the computed representation. --- Libraries/LibWeb/CSS/ComputedValues.cpp | 71 ++++++++++++++++--- Libraries/LibWeb/CSS/ComputedValues.h | 4 +- .../LibWeb/CSS/Rust/src/computed_values.rs | 23 +++++- 3 files changed, 86 insertions(+), 12 deletions(-) diff --git a/Libraries/LibWeb/CSS/ComputedValues.cpp b/Libraries/LibWeb/CSS/ComputedValues.cpp index 4d52dea0b0a82..b9339d52f304b 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.cpp +++ b/Libraries/LibWeb/CSS/ComputedValues.cpp @@ -101,6 +101,18 @@ static constexpr Array alignment_group_properties { PropertyID::RowGap, }; +// The properties feeding the text reset group's descriptors, in registration +// order. +static constexpr Array text_reset_group_properties { + PropertyID::TextDecorationLine, + PropertyID::TextDecorationThickness, + PropertyID::TextDecorationStyle, + PropertyID::TextDecorationColor, + PropertyID::TextOverflow, + PropertyID::UnicodeBidi, + PropertyID::WhiteSpaceTrim, +}; + static void register_style_group_field_descriptors() { using namespace ComputedValuesFFI; @@ -138,6 +150,17 @@ static void register_style_group_field_descriptors() add(alignment, PropertyID::ColumnGap, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::Normal), nullptr); add(alignment, PropertyID::RowGap, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::Normal), nullptr); + static_assert(sizeof(Color) == sizeof(u32)); + using TextReset = ComputedValues::TextResetValues; + constexpr auto text_reset = to_underlying(StyleGroupIndex::TextResetValues); + add(text_reset, PropertyID::TextDecorationLine, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::None), nullptr); + add(text_reset, PropertyID::TextDecorationThickness, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::Auto), nullptr); + add(text_reset, PropertyID::TextDecorationStyle, offsetof(TextReset, text_decoration_style), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(text_reset, PropertyID::TextDecorationColor, offsetof(TextReset, text_decoration_color), GROUP_FIELD_COLOR, 0, nullptr); + add(text_reset, PropertyID::TextOverflow, offsetof(TextReset, text_overflow), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(text_reset, PropertyID::UnicodeBidi, offsetof(TextReset, unicode_bidi), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(text_reset, PropertyID::WhiteSpaceTrim, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::None), nullptr); + rust_style_group_register_field_descriptors(descriptors.data(), descriptors.size()); } @@ -327,11 +350,15 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co if (inherited_table_adopted) computed_values.adopt_inherited_table_group(const_cast(inherited_table_payload)); - Array alignment_group_values; - for (size_t i = 0; i < alignment_group_properties.size(); ++i) { - auto const& value = computed_style.property(alignment_group_properties[i]); - alignment_group_values[i] = { &value, value.rust_style_value_data() }; - } + auto gather_group_values = [&](Array const& properties, Array& entries) { + for (size_t i = 0; i < N; ++i) { + auto const& value = computed_style.property(properties[i]); + entries[i] = { &value, value.rust_style_value_data(), 0, false }; + } + }; + + Array alignment_group_values; + gather_group_values(alignment_group_properties, alignment_group_values); auto* alignment_payload = ComputedValuesFFI::rust_build_style_group( AlignmentValues::style_group_index, alignment_group_values.data(), @@ -341,6 +368,25 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co if (alignment_adopted) computed_values.adopt_alignment_group(const_cast(alignment_payload)); + // The gather resolves color fields against the element's colors, since the + // builder core cannot; the resolved raw color travels with the entry. + Array text_reset_group_values; + gather_group_values(text_reset_group_properties, text_reset_group_values); + for (size_t i = 0; i < text_reset_group_properties.size(); ++i) { + if (text_reset_group_properties[i] == PropertyID::TextDecorationColor) { + text_reset_group_values[i].resolved_color = computed_style.color(PropertyID::TextDecorationColor, color_resolution_context).value(); + text_reset_group_values[i].has_resolved_color = true; + } + } + auto* text_reset_payload = ComputedValuesFFI::rust_build_style_group( + TextResetValues::style_group_index, + text_reset_group_values.data(), + text_reset_group_values.size(), + inherit_parent ? static_cast(inherit_parent->m_noninherited.text_reset.operator->()) : nullptr); + bool const text_reset_adopted = text_reset_payload != nullptr; + if (text_reset_adopted) + computed_values.adopt_text_reset_group(const_cast(text_reset_payload)); + auto custom_ident_list = [&](PropertyID property_id) { Vector names; auto append_name = [&](StyleValue const& value) { @@ -853,7 +899,8 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co computed_values.set_tab_size(computed_style.tab_size()); computed_values.set_white_space_collapse(computed_style.white_space_collapse()); - computed_values.set_white_space_trim(computed_style.white_space_trim()); + if (!text_reset_adopted) + computed_values.set_white_space_trim(computed_style.white_space_trim()); computed_values.set_word_break(computed_style.word_break()); switch (computed_style.property(CSS::PropertyID::OverflowWrap).to_keyword()) { case CSS::Keyword::Normal: @@ -904,9 +951,11 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co if (!inherited_box_adopted) computed_values.set_image_rendering(computed_style.image_rendering()); computed_values.set_pointer_events(computed_style.pointer_events()); - computed_values.set_text_decoration_line(computed_style.text_decoration_line()); + if (!text_reset_adopted) + computed_values.set_text_decoration_line(computed_style.text_decoration_line()); computed_values.set_text_decoration_skip_ink(computed_style.text_decoration_skip_ink()); - computed_values.set_text_decoration_style(computed_style.text_decoration_style()); + if (!text_reset_adopted) + computed_values.set_text_decoration_style(computed_style.text_decoration_style()); computed_values.set_text_transform(computed_style.text_transform()); auto list_style_type = computed_style.list_style_type(style_scope); @@ -928,8 +977,10 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co if (list_style_image.is_abstract_image()) computed_values.set_list_style_image(list_style_image.as_abstract_image()); - computed_values.set_text_decoration_color(computed_style.color(CSS::PropertyID::TextDecorationColor, color_resolution_context)); - computed_values.set_text_decoration_thickness(computed_style.text_decoration_thickness()); + if (!text_reset_adopted) + computed_values.set_text_decoration_color(computed_style.color(CSS::PropertyID::TextDecorationColor, color_resolution_context)); + if (!text_reset_adopted) + computed_values.set_text_decoration_thickness(computed_style.text_decoration_thickness()); auto const& webkit_text_fill_color = computed_style.property(CSS::PropertyID::WebkitTextFillColor); computed_values.set_webkit_text_fill_color( diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index 816e133948878..c749552879465 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -1655,7 +1655,8 @@ class WEB_API ComputedValues final : public RefCounted { struct TextResetValues { static constexpr size_t style_group_index = to_underlying(StyleGroupIndex::TextResetValues); - Vector text_decoration_line { InitialValues::text_decoration_line() }; + // NB: A computed text-decoration-line of none is the empty list. + Vector text_decoration_line {}; TextDecorationThickness text_decoration_thickness { TextDecorationThickness::Auto {} }; TextDecorationStyle text_decoration_style { InitialValues::text_decoration_style() }; Color text_decoration_color { InitialValues::color() }; @@ -1897,6 +1898,7 @@ class ComputedValues::Mutator final { void adopt_inherited_box_group(void* payload) { m_values.m_inherited.box.adopt(payload); } void adopt_inherited_table_group(void* payload) { m_values.m_inherited.table.adopt(payload); } void adopt_alignment_group(void* payload) { m_values.m_noninherited.alignment.adopt(payload); } + void adopt_text_reset_group(void* payload) { m_values.m_noninherited.text_reset.adopt(payload); } void set_aspect_ratio(AspectRatio aspect_ratio) { diff --git a/Libraries/LibWeb/CSS/Rust/src/computed_values.rs b/Libraries/LibWeb/CSS/Rust/src/computed_values.rs index f0476df5cb943..59b4fc654c3a0 100644 --- a/Libraries/LibWeb/CSS/Rust/src/computed_values.rs +++ b/Libraries/LibWeb/CSS/Rust/src/computed_values.rs @@ -233,6 +233,19 @@ pub const GROUP_FIELD_U64: u8 = 4; pub const GROUP_FIELD_REQUIRE_KEYWORD: u8 = 5; /// An integer stored as i32. pub const GROUP_FIELD_I32: u8 = 6; +/// A color stored as the C++ Color's raw 32-bit value, resolved by the C++ +/// gather loop, which owns the color resolution context. +pub const GROUP_FIELD_COLOR: u8 = 7; + +/// One gathered value for the generic group builder: the computed value's +/// shell and data, plus the resolved raw color for color-kind fields. +#[repr(C)] +pub struct FfiGroupValueEntry { + pub shell: *const c_void, + pub data: *const c_void, + pub resolved_color: u32, + pub has_resolved_color: bool, +} struct FieldDescriptors(Box<[FfiGroupFieldDescriptor]>); @@ -278,7 +291,7 @@ pub unsafe extern "C" fn rust_style_group_register_field_descriptors( #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_build_style_group( group_index: usize, - values: *const crate::style_compute::FfiShellAndData, + values: *const FfiGroupValueEntry, count: usize, parent_payload: *const c_void, ) -> *const c_void { @@ -301,6 +314,7 @@ pub unsafe extern "C" fn rust_build_style_group( F64(u32, f64), I32(u32, i32), U64(u32, u64), + U32(u32, u32), } let mut pokes = Vec::with_capacity(count); for (descriptor, value) in descriptors.iter().zip(values) { @@ -366,6 +380,12 @@ pub unsafe extern "C" fn rust_build_style_group( }; pokes.push(Poke::I32(descriptor.offset, *value)); } + GROUP_FIELD_COLOR => { + if !value.has_resolved_color { + return None; + } + pokes.push(Poke::U32(descriptor.offset, value.resolved_color)); + } _ => return None, } } @@ -384,6 +404,7 @@ pub unsafe extern "C" fn rust_build_style_group( Poke::F64(offset, value) => *(base.add(offset as usize) as *mut f64) = value, Poke::I32(offset, value) => *(base.add(offset as usize) as *mut i32) = value, Poke::U64(offset, value) => *(base.add(offset as usize) as *mut u64) = value, + Poke::U32(offset, value) => *(base.add(offset as usize) as *mut u32) = value, } } } From 2ce1304b0704dfc71a168bc3832c8da0ee7b0398 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 15:15:42 +0200 Subject: [PATCH 15/33] LibWeb: Build the effects style group through the descriptors Opacity's normalization has not moved into the core, so the gathered value entries grow a resolved number that the C++ gather loop fills, mirroring the resolved-color arrangement, and a resolved-f32 field kind pokes it. The blend mode and isolation fields map as keyword enums, and the filters, box-shadow and clip register as keyword constraints. A pixel-length constraint kind joins the builder alongside, for fields like the scroll margins whose initial values are zero lengths rather than keywords. --- Libraries/LibWeb/CSS/ComputedValues.cpp | 97 ++++++++++++++----- Libraries/LibWeb/CSS/ComputedValues.h | 1 + .../LibWeb/CSS/Rust/src/computed_values.rs | 24 +++++ 3 files changed, 97 insertions(+), 25 deletions(-) diff --git a/Libraries/LibWeb/CSS/ComputedValues.cpp b/Libraries/LibWeb/CSS/ComputedValues.cpp index b9339d52f304b..8eac2c1d2c6a6 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.cpp +++ b/Libraries/LibWeb/CSS/ComputedValues.cpp @@ -113,6 +113,18 @@ static constexpr Array text_reset_group_properties { PropertyID::WhiteSpaceTrim, }; +// The properties feeding the effects group's descriptors, in registration +// order. +static constexpr Array effects_group_properties { + PropertyID::Opacity, + PropertyID::Filter, + PropertyID::BackdropFilter, + PropertyID::MixBlendMode, + PropertyID::Isolation, + PropertyID::BoxShadow, + PropertyID::Clip, +}; + static void register_style_group_field_descriptors() { using namespace ComputedValuesFFI; @@ -121,13 +133,14 @@ static void register_style_group_field_descriptors() && sizeof(JustifyItems) == 1 && sizeof(JustifySelf) == 1); Vector descriptors; - auto add = [&](size_t group_index, PropertyID property, u32 offset, u8 kind, u16 keyword, Array const* keyword_table) { + auto add = [&](size_t group_index, PropertyID property, u32 offset, u8 kind, u16 keyword, Array const* keyword_table, double required_px = 0) { descriptors.append({ .group_index = static_cast(group_index), .property_id = static_cast(to_underlying(property)), .offset = offset, .kind = kind, .keyword = keyword, + .required_px = required_px, .keyword_table = keyword_table ? keyword_table->data() : nullptr, .keyword_table_length = keyword_table ? keyword_table->size() : 0, }); @@ -161,6 +174,16 @@ static void register_style_group_field_descriptors() add(text_reset, PropertyID::UnicodeBidi, offsetof(TextReset, unicode_bidi), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); add(text_reset, PropertyID::WhiteSpaceTrim, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::None), nullptr); + using Effects = ComputedValues::EffectsValues; + constexpr auto effects = to_underlying(StyleGroupIndex::EffectsValues); + add(effects, PropertyID::Opacity, offsetof(Effects, opacity), GROUP_FIELD_RESOLVED_F32, 0, nullptr); + add(effects, PropertyID::Filter, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::None), nullptr); + add(effects, PropertyID::BackdropFilter, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::None), nullptr); + add(effects, PropertyID::MixBlendMode, offsetof(Effects, mix_blend_mode), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(effects, PropertyID::Isolation, offsetof(Effects, isolation), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(effects, PropertyID::BoxShadow, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::None), nullptr); + add(effects, PropertyID::Clip, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::Auto), nullptr); + rust_style_group_register_field_descriptors(descriptors.data(), descriptors.size()); } @@ -353,7 +376,7 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co auto gather_group_values = [&](Array const& properties, Array& entries) { for (size_t i = 0; i < N; ++i) { auto const& value = computed_style.property(properties[i]); - entries[i] = { &value, value.rust_style_value_data(), 0, false }; + entries[i] = { &value, value.rust_style_value_data(), 0, false, 0, false }; } }; @@ -368,24 +391,22 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co if (alignment_adopted) computed_values.adopt_alignment_group(const_cast(alignment_payload)); - // The gather resolves color fields against the element's colors, since the - // builder core cannot; the resolved raw color travels with the entry. - Array text_reset_group_values; - gather_group_values(text_reset_group_properties, text_reset_group_values); - for (size_t i = 0; i < text_reset_group_properties.size(); ++i) { - if (text_reset_group_properties[i] == PropertyID::TextDecorationColor) { - text_reset_group_values[i].resolved_color = computed_style.color(PropertyID::TextDecorationColor, color_resolution_context).value(); - text_reset_group_values[i].has_resolved_color = true; + Array effects_group_values; + gather_group_values(effects_group_properties, effects_group_values); + for (size_t i = 0; i < effects_group_properties.size(); ++i) { + if (effects_group_properties[i] == PropertyID::Opacity) { + effects_group_values[i].resolved_number = computed_style.opacity(); + effects_group_values[i].has_resolved_number = true; } } - auto* text_reset_payload = ComputedValuesFFI::rust_build_style_group( - TextResetValues::style_group_index, - text_reset_group_values.data(), - text_reset_group_values.size(), - inherit_parent ? static_cast(inherit_parent->m_noninherited.text_reset.operator->()) : nullptr); - bool const text_reset_adopted = text_reset_payload != nullptr; - if (text_reset_adopted) - computed_values.adopt_text_reset_group(const_cast(text_reset_payload)); + auto* effects_payload = ComputedValuesFFI::rust_build_style_group( + EffectsValues::style_group_index, + effects_group_values.data(), + effects_group_values.size(), + inherit_parent ? static_cast(inherit_parent->m_noninherited.effects.operator->()) : nullptr); + bool const effects_adopted = effects_payload != nullptr; + if (effects_adopted) + computed_values.adopt_effects_group(const_cast(effects_payload)); auto custom_ident_list = [&](PropertyID property_id) { Vector names; @@ -554,6 +575,25 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co // FIXME: We should resolve colors to their absolute forms at compute time (i.e. by implementing the relevant absolutized methods) color_resolution_context.current_color = color; + // NB: The text reset group builds only after the element's color lands in the + // resolution context above, since text-decoration-color may be currentcolor. + Array text_reset_group_values; + gather_group_values(text_reset_group_properties, text_reset_group_values); + for (size_t i = 0; i < text_reset_group_properties.size(); ++i) { + if (text_reset_group_properties[i] == PropertyID::TextDecorationColor) { + text_reset_group_values[i].resolved_color = computed_style.color(PropertyID::TextDecorationColor, color_resolution_context).value(); + text_reset_group_values[i].has_resolved_color = true; + } + } + auto* text_reset_payload = ComputedValuesFFI::rust_build_style_group( + TextResetValues::style_group_index, + text_reset_group_values.data(), + text_reset_group_values.size(), + inherit_parent ? static_cast(inherit_parent->m_noninherited.text_reset.operator->()) : nullptr); + bool const text_reset_adopted = text_reset_payload != nullptr; + if (text_reset_adopted) + computed_values.adopt_text_reset_group(const_cast(text_reset_payload)); + auto const& accent_color_value = computed_style.property(CSS::PropertyID::AccentColor); CSS::ColorOrAuto accent_color; accent_color.used_value = computed_style.accent_color(color_resolution_context); @@ -660,10 +700,13 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co computed_values.set_flex_shrink(computed_style.flex_shrink()); if (!alignment_adopted) computed_values.set_order(computed_style.order()); - computed_values.set_clip(computed_style.clip()); + if (!effects_adopted) + computed_values.set_clip(computed_style.clip()); - computed_values.set_backdrop_filter(computed_style.backdrop_filter()); - computed_values.set_filter(computed_style.filter()); + if (!effects_adopted) + computed_values.set_backdrop_filter(computed_style.backdrop_filter()); + if (!effects_adopted) + computed_values.set_filter(computed_style.filter()); computed_values.set_flood_color(computed_style.color(CSS::PropertyID::FloodColor, color_resolution_context)); computed_values.set_flood_opacity(computed_style.flood_opacity()); @@ -990,7 +1033,8 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co computed_values.set_text_shadow(computed_style.text_shadow(color_resolution_context)); computed_values.set_z_index(computed_style.z_index()); - computed_values.set_opacity(computed_style.opacity()); + if (!effects_adopted) + computed_values.set_opacity(computed_style.opacity()); if (!inherited_box_adopted) computed_values.set_visibility(computed_style.visibility()); @@ -1035,7 +1079,8 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co computed_values.set_overflow_clip_margin(data); } - computed_values.set_box_shadow(computed_style.box_shadow(color_resolution_context)); + if (!effects_adopted) + computed_values.set_box_shadow(computed_style.box_shadow(color_resolution_context)); computed_values.set_rotate(computed_style.rotate()); computed_values.set_translate(computed_style.translate()); @@ -1279,8 +1324,10 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co if (!inherited_box_adopted) computed_values.set_writing_mode(computed_style.writing_mode()); computed_values.set_user_select(computed_style.user_select()); - computed_values.set_isolation(computed_style.isolation()); - computed_values.set_mix_blend_mode(computed_style.mix_blend_mode()); + if (!effects_adopted) + computed_values.set_isolation(computed_style.isolation()); + if (!effects_adopted) + computed_values.set_mix_blend_mode(computed_style.mix_blend_mode()); computed_values.set_view_transition_name(computed_style.view_transition_name()); computed_values.set_contain(computed_style.contain()); computed_values.set_container_name(computed_style.container_name()); diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index c749552879465..ce120396eab9f 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -1899,6 +1899,7 @@ class ComputedValues::Mutator final { void adopt_inherited_table_group(void* payload) { m_values.m_inherited.table.adopt(payload); } void adopt_alignment_group(void* payload) { m_values.m_noninherited.alignment.adopt(payload); } void adopt_text_reset_group(void* payload) { m_values.m_noninherited.text_reset.adopt(payload); } + void adopt_effects_group(void* payload) { m_values.m_noninherited.effects.adopt(payload); } void set_aspect_ratio(AspectRatio aspect_ratio) { diff --git a/Libraries/LibWeb/CSS/Rust/src/computed_values.rs b/Libraries/LibWeb/CSS/Rust/src/computed_values.rs index 59b4fc654c3a0..fed16f6af7995 100644 --- a/Libraries/LibWeb/CSS/Rust/src/computed_values.rs +++ b/Libraries/LibWeb/CSS/Rust/src/computed_values.rs @@ -214,6 +214,8 @@ pub struct FfiGroupFieldDescriptor { pub kind: u8, /// For GROUP_FIELD_REQUIRE_KEYWORD: the required keyword. pub keyword: u16, + /// For GROUP_FIELD_REQUIRE_PX: the required pixel value. + pub required_px: f64, /// For GROUP_FIELD_ENUM_KEYWORD: keyword code -> enum code, 255 invalid. pub keyword_table: *const u8, pub keyword_table_length: usize, @@ -236,6 +238,12 @@ pub const GROUP_FIELD_I32: u8 = 6; /// A color stored as the C++ Color's raw 32-bit value, resolved by the C++ /// gather loop, which owns the color resolution context. pub const GROUP_FIELD_COLOR: u8 = 7; +/// A number stored as f32, resolved by the C++ gather loop for values whose +/// normalization has not moved into the core, like opacity. +pub const GROUP_FIELD_RESOLVED_F32: u8 = 8; +/// A constraint: the value must be a pixel length equal to `required_px`; +/// nothing is written. +pub const GROUP_FIELD_REQUIRE_PX: u8 = 9; /// One gathered value for the generic group builder: the computed value's /// shell and data, plus the resolved raw color for color-kind fields. @@ -245,6 +253,8 @@ pub struct FfiGroupValueEntry { pub data: *const c_void, pub resolved_color: u32, pub has_resolved_color: bool, + pub resolved_number: f64, + pub has_resolved_number: bool, } struct FieldDescriptors(Box<[FfiGroupFieldDescriptor]>); @@ -386,6 +396,20 @@ pub unsafe extern "C" fn rust_build_style_group( } pokes.push(Poke::U32(descriptor.offset, value.resolved_color)); } + GROUP_FIELD_RESOLVED_F32 => { + if !value.has_resolved_number { + return None; + } + pokes.push(Poke::F32(descriptor.offset, value.resolved_number as f32)); + } + GROUP_FIELD_REQUIRE_PX => { + let StyleValueData::Length { value, unit } = data else { + return None; + }; + if *unit != crate::style_compute::px_length_unit() || *value != descriptor.required_px { + return None; + } + } _ => return None, } } From 98ef33b9f946aadf2877ed816031fbd4017845a0 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 15:29:32 +0200 Subject: [PATCH 16/33] LibWeb: Build the misc reset style group through the descriptors The largest descriptor table so far: thirty-four descriptors cover the group's twenty-six fields, with four new field kinds rounding out the builder. A color-or-keyword kind lets outline-color's auto keyword leave the constructor default standing while resolvable colors poke through the gather-resolved path; an initial-value constraint compares the value data against the initial table by pointer identity, which holds exactly for untouched properties since the driver selects the table's entries directly; a non-negative pixel kind clamps outline-width the way its setter does; and a resolved-f64 kind carries shape-image-threshold's normalized number. Appearance feeds two descriptors from one property: the appearance field maps through a keyword table with the compatibility keywords excluded, so pages using them take the C++ path, while computed_appearance maps the raw keyword. The conditional setter sites keep their conditions and gain the adoption gate. --- Libraries/LibWeb/CSS/ComputedValues.cpp | 196 +++++++++++++++--- Libraries/LibWeb/CSS/ComputedValues.h | 1 + .../LibWeb/CSS/Rust/src/computed_values.rs | 44 ++++ 3 files changed, 215 insertions(+), 26 deletions(-) diff --git a/Libraries/LibWeb/CSS/ComputedValues.cpp b/Libraries/LibWeb/CSS/ComputedValues.cpp index 8eac2c1d2c6a6..7c9e2fa33b6fd 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.cpp +++ b/Libraries/LibWeb/CSS/ComputedValues.cpp @@ -125,6 +125,72 @@ static constexpr Array effects_group_properties { PropertyID::Clip, }; +// The appearance keyword mapping with the compatibility keywords excluded: +// they normalize to auto for the appearance field but stay raw for +// computed_appearance, so their pages take the C++ population path. +static Optional appearance_without_compat_from_keyword(Keyword keyword) +{ + auto appearance = keyword_to_appearance(keyword); + if (!appearance.has_value()) + return {}; + switch (*appearance) { + case Appearance::Searchfield: + case Appearance::Textarea: + case Appearance::PushButton: + case Appearance::SliderHorizontal: + case Appearance::Checkbox: + case Appearance::Radio: + case Appearance::SquareButton: + case Appearance::Menulist: + case Appearance::Listbox: + case Appearance::Meter: + case Appearance::ProgressBar: + case Appearance::Button: + return {}; + default: + return appearance; + } +} + +// The properties feeding the misc reset group's descriptors, in registration +// order; Appearance appears twice, once per derived field. +static constexpr Array misc_reset_group_properties { + PropertyID::ScrollMarginTop, + PropertyID::ScrollMarginRight, + PropertyID::ScrollMarginBottom, + PropertyID::ScrollMarginLeft, + PropertyID::ScrollPaddingTop, + PropertyID::ScrollPaddingRight, + PropertyID::ScrollPaddingBottom, + PropertyID::ScrollPaddingLeft, + PropertyID::OverflowClipMarginTop, + PropertyID::OverflowClipMarginRight, + PropertyID::OverflowClipMarginBottom, + PropertyID::OverflowClipMarginLeft, + PropertyID::ColumnSpan, + PropertyID::Appearance, + PropertyID::Appearance, + PropertyID::OutlineStyle, + PropertyID::ObjectFit, + PropertyID::ColumnCount, + PropertyID::ColumnWidth, + PropertyID::ColumnHeight, + PropertyID::OutlineColor, + PropertyID::OutlineOffset, + PropertyID::OutlineWidth, + PropertyID::TableLayout, + PropertyID::UserSelect, + PropertyID::ObjectPosition, + PropertyID::ViewTransitionName, + PropertyID::TouchAction, + PropertyID::ScrollBehavior, + PropertyID::ScrollbarGutter, + PropertyID::ScrollbarWidth, + PropertyID::ShapeImageThreshold, + PropertyID::ShapeMargin, + PropertyID::ShapeOutside, +}; + static void register_style_group_field_descriptors() { using namespace ComputedValuesFFI; @@ -184,6 +250,37 @@ static void register_style_group_field_descriptors() add(effects, PropertyID::BoxShadow, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::None), nullptr); add(effects, PropertyID::Clip, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::Auto), nullptr); + using MiscReset = ComputedValues::MiscResetValues; + constexpr auto misc_reset = to_underlying(StyleGroupIndex::MiscResetValues); + for (auto property : { PropertyID::ScrollMarginTop, PropertyID::ScrollMarginRight, PropertyID::ScrollMarginBottom, PropertyID::ScrollMarginLeft }) + add(misc_reset, property, 0, GROUP_FIELD_REQUIRE_PX, 0, nullptr, 0); + for (auto property : { PropertyID::ScrollPaddingTop, PropertyID::ScrollPaddingRight, PropertyID::ScrollPaddingBottom, PropertyID::ScrollPaddingLeft }) + add(misc_reset, property, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::Auto), nullptr); + for (auto property : { PropertyID::OverflowClipMarginTop, PropertyID::OverflowClipMarginRight, PropertyID::OverflowClipMarginBottom, PropertyID::OverflowClipMarginLeft }) + add(misc_reset, property, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); + add(misc_reset, PropertyID::ColumnSpan, offsetof(MiscReset, column_span), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(misc_reset, PropertyID::Appearance, offsetof(MiscReset, appearance), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(misc_reset, PropertyID::Appearance, offsetof(MiscReset, computed_appearance), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(misc_reset, PropertyID::OutlineStyle, offsetof(MiscReset, outline_style), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(misc_reset, PropertyID::ObjectFit, offsetof(MiscReset, object_fit), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(misc_reset, PropertyID::ColumnCount, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::Auto), nullptr); + add(misc_reset, PropertyID::ColumnWidth, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::Auto), nullptr); + add(misc_reset, PropertyID::ColumnHeight, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::Auto), nullptr); + add(misc_reset, PropertyID::OutlineColor, offsetof(MiscReset, outline_color), GROUP_FIELD_COLOR_OR_KEYWORD, to_underlying(Keyword::Auto), nullptr); + add(misc_reset, PropertyID::OutlineOffset, 0, GROUP_FIELD_REQUIRE_PX, 0, nullptr, 0); + add(misc_reset, PropertyID::OutlineWidth, offsetof(MiscReset, outline_width), GROUP_FIELD_CSS_PIXELS_NON_NEGATIVE, 0, nullptr); + add(misc_reset, PropertyID::TableLayout, offsetof(MiscReset, table_layout), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(misc_reset, PropertyID::UserSelect, offsetof(MiscReset, user_select), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(misc_reset, PropertyID::ObjectPosition, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); + add(misc_reset, PropertyID::ViewTransitionName, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::None), nullptr); + add(misc_reset, PropertyID::TouchAction, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); + add(misc_reset, PropertyID::ScrollBehavior, offsetof(MiscReset, scroll_behavior), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(misc_reset, PropertyID::ScrollbarGutter, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); + add(misc_reset, PropertyID::ScrollbarWidth, offsetof(MiscReset, scrollbar_width), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(misc_reset, PropertyID::ShapeImageThreshold, offsetof(MiscReset, shape_image_threshold), GROUP_FIELD_RESOLVED_F64, 0, nullptr); + add(misc_reset, PropertyID::ShapeMargin, 0, GROUP_FIELD_REQUIRE_PX, 0, nullptr, 0); + add(misc_reset, PropertyID::ShapeOutside, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::None), nullptr); + rust_style_group_register_field_descriptors(descriptors.data(), descriptors.size()); } @@ -594,6 +691,30 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co if (text_reset_adopted) computed_values.adopt_text_reset_group(const_cast(text_reset_payload)); + Array misc_reset_group_values; + gather_group_values(misc_reset_group_properties, misc_reset_group_values); + for (size_t i = 0; i < misc_reset_group_properties.size(); ++i) { + if (misc_reset_group_properties[i] == PropertyID::OutlineColor) { + if (auto const& outline_color = computed_style.property(PropertyID::OutlineColor); outline_color.has_color()) { + if (auto resolved = outline_color.to_color(color_resolution_context); resolved.has_value()) { + misc_reset_group_values[i].resolved_color = resolved->value(); + misc_reset_group_values[i].has_resolved_color = true; + } + } + } else if (misc_reset_group_properties[i] == PropertyID::ShapeImageThreshold) { + misc_reset_group_values[i].resolved_number = computed_style.property(PropertyID::ShapeImageThreshold).as_opacity_value().resolved(); + misc_reset_group_values[i].has_resolved_number = true; + } + } + auto* misc_reset_payload = ComputedValuesFFI::rust_build_style_group( + MiscResetValues::style_group_index, + misc_reset_group_values.data(), + misc_reset_group_values.size(), + inherit_parent ? static_cast(inherit_parent->m_noninherited.misc.operator->()) : nullptr); + bool const misc_reset_adopted = misc_reset_payload != nullptr; + if (misc_reset_adopted) + computed_values.adopt_misc_reset_group(const_cast(misc_reset_payload)); + auto const& accent_color_value = computed_style.property(CSS::PropertyID::AccentColor); CSS::ColorOrAuto accent_color; accent_color.used_value = computed_style.accent_color(color_resolution_context); @@ -725,8 +846,10 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co if (!alignment_adopted) computed_values.set_align_self(computed_style.align_self()); - computed_values.set_appearance(computed_style.appearance()); - computed_values.set_computed_appearance(keyword_to_appearance(computed_style.property(PropertyID::Appearance).to_keyword()).release_value()); + if (!misc_reset_adopted) + computed_values.set_appearance(computed_style.appearance()); + if (!misc_reset_adopted) + computed_values.set_computed_appearance(keyword_to_appearance(computed_style.property(PropertyID::Appearance).to_keyword()).release_value()); computed_values.set_position(computed_style.position()); @@ -1055,9 +1178,11 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co } computed_values.set_margin(computed_style.length_box(CSS::PropertyID::MarginLeft, CSS::PropertyID::MarginTop, CSS::PropertyID::MarginRight, CSS::PropertyID::MarginBottom, CSS::Length::make_px(0))); computed_values.set_padding(computed_style.length_box(CSS::PropertyID::PaddingLeft, CSS::PropertyID::PaddingTop, CSS::PropertyID::PaddingRight, CSS::PropertyID::PaddingBottom, CSS::Length::make_px(0))); - computed_values.set_scroll_margin(computed_style.length_box(CSS::PropertyID::ScrollMarginLeft, CSS::PropertyID::ScrollMarginTop, CSS::PropertyID::ScrollMarginRight, CSS::PropertyID::ScrollMarginBottom, CSS::Length::make_px(0))); - computed_values.set_scroll_padding(computed_style.length_box(CSS::PropertyID::ScrollPaddingLeft, CSS::PropertyID::ScrollPaddingTop, CSS::PropertyID::ScrollPaddingRight, CSS::PropertyID::ScrollPaddingBottom, CSS::LengthPercentageOrAuto::make_auto())); - { + if (!misc_reset_adopted) + computed_values.set_scroll_margin(computed_style.length_box(CSS::PropertyID::ScrollMarginLeft, CSS::PropertyID::ScrollMarginTop, CSS::PropertyID::ScrollMarginRight, CSS::PropertyID::ScrollMarginBottom, CSS::Length::make_px(0))); + if (!misc_reset_adopted) + computed_values.set_scroll_padding(computed_style.length_box(CSS::PropertyID::ScrollPaddingLeft, CSS::PropertyID::ScrollPaddingTop, CSS::PropertyID::ScrollPaddingRight, CSS::PropertyID::ScrollPaddingBottom, CSS::LengthPercentageOrAuto::make_auto())); + if (!misc_reset_adopted) { auto extract_side = [&](CSS::PropertyID property_id) -> CSS::OverflowClipMarginSide { auto const& value = computed_style.property(property_id); if (value.is_overflow_clip_margin()) { @@ -1130,18 +1255,22 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co computed_values.set_border_right_color_style_value(computed_style.property(CSS::PropertyID::BorderRightColor)); computed_values.set_border_bottom_color_style_value(computed_style.property(CSS::PropertyID::BorderBottomColor)); - if (auto const& outline_color = computed_style.property(CSS::PropertyID::OutlineColor); outline_color.has_color()) + if (auto const& outline_color = computed_style.property(CSS::PropertyID::OutlineColor); !misc_reset_adopted && outline_color.has_color()) computed_values.set_outline_color(outline_color.to_color(color_resolution_context).value()); auto const& outline_offset = computed_style.property(CSS::PropertyID::OutlineOffset); auto resolved_outline_offset = outline_offset.is_calculated() ? outline_offset.as_calculated().resolve_length(color_resolution_context.calculation_resolution_context).value() : outline_offset.as_length().length(); - computed_values.set_outline_offset(resolved_outline_offset.absolute_length_to_px()); - computed_values.set_outline_offset_style_value(outline_offset); - computed_values.set_outline_style(computed_style.outline_style()); + if (!misc_reset_adopted) + computed_values.set_outline_offset(resolved_outline_offset.absolute_length_to_px()); + if (!misc_reset_adopted) + computed_values.set_outline_offset_style_value(outline_offset); + if (!misc_reset_adopted) + computed_values.set_outline_style(computed_style.outline_style()); // FIXME: Interpolation can cause negative values - we clamp here but should instead clamp as part of interpolation. - computed_values.set_outline_width(max(CSSPixels { 0 }, computed_style.length(CSS::PropertyID::OutlineWidth).absolute_length_to_px())); + if (!misc_reset_adopted) + computed_values.set_outline_width(max(CSSPixels { 0 }, computed_style.length(CSS::PropertyID::OutlineWidth).absolute_length_to_px())); computed_values.set_grid_auto_columns(computed_style.grid_auto_columns()); computed_values.set_grid_auto_rows(computed_style.grid_auto_rows()); @@ -1231,13 +1360,16 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co computed_values.set_text_anchor(computed_style.text_anchor()); computed_values.set_dominant_baseline(computed_style.dominant_baseline()); - if (auto const& column_count = computed_style.property(CSS::PropertyID::ColumnCount); column_count.to_keyword() != Keyword::Auto) + if (auto const& column_count = computed_style.property(CSS::PropertyID::ColumnCount); !misc_reset_adopted && column_count.to_keyword() != Keyword::Auto) computed_values.set_column_count(CSS::ColumnCount::make_integer(int_from_style_value(NonnullRefPtr { column_count }))); - computed_values.set_column_span(computed_style.column_span()); + if (!misc_reset_adopted) + computed_values.set_column_span(computed_style.column_span()); - computed_values.set_column_width(computed_style.size_value(CSS::PropertyID::ColumnWidth)); - computed_values.set_column_height(computed_style.size_value(CSS::PropertyID::ColumnHeight)); + if (!misc_reset_adopted) + computed_values.set_column_width(computed_style.size_value(CSS::PropertyID::ColumnWidth)); + if (!misc_reset_adopted) + computed_values.set_column_height(computed_style.size_value(CSS::PropertyID::ColumnHeight)); if (!alignment_adopted) computed_values.set_column_gap(computed_style.gap_value(CSS::PropertyID::ColumnGap)); @@ -1250,7 +1382,8 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co if (!inherited_table_adopted) computed_values.set_empty_cells(computed_style.empty_cells()); - computed_values.set_table_layout(computed_style.table_layout()); + if (!misc_reset_adopted) + computed_values.set_table_layout(computed_style.table_layout()); auto const& aspect_ratio = computed_style.property(CSS::PropertyID::AspectRatio); if (aspect_ratio.is_value_list()) { @@ -1275,7 +1408,8 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co computed_values.set_aspect_ratio({ false, aspect_ratio.as_ratio().resolved(), false, aspect_ratio.as_ratio().resolved() }); } - computed_values.set_touch_action(computed_style.touch_action()); + if (!misc_reset_adopted) + computed_values.set_touch_action(computed_style.touch_action()); auto const& math_shift_value = computed_style.property(CSS::PropertyID::MathShift); if (auto math_shift = keyword_to_math_shift(math_shift_value.to_keyword()); math_shift.has_value()) @@ -1291,17 +1425,24 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co computed_values.set_counter_reset(computed_style.counter_data(CSS::PropertyID::CounterReset)); computed_values.set_counter_set(computed_style.counter_data(CSS::PropertyID::CounterSet)); - computed_values.set_object_fit(computed_style.object_fit()); - computed_values.set_object_position(computed_style.object_position()); + if (!misc_reset_adopted) + computed_values.set_object_fit(computed_style.object_fit()); + if (!misc_reset_adopted) + computed_values.set_object_position(computed_style.object_position()); if (!inherited_box_adopted) computed_values.set_direction(computed_style.direction()); computed_values.set_unicode_bidi(computed_style.unicode_bidi()); - computed_values.set_scroll_behavior(CSS::keyword_to_scroll_behavior(computed_style.property(CSS::PropertyID::ScrollBehavior).to_keyword()).release_value()); + if (!misc_reset_adopted) + computed_values.set_scroll_behavior(CSS::keyword_to_scroll_behavior(computed_style.property(CSS::PropertyID::ScrollBehavior).to_keyword()).release_value()); computed_values.set_scrollbar_color(computed_style.scrollbar_color(color_resolution_context)); - computed_values.set_scrollbar_gutter(computed_style.property(CSS::PropertyID::ScrollbarGutter).as_scrollbar_gutter().value()); - computed_values.set_scrollbar_width(computed_style.scrollbar_width()); - computed_values.set_shape_image_threshold(computed_style.property(CSS::PropertyID::ShapeImageThreshold).as_opacity_value().resolved()); - computed_values.set_shape_margin(CSS::LengthPercentage::from_style_value(computed_style.property(CSS::PropertyID::ShapeMargin))); + if (!misc_reset_adopted) + computed_values.set_scrollbar_gutter(computed_style.property(CSS::PropertyID::ScrollbarGutter).as_scrollbar_gutter().value()); + if (!misc_reset_adopted) + computed_values.set_scrollbar_width(computed_style.scrollbar_width()); + if (!misc_reset_adopted) + computed_values.set_shape_image_threshold(computed_style.property(CSS::PropertyID::ShapeImageThreshold).as_opacity_value().resolved()); + if (!misc_reset_adopted) + computed_values.set_shape_margin(CSS::LengthPercentage::from_style_value(computed_style.property(CSS::PropertyID::ShapeMargin))); CSS::ShapeOutsideData shape_outside; auto apply_shape_outside_item = [&](CSS::StyleValue const& item) { if (item.is_url()) @@ -1320,15 +1461,18 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co } else { apply_shape_outside_item(shape_outside_value); } - computed_values.set_shape_outside(move(shape_outside)); + if (!misc_reset_adopted) + computed_values.set_shape_outside(move(shape_outside)); if (!inherited_box_adopted) computed_values.set_writing_mode(computed_style.writing_mode()); - computed_values.set_user_select(computed_style.user_select()); + if (!misc_reset_adopted) + computed_values.set_user_select(computed_style.user_select()); if (!effects_adopted) computed_values.set_isolation(computed_style.isolation()); if (!effects_adopted) computed_values.set_mix_blend_mode(computed_style.mix_blend_mode()); - computed_values.set_view_transition_name(computed_style.view_transition_name()); + if (!misc_reset_adopted) + computed_values.set_view_transition_name(computed_style.view_transition_name()); computed_values.set_contain(computed_style.contain()); computed_values.set_container_name(computed_style.container_name()); computed_values.set_container_type(computed_style.container_type()); diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index ce120396eab9f..ed50d7e2d2424 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -1900,6 +1900,7 @@ class ComputedValues::Mutator final { void adopt_alignment_group(void* payload) { m_values.m_noninherited.alignment.adopt(payload); } void adopt_text_reset_group(void* payload) { m_values.m_noninherited.text_reset.adopt(payload); } void adopt_effects_group(void* payload) { m_values.m_noninherited.effects.adopt(payload); } + void adopt_misc_reset_group(void* payload) { m_values.m_noninherited.misc.adopt(payload); } void set_aspect_ratio(AspectRatio aspect_ratio) { diff --git a/Libraries/LibWeb/CSS/Rust/src/computed_values.rs b/Libraries/LibWeb/CSS/Rust/src/computed_values.rs index fed16f6af7995..d3d6afc2f17f1 100644 --- a/Libraries/LibWeb/CSS/Rust/src/computed_values.rs +++ b/Libraries/LibWeb/CSS/Rust/src/computed_values.rs @@ -244,6 +244,18 @@ pub const GROUP_FIELD_RESOLVED_F32: u8 = 8; /// A constraint: the value must be a pixel length equal to `required_px`; /// nothing is written. pub const GROUP_FIELD_REQUIRE_PX: u8 = 9; +/// A color like GROUP_FIELD_COLOR, except that the descriptor's keyword +/// leaves the constructor's initial value standing, for fields like +/// outline-color whose auto keyword is not a resolvable color. +pub const GROUP_FIELD_COLOR_OR_KEYWORD: u8 = 10; +/// A constraint: the value must be the property's initial value, compared by +/// data pointer identity, which holds exactly for untouched properties since +/// the driver selects the initial table's entries directly. +pub const GROUP_FIELD_REQUIRE_INITIAL_VALUE: u8 = 11; +/// A pixel length stored as raw CSSPixels, clamped at zero. +pub const GROUP_FIELD_CSS_PIXELS_NON_NEGATIVE: u8 = 12; +/// A number stored as f64, resolved by the C++ gather loop. +pub const GROUP_FIELD_RESOLVED_F64: u8 = 13; /// One gathered value for the generic group builder: the computed value's /// shell and data, plus the resolved raw color for color-kind fields. @@ -410,6 +422,38 @@ pub unsafe extern "C" fn rust_build_style_group( return None; } } + GROUP_FIELD_COLOR_OR_KEYWORD => match data { + StyleValueData::Keyword { keyword } if *keyword == descriptor.keyword => {} + _ => { + if !value.has_resolved_color { + return None; + } + pokes.push(Poke::U32(descriptor.offset, value.resolved_color)); + } + }, + GROUP_FIELD_REQUIRE_INITIAL_VALUE => { + if value.data != crate::style_compute::initial_value(descriptor.property_id).data { + return None; + } + } + GROUP_FIELD_CSS_PIXELS_NON_NEGATIVE => { + let StyleValueData::Length { value, unit } = data else { + return None; + }; + if *unit != crate::style_compute::px_length_unit() { + return None; + } + pokes.push(Poke::I32( + descriptor.offset, + crate::css_pixels::CssPixels::nearest_value_for(value.max(0.0)).raw_value(), + )); + } + GROUP_FIELD_RESOLVED_F64 => { + if !value.has_resolved_number { + return None; + } + pokes.push(Poke::F64(descriptor.offset, value.resolved_number)); + } _ => return None, } } From 1d295477c20bfab4675d8619d90c7c2ed39241f9 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 15:46:16 +0200 Subject: [PATCH 17/33] LibWeb: Build the inherited text style group through the descriptors The largest inherited group brings two field kinds that unlock the remaining shell-bearing groups. A retained-shell kind pokes a style value shell into a single-pointer reference slot, retaining it through the core's existing shell reference bridge; the slot's constructor default must be null, and parent sharing falls out of pointer equality since inherited values and untouched initial values reference the same process-wide shells. A keyword-equality kind pokes derived booleans like whether -webkit-text-fill-color is currentcolor. The group builds right after the element's color is computed, resolving its color fields against a context copy that already carries that color, since the shared context only receives it further down. overflow-wrap maps through a hand-written converter matching the switch it replaces, having no generated one, and text-underline-position stays constraint-only since its compound forms have no single-keyword mapping. --- Libraries/LibWeb/CSS/ComputedValues.cpp | 204 ++++++++++++++---- Libraries/LibWeb/CSS/ComputedValues.h | 1 + .../LibWeb/CSS/Rust/src/computed_values.rs | 22 ++ Libraries/LibWeb/CSS/Rust/src/style_value.rs | 8 + 4 files changed, 195 insertions(+), 40 deletions(-) diff --git a/Libraries/LibWeb/CSS/ComputedValues.cpp b/Libraries/LibWeb/CSS/ComputedValues.cpp index 7c9e2fa33b6fd..222aab34b4591 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.cpp +++ b/Libraries/LibWeb/CSS/ComputedValues.cpp @@ -191,6 +191,51 @@ static constexpr Array misc_reset_group_properties { PropertyID::ShapeOutside, }; +// overflow-wrap has no generated keyword converter; the mapping matches the +// switch in create(). +static Optional overflow_wrap_from_keyword(Keyword keyword) +{ + switch (keyword) { + case Keyword::Normal: + return OverflowWrap::Normal; + case Keyword::BreakWord: + return OverflowWrap::BreakWord; + case Keyword::Anywhere: + return OverflowWrap::Anywhere; + default: + return {}; + } +} + +// The properties feeding the inherited text group's descriptors, in +// registration order; the doubled properties feed two fields each. +static constexpr Array inherited_text_group_properties { + PropertyID::Color, + PropertyID::Color, + PropertyID::WebkitTextFillColor, + PropertyID::WebkitTextFillColor, + PropertyID::TextShadow, + PropertyID::TextAlign, + PropertyID::TextJustify, + PropertyID::TextTransform, + PropertyID::TextWrapMode, + PropertyID::TextWrapStyle, + PropertyID::TextDecorationSkipInk, + PropertyID::TextUnderlinePosition, + PropertyID::TextUnderlineOffset, + PropertyID::TextIndent, + PropertyID::TabSize, + PropertyID::WhiteSpaceCollapse, + PropertyID::WordBreak, + PropertyID::OverflowWrap, + PropertyID::WordSpacing, + PropertyID::WordSpacing, + PropertyID::LetterSpacing, + PropertyID::LetterSpacing, + PropertyID::Orphans, + PropertyID::Widows, +}; + static void register_style_group_field_descriptors() { using namespace ComputedValuesFFI; @@ -281,6 +326,33 @@ static void register_style_group_field_descriptors() add(misc_reset, PropertyID::ShapeMargin, 0, GROUP_FIELD_REQUIRE_PX, 0, nullptr, 0); add(misc_reset, PropertyID::ShapeOutside, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::None), nullptr); + using InheritedText = ComputedValues::InheritedTextValues; + constexpr auto inherited_text = to_underlying(StyleGroupIndex::InheritedTextValues); + add(inherited_text, PropertyID::Color, offsetof(InheritedText, color), GROUP_FIELD_COLOR, 0, nullptr); + add(inherited_text, PropertyID::Color, offsetof(InheritedText, color_style_value), GROUP_FIELD_RETAINED_SHELL, 0, nullptr); + add(inherited_text, PropertyID::WebkitTextFillColor, offsetof(InheritedText, webkit_text_fill_color), GROUP_FIELD_COLOR, 0, nullptr); + add(inherited_text, PropertyID::WebkitTextFillColor, offsetof(InheritedText, webkit_text_fill_color_is_current_color), GROUP_FIELD_KEYWORD_EQUALS_BOOL, to_underlying(Keyword::Currentcolor), nullptr); + add(inherited_text, PropertyID::TextShadow, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::None), nullptr); + add(inherited_text, PropertyID::TextAlign, offsetof(InheritedText, text_align), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(inherited_text, PropertyID::TextJustify, offsetof(InheritedText, text_justify), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(inherited_text, PropertyID::TextTransform, offsetof(InheritedText, text_transform), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(inherited_text, PropertyID::TextWrapMode, offsetof(InheritedText, text_wrap_mode), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(inherited_text, PropertyID::TextWrapStyle, offsetof(InheritedText, text_wrap_style), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(inherited_text, PropertyID::TextDecorationSkipInk, offsetof(InheritedText, text_decoration_skip_ink), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(inherited_text, PropertyID::TextUnderlinePosition, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::Auto), nullptr); + add(inherited_text, PropertyID::TextUnderlineOffset, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::Auto), nullptr); + add(inherited_text, PropertyID::TextIndent, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); + add(inherited_text, PropertyID::TabSize, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); + add(inherited_text, PropertyID::WhiteSpaceCollapse, offsetof(InheritedText, white_space_collapse), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(inherited_text, PropertyID::WordBreak, offsetof(InheritedText, word_break), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(inherited_text, PropertyID::OverflowWrap, offsetof(InheritedText, overflow_wrap), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(inherited_text, PropertyID::WordSpacing, offsetof(InheritedText, word_spacing), GROUP_FIELD_CSS_PIXELS, 0, nullptr); + add(inherited_text, PropertyID::WordSpacing, offsetof(InheritedText, word_spacing_style_value), GROUP_FIELD_RETAINED_SHELL, 0, nullptr); + add(inherited_text, PropertyID::LetterSpacing, offsetof(InheritedText, letter_spacing), GROUP_FIELD_CSS_PIXELS, 0, nullptr); + add(inherited_text, PropertyID::LetterSpacing, offsetof(InheritedText, letter_spacing_style_value), GROUP_FIELD_RETAINED_SHELL, 0, nullptr); + add(inherited_text, PropertyID::Orphans, offsetof(InheritedText, orphans), GROUP_FIELD_U64, 0, nullptr); + add(inherited_text, PropertyID::Widows, offsetof(InheritedText, widows), GROUP_FIELD_U64, 0, nullptr); + rust_style_group_register_field_descriptors(descriptors.data(), descriptors.size()); } @@ -665,8 +737,37 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co // NOTE: color must be set after color-scheme to ensure currentColor can be resolved in other properties (e.g. background-color). // NOTE: color must be set after font_size as `CalculatedStyleValue`s can rely on it being set for resolving lengths. auto color = computed_style.color(CSS::PropertyID::Color, color_resolution_context); - computed_values.set_color(color); - computed_values.set_color_style_value(&computed_style.property(CSS::PropertyID::Color)); + // NB: The inherited text group resolves its color fields against the element's + // own color, which reaches the shared resolution context only further down. + auto own_color_resolution_context = color_resolution_context; + own_color_resolution_context.current_color = color; + Array inherited_text_group_values; + gather_group_values(inherited_text_group_properties, inherited_text_group_values); + for (size_t i = 0; i < inherited_text_group_properties.size(); ++i) { + auto gather_property_id = inherited_text_group_properties[i]; + if (gather_property_id == PropertyID::Color) { + inherited_text_group_values[i].resolved_color = color.value(); + inherited_text_group_values[i].has_resolved_color = true; + } else if (gather_property_id == PropertyID::WebkitTextFillColor) { + if (auto resolved = computed_style.property(PropertyID::WebkitTextFillColor).to_color(own_color_resolution_context); resolved.has_value()) { + inherited_text_group_values[i].resolved_color = resolved->value(); + inherited_text_group_values[i].has_resolved_color = true; + } + } + } + auto* inherited_text_payload = ComputedValuesFFI::rust_build_style_group( + InheritedTextValues::style_group_index, + inherited_text_group_values.data(), + inherited_text_group_values.size(), + inherit_parent ? static_cast(inherit_parent->m_inherited.text.operator->()) : nullptr); + bool const inherited_text_adopted = inherited_text_payload != nullptr; + if (inherited_text_adopted) + computed_values.adopt_inherited_text_group(const_cast(inherited_text_payload)); + + if (!inherited_text_adopted) + computed_values.set_color(color); + if (!inherited_text_adopted) + computed_values.set_color_style_value(&computed_style.property(CSS::PropertyID::Color)); // NOTE: This color resolution context must be created after we set color above so that currentColor resolves correctly // FIXME: We should resolve colors to their absolute forms at compute time (i.e. by implementing the relevant absolutized methods) @@ -1048,38 +1149,50 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co }); computed_values.set_transition_behaviors(move(transition_behaviors)); - computed_values.set_text_align(computed_style.text_align()); - computed_values.set_text_justify(computed_style.text_justify()); + if (!inherited_text_adopted) + computed_values.set_text_align(computed_style.text_align()); + if (!inherited_text_adopted) + computed_values.set_text_justify(computed_style.text_justify()); computed_values.set_text_overflow(computed_style.text_overflow()); auto const& text_underline_offset_value = computed_style.property(CSS::PropertyID::TextUnderlineOffset); CSS::TextUnderlineOffset text_underline_offset; text_underline_offset.used_value = computed_style.text_underline_offset(); if (text_underline_offset_value.to_keyword() != CSS::Keyword::Auto) text_underline_offset.computed_value = CSS::LengthPercentage::from_style_value(text_underline_offset_value); - computed_values.set_text_underline_offset(move(text_underline_offset)); - computed_values.set_text_underline_position(computed_style.text_underline_position()); - - computed_values.set_text_indent(computed_style.text_indent()); - computed_values.set_text_wrap_mode(computed_style.text_wrap_mode()); - computed_values.set_text_wrap_style(CSS::keyword_to_text_wrap_style(computed_style.property(CSS::PropertyID::TextWrapStyle).to_keyword()).release_value()); - computed_values.set_tab_size(computed_style.tab_size()); - - computed_values.set_white_space_collapse(computed_style.white_space_collapse()); + if (!inherited_text_adopted) + computed_values.set_text_underline_offset(move(text_underline_offset)); + if (!inherited_text_adopted) + computed_values.set_text_underline_position(computed_style.text_underline_position()); + + if (!inherited_text_adopted) + computed_values.set_text_indent(computed_style.text_indent()); + if (!inherited_text_adopted) + computed_values.set_text_wrap_mode(computed_style.text_wrap_mode()); + if (!inherited_text_adopted) + computed_values.set_text_wrap_style(CSS::keyword_to_text_wrap_style(computed_style.property(CSS::PropertyID::TextWrapStyle).to_keyword()).release_value()); + if (!inherited_text_adopted) + computed_values.set_tab_size(computed_style.tab_size()); + + if (!inherited_text_adopted) + computed_values.set_white_space_collapse(computed_style.white_space_collapse()); if (!text_reset_adopted) computed_values.set_white_space_trim(computed_style.white_space_trim()); - computed_values.set_word_break(computed_style.word_break()); - switch (computed_style.property(CSS::PropertyID::OverflowWrap).to_keyword()) { - case CSS::Keyword::Normal: - computed_values.set_overflow_wrap(CSS::OverflowWrap::Normal); - break; - case CSS::Keyword::BreakWord: - computed_values.set_overflow_wrap(CSS::OverflowWrap::BreakWord); - break; - case CSS::Keyword::Anywhere: - computed_values.set_overflow_wrap(CSS::OverflowWrap::Anywhere); - break; - default: - VERIFY_NOT_REACHED(); + if (!inherited_text_adopted) + computed_values.set_word_break(computed_style.word_break()); + if (!inherited_text_adopted) { + switch (computed_style.property(CSS::PropertyID::OverflowWrap).to_keyword()) { + case CSS::Keyword::Normal: + computed_values.set_overflow_wrap(CSS::OverflowWrap::Normal); + break; + case CSS::Keyword::BreakWord: + computed_values.set_overflow_wrap(CSS::OverflowWrap::BreakWord); + break; + case CSS::Keyword::Anywhere: + computed_values.set_overflow_wrap(CSS::OverflowWrap::Anywhere); + break; + default: + VERIFY_NOT_REACHED(); + } } auto integer_from_style_value = [](CSS::StyleValue const& value) -> u64 { i32 integer; @@ -1090,13 +1203,19 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co VERIFY(integer >= 0); return integer; }; - computed_values.set_orphans(integer_from_style_value(computed_style.property(CSS::PropertyID::Orphans))); - computed_values.set_widows(integer_from_style_value(computed_style.property(CSS::PropertyID::Widows))); - - computed_values.set_word_spacing(computed_style.word_spacing()); - computed_values.set_letter_spacing(computed_style.letter_spacing()); - computed_values.set_word_spacing_style_value(computed_style.property(PropertyID::WordSpacing)); - computed_values.set_letter_spacing_style_value(computed_style.property(PropertyID::LetterSpacing)); + if (!inherited_text_adopted) + computed_values.set_orphans(integer_from_style_value(computed_style.property(CSS::PropertyID::Orphans))); + if (!inherited_text_adopted) + computed_values.set_widows(integer_from_style_value(computed_style.property(CSS::PropertyID::Widows))); + + if (!inherited_text_adopted) + computed_values.set_word_spacing(computed_style.word_spacing()); + if (!inherited_text_adopted) + computed_values.set_letter_spacing(computed_style.letter_spacing()); + if (!inherited_text_adopted) + computed_values.set_word_spacing_style_value(computed_style.property(PropertyID::WordSpacing)); + if (!inherited_text_adopted) + computed_values.set_letter_spacing_style_value(computed_style.property(PropertyID::LetterSpacing)); computed_values.set_float(computed_style.float_()); @@ -1119,10 +1238,12 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co computed_values.set_pointer_events(computed_style.pointer_events()); if (!text_reset_adopted) computed_values.set_text_decoration_line(computed_style.text_decoration_line()); - computed_values.set_text_decoration_skip_ink(computed_style.text_decoration_skip_ink()); + if (!inherited_text_adopted) + computed_values.set_text_decoration_skip_ink(computed_style.text_decoration_skip_ink()); if (!text_reset_adopted) computed_values.set_text_decoration_style(computed_style.text_decoration_style()); - computed_values.set_text_transform(computed_style.text_transform()); + if (!inherited_text_adopted) + computed_values.set_text_transform(computed_style.text_transform()); auto list_style_type = computed_style.list_style_type(style_scope); auto const& list_style_type_value = computed_style.property(PropertyID::ListStyleType); @@ -1148,12 +1269,15 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co if (!text_reset_adopted) computed_values.set_text_decoration_thickness(computed_style.text_decoration_thickness()); - auto const& webkit_text_fill_color = computed_style.property(CSS::PropertyID::WebkitTextFillColor); - computed_values.set_webkit_text_fill_color( - webkit_text_fill_color.to_color(color_resolution_context).value(), - webkit_text_fill_color.to_keyword() == Keyword::Currentcolor); + if (!inherited_text_adopted) { + auto const& webkit_text_fill_color = computed_style.property(CSS::PropertyID::WebkitTextFillColor); + computed_values.set_webkit_text_fill_color( + webkit_text_fill_color.to_color(color_resolution_context).value(), + webkit_text_fill_color.to_keyword() == Keyword::Currentcolor); + } - computed_values.set_text_shadow(computed_style.text_shadow(color_resolution_context)); + if (!inherited_text_adopted) + computed_values.set_text_shadow(computed_style.text_shadow(color_resolution_context)); computed_values.set_z_index(computed_style.z_index()); if (!effects_adopted) diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index ed50d7e2d2424..67798c91784bd 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -1901,6 +1901,7 @@ class ComputedValues::Mutator final { void adopt_text_reset_group(void* payload) { m_values.m_noninherited.text_reset.adopt(payload); } void adopt_effects_group(void* payload) { m_values.m_noninherited.effects.adopt(payload); } void adopt_misc_reset_group(void* payload) { m_values.m_noninherited.misc.adopt(payload); } + void adopt_inherited_text_group(void* payload) { m_values.m_inherited.text.adopt(payload); } void set_aspect_ratio(AspectRatio aspect_ratio) { diff --git a/Libraries/LibWeb/CSS/Rust/src/computed_values.rs b/Libraries/LibWeb/CSS/Rust/src/computed_values.rs index d3d6afc2f17f1..e9f2be6e9d458 100644 --- a/Libraries/LibWeb/CSS/Rust/src/computed_values.rs +++ b/Libraries/LibWeb/CSS/Rust/src/computed_values.rs @@ -256,6 +256,11 @@ pub const GROUP_FIELD_REQUIRE_INITIAL_VALUE: u8 = 11; pub const GROUP_FIELD_CSS_PIXELS_NON_NEGATIVE: u8 = 12; /// A number stored as f64, resolved by the C++ gather loop. pub const GROUP_FIELD_RESOLVED_F64: u8 = 13; +/// The value's shell stored into a single-pointer reference slot, retaining +/// one reference; the slot's constructor default must be null. +pub const GROUP_FIELD_RETAINED_SHELL: u8 = 14; +/// A bool stored as one byte: whether the value is the descriptor's keyword. +pub const GROUP_FIELD_KEYWORD_EQUALS_BOOL: u8 = 15; /// One gathered value for the generic group builder: the computed value's /// shell and data, plus the resolved raw color for color-kind fields. @@ -337,6 +342,7 @@ pub unsafe extern "C" fn rust_build_style_group( I32(u32, i32), U64(u32, u64), U32(u32, u32), + Shell(u32, *const c_void), } let mut pokes = Vec::with_capacity(count); for (descriptor, value) in descriptors.iter().zip(values) { @@ -454,6 +460,17 @@ pub unsafe extern "C" fn rust_build_style_group( } pokes.push(Poke::F64(descriptor.offset, value.resolved_number)); } + GROUP_FIELD_RETAINED_SHELL => { + if value.shell.is_null() { + return None; + } + pokes.push(Poke::Shell(descriptor.offset, value.shell)); + } + GROUP_FIELD_KEYWORD_EQUALS_BOOL => { + let is_keyword = + matches!(data, StyleValueData::Keyword { keyword } if *keyword == descriptor.keyword); + pokes.push(Poke::U8(descriptor.offset, is_keyword as u8)); + } _ => return None, } } @@ -473,6 +490,11 @@ pub unsafe extern "C" fn rust_build_style_group( Poke::I32(offset, value) => *(base.add(offset as usize) as *mut i32) = value, Poke::U64(offset, value) => *(base.add(offset as usize) as *mut u64) = value, Poke::U32(offset, value) => *(base.add(offset as usize) as *mut u32) = value, + Poke::Shell(offset, shell) => { + // The slot's constructor default is null, so nothing is released. + crate::style_value::retain_shell_pointer(shell); + *(base.add(offset as usize) as *mut *const c_void) = shell; + } } } } diff --git a/Libraries/LibWeb/CSS/Rust/src/style_value.rs b/Libraries/LibWeb/CSS/Rust/src/style_value.rs index 262f303205fbc..f5e232581157f 100644 --- a/Libraries/LibWeb/CSS/Rust/src/style_value.rs +++ b/Libraries/LibWeb/CSS/Rust/src/style_value.rs @@ -62,6 +62,14 @@ impl RetainedStyleValue { } } +/// Retains one strong reference to a C++ StyleValue shell for a reference +/// slot poked into a style group payload. +pub(crate) fn retain_shell_pointer(pointer: *const c_void) { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::StyleValueShellRetainCallback); + // SAFETY: The caller guarantees a live shell. + unsafe { ladybird_style_value_ref(pointer) }; +} + impl Drop for RetainedStyleValue { fn drop(&mut self) { // A null pointer represents an absent optional reference. From 5c119d23e2dc8c58f743baee0151d269aad4d07c Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 16:00:36 +0200 Subject: [PATCH 18/33] LibWeb: Build the inherited UI style group through the descriptors The caret and accent colors pair a gather-resolved used color, poked at the nested offset inside their color-or-auto fields, with an auto keyword constraint on the computed part, since the used value resolves against the element's colors even when the computed value stays auto. A resolved-byte kind carries the used color-scheme, which depends on the page's preference rather than the value alone, alongside an initial-value constraint covering the scheme list fields. The color-scheme setters move from the top of create() down beside the group build: nothing in the function reads the group's scheme fields, since color resolution carries the scheme in its own context. --- Libraries/LibWeb/CSS/ComputedValues.cpp | 96 +++++++++++++++---- Libraries/LibWeb/CSS/ComputedValues.h | 1 + .../LibWeb/CSS/Rust/src/computed_values.rs | 9 ++ 3 files changed, 89 insertions(+), 17 deletions(-) diff --git a/Libraries/LibWeb/CSS/ComputedValues.cpp b/Libraries/LibWeb/CSS/ComputedValues.cpp index 222aab34b4591..b6373ccf58404 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.cpp +++ b/Libraries/LibWeb/CSS/ComputedValues.cpp @@ -236,6 +236,20 @@ static constexpr Array inherited_text_group_properties { PropertyID::Widows, }; +// The properties feeding the inherited UI group's descriptors, in +// registration order; the doubled properties feed two fields each. +static constexpr Array inherited_ui_group_properties { + PropertyID::CaretColor, + PropertyID::CaretColor, + PropertyID::AccentColor, + PropertyID::AccentColor, + PropertyID::Cursor, + PropertyID::PointerEvents, + PropertyID::ScrollbarColor, + PropertyID::ColorScheme, + PropertyID::ColorScheme, +}; + static void register_style_group_field_descriptors() { using namespace ComputedValuesFFI; @@ -353,6 +367,18 @@ static void register_style_group_field_descriptors() add(inherited_text, PropertyID::Orphans, offsetof(InheritedText, orphans), GROUP_FIELD_U64, 0, nullptr); add(inherited_text, PropertyID::Widows, offsetof(InheritedText, widows), GROUP_FIELD_U64, 0, nullptr); + using InheritedUI = ComputedValues::InheritedUIValues; + constexpr auto inherited_ui = to_underlying(StyleGroupIndex::InheritedUIValues); + add(inherited_ui, PropertyID::CaretColor, offsetof(InheritedUI, caret_color) + offsetof(ColorOrAuto, used_value), GROUP_FIELD_COLOR, 0, nullptr); + add(inherited_ui, PropertyID::CaretColor, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::Auto), nullptr); + add(inherited_ui, PropertyID::AccentColor, offsetof(InheritedUI, accent_color) + offsetof(ColorOrAuto, used_value), GROUP_FIELD_COLOR, 0, nullptr); + add(inherited_ui, PropertyID::AccentColor, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::Auto), nullptr); + add(inherited_ui, PropertyID::Cursor, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::Auto), nullptr); + add(inherited_ui, PropertyID::PointerEvents, offsetof(InheritedUI, pointer_events), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(inherited_ui, PropertyID::ScrollbarColor, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::Auto), nullptr); + add(inherited_ui, PropertyID::ColorScheme, offsetof(InheritedUI, color_scheme), GROUP_FIELD_RESOLVED_U8, 0, nullptr); + add(inherited_ui, PropertyID::ColorScheme, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); + rust_style_group_register_field_descriptors(descriptors.data(), descriptors.size()); } @@ -604,9 +630,7 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co // NOTE: color-scheme must be set first to ensure system colors can be resolved correctly. auto const& color_scheme_style_value = computed_style.property(PropertyID::ColorScheme).as_color_scheme(); - computed_values.set_color_schemes(color_scheme_style_value.schemes(), color_scheme_style_value.only()); auto color_scheme = computed_style.color_scheme(document.page().preferred_color_scheme(), document.supported_color_schemes()); - computed_values.set_color_scheme(color_scheme); color_resolution_context.color_scheme = color_scheme; computed_values.set_anchor_names(custom_ident_list(PropertyID::AnchorName)); @@ -755,6 +779,37 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co } } } + Array inherited_ui_group_values; + gather_group_values(inherited_ui_group_properties, inherited_ui_group_values); + for (size_t i = 0; i < inherited_ui_group_properties.size(); ++i) { + auto ui_property_id = inherited_ui_group_properties[i]; + if (ui_property_id == PropertyID::CaretColor) { + inherited_ui_group_values[i].resolved_color = computed_style.caret_color(own_color_resolution_context).value(); + inherited_ui_group_values[i].has_resolved_color = true; + } else if (ui_property_id == PropertyID::AccentColor) { + inherited_ui_group_values[i].resolved_color = computed_style.accent_color(own_color_resolution_context).value(); + inherited_ui_group_values[i].has_resolved_color = true; + } else if (ui_property_id == PropertyID::ColorScheme) { + inherited_ui_group_values[i].resolved_number = static_cast(to_underlying(color_scheme)); + inherited_ui_group_values[i].has_resolved_number = true; + } + } + auto* inherited_ui_payload = ComputedValuesFFI::rust_build_style_group( + InheritedUIValues::style_group_index, + inherited_ui_group_values.data(), + inherited_ui_group_values.size(), + inherit_parent ? static_cast(inherit_parent->m_inherited.ui.operator->()) : nullptr); + bool const inherited_ui_adopted = inherited_ui_payload != nullptr; + if (inherited_ui_adopted) { + computed_values.adopt_inherited_ui_group(const_cast(inherited_ui_payload)); + } else { + // NB: Nothing in this function reads the group's color-scheme fields; the + // resolution context carries its own copy, so setting them here rather + // than first is equivalent. + computed_values.set_color_schemes(color_scheme_style_value.schemes(), color_scheme_style_value.only()); + computed_values.set_color_scheme(color_scheme); + } + auto* inherited_text_payload = ComputedValuesFFI::rust_build_style_group( InheritedTextValues::style_group_index, inherited_text_group_values.data(), @@ -816,12 +871,14 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co if (misc_reset_adopted) computed_values.adopt_misc_reset_group(const_cast(misc_reset_payload)); - auto const& accent_color_value = computed_style.property(CSS::PropertyID::AccentColor); - CSS::ColorOrAuto accent_color; - accent_color.used_value = computed_style.accent_color(color_resolution_context); - if (accent_color_value.to_keyword() != CSS::Keyword::Auto) - accent_color.computed_value = accent_color.used_value; - computed_values.set_accent_color(move(accent_color)); + if (!inherited_ui_adopted) { + auto const& accent_color_value = computed_style.property(CSS::PropertyID::AccentColor); + CSS::ColorOrAuto accent_color; + accent_color.used_value = computed_style.accent_color(color_resolution_context); + if (accent_color_value.to_keyword() != CSS::Keyword::Auto) + accent_color.computed_value = accent_color.used_value; + computed_values.set_accent_color(move(accent_color)); + } computed_values.set_vertical_align(computed_style.vertical_align()); @@ -1232,10 +1289,12 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co if (!inherited_box_adopted) computed_values.set_content_visibility(computed_style.content_visibility()); auto cursor = computed_style.cursor(); - computed_values.set_cursor(move(cursor)); + if (!inherited_ui_adopted) + computed_values.set_cursor(move(cursor)); if (!inherited_box_adopted) computed_values.set_image_rendering(computed_style.image_rendering()); - computed_values.set_pointer_events(computed_style.pointer_events()); + if (!inherited_ui_adopted) + computed_values.set_pointer_events(computed_style.pointer_events()); if (!text_reset_adopted) computed_values.set_text_decoration_line(computed_style.text_decoration_line()); if (!inherited_text_adopted) @@ -1558,7 +1617,8 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co computed_values.set_unicode_bidi(computed_style.unicode_bidi()); if (!misc_reset_adopted) computed_values.set_scroll_behavior(CSS::keyword_to_scroll_behavior(computed_style.property(CSS::PropertyID::ScrollBehavior).to_keyword()).release_value()); - computed_values.set_scrollbar_color(computed_style.scrollbar_color(color_resolution_context)); + if (!inherited_ui_adopted) + computed_values.set_scrollbar_color(computed_style.scrollbar_color(color_resolution_context)); if (!misc_reset_adopted) computed_values.set_scrollbar_gutter(computed_style.property(CSS::PropertyID::ScrollbarGutter).as_scrollbar_gutter().value()); if (!misc_reset_adopted) @@ -1602,12 +1662,14 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co computed_values.set_container_type(computed_style.container_type()); computed_values.set_will_change(computed_style.will_change()); - auto const& caret_color_value = computed_style.property(CSS::PropertyID::CaretColor); - CSS::ColorOrAuto caret_color; - caret_color.used_value = computed_style.caret_color(color_resolution_context); - if (caret_color_value.to_keyword() != CSS::Keyword::Auto) - caret_color.computed_value = caret_color.used_value; - computed_values.set_caret_color(move(caret_color)); + if (!inherited_ui_adopted) { + auto const& caret_color_value = computed_style.property(CSS::PropertyID::CaretColor); + CSS::ColorOrAuto caret_color; + caret_color.used_value = computed_style.caret_color(color_resolution_context); + if (caret_color_value.to_keyword() != CSS::Keyword::Auto) + caret_color.computed_value = caret_color.used_value; + computed_values.set_caret_color(move(caret_color)); + } computed_values.set_color_interpolation(computed_style.color_interpolation()); computed_values.set_color_interpolation_filters(computed_style.color_interpolation_filters()); computed_values.set_resize(computed_style.resize()); diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index 67798c91784bd..d5b914391a308 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -1902,6 +1902,7 @@ class ComputedValues::Mutator final { void adopt_effects_group(void* payload) { m_values.m_noninherited.effects.adopt(payload); } void adopt_misc_reset_group(void* payload) { m_values.m_noninherited.misc.adopt(payload); } void adopt_inherited_text_group(void* payload) { m_values.m_inherited.text.adopt(payload); } + void adopt_inherited_ui_group(void* payload) { m_values.m_inherited.ui.adopt(payload); } void set_aspect_ratio(AspectRatio aspect_ratio) { diff --git a/Libraries/LibWeb/CSS/Rust/src/computed_values.rs b/Libraries/LibWeb/CSS/Rust/src/computed_values.rs index e9f2be6e9d458..a5337f1c4e7fc 100644 --- a/Libraries/LibWeb/CSS/Rust/src/computed_values.rs +++ b/Libraries/LibWeb/CSS/Rust/src/computed_values.rs @@ -261,6 +261,9 @@ pub const GROUP_FIELD_RESOLVED_F64: u8 = 13; pub const GROUP_FIELD_RETAINED_SHELL: u8 = 14; /// A bool stored as one byte: whether the value is the descriptor's keyword. pub const GROUP_FIELD_KEYWORD_EQUALS_BOOL: u8 = 15; +/// A byte resolved by the C++ gather loop, carried in the resolved number, +/// for derived enum fields like the used color-scheme. +pub const GROUP_FIELD_RESOLVED_U8: u8 = 16; /// One gathered value for the generic group builder: the computed value's /// shell and data, plus the resolved raw color for color-kind fields. @@ -471,6 +474,12 @@ pub unsafe extern "C" fn rust_build_style_group( matches!(data, StyleValueData::Keyword { keyword } if *keyword == descriptor.keyword); pokes.push(Poke::U8(descriptor.offset, is_keyword as u8)); } + GROUP_FIELD_RESOLVED_U8 => { + if !value.has_resolved_number { + return None; + } + pokes.push(Poke::U8(descriptor.offset, value.resolved_number as u8)); + } _ => return None, } } From b85a806f7f8f6fe19b7ed40b4904e55e27972717 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 16:11:58 +0200 Subject: [PATCH 19/33] LibWeb: Build the sizing style group through the descriptors All six size fields register as keyword constraints, so the group adopts a shared payload when every size is untouched and falls back to the setters otherwise, until the core learns the size representation. --- Libraries/LibWeb/CSS/ComputedValues.cpp | 52 +++++++++++++++++++++---- Libraries/LibWeb/CSS/ComputedValues.h | 1 + 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/Libraries/LibWeb/CSS/ComputedValues.cpp b/Libraries/LibWeb/CSS/ComputedValues.cpp index b6373ccf58404..5725ae1395ba8 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.cpp +++ b/Libraries/LibWeb/CSS/ComputedValues.cpp @@ -250,6 +250,19 @@ static constexpr Array inherited_ui_group_properties { PropertyID::ColorScheme, }; +// The properties feeding the sizing group's descriptors, in registration +// order. All six register as keyword constraints: the group adopts a shared +// payload when every size is untouched and falls back to the setters +// otherwise, until the core learns the size representation. +static constexpr Array sizing_group_properties { + PropertyID::Width, + PropertyID::MinWidth, + PropertyID::MaxWidth, + PropertyID::Height, + PropertyID::MinHeight, + PropertyID::MaxHeight, +}; + static void register_style_group_field_descriptors() { using namespace ComputedValuesFFI; @@ -379,6 +392,14 @@ static void register_style_group_field_descriptors() add(inherited_ui, PropertyID::ColorScheme, offsetof(InheritedUI, color_scheme), GROUP_FIELD_RESOLVED_U8, 0, nullptr); add(inherited_ui, PropertyID::ColorScheme, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); + constexpr auto sizing = to_underlying(StyleGroupIndex::SizingValues); + add(sizing, PropertyID::Width, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::Auto), nullptr); + add(sizing, PropertyID::MinWidth, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::Auto), nullptr); + add(sizing, PropertyID::MaxWidth, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::None), nullptr); + add(sizing, PropertyID::Height, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::Auto), nullptr); + add(sizing, PropertyID::MinHeight, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::Auto), nullptr); + add(sizing, PropertyID::MaxHeight, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::None), nullptr); + rust_style_group_register_field_descriptors(descriptors.data(), descriptors.size()); } @@ -586,6 +607,17 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co if (alignment_adopted) computed_values.adopt_alignment_group(const_cast(alignment_payload)); + Array sizing_group_values; + gather_group_values(sizing_group_properties, sizing_group_values); + auto* sizing_payload = ComputedValuesFFI::rust_build_style_group( + SizingValues::style_group_index, + sizing_group_values.data(), + sizing_group_values.size(), + inherit_parent ? static_cast(inherit_parent->m_noninherited.sizing.operator->()) : nullptr); + bool const sizing_adopted = sizing_payload != nullptr; + if (sizing_adopted) + computed_values.adopt_sizing_group(const_cast(sizing_payload)); + Array effects_group_values; gather_group_values(effects_group_properties, effects_group_values); for (size_t i = 0; i < effects_group_properties.size(); ++i) { @@ -1345,13 +1377,19 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co if (!inherited_box_adopted) computed_values.set_visibility(computed_style.visibility()); - computed_values.set_width(computed_style.size_value(CSS::PropertyID::Width)); - computed_values.set_min_width(computed_style.size_value(CSS::PropertyID::MinWidth)); - computed_values.set_max_width(computed_style.size_value(CSS::PropertyID::MaxWidth)); - - computed_values.set_height(computed_style.size_value(CSS::PropertyID::Height)); - computed_values.set_min_height(computed_style.size_value(CSS::PropertyID::MinHeight)); - computed_values.set_max_height(computed_style.size_value(CSS::PropertyID::MaxHeight)); + if (!sizing_adopted) + computed_values.set_width(computed_style.size_value(CSS::PropertyID::Width)); + if (!sizing_adopted) + computed_values.set_min_width(computed_style.size_value(CSS::PropertyID::MinWidth)); + if (!sizing_adopted) + computed_values.set_max_width(computed_style.size_value(CSS::PropertyID::MaxWidth)); + + if (!sizing_adopted) + computed_values.set_height(computed_style.size_value(CSS::PropertyID::Height)); + if (!sizing_adopted) + computed_values.set_min_height(computed_style.size_value(CSS::PropertyID::MinHeight)); + if (!sizing_adopted) + computed_values.set_max_height(computed_style.size_value(CSS::PropertyID::MaxHeight)); computed_values.set_inset(computed_style.length_box(CSS::PropertyID::Left, CSS::PropertyID::Top, CSS::PropertyID::Right, CSS::PropertyID::Bottom, CSS::LengthPercentageOrAuto::make_auto())); for (auto property_id : { PropertyID::Top, PropertyID::Right, PropertyID::Bottom, PropertyID::Left }) { diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index d5b914391a308..7fe7d06c6af86 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -1903,6 +1903,7 @@ class ComputedValues::Mutator final { void adopt_misc_reset_group(void* payload) { m_values.m_noninherited.misc.adopt(payload); } void adopt_inherited_text_group(void* payload) { m_values.m_inherited.text.adopt(payload); } void adopt_inherited_ui_group(void* payload) { m_values.m_inherited.ui.adopt(payload); } + void adopt_sizing_group(void* payload) { m_values.m_noninherited.sizing.adopt(payload); } void set_aspect_ratio(AspectRatio aspect_ratio) { From 05cc04a316e196a5294be00e722c82e3d5d07c47 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 16:23:30 +0200 Subject: [PATCH 20/33] LibWeb: Build the transform style group through the descriptors The transform list, the individual transform properties and perspective register as none-keyword constraints, the origins as initial-value constraints, and the box and style fields map as keyword enums. The group's seeded default payload keeps the comparisons sound. --- Libraries/LibWeb/CSS/ComputedValues.cpp | 64 +++++++++++++++++++++---- Libraries/LibWeb/CSS/ComputedValues.h | 1 + 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/Libraries/LibWeb/CSS/ComputedValues.cpp b/Libraries/LibWeb/CSS/ComputedValues.cpp index 5725ae1395ba8..353ed7971fe58 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.cpp +++ b/Libraries/LibWeb/CSS/ComputedValues.cpp @@ -263,6 +263,20 @@ static constexpr Array sizing_group_properties { PropertyID::MaxHeight, }; +// The properties feeding the transform group's descriptors, in registration +// order. +static constexpr Array transform_group_properties { + PropertyID::Transform, + PropertyID::TransformBox, + PropertyID::TransformOrigin, + PropertyID::TransformStyle, + PropertyID::Rotate, + PropertyID::Translate, + PropertyID::Scale, + PropertyID::Perspective, + PropertyID::PerspectiveOrigin, +}; + static void register_style_group_field_descriptors() { using namespace ComputedValuesFFI; @@ -400,6 +414,18 @@ static void register_style_group_field_descriptors() add(sizing, PropertyID::MinHeight, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::Auto), nullptr); add(sizing, PropertyID::MaxHeight, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::None), nullptr); + using Transform = ComputedValues::TransformValues; + constexpr auto transform = to_underlying(StyleGroupIndex::TransformValues); + add(transform, PropertyID::Transform, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::None), nullptr); + add(transform, PropertyID::TransformBox, offsetof(Transform, transform_box), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(transform, PropertyID::TransformOrigin, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); + add(transform, PropertyID::TransformStyle, offsetof(Transform, transform_style), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(transform, PropertyID::Rotate, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::None), nullptr); + add(transform, PropertyID::Translate, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::None), nullptr); + add(transform, PropertyID::Scale, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::None), nullptr); + add(transform, PropertyID::Perspective, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::None), nullptr); + add(transform, PropertyID::PerspectiveOrigin, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); + rust_style_group_register_field_descriptors(descriptors.data(), descriptors.size()); } @@ -618,6 +644,17 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co if (sizing_adopted) computed_values.adopt_sizing_group(const_cast(sizing_payload)); + Array transform_group_values; + gather_group_values(transform_group_properties, transform_group_values); + auto* transform_payload = ComputedValuesFFI::rust_build_style_group( + TransformValues::style_group_index, + transform_group_values.data(), + transform_group_values.size(), + inherit_parent ? static_cast(inherit_parent->m_noninherited.transform.operator->()) : nullptr); + bool const transform_adopted = transform_payload != nullptr; + if (transform_adopted) + computed_values.adopt_transform_group(const_cast(transform_payload)); + Array effects_group_values; gather_group_values(effects_group_properties, effects_group_values); for (size_t i = 0; i < effects_group_properties.size(); ++i) { @@ -1428,15 +1465,24 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co if (!effects_adopted) computed_values.set_box_shadow(computed_style.box_shadow(color_resolution_context)); - computed_values.set_rotate(computed_style.rotate()); - computed_values.set_translate(computed_style.translate()); - computed_values.set_scale(computed_style.scale()); - computed_values.set_transformations(computed_style.transformations()); - computed_values.set_transform_box(computed_style.transform_box()); - computed_values.set_transform_origin(computed_style.transform_origin()); - computed_values.set_transform_style(computed_style.transform_style()); - computed_values.set_perspective(computed_style.perspective()); - computed_values.set_perspective_origin(computed_style.perspective_origin()); + if (!transform_adopted) + computed_values.set_rotate(computed_style.rotate()); + if (!transform_adopted) + computed_values.set_translate(computed_style.translate()); + if (!transform_adopted) + computed_values.set_scale(computed_style.scale()); + if (!transform_adopted) + computed_values.set_transformations(computed_style.transformations()); + if (!transform_adopted) + computed_values.set_transform_box(computed_style.transform_box()); + if (!transform_adopted) + computed_values.set_transform_origin(computed_style.transform_origin()); + if (!transform_adopted) + computed_values.set_transform_style(computed_style.transform_style()); + if (!transform_adopted) + computed_values.set_perspective(computed_style.perspective()); + if (!transform_adopted) + computed_values.set_perspective_origin(computed_style.perspective_origin()); struct NamedBorderAndWidth { CSS::BorderData border; diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index 7fe7d06c6af86..340adf93f9fb2 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -1904,6 +1904,7 @@ class ComputedValues::Mutator final { void adopt_inherited_text_group(void* payload) { m_values.m_inherited.text.adopt(payload); } void adopt_inherited_ui_group(void* payload) { m_values.m_inherited.ui.adopt(payload); } void adopt_sizing_group(void* payload) { m_values.m_noninherited.sizing.adopt(payload); } + void adopt_transform_group(void* payload) { m_values.m_noninherited.transform.adopt(payload); } void set_aspect_ratio(AspectRatio aspect_ratio) { From c96610a46516431e5f7170ff5d338003d6e27025 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 16:35:20 +0200 Subject: [PATCH 21/33] LibWeb: Build the mask style group through the descriptors The mask image and clip-path register as none-keyword constraints, the mask type as a keyword enum, and the seven coordinated mask layer properties as initial-value constraints, so the group adopts a shared payload whenever no masking applies and falls back to the setters otherwise. The url and image conditional setter sites keep their conditions inside the adoption gate. --- Libraries/LibWeb/CSS/ComputedValues.cpp | 70 ++++++++++++++++++++----- Libraries/LibWeb/CSS/ComputedValues.h | 1 + 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/Libraries/LibWeb/CSS/ComputedValues.cpp b/Libraries/LibWeb/CSS/ComputedValues.cpp index 353ed7971fe58..66e82fb46c883 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.cpp +++ b/Libraries/LibWeb/CSS/ComputedValues.cpp @@ -277,6 +277,21 @@ static constexpr Array transform_group_properties { PropertyID::PerspectiveOrigin, }; +// The properties feeding the mask group's descriptors, in registration +// order. +static constexpr Array mask_group_properties { + PropertyID::MaskImage, + PropertyID::MaskType, + PropertyID::ClipPath, + PropertyID::MaskMode, + PropertyID::MaskRepeat, + PropertyID::MaskPosition, + PropertyID::MaskClip, + PropertyID::MaskOrigin, + PropertyID::MaskSize, + PropertyID::MaskComposite, +}; + static void register_style_group_field_descriptors() { using namespace ComputedValuesFFI; @@ -426,6 +441,19 @@ static void register_style_group_field_descriptors() add(transform, PropertyID::Perspective, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::None), nullptr); add(transform, PropertyID::PerspectiveOrigin, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); + using Mask = ComputedValues::MaskValues; + constexpr auto mask = to_underlying(StyleGroupIndex::MaskValues); + add(mask, PropertyID::MaskImage, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::None), nullptr); + add(mask, PropertyID::MaskType, offsetof(Mask, mask_type), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(mask, PropertyID::ClipPath, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::None), nullptr); + add(mask, PropertyID::MaskMode, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); + add(mask, PropertyID::MaskRepeat, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); + add(mask, PropertyID::MaskPosition, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); + add(mask, PropertyID::MaskClip, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); + add(mask, PropertyID::MaskOrigin, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); + add(mask, PropertyID::MaskSize, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); + add(mask, PropertyID::MaskComposite, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); + rust_style_group_register_field_descriptors(descriptors.data(), descriptors.size()); } @@ -644,6 +672,17 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co if (sizing_adopted) computed_values.adopt_sizing_group(const_cast(sizing_payload)); + Array mask_group_values; + gather_group_values(mask_group_properties, mask_group_values); + auto* mask_payload = ComputedValuesFFI::rust_build_style_group( + MaskValues::style_group_index, + mask_group_values.data(), + mask_group_values.size(), + inherit_parent ? static_cast(inherit_parent->m_noninherited.mask_data.operator->()) : nullptr); + bool const mask_adopted = mask_payload != nullptr; + if (mask_adopted) + computed_values.adopt_mask_group(const_cast(mask_payload)); + Array transform_group_values; gather_group_values(transform_group_properties, transform_group_values); auto* transform_payload = ComputedValuesFFI::rust_build_style_group( @@ -955,7 +994,8 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co computed_values.set_background_layers(move(background_layers)); auto mask_layers = computed_style.mask_layers(); - computed_values.set_mask_layers(move(mask_layers)); + if (!mask_adopted) + computed_values.set_mask_layers(move(mask_layers)); Vector mask_positions; for_each_comma_separated_value(CSS::PropertyID::MaskPosition, [&](CSS::StyleValue const& value) { auto const& position = value.as_position(); @@ -964,7 +1004,8 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co .offset_y = CSS::LengthPercentage::from_style_value(position.edge_y()->as_edge().offset()), }); }); - computed_values.set_mask_positions(move(mask_positions)); + if (!mask_adopted) + computed_values.set_mask_positions(move(mask_positions)); auto border_image = computed_style.border_image(); computed_values.set_border_image(move(border_image)); @@ -1588,20 +1629,25 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co return value; }(); - if (mask_image.is_url()) { - computed_values.set_mask(mask_image.as_url().url()); - } else if (mask_image.is_abstract_image()) { - auto const& abstract_image = mask_image.as_abstract_image(); - computed_values.set_mask_image(abstract_image); + if (!mask_adopted) { + if (mask_image.is_url()) { + computed_values.set_mask(mask_image.as_url().url()); + } else if (mask_image.is_abstract_image()) { + auto const& abstract_image = mask_image.as_abstract_image(); + computed_values.set_mask_image(abstract_image); + } } - computed_values.set_mask_type(computed_style.mask_type()); + if (!mask_adopted) + computed_values.set_mask_type(computed_style.mask_type()); auto const& clip_path = computed_style.property(CSS::PropertyID::ClipPath); - if (clip_path.is_url()) - computed_values.set_clip_path(clip_path.as_url().url()); - else if (clip_path.is_basic_shape()) - computed_values.set_clip_path(clip_path.as_basic_shape()); + if (!mask_adopted) { + if (clip_path.is_url()) + computed_values.set_clip_path(clip_path.as_url().url()); + else if (clip_path.is_basic_shape()) + computed_values.set_clip_path(clip_path.as_basic_shape()); + } computed_values.set_clip_rule(computed_style.clip_rule()); computed_values.set_fill_rule(computed_style.fill_rule()); diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index 340adf93f9fb2..4bb622ecb5cd5 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -1905,6 +1905,7 @@ class ComputedValues::Mutator final { void adopt_inherited_ui_group(void* payload) { m_values.m_inherited.ui.adopt(payload); } void adopt_sizing_group(void* payload) { m_values.m_noninherited.sizing.adopt(payload); } void adopt_transform_group(void* payload) { m_values.m_noninherited.transform.adopt(payload); } + void adopt_mask_group(void* payload) { m_values.m_noninherited.mask_data.adopt(payload); } void set_aspect_ratio(AspectRatio aspect_ratio) { From bb3988377adca084a0c323b1847c5eba0cd027ac Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 16:46:35 +0200 Subject: [PATCH 22/33] LibWeb: Build the grid style group through the descriptors Every grid field registers as an initial-value constraint, so the group adopts a shared payload for the overwhelmingly common case of an element with no grid properties, until the core learns the grid track and placement representations. --- Libraries/LibWeb/CSS/ComputedValues.cpp | 61 +++++++++++++++++++++---- Libraries/LibWeb/CSS/ComputedValues.h | 1 + 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/Libraries/LibWeb/CSS/ComputedValues.cpp b/Libraries/LibWeb/CSS/ComputedValues.cpp index 66e82fb46c883..6d1dadbb619f9 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.cpp +++ b/Libraries/LibWeb/CSS/ComputedValues.cpp @@ -292,6 +292,22 @@ static constexpr Array mask_group_properties { PropertyID::MaskComposite, }; +// The properties feeding the grid group's descriptors, in registration +// order. Every field registers as an initial-value constraint until the +// core learns the grid representations. +static constexpr Array grid_group_properties { + PropertyID::GridAutoColumns, + PropertyID::GridAutoRows, + PropertyID::GridTemplateColumns, + PropertyID::GridTemplateRows, + PropertyID::GridAutoFlow, + PropertyID::GridColumnEnd, + PropertyID::GridColumnStart, + PropertyID::GridRowEnd, + PropertyID::GridRowStart, + PropertyID::GridTemplateAreas, +}; + static void register_style_group_field_descriptors() { using namespace ComputedValuesFFI; @@ -454,6 +470,10 @@ static void register_style_group_field_descriptors() add(mask, PropertyID::MaskSize, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); add(mask, PropertyID::MaskComposite, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); + constexpr auto grid = to_underlying(StyleGroupIndex::GridValues); + for (auto property : grid_group_properties) + add(grid, property, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); + rust_style_group_register_field_descriptors(descriptors.data(), descriptors.size()); } @@ -672,6 +692,17 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co if (sizing_adopted) computed_values.adopt_sizing_group(const_cast(sizing_payload)); + Array grid_group_values; + gather_group_values(grid_group_properties, grid_group_values); + auto* grid_payload = ComputedValuesFFI::rust_build_style_group( + GridValues::style_group_index, + grid_group_values.data(), + grid_group_values.size(), + inherit_parent ? static_cast(inherit_parent->m_noninherited.grid.operator->()) : nullptr); + bool const grid_adopted = grid_payload != nullptr; + if (grid_adopted) + computed_values.adopt_grid_group(const_cast(grid_payload)); + Array mask_group_values; gather_group_values(mask_group_properties, mask_group_values); auto* mask_payload = ComputedValuesFFI::rust_build_style_group( @@ -1580,16 +1611,26 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co if (!misc_reset_adopted) computed_values.set_outline_width(max(CSSPixels { 0 }, computed_style.length(CSS::PropertyID::OutlineWidth).absolute_length_to_px())); - computed_values.set_grid_auto_columns(computed_style.grid_auto_columns()); - computed_values.set_grid_auto_rows(computed_style.grid_auto_rows()); - computed_values.set_grid_template_columns(computed_style.grid_template_columns()); - computed_values.set_grid_template_rows(computed_style.grid_template_rows()); - computed_values.set_grid_column_end(computed_style.grid_column_end()); - computed_values.set_grid_column_start(computed_style.grid_column_start()); - computed_values.set_grid_row_end(computed_style.grid_row_end()); - computed_values.set_grid_row_start(computed_style.grid_row_start()); - computed_values.set_grid_template_areas(computed_style.grid_template_areas()); - computed_values.set_grid_auto_flow(computed_style.grid_auto_flow()); + if (!grid_adopted) + computed_values.set_grid_auto_columns(computed_style.grid_auto_columns()); + if (!grid_adopted) + computed_values.set_grid_auto_rows(computed_style.grid_auto_rows()); + if (!grid_adopted) + computed_values.set_grid_template_columns(computed_style.grid_template_columns()); + if (!grid_adopted) + computed_values.set_grid_template_rows(computed_style.grid_template_rows()); + if (!grid_adopted) + computed_values.set_grid_column_end(computed_style.grid_column_end()); + if (!grid_adopted) + computed_values.set_grid_column_start(computed_style.grid_column_start()); + if (!grid_adopted) + computed_values.set_grid_row_end(computed_style.grid_row_end()); + if (!grid_adopted) + computed_values.set_grid_row_start(computed_style.grid_row_start()); + if (!grid_adopted) + computed_values.set_grid_template_areas(computed_style.grid_template_areas()); + if (!grid_adopted) + computed_values.set_grid_auto_flow(computed_style.grid_auto_flow()); computed_values.set_cx(CSS::LengthPercentage::from_style_value(computed_style.property(CSS::PropertyID::Cx))); computed_values.set_cy(CSS::LengthPercentage::from_style_value(computed_style.property(CSS::PropertyID::Cy))); diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index 4bb622ecb5cd5..00ad6c4a4750d 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -1906,6 +1906,7 @@ class ComputedValues::Mutator final { void adopt_sizing_group(void* payload) { m_values.m_noninherited.sizing.adopt(payload); } void adopt_transform_group(void* payload) { m_values.m_noninherited.transform.adopt(payload); } void adopt_mask_group(void* payload) { m_values.m_noninherited.mask_data.adopt(payload); } + void adopt_grid_group(void* payload) { m_values.m_noninherited.grid.adopt(payload); } void set_aspect_ratio(AspectRatio aspect_ratio) { From db04d99be269af6be4ce83c7e46269fa2ca8560d Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 16:58:48 +0200 Subject: [PATCH 23/33] LibWeb: Build the animation style group through the descriptors All twenty-one animation, timeline and transition properties register as initial-value constraints, so elements without any of them, the overwhelming majority, adopt a shared payload and skip the whole comma-list construction cluster. --- Libraries/LibWeb/CSS/ComputedValues.cpp | 122 ++++++++++++++++++------ Libraries/LibWeb/CSS/ComputedValues.h | 1 + 2 files changed, 95 insertions(+), 28 deletions(-) diff --git a/Libraries/LibWeb/CSS/ComputedValues.cpp b/Libraries/LibWeb/CSS/ComputedValues.cpp index 6d1dadbb619f9..e7c03dc470506 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.cpp +++ b/Libraries/LibWeb/CSS/ComputedValues.cpp @@ -308,6 +308,33 @@ static constexpr Array grid_group_properties { PropertyID::GridTemplateAreas, }; +// The properties feeding the animation group's descriptors, in registration +// order. Every field registers as an initial-value constraint: elements +// without animations, timelines or transitions adopt a shared payload. +static constexpr Array animation_group_properties { + PropertyID::AnimationName, + PropertyID::AnimationComposition, + PropertyID::AnimationDelay, + PropertyID::AnimationDirection, + PropertyID::AnimationDuration, + PropertyID::AnimationFillMode, + PropertyID::AnimationIterationCount, + PropertyID::AnimationPlayState, + PropertyID::AnimationTimeline, + PropertyID::AnimationTimingFunction, + PropertyID::ScrollTimelineName, + PropertyID::ScrollTimelineAxis, + PropertyID::TimelineScope, + PropertyID::ViewTimelineName, + PropertyID::ViewTimelineAxis, + PropertyID::ViewTimelineInset, + PropertyID::TransitionProperty, + PropertyID::TransitionDuration, + PropertyID::TransitionTimingFunction, + PropertyID::TransitionDelay, + PropertyID::TransitionBehavior, +}; + static void register_style_group_field_descriptors() { using namespace ComputedValuesFFI; @@ -474,6 +501,10 @@ static void register_style_group_field_descriptors() for (auto property : grid_group_properties) add(grid, property, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); + constexpr auto animation = to_underlying(StyleGroupIndex::AnimationValues); + for (auto property : animation_group_properties) + add(animation, property, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); + rust_style_group_register_field_descriptors(descriptors.data(), descriptors.size()); } @@ -812,6 +843,17 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co computed_values.set_line_height(computed_style.line_height_data(document.font_computer())); computed_values.set_font_variant_emoji(computed_style.font_variant_emoji()); + Array animation_group_values; + gather_group_values(animation_group_properties, animation_group_values); + auto* animation_payload = ComputedValuesFFI::rust_build_style_group( + AnimationValues::style_group_index, + animation_group_values.data(), + animation_group_values.size(), + inherit_parent ? static_cast(inherit_parent->m_noninherited.animation.operator->()) : nullptr); + bool const animation_adopted = animation_payload != nullptr; + if (animation_adopted) + computed_values.adopt_animation_group(const_cast(animation_payload)); + Vector animation_names; for (auto const& name : computed_style.property(PropertyID::AnimationName).as_value_list().values()) { if (name->to_keyword() == Keyword::None) { @@ -823,32 +865,40 @@ NonnullRefPtr ComputedValues::create(ComputedProperties co }); } } - computed_values.set_animation_names(move(animation_names)); + if (!animation_adopted) + computed_values.set_animation_names(move(animation_names)); Vector animation_compositions; for_each_comma_separated_value(PropertyID::AnimationComposition, [&](StyleValue const& value) { animation_compositions.append(keyword_to_animation_composition(value.to_keyword()).release_value()); }); - computed_values.set_animation_compositions(move(animation_compositions)); + if (!animation_adopted) + computed_values.set_animation_compositions(move(animation_compositions)); Vector