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"> <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_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_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>
<div gap="4"> <div gap="4">
<Button id="button_click_me" text="Click me" width="128" height="24" color="#FFFFFF" /> <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(); .unwrap();
if debug_draw_enabled { 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(); 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)); 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 { let (pin_left, pin_top, pin_align_items, pin_justify_content) = match params.info.side {
TooltipSide::Left => ( TooltipSide::Left => (
absolute_boundary.left() - spacing, absolute_boundary.left() - spacing - pin_size,
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::AlignItems::Center,
taffy::JustifyContent::End, taffy::JustifyContent::End,
), ),
TooltipSide::Right => ( TooltipSide::Right => (
absolute_boundary.left() + absolute_boundary.size.x + spacing, 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::AlignItems::Center,
taffy::JustifyContent::Start, taffy::JustifyContent::Start,
), ),
TooltipSide::Top => ( TooltipSide::Top => (
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() - spacing, absolute_boundary.top() - spacing - pin_size,
taffy::AlignItems::End, taffy::AlignItems::End,
taffy::JustifyContent::Center, taffy::JustifyContent::Center,
), ),
TooltipSide::Bottom => ( 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, absolute_boundary.top() + absolute_boundary.size.y + spacing,
taffy::AlignItems::Baseline, taffy::AlignItems::Baseline,
taffy::JustifyContent::Center, taffy::JustifyContent::Center,
@@ -173,8 +180,8 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul
}, },
/* important, to make it centered! */ /* important, to make it centered! */
size: taffy::Size { size: taffy::Size {
width: length(0.0), width: length(pin_size),
height: length(0.0), height: length(pin_size),
}, },
..Default::default() ..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( pub fn push_scissor_stack(
transform_stack: &mut TransformStack, transform_stack: &mut TransformStack,
scissor_stack: &mut ScissorStack, scissor_stack: &mut ScissorStack,
scroll_shift: Vec2, scroll_shift: Vec2,
info: &Option<ScrollbarInfo>, info: &Option<ScrollbarInfo>,
style: &taffy::Style, style: &taffy::Style,
) -> bool { ) -> PushScissorStackResult {
let scissor_pushed = info.is_some() && has_overflow_clip(style);
if !scissor_pushed {
return false;
}
let mut boundary_absolute = drawing::Boundary::construct_absolute(transform_stack); let mut boundary_absolute = drawing::Boundary::construct_absolute(transform_stack);
boundary_absolute.pos += scroll_shift; boundary_absolute.pos += scroll_shift;
let do_clip = info.is_some() && has_overflow_clip(style);
scissor_stack.push(ScissorBoundary(boundary_absolute)); 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( fn draw_widget(
@@ -313,6 +337,7 @@ fn draw_widget(
state: &mut DrawState, state: &mut DrawState,
node_id: taffy::NodeId, node_id: taffy::NodeId,
style: &taffy::Style, style: &taffy::Style,
internal: &mut DrawWidgetInternal,
widget: &Widget, widget: &Widget,
) { ) {
let Ok(l) = params.layout.state.tree.layout(node_id) else { 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 { if params.debug_draw {
let mut boundary_relative = drawing::Boundary::construct_relative(state.transform_stack); let mut boundary_relative = drawing::Boundary::construct_relative(state.transform_stack);
boundary_relative.pos += scroll_shift; boundary_relative.pos += scroll_shift;
@@ -358,10 +385,10 @@ fn draw_widget(
Color::new(1.0, 0.0, 1.0, 1.0), Color::new(1.0, 0.0, 1.0, 1.0),
)); ));
} }
state state
.primitives .primitives
.push(drawing::RenderPrimitive::ScissorSet(*state.scissor_stack.get())); .push(drawing::RenderPrimitive::ScissorSet(*state.scissor_stack.get()));
internal.scissor_set_count += 1;
} }
let draw_params = widget::DrawParams { let draw_params = widget::DrawParams {
@@ -370,12 +397,16 @@ fn draw_widget(
style, 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 { let current_scissor_set_count = internal.scissor_set_count;
state.scissor_stack.pop();
if current_scissor_set_count > starting_scissor_set_count {
state state
.primitives .primitives
.push(drawing::RenderPrimitive::ScissorSet(*state.scissor_stack.get())); .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; let layout = &params.layout;
for node_id in layout.state.tree.child_ids(parent_node_id) { 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; continue;
}; };
draw_widget(params, state, node_id, style, widget); draw_widget(params, state, node_id, style, internal, widget);
if is_topmost { if is_topmost {
state.primitives.push(RenderPrimitive::NewPass); state.primitives.push(RenderPrimitive::NewPass);
@@ -439,7 +476,9 @@ pub fn draw(params: &mut DrawParams) -> anyhow::Result<Vec<RenderPrimitive>> {
alterables: &mut alterables, 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)?; 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); 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.transform_stack,
&mut alterables.scissor_stack, &mut alterables.scissor_stack,
scroll_shift, scroll_shift,
@@ -450,24 +450,24 @@ impl Layout {
style, style,
); );
// check children first if scissor_result.should_display() {
self.push_event_children(node_id, event, event_result, alterables, user_data)?; // check children first
self.push_event_children(node_id, event, event_result, alterables, user_data)?;
if event_result.can_propagate() { if event_result.can_propagate() {
let mut params = EventParams { let mut params = EventParams {
state: &self.state, state: &self.state,
layout: l, layout: l,
alterables, alterables,
node_id, node_id,
style, 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(); alterables.transform_stack.pop();
Ok(()) Ok(())

View File

@@ -173,6 +173,7 @@ pub struct Context {
pub struct ContextDrawResult { pub struct ContextDrawResult {
pub pass_count: u32, pub pass_count: u32,
pub primitive_commands_count: u32,
} }
impl Context { impl Context {
@@ -239,7 +240,7 @@ impl Context {
let mut passes = Vec::<RendererPass>::new(); let mut passes = Vec::<RendererPass>::new();
let mut needs_new_pass = true; 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 { for primitive in primitives {
if needs_new_pass { if needs_new_pass {
@@ -247,7 +248,7 @@ impl Context {
&mut atlas.text_atlas, &mut atlas.text_atlas,
shared.rect_pipeline.clone(), shared.rect_pipeline.clone(),
shared.image_pipeline.clone(), shared.image_pipeline.clone(),
next_scissor, cur_scissor,
self.pixel_scale, self.pixel_scale,
)?); )?);
needs_new_pass = false; needs_new_pass = false;
@@ -309,14 +310,26 @@ impl Context {
.add_image(extent.boundary, image.clone(), &extent.transform); .add_image(extent.boundary, image.clone(), &extent.transform);
} }
drawing::RenderPrimitive::ScissorSet(boundary) => { drawing::RenderPrimitive::ScissorSet(boundary) => {
next_scissor = Some(boundary.0); let skip = if let Some(cur_scissor) = cur_scissor {
needs_new_pass = true; // 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 { let res = ContextDrawResult {
pass_count: passes.len() as u32, pass_count: passes.len() as u32,
primitive_commands_count: primitives.len() as u32,
}; };
for mut pass in passes { for mut pass in passes {

View File

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