Say how many widgets a shrunk fixture has, and share what tests repeat

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>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-20 03:18:18 -04:00
1 parent f8aa0c5cdf
commit 77ed7a24c0
10 files changed
+125 -230

No files matched your search

+6 -4
View File
@@ -70,11 +70,13 @@ fn editing_a_plan_is_growing_one_with_those_edits() {
} }
} }
/// Every simplification is strictly smaller, so taking them in turn reaches a /// No simplification is larger, which is the half of "the shrinker stops" a
/// fixed point instead of circling. A shrinker that can return to a tree it /// widget count can see. Most are not smaller either -- a dropped alignment
/// has already tried does not stop. /// and a simpler leaf both keep the count -- so what rules out circling is
/// that those are one-way too: a `Some` becomes a `None`, and a kind steps
/// down a ladder with no way back up.
#[test] #[test]
fn every_simplification_of_a_plan_is_smaller_than_it() { fn no_simplification_of_a_plan_is_larger_than_it() {
for seed in 1..=60 { for seed in 1..=60 {
let tree = plan(seed, 4, &Edits::default()); let tree = plan(seed, 4, &Edits::default());
let mut queue = vec![tree]; let mut queue = vec![tree];
+22 -32
View File
@@ -45,7 +45,6 @@ fn counted(h: &mut Harness, size: Size, reads_box: bool) -> (WeakWidget<Counted>
struct Layered { struct Layered {
children: [StrongWidget<Rect>; 2], children: [StrongWidget<Rect>; 2],
_revision: usize,
} }
impl Widget for Layered { impl Widget for Layered {
@@ -65,14 +64,10 @@ fn a_redrawn_layered_widget_keeps_the_layer_it_was_entered_on() {
rect(Color::RED).add_strong(&mut h.rsc), rect(Color::RED).add_strong(&mut h.rsc),
rect(Color::BLUE).add_strong(&mut h.rsc), rect(Color::BLUE).add_strong(&mut h.rsc),
]; ];
let root = Layered { let root = Layered { children }.add(&mut h.rsc);
children,
_revision: 0,
}
.add(&mut h.rsc);
h.set_root(root); h.set_root(root);
h.rsc[root]._revision += 1; h.rsc.widgets_mut().get_dyn_mut(root.id());
h.frame(); h.frame();
let label = h.rsc.widgets().label(root.id()); let label = h.rsc.widgets().label(root.id());
@@ -774,6 +769,17 @@ fn a_subtree_that_changed_parents_settles_at_the_depth_it_moved_to() {
); );
} }
/// Where a mask slot clips, in window pixels: the region it holds, carried
/// through whatever move entry it hangs from. Taken by slot rather than by
/// widget, so a test can name the slot it expects a redraw to keep.
fn mask_bounds(h: &Harness, mask: MaskIdx) -> PixelRegion {
let mask = &h.rsc.ui().masks[mask.idx()];
h.render
.moves
.resolve(mask.move_idx, mask.region)
.to_px(h.render.output_size())
}
fn primitive_bounds(h: &Harness, id: WidgetId) -> Vec<PixelRegion> { fn primitive_bounds(h: &Harness, id: WidgetId) -> Vec<PixelRegion> {
h.render.active[&id] h.render.active[&id]
.primitives .primitives
@@ -919,15 +925,10 @@ fn resizing_a_fixed_frame_recomposes_its_contents_without_drawing_them() {
primitive_bounds(&warm, leaf.id()), primitive_bounds(&warm, leaf.id()),
primitive_bounds(&cold, other.id()) primitive_bounds(&cold, other.id())
); );
let mask = |h: &Harness, id: WidgetId| { assert_eq!(
let active = &h.render.active[&id]; mask_bounds(&warm, warm.render.active[&leaf.id()].mask),
let mask = &h.rsc.ui().masks[active.mask.idx()]; mask_bounds(&cold, cold.render.active[&other.id()].mask)
h.render );
.moves
.resolve(mask.move_idx, mask.region)
.to_px(h.render.output_size())
};
assert_eq!(mask(&warm, leaf.id()), mask(&cold, other.id()));
} }
} }
@@ -1184,15 +1185,10 @@ fn padding_and_stack_boxes_follow_the_region_without_drawing_again() {
assert_eq!(warm.region(&a), cold.region(&b)); assert_eq!(warm.region(&a), cold.region(&b));
assert_eq!(primitive_bounds(&warm, a), primitive_bounds(&cold, b)); assert_eq!(primitive_bounds(&warm, a), primitive_bounds(&cold, b));
} }
let mask = |h: &Harness, id: WidgetId| { assert_eq!(
let active = &h.render.active[&id]; mask_bounds(&warm, warm.render.active[&leaf.id()].mask),
let mask = &h.rsc.ui().masks[active.mask.idx()]; mask_bounds(&cold, cold.render.active[&other.id()].mask)
h.render );
.moves
.resolve(mask.move_idx, mask.region)
.to_px(h.render.output_size())
};
assert_eq!(mask(&warm, leaf.id()), mask(&cold, other.id()));
} }
} }
} }
@@ -1381,13 +1377,7 @@ fn a_redrawn_mask_keeps_reused_primitives_clipped_when_it_moves() {
assert_eq!(h.render.active[&masked.id()].mask, mask); assert_eq!(h.render.active[&masked.id()].mask, mask);
h.set_len(first, Axis::Y, 10); h.set_len(first, Axis::Y, 10);
h.frame(); h.frame();
let clip = h.rsc.ui().masks[mask.idx()]; assert_eq!(mask_bounds(&h, mask), h.region(&masked).unwrap());
let clip = h
.render
.moves
.resolve(clip.move_idx, clip.region)
.to_px(h.render.output_size());
assert_eq!(clip, h.region(&masked).unwrap());
assert_corners!(h, inner, (0, 10), (400, 200)); assert_corners!(h, inner, (0, 10), (400, 200));
} }
} }
+1 -1
View File
@@ -142,8 +142,8 @@ fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
let mut h = Harness::new((100, 100)); let mut h = Harness::new((100, 100));
let tall = rect(Color::RED).height(400).add_strong(&mut h.rsc); let tall = rect(Color::RED).height(400).add_strong(&mut h.rsc);
let clipper = Clipper(tall).add(&mut h.rsc); let clipper = Clipper(tall).add(&mut h.rsc);
// `set_root` lays the tree out, so this is where it is caught.
h.set_root(clipper); h.set_root(clipper);
h.frame();
} }
/// Content that fits sits in the viewport, not in a box of the window's /// Content that fits sits in the viewport, not in a box of the window's
+30 -98
View File
@@ -14,6 +14,9 @@ use iris::harness::Harness;
use iris::prelude::*; use iris::prelude::*;
use iris::random::Branch; use iris::random::Branch;
/// Every widget in the same place warm as cold, reported all at once: which
/// of a dozen boxes moved is the whole of what a shrunk case has to say.
#[track_caller]
fn assert_same_regions( fn assert_same_regions(
warm: &Harness, warm: &Harness,
warm_ids: &[WidgetId], warm_ids: &[WidgetId],
@@ -188,36 +191,31 @@ fn reordering_nested_spans_keeps_the_answer_from_the_decided_box() {
assert_same_regions(&warm, &ids, &cold, &cold_ids); assert_same_regions(&warm, &ids, &cold, &cold_ids);
} }
/// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about /// Four widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about
/// the tree changes -- every widget is marked for redraw and the frame is /// the tree changes -- every widget is marked for redraw and the frame is
/// taken again -- so no box may move, and a warm frame has to land where a /// taken again -- so no box may move, and a warm frame has to land where a
/// cold one does. /// cold one does.
fn plant(h: &mut Harness) -> Vec<WidgetId> { fn plant(h: &mut Harness) -> Vec<WidgetId> {
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc); let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc); let wrapped = wtext("Wrapping shapes")
let sized = wrapped.width(76).add(&mut h.rsc); .size(16)
let aligned = sized; .wrap(true)
.width(76)
.add(&mut h.rsc);
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_alignment(sized, Axis::X, AxisAlign::POS); .set_alignment(wrapped, Axis::X, AxisAlign::POS);
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_alignment(sized, Axis::Y, AxisAlign::POS); .set_alignment(wrapped, Axis::Y, AxisAlign::POS);
let stack = Stack { let stack = Stack {
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)], children: vec![plain.add_strong(&mut h.rsc), wrapped.add_strong(&mut h.rsc)],
size: StackSize::Child(0), size: StackSize::Child(0),
} }
.add(&mut h.rsc); .add(&mut h.rsc);
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc); let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root(root); h.set_root(root);
vec![ vec![plain.id(), wrapped.id(), stack.id(), root.id()]
plain.id(),
wrapped.id(),
sized.id(),
aligned.id(),
stack.id(),
root.id(),
]
} }
/// The first frame does not reach the layout a second one does, so "cold" is /// The first frame does not reach the layout a second one does, so "cold" is
@@ -259,39 +257,23 @@ fn repainting_everything_moves_nothing() {
let mut cold = Harness::new((640, 900)); let mut cold = Harness::new((640, 900));
let cold_ids = plant(&mut cold); let cold_ids = plant(&mut cold);
let mut wrong = Vec::new(); assert_same_regions(&warm, &ids, &cold, &cold_ids);
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
} }
/// Six widgets, shrunk from 905. Everything inside the declared 189x176 box /// Four widgets, shrunk from 905. Everything inside the declared 189x176 box
/// is the same size whatever the output is, so a resize may not change any of /// is the same size whatever the output is, so a resize may not change any of
/// it -- but the text comes out 3.92px narrower warm than cold. /// it -- but the text comes out 3.92px narrower warm than cold.
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> { fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
let words = "Wrapping shapes one source into as many lines as the box leaves"; let words = "Wrapping shapes one source into as many lines as the box leaves";
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc); let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
let aligned = text;
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_alignment(text, Axis::X, AxisAlign::NEG); .set_alignment(text, Axis::X, AxisAlign::NEG);
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc); let inner = (text,).span(Dir::RIGHT).sized((189, 176)).add(&mut h.rsc);
let sized = inner.sized((189, 176)).add(&mut h.rsc);
let filler = rect(Color::RED).add(&mut h.rsc); let filler = rect(Color::RED).add(&mut h.rsc);
let root = (filler, sized).span(Dir::RIGHT).add(&mut h.rsc); let root = (filler, inner).span(Dir::RIGHT).add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc)); h.state.root = Some(root.add_strong(&mut h.rsc));
vec![ vec![text.id(), inner.id(), filler.id(), root.id()]
text.id(),
aligned.id(),
inner.id(),
sized.id(),
filler.id(),
root.id(),
]
} }
#[test] #[test]
@@ -306,17 +288,10 @@ fn a_resize_does_not_reach_inside_a_box_of_declared_pixels() {
let cold_ids = plant_fixed(&mut cold); let cold_ids = plant_fixed(&mut cold);
cold.frame(); cold.frame();
let mut wrong = Vec::new(); assert_same_regions(&warm, &ids, &cold, &cold_ids);
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
} }
/// Four widgets, shrunk from 486. A span's two children are swapped: warm by /// Three widgets, shrunk from 486. A span's two children are swapped: warm by
/// moving them, cold by growing them that way. Same widgets, same sizes, one /// moving them, cold by growing them that way. Same widgets, same sizes, one
/// ends up 29.9px from where the other does. /// ends up 29.9px from where the other does.
fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span>) { fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span>) {
@@ -340,16 +315,11 @@ fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span
gap: Px::ZERO, gap: Px::ZERO,
} }
.add(&mut h.rsc); .add(&mut h.rsc);
let span_handle = span;
let aligned = span;
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_alignment(span, Axis::X, AxisAlign::CENTER); .set_alignment(span, Axis::X, AxisAlign::CENTER);
h.state.root = Some(aligned.add_strong(&mut h.rsc)); h.state.root = Some(span.add_strong(&mut h.rsc));
( (vec![wrapped.id(), plain.id(), span.id()], span)
vec![wrapped.id(), plain.id(), span.id(), aligned.id()],
span_handle,
)
} }
#[test] #[test]
@@ -364,17 +334,10 @@ fn swapping_two_children_lands_where_growing_them_that_way_does() {
let (cold_ids, _) = plant_pair(&mut cold, true); let (cold_ids, _) = plant_pair(&mut cold, true);
cold.frame(); cold.frame();
let mut wrong = Vec::new(); assert_same_regions(&warm, &ids, &cold, &cold_ids);
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
} }
/// Eight widgets, shrunk from 80. The scroll decides how wide to make its /// Seven widgets, shrunk from 80. The scroll decides how wide to make its
/// content from what the content says, and hands that box down through a /// content from what the content says, and hands that box down through a
/// pass-through; the span under it was given that box once, so nothing at its /// pass-through; the span under it was given that box once, so nothing at its
/// own edge says the box was its own answer. /// own edge says the box was its own answer.
@@ -393,8 +356,7 @@ fn plant_scrolled(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget
gap: Px::ZERO, gap: Px::ZERO,
} }
.add(&mut h.rsc); .add(&mut h.rsc);
let block = rect(Color::RED).add(&mut h.rsc); let fixed = rect(Color::RED).width(87).add(&mut h.rsc);
let fixed = block.width(87).add(&mut h.rsc);
let mut outer_children: Vec<StrongWidget> = let mut outer_children: Vec<StrongWidget> =
vec![fixed.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)]; vec![fixed.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)];
if swapped { if swapped {
@@ -416,7 +378,6 @@ fn plant_scrolled(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget
text.id(), text.id(),
filler.id(), filler.id(),
inner.id(), inner.id(),
block.id(),
fixed.id(), fixed.id(),
outer.id(), outer.id(),
through.id(), through.id(),
@@ -440,14 +401,7 @@ fn a_span_given_the_box_its_answer_decided_matches_a_cold_layout() {
let (cold_ids, _) = plant_scrolled(&mut cold, true); let (cold_ids, _) = plant_scrolled(&mut cold, true);
cold.frame(); cold.frame();
let mut wrong = Vec::new(); assert_same_regions(&warm, &ids, &cold, &cold_ids);
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
} }
/// Reports a width derived from the box it is asked in. Reading through the /// Reports a width derived from the box it is asked in. Reading through the
@@ -471,11 +425,10 @@ impl Widget for Wider {
fn plant_wider(h: &mut Harness, extra: f32) -> (WeakWidget<Wider>, WidgetId) { fn plant_wider(h: &mut Harness, extra: f32) -> (WeakWidget<Wider>, WidgetId) {
let content = Wider { extra }.add(&mut h.rsc); let content = Wider { extra }.add(&mut h.rsc);
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc); let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
let root = scroll;
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_alignment(scroll, Axis::X, AxisAlign::NEG); .set_alignment(scroll, Axis::X, AxisAlign::NEG);
h.set_root(root); h.set_root(scroll);
(content, scroll.id()) (content, scroll.id())
} }
@@ -567,14 +520,7 @@ fn a_box_that_only_rounds_past_its_fixed_children_leaves_nothing_over() {
let (cold_ids, _) = plant_boundary(&mut cold, true); let (cold_ids, _) = plant_boundary(&mut cold, true);
cold.frame(); cold.frame();
let mut wrong = Vec::new(); assert_same_regions(&warm, &ids, &cold, &cold_ids);
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
} }
/// Five widgets, shrunk by `tests/shrink.rs` from the 277 the oracle's seed /// Five widgets, shrunk by `tests/shrink.rs` from the 277 the oracle's seed
@@ -622,14 +568,7 @@ fn redrawing_one_widget_does_not_move_what_scrolls_around_it() {
let mut cold = Harness::new((900, 1200)); let mut cold = Harness::new((900, 1200));
let cold_ids = plant_nested_scrolls(&mut cold); let cold_ids = plant_nested_scrolls(&mut cold);
let mut wrong = Vec::new(); assert_same_regions(&warm, &ids, &cold, &cold_ids);
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
} }
/// Ten widgets, of the shape `tests/shrink.rs` reduces the oracle's seed 220 /// Ten widgets, of the shape `tests/shrink.rs` reduces the oracle's seed 220
@@ -720,14 +659,7 @@ fn a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered() {
let (cold_ids, _) = plant_under_a_node(&mut cold, true); let (cold_ids, _) = plant_under_a_node(&mut cold, true);
cold.frame(); cold.frame();
let mut wrong = Vec::new(); assert_same_regions(&warm, &ids, &cold, &cold_ids);
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
} }
const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the \ const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the \
+5 -29
View File
@@ -11,8 +11,6 @@
//! The instances are two pixels wide so that vertex work dominates; a chain //! 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 //! walk that does not show up against small quads will not show up against
//! anything. //! anything.
//!
//! The instance is leaked deliberately, for the reason `draw_cost.rs` gives.
use iris::prelude::*; use iris::prelude::*;
use iris_core::{ use iris_core::{
@@ -21,6 +19,9 @@ use iris_core::{
}; };
use wgpu::{Color as GpuColor, *}; use wgpu::{Color as GpuColor, *};
#[path = "gpu/mod.rs"]
mod gpu;
const SIZE: u32 = 1024; const SIZE: u32 = 1024;
const INSTANCES: usize = 200_000; const INSTANCES: usize = 200_000;
const FRAMES: u32 = 20; const FRAMES: u32 = 20;
@@ -29,18 +30,7 @@ const FRAMES: u32 = 20;
const BATCHES: u32 = 8; const BATCHES: u32 = 8;
fn gpu() -> Option<(Device, Queue, f32)> { fn gpu() -> Option<(Device, Queue, f32)> {
let all = Instance::new(InstanceDescriptor::new_without_display_handle()); let adapter = gpu::adapter()?;
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
{
Ok(_) => all,
Err(_) => Instance::new(InstanceDescriptor {
backends: Backends::GL,
..InstanceDescriptor::new_without_display_handle()
}),
};
let instance: &'static Instance = Box::leak(Box::new(instance));
let adapter =
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
if !adapter.features().contains(Features::TIMESTAMP_QUERY) { if !adapter.features().contains(Features::TIMESTAMP_QUERY) {
println!("no timestamp queries on {:?}", adapter.get_info().name); println!("no timestamp queries on {:?}", adapter.get_info().name);
return None; return None;
@@ -55,20 +45,6 @@ fn gpu() -> Option<(Device, Queue, f32)> {
Some((device, queue, period)) Some((device, queue, period))
} }
fn config(format: TextureFormat) -> SurfaceConfiguration {
SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format,
color_space: SurfaceColorSpace::Auto,
width: SIZE,
height: SIZE,
present_mode: PresentMode::Fifo,
desired_maximum_frame_latency: 2,
alpha_mode: CompositeAlphaMode::Auto,
view_formats: vec![],
}
}
/// A chain `depth` slots long, and instances that all resolve through its end. /// 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) {
let kind = ui.primitives.kind::<RectPrimitive>(); let kind = ui.primitives.kind::<RectPrimitive>();
@@ -103,7 +79,7 @@ fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
/// Nanoseconds the pass took on the GPU, best of `BATCHES`. /// 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) -> f64 {
let format = TextureFormat::Bgra8Unorm; let format = TextureFormat::Bgra8Unorm;
let mut node = UiRenderNode::new(device, &config(format)); let mut node = UiRenderNode::new(device, &gpu::config(format, SIZE));
let mut ui = UiData::default(); let mut ui = UiData::default();
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
fill(&mut ui, &mut render, depth); fill(&mut ui, &mut render, depth);
+5 -34
View File
@@ -13,10 +13,6 @@
//! That is how `PrimitiveRender` was measured against a match in the renderer: //! That is how `PrimitiveRender` was measured against a match in the renderer:
//! 6 instructions per list drawn, against the ~5,400 wgpu spends recording //! 6 instructions per list drawn, against the ~5,400 wgpu spends recording
//! one. //! one.
//!
//! The instance is leaked deliberately. A Vulkan loader may unload the driver
//! when the last one drops, which can fault as a thread that used it exits --
//! and every test runs on a spawned thread.
use std::time::Instant; use std::time::Instant;
@@ -27,6 +23,9 @@ use iris_core::{
}; };
use wgpu::{Color as GpuColor, *}; use wgpu::{Color as GpuColor, *};
#[path = "gpu/mod.rs"]
mod gpu;
const SIZE: u32 = 1024; const SIZE: u32 = 1024;
const FRAMES: u32 = 200; const FRAMES: u32 = 200;
/// Reported as the best of this many batches, since the mean moves by more /// Reported as the best of this many batches, since the mean moves by more
@@ -34,39 +33,11 @@ const FRAMES: u32 = 200;
const BATCHES: u32 = 8; const BATCHES: u32 = 8;
fn gpu() -> Option<(Device, Queue)> { fn gpu() -> Option<(Device, Queue)> {
// Probed rather than assumed: there may be no Vulkan adapter, and GL is let adapter = gpu::adapter()?;
// what is left when there is not.
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
{
Ok(_) => all,
Err(_) => Instance::new(InstanceDescriptor {
backends: Backends::GL,
..InstanceDescriptor::new_without_display_handle()
}),
};
// Leaked rather than dropped: see the note at the top of the file.
let instance: &'static Instance = Box::leak(Box::new(instance));
let adapter =
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
println!("adapter: {:?}", adapter.get_info()); println!("adapter: {:?}", adapter.get_info());
pollster::block_on(adapter.request_device(&DeviceDescriptor::default())).ok() pollster::block_on(adapter.request_device(&DeviceDescriptor::default())).ok()
} }
fn config(format: TextureFormat) -> SurfaceConfiguration {
SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format,
color_space: SurfaceColorSpace::Auto,
width: SIZE,
height: SIZE,
present_mode: PresentMode::Fifo,
desired_maximum_frame_latency: 2,
alpha_mode: CompositeAlphaMode::Auto,
view_formats: vec![],
}
}
/// Every layer draws all three primitives, so the renderer takes a different /// 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 /// 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. /// would never exercise. Images are bound per instance, so there are few.
@@ -136,7 +107,7 @@ fn fill(
fn frame_cost(device: &Device, queue: &Queue, layers: usize, per_layer: usize) -> f64 { fn frame_cost(device: &Device, queue: &Queue, layers: usize, per_layer: usize) -> f64 {
let format = TextureFormat::Bgra8Unorm; let format = TextureFormat::Bgra8Unorm;
let mut node = UiRenderNode::new(device, &config(format)); let mut node = UiRenderNode::new(device, &gpu::config(format, SIZE));
let mut ui = UiData::default(); let mut ui = UiData::default();
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
let _handles = fill(&mut ui, &mut render, layers, per_layer); let _handles = fill(&mut ui, &mut render, layers, per_layer);
+2 -2
View File
@@ -25,7 +25,7 @@ fn depth() -> usize {
env("IRIS_GENERATED_DEPTH", 4) env("IRIS_GENERATED_DEPTH", 4)
} }
/// The seeds the ordinary tests take. Seven that have never failed; 86, /// The seeds the ordinary tests take. Eight that have never failed; 86,
/// which a `Scroll` fixed point once settled differently on; and 20, which /// which a `Scroll` fixed point once settled differently on; and 20, which
/// caught a locally redrawn widget being placed twice in the box its parent /// caught a locally redrawn widget being placed twice in the box its parent
/// had already placed it in. /// had already placed it in.
@@ -104,7 +104,7 @@ fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
} }
#[test] #[test]
#[ignore = "as many seeds as it is asked for, rather than the nine the others check"] #[ignore = "as many seeds as it is asked for, rather than the ten the others check"]
fn a_long_run_of_seeds_agrees() { fn a_long_run_of_seeds_agrees() {
let depth = depth(); let depth = depth();
let seeds: Vec<u64> = match std::env::var("IRIS_GENERATED_SEED") let seeds: Vec<u64> = match std::env::var("IRIS_GENERATED_SEED")
+40
View File
@@ -0,0 +1,40 @@
//! The adapter and the surface configuration the GPU measurement rigs share,
//! so the two cannot probe for a device in two different ways.
use wgpu::*;
/// An adapter on whatever this machine has, or `None` where there is none.
///
/// Probed rather than assumed: there may be no Vulkan adapter, and GL is what
/// is left when there is not.
///
/// The instance is leaked deliberately. A Vulkan loader may unload the driver
/// when the last one drops, which can fault as a thread that used it exits --
/// and every test runs on a spawned thread.
pub fn adapter() -> Option<Adapter> {
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
{
Ok(_) => all,
Err(_) => Instance::new(InstanceDescriptor {
backends: Backends::GL,
..InstanceDescriptor::new_without_display_handle()
}),
};
let instance: &'static Instance = Box::leak(Box::new(instance));
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()
}
pub fn config(format: TextureFormat, size: u32) -> SurfaceConfiguration {
SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format,
color_space: SurfaceColorSpace::Auto,
width: size,
height: size,
present_mode: PresentMode::Fifo,
desired_maximum_frame_latency: 2,
alpha_mode: CompositeAlphaMode::Auto,
view_formats: vec![],
}
}
-2
View File
@@ -219,7 +219,6 @@ fn layout_cost() {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
let _ = iris::core::layout_diagnostics::take(); let _ = iris::core::layout_diagnostics::take();
run("cold", 1, &mut harness, |_, _| {}); run("cold", 1, &mut harness, |_, _| {});
drop(tree);
} }
if selected("repaint") { if selected("repaint") {
@@ -275,6 +274,5 @@ fn layout_cost() {
run("resize", frames, &mut harness, |harness, frame| { run("resize", frames, &mut harness, |harness, frame| {
harness.resize((OUTPUT.0 - ((frame + 1) % 2) as f32 * 8.0, OUTPUT.1)); harness.resize((OUTPUT.0 - ((frame + 1) % 2) as f32 * 8.0, OUTPUT.1));
}); });
drop(tree);
} }
} }
+14 -28
View File
@@ -1,5 +1,5 @@
//! Traces the six-widget tree in `unsettled.rs`, to see what box its text is //! Traces the four-widget trees in `unsettled.rs`, to see what box their text
//! actually drawn in on a first frame against a settled one. //! is actually drawn in on a first frame against a settled one.
#![cfg(feature = "layout-diagnostics")] #![cfg(feature = "layout-diagnostics")]
@@ -9,30 +9,25 @@ use iris::prelude::*;
fn plant(h: &mut Harness) -> Vec<WidgetId> { fn plant(h: &mut Harness) -> Vec<WidgetId> {
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc); let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc); let wrapped = wtext("Wrapping shapes")
let sized = wrapped.width(76).add(&mut h.rsc); .size(16)
let aligned = sized; .wrap(true)
.width(76)
.add(&mut h.rsc);
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_alignment(sized, Axis::X, AxisAlign::POS); .set_alignment(wrapped, Axis::X, AxisAlign::POS);
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_alignment(sized, Axis::Y, AxisAlign::POS); .set_alignment(wrapped, Axis::Y, AxisAlign::POS);
let stack = Stack { let stack = Stack {
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)], children: vec![plain.add_strong(&mut h.rsc), wrapped.add_strong(&mut h.rsc)],
size: StackSize::Child(0), size: StackSize::Child(0),
} }
.add(&mut h.rsc); .add(&mut h.rsc);
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc); let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc)); h.state.root = Some(root.add_strong(&mut h.rsc));
vec![ vec![plain.id(), wrapped.id(), stack.id(), root.id()]
plain.id(),
wrapped.id(),
sized.id(),
aligned.id(),
stack.id(),
root.id(),
]
} }
fn dump(label: &str, report: &diag::Report, text: WidgetId) { fn dump(label: &str, report: &diag::Report, text: WidgetId) {
@@ -93,23 +88,14 @@ fn what_box_the_text_is_drawn_in() {
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> { fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
let words = "Wrapping shapes one source into as many lines as the box leaves"; let words = "Wrapping shapes one source into as many lines as the box leaves";
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc); let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
let aligned = text;
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_alignment(text, Axis::X, AxisAlign::NEG); .set_alignment(text, Axis::X, AxisAlign::NEG);
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc); let inner = (text,).span(Dir::RIGHT).sized((189, 176)).add(&mut h.rsc);
let sized = inner.sized((189, 176)).add(&mut h.rsc);
let filler = rect(Color::RED).add(&mut h.rsc); let filler = rect(Color::RED).add(&mut h.rsc);
let root = (filler, sized).span(Dir::RIGHT).add(&mut h.rsc); let root = (filler, inner).span(Dir::RIGHT).add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc)); h.state.root = Some(root.add_strong(&mut h.rsc));
vec![ vec![text.id(), inner.id(), filler.id(), root.id()]
text.id(),
aligned.id(),
inner.id(),
sized.id(),
filler.id(),
root.id(),
]
} }
#[test] #[test]