An eighth sweep, over the part no earlier round named: the 6,300 lines of
tests, and once more over the seventh sweep's own commit, which was itself
unreviewed.
Four of the shrunk fuzz fixtures name one widget two or three times. `width`,
`sized` and `align` set a rule on the widget they are given and return its own
id -- only `pad` and `wrapper` make a new one -- so `let sized =
wrapped.width(76).add(..)` and the `let aligned = sized` beside it are three
names for one text. Each name then went into the list of ids the case compares
warm against cold, so a case that says it checks six boxes checks four, and
three doc comments quote that inflated count as the size of the tree the
shrinker reduced to. Measured: `plant` and `plant_fixed` list 6 and hold 4,
`plant_pair` lists 4 and holds 3, `plant_scrolled` lists 8 and holds 7. The
aliases are gone and the counts say what the fixtures build; each rebuilt
fixture was diffed against the old one, and both the widget slots and every
region are identical, for both settings of `swapped`.
`assert_same_regions` sits at the top of `unsettled.rs` and six tests call it.
Seven more spell its body out instead, byte for byte. They call it now, and it
is `#[track_caller]` so the panic names the case.
`tests/gpu/mod.rs` holds the adapter probe and the surface configuration that
`draw_cost` and `chain_cost` had a copy of each -- `config` identical, and the
probe identical but for the feature it asks for. The leak's justification lived
in one file with the other referring to it; it now sits on the thing it is
about. Shared through `#[path]`, the way `scenario/mod.rs` already is.
The mask a widget is clipped by was resolved in three places, two of them a
byte-identical closure. `mask_bounds` takes the slot rather than the widget,
because the third site deliberately reads the slot it saved before the frame:
that a redraw keeps the slot is what it is checking.
`Layered::_revision` was a field nothing reads, incremented to mark the widget
dirty. Two tests in the same file already do that with
`get_dyn_mut`, which is what the underscore was hiding.
`plan.rs` claimed every simplification is strictly smaller, and asserted `<=`.
Measured: 53 of one tree's 101 simplifications keep the widget count, since a
dropped alignment and a simpler leaf both do. The assertion is right and the
claim was not; the comment now gives the argument that does hold.
`generated.rs` said "Seven that have never failed" and "the nine the others
check" of a ten-seed array. The `should_panic` scroll test ended in an
`h.frame()` that cannot run, since `set_root` lays out and is where the panic
comes from. Two `drop(tree)` at the end of their own scope did nothing.
Format, clippy with and without layout-diagnostics, and the suite (131 + 19 +
13 + 4) are clean. The cold dump over 400 depth-5 trees is byte-identical to
f8aa0c5 across all 34,490 boxes. No library code changed, so the seed scans
have nothing to find. Both GPU rigs were rebuilt and run: chain cost +470% at
depth 64, draw cost ~4.4 us per layer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
180 lines
6.2 KiB
Rust
180 lines
6.2 KiB
Rust
//! What one frame of `UiRenderNode::draw` costs on the CPU, against the number
|
|
//! of layers it walks. Recording only: the pass is built and dropped without
|
|
//! being submitted, so this is the loop's cost and not the GPU's.
|
|
//!
|
|
//! cargo test --release --test draw_cost -- --ignored --nocapture
|
|
//!
|
|
//! Wall time is the wrong number to read for anything under a few percent --
|
|
//! it varied by 2x between runs of one unchanged binary where instructions
|
|
//! retired varied by 0.1%. Count those instead:
|
|
//!
|
|
//! perf stat -e instructions:u target/release/.../draw_cost-* --ignored
|
|
//!
|
|
//! That is how `PrimitiveRender` was measured against a match in the renderer:
|
|
//! 6 instructions per list drawn, against the ~5,400 wgpu spends recording
|
|
//! one.
|
|
|
|
use std::time::Instant;
|
|
|
|
use iris::prelude::*;
|
|
use iris_core::{
|
|
GlyphPrimitive, MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, TextureHandle,
|
|
TexturePrimitive, UiData, UiRegion, UiRenderNode, UiRenderState,
|
|
};
|
|
use wgpu::{Color as GpuColor, *};
|
|
|
|
#[path = "gpu/mod.rs"]
|
|
mod gpu;
|
|
|
|
const SIZE: u32 = 1024;
|
|
const FRAMES: u32 = 200;
|
|
/// Reported as the best of this many batches, since the mean moves by more
|
|
/// than the thing being measured.
|
|
const BATCHES: u32 = 8;
|
|
|
|
fn gpu() -> Option<(Device, Queue)> {
|
|
let adapter = gpu::adapter()?;
|
|
println!("adapter: {:?}", adapter.get_info());
|
|
pollster::block_on(adapter.request_device(&DeviceDescriptor::default())).ok()
|
|
}
|
|
|
|
/// Every layer draws all three primitives, so the renderer takes a different
|
|
/// path for each list it walks -- which is the case a single-primitive layer
|
|
/// would never exercise. Images are bound per instance, so there are few.
|
|
fn fill(
|
|
ui: &mut UiData,
|
|
render: &mut UiRenderState,
|
|
layers: usize,
|
|
per_layer: usize,
|
|
) -> Vec<TextureHandle> {
|
|
let rect = ui.primitives.kind::<RectPrimitive>();
|
|
let glyph = ui.primitives.kind::<GlyphPrimitive>();
|
|
let texture = ui.primitives.kind::<TexturePrimitive>();
|
|
let id = ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id();
|
|
let handles: Vec<_> = (0..4)
|
|
.map(|_| ui.textures.add(image::RgbaImage::new(4, 4)))
|
|
.collect();
|
|
|
|
let mut layer = 0;
|
|
for _ in 0..layers {
|
|
for _ in 0..per_layer {
|
|
render.layers.write(
|
|
layer,
|
|
PrimitiveInst {
|
|
kind: rect,
|
|
id,
|
|
primitive: RectPrimitive::color(UiColor::WHITE),
|
|
region: UiRegion::FULL,
|
|
mask_idx: MaskIdx::NONE,
|
|
move_idx: MoveIdx::NONE,
|
|
},
|
|
);
|
|
render.layers.write(
|
|
layer,
|
|
PrimitiveInst {
|
|
kind: glyph,
|
|
id,
|
|
primitive: GlyphPrimitive {
|
|
uv_min: vec2(0.0, 0.0),
|
|
uv_max: vec2(1.0, 1.0),
|
|
layer: 0,
|
|
color: UiColor::WHITE,
|
|
flags: 0,
|
|
},
|
|
region: UiRegion::FULL,
|
|
mask_idx: MaskIdx::NONE,
|
|
move_idx: MoveIdx::NONE,
|
|
},
|
|
);
|
|
}
|
|
for h in &handles[..2] {
|
|
render.layers.write(
|
|
layer,
|
|
PrimitiveInst {
|
|
kind: texture,
|
|
id,
|
|
primitive: TexturePrimitive::from(h),
|
|
region: UiRegion::FULL,
|
|
mask_idx: MaskIdx::NONE,
|
|
move_idx: MoveIdx::NONE,
|
|
},
|
|
);
|
|
}
|
|
layer = render.layers.next(layer);
|
|
}
|
|
handles
|
|
}
|
|
|
|
fn frame_cost(device: &Device, queue: &Queue, layers: usize, per_layer: usize) -> 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();
|
|
let _handles = fill(&mut ui, &mut render, layers, per_layer);
|
|
node.update(device, queue, &mut ui, &mut render);
|
|
|
|
let target = device.create_texture(&TextureDescriptor {
|
|
label: Some("draw 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 view = target.create_view(&TextureViewDescriptor::default());
|
|
|
|
let record = |frames: u32| {
|
|
let start = Instant::now();
|
|
for _ in 0..frames {
|
|
let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor::default());
|
|
{
|
|
let pass = &mut encoder.begin_render_pass(&RenderPassDescriptor {
|
|
color_attachments: &[Some(RenderPassColorAttachment {
|
|
view: &view,
|
|
resolve_target: None,
|
|
ops: Operations {
|
|
load: LoadOp::Clear(GpuColor::BLACK),
|
|
store: StoreOp::Store,
|
|
},
|
|
depth_slice: None,
|
|
})],
|
|
..Default::default()
|
|
});
|
|
node.draw(pass);
|
|
}
|
|
drop(encoder.finish());
|
|
}
|
|
start.elapsed().as_secs_f64() / frames as f64
|
|
};
|
|
record(FRAMES / 4);
|
|
(0..BATCHES)
|
|
.map(|_| record(FRAMES))
|
|
.fold(f64::MAX, f64::min)
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "measurement, not a check"]
|
|
fn draw_cost_by_layer_count() {
|
|
let Some((device, queue)) = gpu() else {
|
|
panic!("no wgpu device; see the this-machine-graphics notes");
|
|
};
|
|
println!(
|
|
"layers, each 8 rects + 8 glyphs + 2 images: us/frame (us per layer), best of {BATCHES}"
|
|
);
|
|
let base = frame_cost(&device, &queue, 1, 8) * 1e6;
|
|
for layers in [8, 64, 256, 1024] {
|
|
let per_frame = frame_cost(&device, &queue, layers, 8) * 1e6;
|
|
// Net of the empty pass, which is the same in any version of this.
|
|
println!(
|
|
"{layers:>5}: {per_frame:8.1} us ({:.3} us)",
|
|
(per_frame - base).max(0.0) / layers as f64
|
|
);
|
|
}
|
|
}
|