Merge pull request #416 from wlx-team/staging

wgui: perf: Scissor render culling
This commit is contained in:
oo8dev
2026-01-27 17:53:26 +01:00
committed by GitHub
7 changed files with 120 additions and 49 deletions

View File

@@ -36,7 +36,7 @@
<div gap="4">
<Button id="button_red" text="Red button" width="150" height="32" color="#FF0000" tooltip_str="I'm at the top" tooltip_side="top" />
<Button id="button_aqua" text="Aqua button" width="150" height="32" color="#00FFFF" tooltip_str="I'm at the bottom" tooltip_side="bottom" />
<Button id="button_yellow" text="Yellow button" width="150" height="32" color="#FFFF00" tooltip="TESTBED.HELLO_WORLD" tooltip_side="right" />
<Button id="button_yellow" text="Yellow button" width="150" height="32" color="#FFFF00" tooltip="TESTBED.HELLO_WORLD" tooltip_side="left" />
</div>
<div gap="4">
<Button id="button_click_me" text="Click me" width="128" height="24" color="#FFFFFF" />

View File

@@ -359,7 +359,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.unwrap();
if debug_draw_enabled {
log::debug!("pass count: {}", draw_result.pass_count);
log::debug!(
"pass count: {}, primitive commands count: {}",
draw_result.pass_count,
draw_result.primitive_commands_count
);
}
cmd_buf.end_rendering().unwrap();

View File

@@ -129,27 +129,34 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul
let transform = Mat4::from_translation(Vec3::new(-0.5, 0.0, 0.0));
// this value needs to be bigger than rectangle padding sizes due to the
// transform stack & scissoring design. Needs investigation, zero-size objects
// would result in PushScissorStackResult::OutOfBounds otherwise preventing us
// to render the label. Didn't find the best solution for this edge-case yet,
// so here it is.
let pin_size = 32.0;
let (pin_left, pin_top, pin_align_items, pin_justify_content) = match params.info.side {
TooltipSide::Left => (
absolute_boundary.left() - spacing,
absolute_boundary.top() + absolute_boundary.size.y / 2.0,
absolute_boundary.left() - spacing - pin_size,
absolute_boundary.top() + absolute_boundary.size.y / 2.0 - pin_size / 2.0,
taffy::AlignItems::Center,
taffy::JustifyContent::End,
),
TooltipSide::Right => (
absolute_boundary.left() + absolute_boundary.size.x + spacing,
absolute_boundary.top() + absolute_boundary.size.y / 2.0,
absolute_boundary.top() + absolute_boundary.size.y / 2.0 - pin_size / 2.0,
taffy::AlignItems::Center,
taffy::JustifyContent::Start,
),
TooltipSide::Top => (
absolute_boundary.left() + absolute_boundary.size.x / 2.0,
absolute_boundary.top() - spacing,
absolute_boundary.left() + absolute_boundary.size.x / 2.0 - pin_size / 2.0,
absolute_boundary.top() - spacing - pin_size,
taffy::AlignItems::End,
taffy::JustifyContent::Center,
),
TooltipSide::Bottom => (
absolute_boundary.left() + absolute_boundary.size.x / 2.0,
absolute_boundary.left() + absolute_boundary.size.x / 2.0 - pin_size / 2.0,
absolute_boundary.top() + absolute_boundary.size.y + spacing,
taffy::AlignItems::Baseline,
taffy::JustifyContent::Center,
@@ -173,8 +180,8 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul
},
/* important, to make it centered! */
size: taffy::Size {
width: length(0.0),
height: length(0.0),
width: length(pin_size),
height: length(pin_size),
},
..Default::default()
},

View File

