mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
StyleManager: vectorize rule specificity checks with SIMD
This commit is contained in:
@@ -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| {
|
||||
|
||||
Reference in New Issue
Block a user