From 3e2be5b31742266c66c0e7af302082001ba7f2dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Fri, 20 Mar 2026 12:13:52 +0900 Subject: [PATCH] StyleManager: vectorize rule specificity checks with SIMD --- src/browser/StyleManager.zig | 232 +++++++++++++++++++++-------------- 1 file changed, 143 insertions(+), 89 deletions(-) diff --git a/src/browser/StyleManager.zig b/src/browser/StyleManager.zig index a7c8bb21..72a0a322 100644 --- a/src/browser/StyleManager.zig +++ b/src/browser/StyleManager.zig @@ -166,25 +166,25 @@ pub fn isHidden(self: *StyleManager, el: *Element, cache: ?*VisibilityCache, opt /// Check if a single element (not ancestors) is hidden. fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOptions) bool { - // Track best match per property (value + specificity) - // Initialize spec to INLINE_SPECIFICITY for properties we don't care about - this makes - // the loop naturally skip them since no stylesheet rule can have specificity >= INLINE_SPECIFICITY + // Track best match per property (value + priority) + // Initialize priority to INLINE_PRIORITY for properties we don't care about - this makes + // the loop naturally skip them since no stylesheet rule can have priority >= INLINE_PRIORITY var display_none: ?bool = null; - var display_spec: u32 = 0; + var display_priority: u64 = 0; var visibility_hidden: ?bool = null; - var visibility_spec: u32 = 0; + var visibility_priority: u64 = 0; var opacity_zero: ?bool = null; - var opacity_spec: u32 = 0; + var opacity_priority: u64 = 0; - // Check inline styles FIRST - they use INLINE_SPECIFICITY so no stylesheet can beat them + // Check inline styles FIRST - they use INLINE_PRIORITY so no stylesheet can beat them if (getInlineStyleProperty(el, comptime .wrap("display"), self.page)) |property| { if (property._value.eql(comptime .wrap("none"))) { return true; // Early exit for hiding value } display_none = false; - display_spec = INLINE_SPECIFICITY; + display_priority = INLINE_PRIORITY; } if (options.check_visibility) { @@ -193,13 +193,13 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp return true; } visibility_hidden = false; - visibility_spec = INLINE_SPECIFICITY; + visibility_priority = INLINE_PRIORITY; } } else { // This can't be beat. Setting this means that, when checking rules // we no longer have to check if options.check_visibility is enabled. - // We can just compare the specificity. - visibility_spec = INLINE_SPECIFICITY; + // We can just compare the priority. + visibility_priority = INLINE_PRIORITY; } if (options.check_opacity) { @@ -208,87 +208,118 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp return true; } opacity_zero = false; - opacity_spec = INLINE_SPECIFICITY; + opacity_priority = INLINE_PRIORITY; } } else { - opacity_spec = INLINE_SPECIFICITY; + opacity_priority = INLINE_PRIORITY; } - if (display_spec == INLINE_SPECIFICITY and visibility_spec == INLINE_SPECIFICITY and opacity_spec == INLINE_SPECIFICITY) { + if (display_priority == INLINE_PRIORITY and visibility_priority == INLINE_PRIORITY and opacity_priority == INLINE_PRIORITY) { return false; } self.rebuildIfDirty() catch return false; - // Track doc_order for tie-breaking (0 = inline style, which always wins) - var display_doc_order: u32 = 0; - var visibility_doc_order: u32 = 0; - var opacity_doc_order: u32 = 0; - // Helper to check a single rule const Ctx = struct { display_none: *?bool, - display_spec: *u32, - display_doc_order: *u32, + display_priority: *u64, visibility_hidden: *?bool, - visibility_spec: *u32, - visibility_doc_order: *u32, + visibility_priority: *u64, opacity_zero: *?bool, - opacity_spec: *u32, - opacity_doc_order: *u32, + opacity_priority: *u64, el: *Element, page: *Page, - // Returns true if (spec, doc_order) beats (best_spec, best_doc_order) - fn beats(spec: u32, doc_order: u32, best_spec: u32, best_doc_order: u32) bool { - return spec > best_spec or (spec == best_spec and doc_order > best_doc_order); - } - fn checkRules(ctx: @This(), rules: *const RuleList) void { - if (ctx.display_spec.* == INLINE_SPECIFICITY and - ctx.visibility_spec.* == INLINE_SPECIFICITY and - ctx.opacity_spec.* == INLINE_SPECIFICITY) + if (ctx.display_priority.* == INLINE_PRIORITY and + ctx.visibility_priority.* == INLINE_PRIORITY and + ctx.opacity_priority.* == INLINE_PRIORITY) { return; } const len = rules.len; - const specificities = rules.items(.specificity); - const doc_orders = rules.items(.doc_order); + const priorities = rules.items(.priority); - for (0..len) |i| { - const spec = specificities[i]; - const doc_order = doc_orders[i]; + const vec_len = std.simd.suggestVectorLength(u64) orelse 4; + var i: usize = 0; - if (!beats(spec, doc_order, ctx.display_spec.*, ctx.display_doc_order.*) and - !beats(spec, doc_order, ctx.visibility_spec.*, ctx.visibility_doc_order.*) and - !beats(spec, doc_order, ctx.opacity_spec.*, ctx.opacity_doc_order.*)) - { + while (i + vec_len <= len) { + const p_chunk: @Vector(vec_len, u64) = priorities[i..][0..vec_len].*; + const min_priority = @min(@min(ctx.display_priority.*, ctx.visibility_priority.*), ctx.opacity_priority.*); + const min_p_vec: @Vector(vec_len, u64) = @splat(min_priority); + + const cmp = p_chunk > min_p_vec; + const any_can_beat = @reduce(.Or, cmp); + + if (!any_can_beat) { + i += vec_len; + continue; + } + + for (0..vec_len) |j| { + const idx = i + j; + const p = priorities[idx]; + + if (p <= ctx.display_priority.* and p <= ctx.visibility_priority.* and p <= ctx.opacity_priority.*) { + continue; + } + + const props = rules.items(.props)[idx]; + const dominated = (props.display_none == null or p <= ctx.display_priority.*) and + (props.visibility_hidden == null or p <= ctx.visibility_priority.*) and + (props.opacity_zero == null or p <= ctx.opacity_priority.*); + + if (dominated) continue; + + const selector = rules.items(.selector)[idx]; + if (matchesSelector(ctx.el, selector, ctx.page)) { + if (props.display_none != null and p > ctx.display_priority.*) { + ctx.display_none.* = props.display_none; + ctx.display_priority.* = p; + } + if (props.visibility_hidden != null and p > ctx.visibility_priority.*) { + ctx.visibility_hidden.* = props.visibility_hidden; + ctx.visibility_priority.* = p; + } + if (props.opacity_zero != null and p > ctx.opacity_priority.*) { + ctx.opacity_zero.* = props.opacity_zero; + ctx.opacity_priority.* = p; + } + } + } + i += vec_len; + } + + // Remainder + while (i < len) : (i += 1) { + const p = priorities[i]; + + if (p <= ctx.display_priority.* and p <= ctx.visibility_priority.* and p <= ctx.opacity_priority.*) { continue; } const props = rules.items(.props)[i]; - const dominated = (props.display_none == null or !beats(spec, doc_order, ctx.display_spec.*, ctx.display_doc_order.*)) and - (props.visibility_hidden == null or !beats(spec, doc_order, ctx.visibility_spec.*, ctx.visibility_doc_order.*)) and - (props.opacity_zero == null or !beats(spec, doc_order, ctx.opacity_spec.*, ctx.opacity_doc_order.*)); + const dominated = (props.display_none == null or p <= ctx.display_priority.*) and + (props.visibility_hidden == null or p <= ctx.visibility_priority.*) and + (props.opacity_zero == null or p <= ctx.opacity_priority.*); + if (dominated) continue; const selector = rules.items(.selector)[i]; if (matchesSelector(ctx.el, selector, ctx.page)) { - if (props.display_none != null and beats(spec, doc_order, ctx.display_spec.*, ctx.display_doc_order.*)) { + if (props.display_none != null and p > ctx.display_priority.*) { ctx.display_none.* = props.display_none; - ctx.display_spec.* = spec; - ctx.display_doc_order.* = doc_order; + ctx.display_priority.* = p; } - if (props.visibility_hidden != null and beats(spec, doc_order, ctx.visibility_spec.*, ctx.visibility_doc_order.*)) { + if (props.visibility_hidden != null and p > ctx.visibility_priority.*) { ctx.visibility_hidden.* = props.visibility_hidden; - ctx.visibility_spec.* = spec; - ctx.visibility_doc_order.* = doc_order; + ctx.visibility_priority.* = p; } - if (props.opacity_zero != null and beats(spec, doc_order, ctx.opacity_spec.*, ctx.opacity_doc_order.*)) { + if (props.opacity_zero != null and p > ctx.opacity_priority.*) { ctx.opacity_zero.* = props.opacity_zero; - ctx.opacity_spec.* = spec; - ctx.opacity_doc_order.* = doc_order; + ctx.opacity_priority.* = p; } } } @@ -296,14 +327,11 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp }; const ctx = Ctx{ .display_none = &display_none, - .display_spec = &display_spec, - .display_doc_order = &display_doc_order, + .display_priority = &display_priority, .visibility_hidden = &visibility_hidden, - .visibility_spec = &visibility_spec, - .visibility_doc_order = &visibility_doc_order, + .visibility_priority = &visibility_priority, .opacity_zero = &opacity_zero, - .opacity_spec = &opacity_spec, - .opacity_doc_order = &opacity_doc_order, + .opacity_priority = &opacity_priority, .el = el, .page = self.page, }; @@ -379,27 +407,58 @@ fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool { self.rebuildIfDirty() catch return false; var result: ?bool = null; - var best_spec: u32 = 0; - var best_doc_order: u32 = 0; + var best_priority: u64 = 0; // Helper to check a single rule const checkRules = struct { - fn beats(spec: u32, doc_order: u32, b_spec: u32, b_doc_order: u32) bool { - return spec > b_spec or (spec == b_spec and doc_order > b_doc_order); - } - - fn check(rules: *const RuleList, res: *?bool, spec: *u32, doc_order: *u32, elem: *Element, p: *Page) void { - if (spec.* == INLINE_SPECIFICITY) return; + fn check(rules: *const RuleList, res: *?bool, current_priority: *u64, elem: *Element, p: *Page) void { + if (current_priority.* == INLINE_PRIORITY) return; const len = rules.len; - const specificities = rules.items(.specificity); - const doc_orders = rules.items(.doc_order); + const priorities = rules.items(.priority); - for (0..len) |i| { - const rule_spec = specificities[i]; - const rule_doc_order = doc_orders[i]; + const vec_len = std.simd.suggestVectorLength(u64) orelse 4; + var i: usize = 0; - if (!beats(rule_spec, rule_doc_order, spec.*, doc_order.*)) { + while (i + vec_len <= len) { + const p_chunk: @Vector(vec_len, u64) = priorities[i..][0..vec_len].*; + const min_p_vec: @Vector(vec_len, u64) = @splat(current_priority.*); + + const cmp = p_chunk > min_p_vec; + const any_can_beat = @reduce(.Or, cmp); + + if (!any_can_beat) { + i += vec_len; + continue; + } + + for (0..vec_len) |j| { + const idx = i + j; + const priority = priorities[idx]; + + if (priority <= current_priority.*) { + continue; + } + + const props = rules.items(.props)[idx]; + if (props.pointer_events_none == null) { + continue; + } + + const selector = rules.items(.selector)[idx]; + if (matchesSelector(elem, selector, p)) { + res.* = props.pointer_events_none; + current_priority.* = priority; + } + } + i += vec_len; + } + + // Remainder + while (i < len) : (i += 1) { + const priority = priorities[i]; + + if (priority <= current_priority.*) { continue; } @@ -411,8 +470,7 @@ fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool { const selector = rules.items(.selector)[i]; if (matchesSelector(elem, selector, p)) { res.* = props.pointer_events_none; - spec.* = rule_spec; - doc_order.* = rule_doc_order; + current_priority.* = priority; } } } @@ -420,7 +478,7 @@ fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool { if (el.getAttributeSafe(comptime .wrap("id"))) |id| { if (self.id_rules.get(id)) |rules| { - checkRules(&rules, &result, &best_spec, &best_doc_order, el, page); + checkRules(&rules, &result, &best_priority, el, page); } } @@ -428,16 +486,16 @@ fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool { var it = std.mem.tokenizeAny(u8, class_attr, &std.ascii.whitespace); while (it.next()) |class| { if (self.class_rules.get(class)) |rules| { - checkRules(&rules, &result, &best_spec, &best_doc_order, el, page); + checkRules(&rules, &result, &best_priority, el, page); } } } if (self.tag_rules.get(el.getTag())) |rules| { - checkRules(&rules, &result, &best_spec, &best_doc_order, el, page); + checkRules(&rules, &result, &best_priority, el, page); } - checkRules(&self.other_rules, &result, &best_spec, &best_doc_order, el, page); + checkRules(&self.other_rules, &result, &best_priority, el, page); return result orelse false; } @@ -479,8 +537,7 @@ fn addRule(self: *StyleManager, style_rule: *CSSStyleRule) !void { const rule = VisibilityRule{ .props = props, .selector = selector, - .specificity = computeSpecificity(selector), - .doc_order = self.next_doc_order, + .priority = (@as(u64, computeSpecificity(selector)) << 32) | @as(u64, self.next_doc_order), }; self.next_doc_order += 1; @@ -656,11 +713,8 @@ const VisibilityRule = struct { selector: Selector.Selector, // Single selector, not a list props: VisibilityProperties, - // Packed specificity: (id_count << 20) | (class_count << 10) | element_count - specificity: u32, - - // Document order for tie-breaking equal specificity (higher = later in document) - doc_order: u32, + // Packed priority: (specificity << 32) | doc_order + priority: u64, }; const CheckVisibilityOptions = struct { @@ -668,8 +722,8 @@ const CheckVisibilityOptions = struct { check_visibility: bool = false, }; -// Inline styles always win over stylesheets - use max u32 as sentinel -const INLINE_SPECIFICITY: u32 = std.math.maxInt(u32); +// Inline styles always win over stylesheets - use max u64 as sentinel +const INLINE_PRIORITY: u64 = std.math.maxInt(u64); fn getInlineStyleProperty(el: *Element, property_name: String, page: *Page) ?*CSSStyleProperty { const style = el.getOrCreateStyle(page) catch |err| {