Files
iris/tests/mask_clip.rs
T
iris-ai 97fca76108 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.
2026-09-20 23:56:12 -04:00

163 lines
5.7 KiB
Rust

//! Which pixels a mask lets through, read back off the GPU.
//!
//! cargo test --release --test mask_clip -- --ignored --nocapture
//!
//! Ignored because it needs a device, which not every machine running the
//! suite has -- and a deliberate run on one without fails rather than passing
//! with nothing checked. Nothing else here sees a mask at all: `iris::harness`
//! draws no pixels, and a mask's rectangle is resolved through its own move
//! chain in the shader, so the CPU's idea of it is not what clips anything.
use iris::prelude::*;
use iris_core::{
Len, Mask, MoveIdx, PrimitiveInst, RectPrimitive, UiData, UiRegion, UiRenderNode,
UiRenderState, UiSpan,
};
use wgpu::{Color as GpuColor, *};
#[path = "gpu/mod.rs"]
mod gpu;
const SIZE: u32 = 256;
/// The mask is a box inside a move chain two links long and the drawing
/// overflows it on both axes, so what comes back is the mask's own rectangle
/// composed through that chain -- and a clip resolved through the wrong one,
/// or not composed at all, lands somewhere else.
#[test]
#[ignore = "needs a gpu"]
fn a_mask_clips_its_own_box_composed_through_its_chain() {
let adapter = gpu::adapter().expect("no adapter to draw with");
println!("adapter: {:?}", adapter.get_info().name);
let (device, queue) = pollster::block_on(adapter.request_device(&DeviceDescriptor::default()))
.expect("no device on that adapter");
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 kind = ui.primitives.kind::<RectPrimitive>();
let id = ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id();
let px = Len::px;
let outer = render.moves.push(MoveIdx::NONE, UiRegion::FULL);
let shift = (16.0, 24.0);
let inner = render.moves.push(
outer,
UiRegion::new(
UiSpan::new(px(shift.0), px(shift.0) + Len::FULL),
UiSpan::new(px(shift.1), px(shift.1) + Len::FULL),
),
);
let clip = (20.0, 30.0, 120.0, 90.0);
let mask = ui.masks.push(Mask {
region: UiRegion::new(
UiSpan::new(px(clip.0), px(clip.2)),
UiSpan::new(px(clip.1), px(clip.3)),
),
move_idx: inner,
});
// The owner's reference, which is what keeps a real one alive.
ui.masks.push_ref(mask);
render.layers.write(
0,
PrimitiveInst {
kind,
id,
primitive: RectPrimitive::color(UiColor::WHITE),
region: UiRegion::new(
UiSpan::new(px(0.0), px(200.0)),
UiSpan::new(px(0.0), px(200.0)),
),
mask_idx: mask,
move_idx: inner,
},
);
node.update(&device, &queue, &mut ui, &mut render);
let target = gpu::target(&device, format, SIZE, true);
let view = target.create_view(&TextureViewDescriptor::default());
let row = SIZE * 4;
let readback = device.create_buffer(&BufferDescriptor {
label: Some("mask clip"),
size: (row * SIZE) as u64,
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor::default());
{
let pass = &mut encoder.begin_render_pass(&RenderPassDescriptor {
label: None,
color_attachments: &[Some(RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(GpuColor::BLACK),
store: StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
node.draw(pass);
}
let whole = Extent3d {
width: SIZE,
height: SIZE,
depth_or_array_layers: 1,
};
encoder.copy_texture_to_buffer(
TexelCopyTextureInfo {
texture: &target,
mip_level: 0,
origin: Origin3d::ZERO,
aspect: TextureAspect::All,
},
TexelCopyBufferInfo {
buffer: &readback,
layout: TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(row),
rows_per_image: Some(SIZE),
},
},
whole,
);
queue.submit(Some(encoder.finish()));
let slice = readback.slice(..);
slice.map_async(MapMode::Read, |_| {});
device
.poll(PollType::Wait {
submission_index: None,
timeout: None,
})
.expect("the pass did not finish");
let pixels = slice.get_mapped_range().expect("the target did not map");
let mut lit = 0;
let mut bounds: Option<(u32, u32, u32, u32)> = None;
for y in 0..SIZE {
for x in 0..SIZE {
if pixels[(y * row + x * 4) as usize] == 0 {
continue;
}
lit += 1;
let (x0, y0, x1, y1) = bounds.unwrap_or((x, y, x, y));
bounds = Some((x0.min(x), y0.min(y), x1.max(x), y1.max(y)));
}
}
// The clip shifted by the chain. Its far edge is exclusive: a fragment
// exactly on it is the first one outside.
let want = (
(clip.0 + shift.0) as u32,
(clip.1 + shift.1) as u32,
(clip.2 + shift.0) as u32 - 1,
(clip.3 + shift.1) as u32 - 1,
);
assert_eq!(bounds, Some(want), "{lit} pixels through the mask");
let (x0, y0, x1, y1) = want;
assert_eq!(lit, (x1 - x0 + 1) * (y1 - y0 + 1), "the clip has a hole");
}