Answer "have I asked this child?" in one read

A container's draw asked it once per child by searching the list of children
it had added so far, and four other per-child steps searched a list too, so
one draw cost the square of its children: 70% of a 1,600-child redraw was
those searches. A draw takes a DrawId and leaves it on every widget it asks
about; one note per widget is enough because the handle a container holds a
child by cannot be cloned. tests/children_cost.rs is the rig that shows it,
and it is the only one here that varies width: 3.680 ms to 0.811 ms at 1,600
children, and flat per child.

Beside it, the rest of the fourteenth sweep of #19: a mask's rectangle
resolved once per fragment instead of once per instance, which takes the
storage buffers out of the fragment stage and is 8.8x on a screenful of
deeply nested clips; TextBuffer::shape copying its attrs before the check
that would not need them, which allocated once per named-family text per
frame; a should_panic test on a debug assertion that made cargo test
--release fail; Fixed::div, reached only by its own test; Moves::remove
re-uploading an array it cannot have changed; and two comments the branch
itself falsified.

docs/LAYOUT_LOG.md has all eight with their measurements, the five things
looked at and left, and what was verified.
This commit is contained in:
iris-ai committed 2026-09-20 23:56:12 -04:00
1 parent cbccfb600a
commit 97fca76108
17 files changed
+588 -149

No files matched your search

+77 -34
View File
@@ -8,13 +8,15 @@
//! submitted and waited on, so this is the GPU's cost and not the recording
//! loop's -- which is what `draw_cost.rs` measures instead.
//!
//! The instances are two pixels wide so that vertex work dominates; a chain
//! walk that does not show up against small quads will not show up against
//! anything.
//! Two fixtures, because the walk happens in both stages. `chain_cost_by_depth`
//! draws instances two pixels wide so that vertex work dominates; a walk that
//! does not show up against small quads will not show up against anything.
//! `mask_cost_by_depth` draws one screenful through a mask instead, which is
//! where a walk in the fragment stage would show and nowhere else.
use iris::prelude::*;
use iris_core::{
Len, MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, UiData, UiRegion, UiRenderNode,
Len, Mask, MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, UiData, UiRegion, UiRenderNode,
UiRenderState, UiSpan,
};
use wgpu::{Color as GpuColor, *};
@@ -45,8 +47,17 @@ fn gpu() -> Option<(Device, Queue, f32)> {
Some((device, queue, period))
}
/// Which stage the fill puts the work in: many small quads, where a walk per
/// vertex is what shows, or one screenful of masked rows, where a walk per
/// fragment would.
#[derive(Clone, Copy)]
enum Fixture {
Quads,
Masked,
}
/// A chain `depth` slots long, and instances that all resolve through its end.
fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize, fixture: Fixture) {
let kind = ui.primitives.kind::<RectPrimitive>();
let id = ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id();
@@ -56,20 +67,51 @@ fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
}
let px = |v: f32| Len::px(v);
for i in 0..INSTANCES {
let x = (i % (SIZE as usize / 2)) as f32 * 2.0;
let y = (i / (SIZE as usize / 2)) as f32;
let rows = SIZE as usize;
let mask_idx = match fixture {
Fixture::Quads => MaskIdx::NONE,
// Its own chain as long as the instances', since a viewport sits as
// deep in the tree as the content it clips.
Fixture::Masked => {
let idx = ui.masks.push(Mask {
region: UiRegion::FULL,
move_idx: slot,
});
// Nothing frees it here, but the owner's reference is what a real
// one is kept alive by.
ui.masks.push_ref(idx);
idx
}
};
let instances = match fixture {
Fixture::Quads => INSTANCES,
Fixture::Masked => rows,
};
for i in 0..instances {
let region = match fixture {
Fixture::Quads => {
let x = (i % (rows / 2)) as f32 * 2.0;
let y = (i / (rows / 2)) as f32;
UiRegion::new(
UiSpan::new(px(x), px(x + 2.0)),
UiSpan::new(px(y), px(y + 1.0)),
)
}
// A full row each, so one screenful of fragments goes through the
// mask and the vertex stage is four corners per row.
Fixture::Masked => UiRegion::new(
UiSpan::new(px(0.0), px(SIZE as f32)),
UiSpan::new(px(i as f32), px(i as f32 + 1.0)),
),
};
render.layers.write(
0,
PrimitiveInst {
kind,
id,
primitive: RectPrimitive::color(UiColor::WHITE),
region: UiRegion::new(
UiSpan::new(px(x), px(x + 2.0)),
UiSpan::new(px(y), px(y + 1.0)),
),
mask_idx: MaskIdx::NONE,
region,
mask_idx,
move_idx: slot,
},
);
@@ -77,28 +119,15 @@ fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
}
/// Nanoseconds the pass took on the GPU, best of `BATCHES`.
fn pass_cost(device: &Device, queue: &Queue, period: f32, depth: usize) -> f64 {
fn pass_cost(device: &Device, queue: &Queue, period: f32, depth: usize, fixture: Fixture) -> f64 {
let format = TextureFormat::Bgra8Unorm;
let mut node = UiRenderNode::new(device, &gpu::config(format, SIZE));
let mut ui = UiData::default();
let mut render = UiRenderState::new();
fill(&mut ui, &mut render, depth);
fill(&mut ui, &mut render, depth, fixture);
node.update(device, queue, &mut ui, &mut render);
let target = device.create_texture(&TextureDescriptor {
label: Some("chain cost"),
size: Extent3d {
width: SIZE,
height: SIZE,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format,
usage: TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let target = gpu::target(device, format, SIZE, false);
let view = target.create_view(&TextureViewDescriptor::default());
let queries = device.create_query_set(&QuerySetDescriptor {
@@ -178,17 +207,15 @@ fn pass_cost(device: &Device, queue: &Queue, period: f32, depth: usize) -> f64 {
best
}
#[test]
#[ignore = "measurement, not a check"]
fn chain_cost_by_depth() {
fn by_depth(fixture: Fixture, instances: usize) {
let Some((device, queue, period)) = gpu() else {
println!("no gpu with timestamps; nothing measured");
return;
};
println!("{INSTANCES} instances, {SIZE}x{SIZE}, best of {BATCHES} batches");
println!("{instances} instances, {SIZE}x{SIZE}, best of {BATCHES} batches");
let mut base = None;
for depth in [1, 2, 4, 8, 16, 32, 64] {
let ns = pass_cost(&device, &queue, period, depth);
let ns = pass_cost(&device, &queue, period, depth, fixture);
let base = *base.get_or_insert(ns);
println!(
"depth {depth:>3}: {:>9.1} us {:+6.1}% against depth 1",
@@ -197,3 +224,19 @@ fn chain_cost_by_depth() {
);
}
}
#[test]
#[ignore = "measurement, not a check"]
fn chain_cost_by_depth() {
by_depth(Fixture::Quads, INSTANCES);
}
/// One screenful of rows, every one clipped by a mask whose own chain is that
/// deep. What this says that the quads cannot is whether a mask costs the walk
/// once per instance or once per fragment: at a screenful of fragments per
/// chain, the second is the difference between these two tables.
#[test]
#[ignore = "measurement, not a check"]
fn mask_cost_by_depth() {
by_depth(Fixture::Masked, SIZE as usize);
}