@@ -288,24 +288,48 @@ pub fn push_transform_stack(
});
}
/// returns true if scissor has been pushed
#[derive(Eq, PartialEq)]
pub enum PushScissorStackResult {
VisibleDontClip, // scissor calculated, but don't clip anything
VisibleAndClip, // scissor should be applied at this stage (ScissorSet primitive needs to be called)
OutOfBounds, // scissor rectangle is out of bounds (negative boundary dimensions)
}
impl PushScissorStackResult {
pub fn should_display(&self) -> bool {
*self != Self::OutOfBounds
}
}
/// Returns true if scissor has been pushed.
pub fn push_scissor_stack(
transform_stack: &mut TransformStack,
scissor_stack: &mut ScissorStack,
scroll_shift: Vec2,
info: &Option<ScrollbarInfo>,
style: &taffy::Style,
) -> bool {
let scissor_pushed = info.is_some() && has_overflow_clip(style);
if !scissor_pushed {
return false;
}
) -> PushScissorStackResult {
let mut boundary_absolute = drawing::Boundary::construct_absolute(transform_stack);
boundary_absolute.pos += scroll_shift;
let do_clip = info.is_some() && has_overflow_clip(style);
scissor_stack.push(ScissorBoundary(boundary_absolute));
true
if scissor_stack.is_out_of_bounds() {
return PushScissorStackResult::OutOfBounds;
}
if do_clip {
PushScissorStackResult::VisibleAndClip
} else {
PushScissorStackResult::VisibleDontClip
}
}
struct DrawWidgetInternal {
// how many times ScissorSet render primitives has been called?
scissor_set_count: u32,
}
fn draw_widget(
@@ -313,6 +337,7 @@ fn draw_widget(
state: &mut DrawState,
node_id: taffy::NodeId,
style: &taffy::Style,
internal: &mut DrawWidgetInternal,
widget: &Widget,
) {
let Ok(l) = params.layout.state.tree.layout(node_id) else {
@@ -346,9 +371,11 @@ fn draw_widget(
));
}
let scissor_pushed = push_scissor_stack(state.transform_stack, state.scissor_stack, scroll_shift, &info, style);
let starting_scissor_set_count = internal.scissor_set_count;
if scissor_pushed {
let scissor_result = push_scissor_stack(state.transform_stack, state.scissor_stack, scroll_shift, &info, style);
if scissor_result == PushScissorStackResult::VisibleAndClip {
if params.debug_draw {
let mut boundary_relative = drawing::Boundary::construct_relative(state.transform_stack);
boundary_relative.pos += scroll_shift;
@@ -358,10 +385,10 @@ fn draw_widget(
Color::new(1.0, 0.0, 1.0, 1.0),
));
}
state
.primitives
.push(drawing::RenderPrimitive::ScissorSet(*state.scissor_stack.get()));
internal.scissor_set_count += 1;
}
let draw_params = widget::DrawParams {
@@ -370,12 +397,16 @@ fn draw_widget(
style,
};
widget_state.draw_all(state, &draw_params);
if scissor_result.should_display() {
widget_state.draw_all(state, &draw_params);
draw_children(params, state, node_id, internal, false);
}
draw_children(params, state, node_id, false);
state.scissor_stack.pop();
if scissor_pushed {
state.scissor_stack.pop();
let current_scissor_set_count = internal.scissor_set_count;
if current_scissor_set_count > starting_scissor_set_count {
state
.primitives
.push(drawing::RenderPrimitive::ScissorSet(*state.scissor_stack.get()));
@@ -392,7 +423,13 @@ fn draw_widget(
}
}
fn draw_children(params: &DrawParams, state: &mut DrawState, parent_node_id: taffy::NodeId, is_topmost: bool) {
fn draw_children(
params: &DrawParams,
state: &mut DrawState,
parent_node_id: taffy::NodeId,
internal: &mut DrawWidgetInternal,
is_topmost: bool,
) {
let layout = &params.layout;
for node_id in layout.state.tree.child_ids(parent_node_id) {
@@ -415,7 +452,7 @@ fn draw_children(params: &DrawParams, state: &mut DrawState, parent_node_id: taf
continue;
};
draw_widget(params, state, node_id, style, widget);
draw_widget(params, state, node_id, style, internal, widget);
if is_topmost {
state.primitives.push(RenderPrimitive::NewPass);
@@ -439,7 +476,9 @@ pub fn draw(params: &mut DrawParams) -> anyhow::Result<Vec<RenderPrimitive>> {
alterables: &mut alterables,
};
draw_children(params, &mut state, params.layout.tree_root_node, true);
let mut internal = DrawWidgetInternal { scissor_set_count: 0 };
draw_children(params, &mut state, params.layout.tree_root_node, &mut internal, true);
params.layout.process_alterables(alterables)?;

View File

@@ -442,7 +442,7 @@ impl Layout {
widget.data.cached_absolute_boundary = drawing::Boundary::construct_absolute(&alterables.transform_stack);
let scissor_pushed = push_scissor_stack(
let scissor_result = push_scissor_stack(
&mut alterables.transform_stack,
&mut alterables.scissor_stack,
scroll_shift,
@@ -450,24 +450,24 @@ impl Layout {
style,
);
// check children first
self.push_event_children(node_id, event, event_result, alterables, user_data)?;
if scissor_result.should_display() {
// check children first
self.push_event_children(node_id, event, event_result, alterables, user_data)?;
if event_result.can_propagate() {
let mut params = EventParams {
state: &self.state,
layout: l,
alterables,
node_id,
style,
};
if event_result.can_propagate() {
let mut params = EventParams {
state: &self.state,
layout: l,
alterables,
node_id,
style,
};
widget.process_event(widget_id, node_id, event, event_result, user_data, &mut params)?;
widget.process_event(widget_id, node_id, event, event_result, user_data, &mut params)?;
}
}
if scissor_pushed {
alterables.scissor_stack.pop();
}
alterables.scissor_stack.pop();
alterables.transform_stack.pop();
Ok(())

View File

@@ -173,6 +173,7 @@ pub struct Context {
pub struct ContextDrawResult {
pub pass_count: u32,
pub primitive_commands_count: u32,
}
impl Context {
@@ -239,7 +240,7 @@ impl Context {
let mut passes = Vec::<RendererPass>::new();
let mut needs_new_pass = true;
let mut next_scissor: Option<drawing::Boundary> = None;
let mut cur_scissor: Option<drawing::Boundary> = None;
for primitive in primitives {
if needs_new_pass {
@@ -247,7 +248,7 @@ impl Context {
&mut atlas.text_atlas,
shared.rect_pipeline.clone(),
shared.image_pipeline.clone(),
next_scissor,
cur_scissor,
self.pixel_scale,
)?);
needs_new_pass = false;
@@ -309,14 +310,26 @@ impl Context {
.add_image(extent.boundary, image.clone(), &extent.transform);
}
drawing::RenderPrimitive::ScissorSet(boundary) => {
next_scissor = Some(boundary.0);
needs_new_pass = true;
let skip = if let Some(cur_scissor) = cur_scissor {
// do not create a new pass if it's not needed (same scissor values)
cur_scissor == boundary.0
} else {
false
};
cur_scissor = Some(boundary.0);
if skip {
//log::debug!("same scissor boundary, re-using the same pass");
} else {
needs_new_pass = true;
}
}
}
}
let res = ContextDrawResult {
pass_count: passes.len() as u32,
primitive_commands_count: primitives.len() as u32,
};
for mut pass in passes {

View File

@@ -6,6 +6,7 @@ pub trait Pushable<T> {
fn push(&mut self, item: &T);
}
#[derive(Debug)]
pub struct GenericStack<T, const STACK_MAX: usize> {
pub stack: [T; STACK_MAX],
top: u8,
@@ -50,7 +51,7 @@ impl<T: StackItem<T>, const STACK_MAX: usize> Default for GenericStack<T, STACK_
// Transform stack
// ########################################
#[derive(Copy, Clone)]
#[derive(Debug, Copy, Clone)]
pub struct Transform {
pub rel_pos: Vec2,
pub visual_dim: Vec2, // for convenience
@@ -95,7 +96,7 @@ pub type TransformStack = GenericStack<Transform, 64>;
// Scissor stack
// ########################################
#[derive(Copy, Clone)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ScissorBoundary(pub drawing::Boundary);
impl Default for ScissorBoundary {
@@ -144,3 +145,10 @@ impl Pushable<ScissorBoundary> for ScissorBoundary {
}
pub type ScissorStack = GenericStack<ScissorBoundary, 64>;
impl ScissorStack {
pub const fn is_out_of_bounds(&self) -> bool {
let boundary = &self.get().0;
boundary.width() < 0.0 || boundary.height() < 0.0
}
}