//! 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::(); 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"); }