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.
163 lines
5.7 KiB
Rust
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");
|
|
}
|