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:
1 parent
cbccfb600a
commit
97fca76108
17 files changed
+588
-149
No files matched your search
@@ -0,0 +1,162 @@
|
||||
//! 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");
|
||||
}
|
||||
Reference in new issue
Block a user