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
@@ -78,3 +78,47 @@ fn unchanged_tree_reuses_layout_storage() {
|
||||
assert_eq!(allocations, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// A text drawn again at the width it already has places no glyphs and shapes
|
||||
/// nothing, so the frame costs nothing at all -- which is what the shaping
|
||||
/// cache is for, and a copy of the attrs made to ask it undid for any text
|
||||
/// naming its font family.
|
||||
///
|
||||
/// Only that case: a text drawn at a width it has not seen places its glyphs,
|
||||
/// and placing them allocates a list to hold them.
|
||||
#[test]
|
||||
fn redrawing_a_text_at_one_width_allocates_nothing() {
|
||||
let mut h = Harness::new((600, 200));
|
||||
let mut col = Span::empty(Dir::DOWN);
|
||||
let mut texts = Vec::new();
|
||||
for _ in 0..8 {
|
||||
let text =
|
||||
wtext("wrapping shapes one source into as many lines as the box leaves room for")
|
||||
.size(16)
|
||||
// Named rather than generic, because a named one is the family
|
||||
// that costs an allocation to copy.
|
||||
.family(Family::Named("sans-serif".into()))
|
||||
.wrap(true)
|
||||
.add_strong(&mut h.rsc);
|
||||
texts.push(text.id());
|
||||
col.push(text);
|
||||
}
|
||||
let root = col.add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
let redraw = |h: &mut Harness| {
|
||||
for &id in &texts {
|
||||
h.rsc.widgets_mut().mark_for_redraw(id);
|
||||
}
|
||||
h.frame();
|
||||
};
|
||||
for _ in 0..8 {
|
||||
redraw(&mut h);
|
||||
}
|
||||
COUNT.set(Some(0));
|
||||
for _ in 0..100 {
|
||||
redraw(&mut h);
|
||||
}
|
||||
let allocations = COUNT.replace(None).unwrap();
|
||||
println!("text: {allocations} allocations over 100 redraws of 8 texts");
|
||||
assert_eq!(allocations, 0);
|
||||
}
|
||||
@@ -1563,3 +1563,30 @@ fn a_contract_this_window_is_outside_is_not_kept() {
|
||||
"the leaf settled once and its parent kept what it settled"
|
||||
);
|
||||
}
|
||||
|
||||
/// A container that measures a child by drawing it, takes that drawing back,
|
||||
/// and then places the child, which `place_at` answers by asking again: taking
|
||||
/// a child back takes it out of the child list, and the note on the child
|
||||
/// saying it is in there has to go with it, or the placement re-expresses a
|
||||
/// drawing that no longer exists.
|
||||
#[test]
|
||||
fn a_child_taken_back_and_placed_again_is_asked_again() {
|
||||
struct Retake(StrongWidget);
|
||||
|
||||
impl Widget for Retake {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
painter.widget(&self.0);
|
||||
painter.undraw(&self.0);
|
||||
painter.place_at(&self.0, PlaceDesc::WHOLE).size()
|
||||
}
|
||||
}
|
||||
|
||||
let mut h = Harness::new((600, 200));
|
||||
let child = rect(Color::RED).add_strong(&mut h.rsc);
|
||||
let id = child.id();
|
||||
let root = Retake(child).add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
h.frame();
|
||||
|
||||
assert_corners!(h, id, (0, 0), (600, 200));
|
||||
}
|
||||
@@ -127,6 +127,11 @@ fn wrapping_content_beside_a_fixed_length_is_stable_warm_and_cold() {
|
||||
/// parent would place the part it cut off, and the framework would put a
|
||||
/// drawing longer than its box somewhere. `Masked` is the second of these
|
||||
/// after `Scroll`, and the assertion in `draw_at` is what says so.
|
||||
// What it checks is a debug assertion, which a release build does not compile
|
||||
// -- and a `should_panic` test of one fails there rather than passing
|
||||
// vacuously, so it is not built either. Every measurement rig here is run in
|
||||
// release, so `cargo test --release` has to pass.
|
||||
#[cfg(debug_assertions)]
|
||||
#[test]
|
||||
#[should_panic = "clips to"]
|
||||
fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
|
||||
|
||||
+77
-34
@@ -8,13 +8,15 @@
|
||||
//! submitted and waited on, so this is the GPU's cost and not the recording
|
||||
//! loop's -- which is what `draw_cost.rs` measures instead.
|
||||
//!
|
||||
//! 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
|
||||
//! anything.
|
||||
//! Two fixtures, because the walk happens in both stages. `chain_cost_by_depth`
|
||||
//! draws instances two pixels wide so that vertex work dominates; a walk that
|
||||
//! does not show up against small quads will not show up against anything.
|
||||
//! `mask_cost_by_depth` draws one screenful through a mask instead, which is
|
||||
//! where a walk in the fragment stage would show and nowhere else.
|
||||
|
||||
use iris::prelude::*;
|
||||
use iris_core::{
|
||||
Len, MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, UiData, UiRegion, UiRenderNode,
|
||||
Len, Mask, MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, UiData, UiRegion, UiRenderNode,
|
||||
UiRenderState, UiSpan,
|
||||
};
|
||||
use wgpu::{Color as GpuColor, *};
|
||||
@@ -45,8 +47,17 @@ fn gpu() -> Option<(Device, Queue, f32)> {
|
||||
Some((device, queue, period))
|
||||
}
|
||||
|
||||
/// Which stage the fill puts the work in: many small quads, where a walk per
|
||||
/// vertex is what shows, or one screenful of masked rows, where a walk per
|
||||
/// fragment would.
|
||||
#[derive(Clone, Copy)]
|
||||
enum Fixture {
|
||||
Quads,
|
||||
Masked,
|
||||
}
|
||||
|
||||
/// 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, fixture: Fixture) {
|
||||
let kind = ui.primitives.kind::<RectPrimitive>();
|
||||
let id = ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id();
|
||||
|
||||
@@ -56,20 +67,51 @@ fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
|
||||
}
|
||||
|
||||
let px = |v: f32| Len::px(v);
|
||||
for i in 0..INSTANCES {
|
||||
let x = (i % (SIZE as usize / 2)) as f32 * 2.0;
|
||||
let y = (i / (SIZE as usize / 2)) as f32;
|
||||
let rows = SIZE as usize;
|
||||
let mask_idx = match fixture {
|
||||
Fixture::Quads => MaskIdx::NONE,
|
||||
// Its own chain as long as the instances', since a viewport sits as
|
||||
// deep in the tree as the content it clips.
|
||||
Fixture::Masked => {
|
||||
let idx = ui.masks.push(Mask {
|
||||
region: UiRegion::FULL,
|
||||
move_idx: slot,
|
||||
});
|
||||
// Nothing frees it here, but the owner's reference is what a real
|
||||
// one is kept alive by.
|
||||
ui.masks.push_ref(idx);
|
||||
idx
|
||||
}
|
||||
};
|
||||
let instances = match fixture {
|
||||
Fixture::Quads => INSTANCES,
|
||||
Fixture::Masked => rows,
|
||||
};
|
||||
for i in 0..instances {
|
||||
let region = match fixture {
|
||||
Fixture::Quads => {
|
||||
let x = (i % (rows / 2)) as f32 * 2.0;
|
||||
let y = (i / (rows / 2)) as f32;
|
||||
UiRegion::new(
|
||||
UiSpan::new(px(x), px(x + 2.0)),
|
||||
UiSpan::new(px(y), px(y + 1.0)),
|
||||
)
|
||||
}
|
||||
// A full row each, so one screenful of fragments goes through the
|
||||
// mask and the vertex stage is four corners per row.
|
||||
Fixture::Masked => UiRegion::new(
|
||||
UiSpan::new(px(0.0), px(SIZE as f32)),
|
||||
UiSpan::new(px(i as f32), px(i as f32 + 1.0)),
|
||||
),
|
||||
};
|
||||
render.layers.write(
|
||||
0,
|
||||
PrimitiveInst {
|
||||
kind,
|
||||
id,
|
||||
primitive: RectPrimitive::color(UiColor::WHITE),
|
||||
region: UiRegion::new(
|
||||
UiSpan::new(px(x), px(x + 2.0)),
|
||||
UiSpan::new(px(y), px(y + 1.0)),
|
||||
),
|
||||
mask_idx: MaskIdx::NONE,
|
||||
region,
|
||||
mask_idx,
|
||||
move_idx: slot,
|
||||
},
|
||||
);
|
||||
@@ -77,28 +119,15 @@ fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
|
||||
}
|
||||
|
||||
/// 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, fixture: Fixture) -> 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();
|
||||
fill(&mut ui, &mut render, depth);
|
||||
fill(&mut ui, &mut render, depth, fixture);
|
||||
node.update(device, queue, &mut ui, &mut render);
|
||||
|
||||
let target = device.create_texture(&TextureDescriptor {
|
||||
label: Some("chain 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 target = gpu::target(device, format, SIZE, false);
|
||||
let view = target.create_view(&TextureViewDescriptor::default());
|
||||
|
||||
let queries = device.create_query_set(&QuerySetDescriptor {
|
||||
@@ -178,17 +207,15 @@ fn pass_cost(device: &Device, queue: &Queue, period: f32, depth: usize) -> f64 {
|
||||
best
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "measurement, not a check"]
|
||||
fn chain_cost_by_depth() {
|
||||
fn by_depth(fixture: Fixture, instances: usize) {
|
||||
let Some((device, queue, period)) = gpu() else {
|
||||
println!("no gpu with timestamps; nothing measured");
|
||||
return;
|
||||
};
|
||||
println!("{INSTANCES} instances, {SIZE}x{SIZE}, best of {BATCHES} batches");
|
||||
println!("{instances} instances, {SIZE}x{SIZE}, best of {BATCHES} batches");
|
||||
let mut base = None;
|
||||
for depth in [1, 2, 4, 8, 16, 32, 64] {
|
||||
let ns = pass_cost(&device, &queue, period, depth);
|
||||
let ns = pass_cost(&device, &queue, period, depth, fixture);
|
||||
let base = *base.get_or_insert(ns);
|
||||
println!(
|
||||
"depth {depth:>3}: {:>9.1} us {:+6.1}% against depth 1",
|
||||
@@ -197,3 +224,19 @@ fn chain_cost_by_depth() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "measurement, not a check"]
|
||||
fn chain_cost_by_depth() {
|
||||
by_depth(Fixture::Quads, INSTANCES);
|
||||
}
|
||||
|
||||
/// One screenful of rows, every one clipped by a mask whose own chain is that
|
||||
/// deep. What this says that the quads cannot is whether a mask costs the walk
|
||||
/// once per instance or once per fragment: at a screenful of fragments per
|
||||
/// chain, the second is the difference between these two tables.
|
||||
#[test]
|
||||
#[ignore = "measurement, not a check"]
|
||||
fn mask_cost_by_depth() {
|
||||
by_depth(Fixture::Masked, SIZE as usize);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
//! What one container's draw costs against the number of children it has.
|
||||
//!
|
||||
//! cargo test --release --test children_cost -- --ignored --nocapture
|
||||
//!
|
||||
//! Every other rig here varies depth, the window, or what changed between
|
||||
//! frames; this one varies width, which is the dimension a container's own
|
||||
//! per-child bookkeeping is counted in. A list of rows is the shape that gets
|
||||
//! wide -- a transcript, a file tree -- and a cost per child that is not flat
|
||||
//! down this table is a cost paid twice for every child added.
|
||||
//!
|
||||
//! Wall time rather than instructions, because what is being told apart here
|
||||
//! is a factor rather than a few percent, and the table says which it is: a
|
||||
//! flat right-hand column is linear and a rising one is not.
|
||||
|
||||
mod rig;
|
||||
|
||||
use iris::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
use rig::env;
|
||||
use std::time::Instant;
|
||||
|
||||
/// A column of leaves each with a length of its own, so the span asks every
|
||||
/// one of them and reads what each answered.
|
||||
fn build(h: &mut Harness, children: usize) -> WidgetId {
|
||||
let mut col = Span::empty(Dir::DOWN);
|
||||
for _ in 0..children {
|
||||
col.push(
|
||||
rect(Color::RED)
|
||||
.height(LayoutLen::px(4.0))
|
||||
.add_strong(&mut h.rsc),
|
||||
);
|
||||
}
|
||||
let root = col.add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
root.id()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "measurement, not a check"]
|
||||
fn draw_cost_by_children() {
|
||||
let frames = env("FRAMES", 40_usize);
|
||||
println!("{frames} full redraws of one span, per child in the last column");
|
||||
for children in [100_usize, 200, 400, 800, 1600] {
|
||||
// Tall enough that no child is collapsed for want of room.
|
||||
let mut h = Harness::new((600.0, children as f32 * 8.0));
|
||||
let root = build(&mut h, children);
|
||||
h.frame();
|
||||
let start = Instant::now();
|
||||
for _ in 0..frames {
|
||||
h.rsc.widgets_mut().mark_for_redraw(root);
|
||||
h.frame();
|
||||
}
|
||||
let ms = start.elapsed().as_secs_f64() * 1000.0 / frames as f64;
|
||||
println!(
|
||||
"children {children:>5}: {ms:>8.3} ms per redraw, {:>7.4} ms each",
|
||||
ms / children as f64
|
||||
);
|
||||
}
|
||||
}
|
||||
+28
-2
@@ -1,5 +1,6 @@
|
||||
//! The adapter and the surface configuration the GPU measurement rigs share,
|
||||
//! so the two cannot probe for a device in two different ways.
|
||||
//! The adapter, the surface configuration and the target the GPU rigs share,
|
||||
//! so no two of them can probe for a device or make a target in different
|
||||
//! ways.
|
||||
|
||||
use wgpu::*;
|
||||
|
||||
@@ -38,3 +39,28 @@ pub fn config(format: TextureFormat, size: u32) -> SurfaceConfiguration {
|
||||
view_formats: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// A square colour target to draw a pass into. `copy` adds the usage a rig
|
||||
/// that reads the pixels back needs; one that only times the pass does not.
|
||||
// This module is compiled into each rig target separately, so a helper the
|
||||
// ones that make no target of their own do not call is dead code there.
|
||||
#[allow(dead_code)]
|
||||
pub fn target(device: &Device, format: TextureFormat, size: u32, copy: bool) -> Texture {
|
||||
device.create_texture(&TextureDescriptor {
|
||||
label: Some("gpu rig target"),
|
||||
size: Extent3d {
|
||||
width: size,
|
||||
height: size,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format,
|
||||
usage: match copy {
|
||||
true => TextureUsages::RENDER_ATTACHMENT | TextureUsages::COPY_SRC,
|
||||
false => TextureUsages::RENDER_ATTACHMENT,
|
||||
},
|
||||
view_formats: &[],
|
||||
})
|
||||
}
|
||||
@@ -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