forked from 1b-agent/opencadstudio-upstream
Preserve fractional viewport placement when compositing paper space
Keep fractional raster placement and matching integer texture coverage through compositing. Apply parent clipping without changing the scale. Fixes #1267
This commit is contained in:
1 parent
7aff9fa046
commit
37cfa06b02
3 files changed
+341
-83
No files matched your search
@@ -547,7 +547,8 @@ fn test_selected_circle_arc_ellipse_highlight_overlay() {
|
||||
pipeline.render(
|
||||
&mut encoder,
|
||||
&target,
|
||||
iced::Size::new(512, 512),
|
||||
iced::Rectangle::with_size(iced::Size::new(512.0, 512.0)),
|
||||
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
|
||||
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
false,
|
||||
@@ -635,7 +636,8 @@ fn test_thick_and_tapered_arc_gpu_rendering() {
|
||||
pipeline.render(
|
||||
&mut encoder,
|
||||
&target,
|
||||
iced::Size::new(512, 512),
|
||||
iced::Rectangle::with_size(iced::Size::new(512.0, 512.0)),
|
||||
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
|
||||
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
false,
|
||||
@@ -740,7 +742,8 @@ fn test_tilted_3d_donut_and_thick_arc_gpu_rendering() {
|
||||
pipeline.render(
|
||||
&mut encoder,
|
||||
&target,
|
||||
iced::Size::new(512, 512),
|
||||
iced::Rectangle::with_size(iced::Size::new(512.0, 512.0)),
|
||||
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
|
||||
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
false,
|
||||
@@ -855,7 +858,8 @@ fn test_selected_ellipse_overlay() {
|
||||
pipeline.render(
|
||||
&mut encoder,
|
||||
&target,
|
||||
iced::Size::new(512, 512),
|
||||
iced::Rectangle::with_size(iced::Size::new(512.0, 512.0)),
|
||||
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
|
||||
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
false,
|
||||
@@ -926,7 +930,8 @@ fn test_pline_arc_switch_preview_and_render() {
|
||||
pipeline.render(
|
||||
&mut encoder,
|
||||
&target,
|
||||
iced::Size::new(512, 512),
|
||||
iced::Rectangle::with_size(iced::Size::new(512.0, 512.0)),
|
||||
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
|
||||
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
false,
|
||||
|
||||
+15
-19
@@ -201,6 +201,7 @@ pub struct Pipeline {
|
||||
/// geometry passes render at this size; the blit UV is scaled by
|
||||
/// `depth_texture_size / alloc_size` so it samples only the filled region.
|
||||
depth_texture_size: Size<u32>,
|
||||
pub(in crate::scene) viewport: Option<crate::scene::view::render::PhysicalViewport>,
|
||||
/// Actual allocated size of the depth / MSAA / resolve textures. Rounded
|
||||
/// up from the requested size to a coarse grid so a divider drag (which
|
||||
/// changes the pane size a few pixels every frame) doesn't recreate these
|
||||
@@ -2321,6 +2322,7 @@ impl Pipeline {
|
||||
wipeout_bgl1,
|
||||
image_bgl1,
|
||||
depth_texture_size: Size::new(1, 1),
|
||||
viewport: None,
|
||||
// (0, 0) forces the first `ensure_depth_texture` to allocate at the
|
||||
// real rounded size — the constructor textures above are placeholders.
|
||||
alloc_size: Size::new(0, 0),
|
||||
@@ -3967,8 +3969,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
|
||||
);
|
||||
}
|
||||
|
||||
/// Render the geometry passes at `vp_size` (the full viewport size — the
|
||||
/// MSAA / resolve textures are this size) and blit the resulting resolve
|
||||
/// Render geometry at its fractional pixel rectangle and blit the resolve
|
||||
/// to `surface_dest` on the swap-chain. The UV crop is read from the
|
||||
/// blit uniform buffer (written by `upload_blit_uv` during `prepare`)
|
||||
/// so a viewport that hangs off the canvas still composites the correct
|
||||
@@ -3977,19 +3978,14 @@ analytic={:.1} regular={:.1} blocks={:.1}",
|
||||
&self,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
target: &wgpu::TextureView,
|
||||
vp_size: Size<u32>,
|
||||
raster: Rectangle,
|
||||
surface_dest: Rectangle<u32>,
|
||||
surface_clip: Rectangle<u32>,
|
||||
bg_color: [f32; 4],
|
||||
mesh_wireframe: bool,
|
||||
hidden_line: bool,
|
||||
show_3d_edges: bool,
|
||||
) {
|
||||
let vp = Rectangle::<u32> {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: vp_size.width,
|
||||
height: vp_size.height,
|
||||
};
|
||||
let msaa = &self.msaa_view;
|
||||
let [r, g, b, a] = bg_color;
|
||||
let clear_color = if self.clip_boundary.is_some() || self.skip_background {
|
||||
@@ -4101,8 +4097,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
// MSAA texture is clip-bounds-sized, so viewport starts at (0, 0).
|
||||
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
|
||||
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
|
||||
// Stamp the viewport clip boundary into the just-cleared stencil
|
||||
// (interior → 1) before any content draws, so every pass below can
|
||||
// clip to the shape with reference 1.
|
||||
@@ -4155,7 +4150,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
|
||||
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
|
||||
pass.set_pipeline(&self.image_pipeline);
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
pass.set_stencil_reference(stencil_ref);
|
||||
@@ -4192,7 +4187,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
|
||||
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
pass.set_stencil_reference(stencil_ref);
|
||||
// Four draw paths share this pass:
|
||||
@@ -4491,7 +4486,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
|
||||
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
pass.set_stencil_reference(stencil_ref);
|
||||
if !fill.chunks_3d.is_empty() {
|
||||
@@ -4562,7 +4557,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
|
||||
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
|
||||
pass.set_pipeline(&self.wire_pipeline);
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
pass.set_stencil_reference(stencil_ref);
|
||||
@@ -4605,7 +4600,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
|
||||
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
|
||||
pass.set_pipeline(&self.wire_pipeline);
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
// In a filled-with-edges mode the mesh outline edges frame the shaded
|
||||
@@ -4756,7 +4751,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
|
||||
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
|
||||
pass.set_pipeline(&self.text_pipeline);
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
pass.set_stencil_reference(stencil_ref);
|
||||
@@ -4809,7 +4804,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
|
||||
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
|
||||
pass.set_pipeline(&self.wipeout_pipeline);
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
pass.set_stencil_reference(stencil_ref);
|
||||
@@ -4859,7 +4854,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
|
||||
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
pass.set_stencil_reference(stencil_ref);
|
||||
if !self.gpu_selected_wires.is_empty() {
|
||||
@@ -5002,6 +4997,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
|
||||
1.0,
|
||||
);
|
||||
pass.set_pipeline(&self.blit_pipeline);
|
||||
pass.set_scissor_rect(surface_clip.x, surface_clip.y, surface_clip.width, surface_clip.height);
|
||||
pass.set_bind_group(0, &self.blit_bind_group, &[]);
|
||||
pass.draw(0..6, 0..1);
|
||||
}
|
||||
|
||||
+316
-59
@@ -395,32 +395,23 @@ impl shader::Primitive for Primitive {
|
||||
inner.slot_id = vp.instance_id;
|
||||
inner.forget_cached_keys();
|
||||
}
|
||||
// The MSAA / depth / resolve textures are always sized to the
|
||||
// FULL viewport rectangle (not the on-canvas-visible portion)
|
||||
// so the camera matrices render at consistent aspect / scale.
|
||||
// The blit step picks the visible sub-rectangle out via the
|
||||
// shader's UV crop uniform, which lets partially off-canvas
|
||||
// viewports composite to their visible surface area without
|
||||
// drift.
|
||||
let clip_size = Size::new(
|
||||
(vp.screen_rect.width * bounds.width * scale).ceil().max(1.0) as u32,
|
||||
(vp.screen_rect.height * bounds.height * scale).ceil().max(1.0) as u32,
|
||||
// Round texture coverage, retaining fractional raster placement.
|
||||
let placement = physical_viewport(
|
||||
Rectangle {
|
||||
x: (bounds.x + vp.screen_rect.x * bounds.width) * scale,
|
||||
y: (bounds.y + vp.screen_rect.y * bounds.height) * scale,
|
||||
width: vp.screen_rect.width * bounds.width * scale,
|
||||
height: vp.screen_rect.height * bounds.height * scale,
|
||||
},
|
||||
viewport.physical_size(),
|
||||
);
|
||||
let clip_size = placement.size;
|
||||
inner.viewport = Some(placement);
|
||||
inner.ensure_depth_texture(device, clip_size);
|
||||
let viewcube_side =
|
||||
(crate::scene::VIEWCUBE_RENDER_PX.ceil() * scale).ceil().max(1.0) as u32;
|
||||
inner
|
||||
.viewcube
|
||||
.ensure_depth_texture(device, Size::new(viewcube_side, viewcube_side));
|
||||
// Compute the UV crop for this viewport. `screen_rect` is in
|
||||
// normalized canvas units (0..1) but may extend negative or
|
||||
// beyond 1 when the viewport hangs off the canvas. The on-
|
||||
// canvas portion in viewport-local UV is straightforward to
|
||||
// derive from how much sticks out on each side.
|
||||
let sr = vp.screen_rect;
|
||||
let (uo_x, us_x) = uv_crop_axis(sr.x, sr.width);
|
||||
let (uo_y, us_y) = uv_crop_axis(sr.y, sr.height);
|
||||
inner.upload_blit_uv(queue, [uo_x, uo_y], [us_x, us_y]);
|
||||
inner.viewcube.ensure_depth_texture(device, Size::new(viewcube_side, viewcube_side));
|
||||
inner.upload_blit_uv(queue, placement.uv_offset, placement.uv_scale);
|
||||
inner.upload_background_images(
|
||||
device,
|
||||
queue,
|
||||
@@ -440,7 +431,7 @@ impl shader::Primitive for Primitive {
|
||||
// drawing size. The ViewCube is excluded from the signature and
|
||||
// keeps updating in its own always-on pass, so cube hover still
|
||||
// tracks while the scene is cached.
|
||||
let sig = render_signature(vp, clip_size.width, clip_size.height);
|
||||
let sig = render_signature(vp, &placement);
|
||||
// REDRAW bypass: pin render_sig to force a full pass this frame
|
||||
// even if the signature is otherwise unchanged. The request is
|
||||
// one-shot per viewport (consumed by the builder that produced
|
||||
@@ -1311,8 +1302,6 @@ retained_contributors={}",
|
||||
) {
|
||||
let nav_render_started = iced::time::Instant::now();
|
||||
pipeline.frame_rendered.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
let cw = clip.width as f32;
|
||||
let ch = clip.height as f32;
|
||||
let clip_right = clip.x + clip.width;
|
||||
let clip_bottom = clip.y + clip.height;
|
||||
for vp in &self.viewports {
|
||||
@@ -1322,27 +1311,25 @@ retained_contributors={}",
|
||||
let Some(inner) = pipeline.inners.get(*slot) else {
|
||||
continue;
|
||||
};
|
||||
// Where the viewport would land on the surface in absolute
|
||||
// pixels (i32 because either edge may stick off the canvas).
|
||||
let vp_full_x = clip.x as i32 + (vp.screen_rect.x * cw) as i32;
|
||||
let vp_full_y = clip.y as i32 + (vp.screen_rect.y * ch) as i32;
|
||||
let vp_full_w = (vp.screen_rect.width * cw).max(1.0) as i32;
|
||||
let vp_full_h = (vp.screen_rect.height * ch).max(1.0) as i32;
|
||||
// Intersect with the surface clip — that's the slice we blit.
|
||||
let dest_x = vp_full_x.max(clip.x as i32);
|
||||
let dest_y = vp_full_y.max(clip.y as i32);
|
||||
let dest_right = (vp_full_x + vp_full_w).min(clip_right as i32);
|
||||
let dest_bottom = (vp_full_y + vp_full_h).min(clip_bottom as i32);
|
||||
if dest_right <= dest_x || dest_bottom <= dest_y {
|
||||
let Some(placement) = inner.viewport else {
|
||||
continue;
|
||||
};
|
||||
let surface_dest = placement.surface;
|
||||
let left = surface_dest.x.max(clip.x);
|
||||
let top = surface_dest.y.max(clip.y);
|
||||
let surface_clip = Rectangle {
|
||||
x: left,
|
||||
y: top,
|
||||
width: (surface_dest.x + surface_dest.width).min(clip_right).saturating_sub(left),
|
||||
height: (surface_dest.y + surface_dest.height).min(clip_bottom).saturating_sub(top),
|
||||
};
|
||||
if surface_clip.width == 0 || surface_clip.height == 0 {
|
||||
continue;
|
||||
}
|
||||
let surface_dest = Rectangle {
|
||||
x: dest_x as u32,
|
||||
y: dest_y as u32,
|
||||
width: (dest_right - dest_x) as u32,
|
||||
height: (dest_bottom - dest_y) as u32,
|
||||
};
|
||||
let vp_size = Size::new(vp_full_w.max(1) as u32, vp_full_h.max(1) as u32);
|
||||
let vp_full_x = surface_dest.x as i32;
|
||||
let vp_full_y = surface_dest.y as i32;
|
||||
let vp_full_w = surface_dest.width as i32;
|
||||
let vp_full_h = surface_dest.height as i32;
|
||||
// `mesh_fill` is false for Wireframe 2D / Wireframe 3D — flip
|
||||
// the draw path so meshes use the wireframe pipeline + the
|
||||
// pre-built triangle-edge index buffer.
|
||||
@@ -1350,8 +1337,9 @@ retained_contributors={}",
|
||||
inner.render(
|
||||
encoder,
|
||||
target,
|
||||
vp_size,
|
||||
placement.raster,
|
||||
surface_dest,
|
||||
surface_clip,
|
||||
self.bg_color,
|
||||
mesh_wireframe,
|
||||
vp.hidden_line,
|
||||
@@ -1363,6 +1351,8 @@ retained_contributors={}",
|
||||
// rejects negative origins, and a clamped cube would scale
|
||||
// distortedly. The active viewport is normally fully visible.
|
||||
if vp.show_viewcube
|
||||
&& surface_dest.width == placement.size.width
|
||||
&& surface_dest.height == placement.size.height
|
||||
&& vp_full_x >= clip.x as i32
|
||||
&& vp_full_y >= clip.y as i32
|
||||
&& vp_full_x + vp_full_w <= clip_right as i32
|
||||
@@ -1408,7 +1398,7 @@ retained_contributors={}",
|
||||
/// live preview IS included (its coordinates), so a rubber-band tracking the
|
||||
/// cursor still renders, and the frame where the preview clears erases it
|
||||
/// instead of freezing the last overlay on screen.
|
||||
fn render_signature(vp: &ViewportData, clip_w: u32, clip_h: u32) -> u64 {
|
||||
fn render_signature(vp: &ViewportData, placement: &PhysicalViewport) -> u64 {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut h = rustc_hash::FxHasher::default();
|
||||
// Camera + per-view shading flags all live in the uniforms (view_rot, eye
|
||||
@@ -1453,8 +1443,11 @@ fn render_signature(vp: &ViewportData, clip_w: u32, clip_h: u32) -> u64 {
|
||||
// frame (skip_hatch flips false) re-renders with hatches and re-caches.
|
||||
vp.skip_hatch.hash(&mut h);
|
||||
vp.skip_background.hash(&mut h);
|
||||
clip_w.hash(&mut h);
|
||||
clip_h.hash(&mut h);
|
||||
placement.size.width.hash(&mut h);
|
||||
placement.size.height.hash(&mut h);
|
||||
for value in [placement.raster.x, placement.raster.y, placement.raster.width, placement.raster.height] {
|
||||
value.to_bits().hash(&mut h);
|
||||
}
|
||||
// Live overlay (command preview / interim / grip drag). Small — a handful
|
||||
// of wires — so hashing its coordinates is cheap and catches the endpoint
|
||||
// moving with the cursor as well as the preview appearing / clearing.
|
||||
@@ -1535,18 +1528,282 @@ fn render_signature(vp: &ViewportData, clip_w: u32, clip_h: u32) -> u64 {
|
||||
h.finish()
|
||||
}
|
||||
|
||||
/// On-canvas-visible UV crop on one axis. `pos` and `size` are in the
|
||||
/// shader widget's normalized 0..1 coords. Returns `(uv_offset, uv_scale)`
|
||||
/// applied as `actual_uv = quad_uv * uv_scale + uv_offset` in the blit
|
||||
/// shader — identity `(0.0, 1.0)` for fully on-canvas viewports.
|
||||
fn uv_crop_axis(pos: f32, size: f32) -> (f32, f32) {
|
||||
if size <= 0.0 {
|
||||
return (0.0, 1.0);
|
||||
#[derive(Clone, Copy)]
|
||||
pub(in crate::scene) struct PhysicalViewport {
|
||||
size: Size<u32>,
|
||||
raster: Rectangle,
|
||||
surface: Rectangle<u32>,
|
||||
uv_offset: [f32; 2],
|
||||
uv_scale: [f32; 2],
|
||||
}
|
||||
|
||||
fn physical_viewport(rect: Rectangle, window: Size<u32>) -> PhysicalViewport {
|
||||
let x = rect.x.floor();
|
||||
let y = rect.y.floor();
|
||||
let right = (rect.x + rect.width).ceil();
|
||||
let bottom = (rect.y + rect.height).ceil();
|
||||
let width = (right - x).max(1.0);
|
||||
let height = (bottom - y).max(1.0);
|
||||
let left = x.clamp(0.0, window.width as f32);
|
||||
let top = y.clamp(0.0, window.height as f32);
|
||||
let visible_w = (right.min(window.width as f32) - left).max(0.0);
|
||||
let visible_h = (bottom.min(window.height as f32) - top).max(0.0);
|
||||
PhysicalViewport {
|
||||
size: Size::new(width as u32, height as u32),
|
||||
raster: Rectangle {
|
||||
x: rect.x - x,
|
||||
y: rect.y - y,
|
||||
width: rect.width.max(0.0),
|
||||
height: rect.height.max(0.0),
|
||||
},
|
||||
surface: Rectangle {
|
||||
x: left as u32,
|
||||
y: top as u32,
|
||||
width: visible_w as u32,
|
||||
height: visible_h as u32,
|
||||
},
|
||||
uv_offset: [(left - x) / width, (top - y) / height],
|
||||
uv_scale: [visible_w / width, visible_h / height],
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod pixel_placement_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn raster_and_blit_preserve_fractional_position_at_different_scales() {
|
||||
for scale in [1.0, 1.25, 1.5, 2.0] {
|
||||
for x in [-17.37, 0.25, 13.61, 199.7] {
|
||||
for width in [0.4, 18.73, 181.83] {
|
||||
let rect = Rectangle {
|
||||
x: x * scale,
|
||||
y: 12.13 * scale,
|
||||
width: width * scale,
|
||||
height: 91.17 * scale,
|
||||
};
|
||||
let placement = physical_viewport(rect, Size::new(256, 256));
|
||||
assert!(placement.raster.x >= 0.0 && placement.raster.x < 1.0);
|
||||
assert!(
|
||||
placement.raster.x + placement.raster.width
|
||||
<= placement.size.width as f32 + 1e-5
|
||||
);
|
||||
if placement.surface.width == 0 {
|
||||
continue;
|
||||
}
|
||||
// Follow a model point through rasterization, texture UV
|
||||
// crop, then the blit; paper geometry lands directly here.
|
||||
for u in [0.0, 0.1, 0.5, 0.9, 1.0] {
|
||||
let texture_x = placement.raster.x + u * placement.raster.width;
|
||||
let output_x = placement.surface.x as f32
|
||||
+ (texture_x / placement.size.width as f32 - placement.uv_offset[0])
|
||||
/ placement.uv_scale[0]
|
||||
* placement.surface.width as f32;
|
||||
assert!((output_x - (rect.x + u * rect.width)).abs() < 0.0001);
|
||||
}
|
||||
assert!(
|
||||
(placement.uv_scale[0] * placement.size.width as f32
|
||||
- placement.surface.width as f32)
|
||||
.abs()
|
||||
< 1e-5
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integer_translation_reuses_the_same_raster_placement() {
|
||||
let rect = Rectangle {
|
||||
x: 13.25,
|
||||
y: 27.5,
|
||||
width: 100.75,
|
||||
height: 51.25,
|
||||
};
|
||||
let first = physical_viewport(rect, Size::new(512, 512));
|
||||
let moved = physical_viewport(
|
||||
Rectangle {
|
||||
x: rect.x + 20.0,
|
||||
..rect
|
||||
},
|
||||
Size::new(512, 512),
|
||||
);
|
||||
assert_eq!(first.raster, moved.raster);
|
||||
assert_eq!(first.size, moved.size);
|
||||
assert_eq!(first.uv_scale, moved.uv_scale);
|
||||
assert_eq!(moved.surface.x, first.surface.x + 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a GPU adapter"]
|
||||
fn fractional_viewports_keep_wire_pixels_aligned_after_blitting() {
|
||||
use iced::futures::executor::block_on;
|
||||
use iced::wgpu;
|
||||
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
|
||||
let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default()))
|
||||
.expect("GPU adapter");
|
||||
let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||
required_limits: adapter.limits(),
|
||||
..Default::default()
|
||||
}))
|
||||
.expect("GPU device");
|
||||
let validation = device.push_error_scope(wgpu::ErrorFilter::Validation);
|
||||
let mut pipeline =
|
||||
crate::scene::pipeline::Pipeline::new(&device, &queue, wgpu::TextureFormat::Bgra8Unorm);
|
||||
let target = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("pixel placement regression"),
|
||||
size: wgpu::Extent3d {
|
||||
width: 256,
|
||||
height: 256,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Bgra8Unorm,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
let view = target.create_view(&Default::default());
|
||||
let readback = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("pixel readback"),
|
||||
size: 256 * 256 * 4,
|
||||
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
for (x, width, parent_clip) in [
|
||||
(13.37, 181.83, false),
|
||||
(-12.61, 187.29, false),
|
||||
(19.75, 173.75, false),
|
||||
(13.37, 181.83, true),
|
||||
(-12.61, 187.29, true),
|
||||
(19.75, 173.75, true),
|
||||
] {
|
||||
let rect = Rectangle {
|
||||
x,
|
||||
y: 18.37,
|
||||
width,
|
||||
height: 181.83,
|
||||
};
|
||||
let placement = physical_viewport(rect, Size::new(256, 256));
|
||||
pipeline.ensure_depth_texture(&device, placement.size);
|
||||
pipeline.upload_blit_uv(&queue, placement.uv_offset, placement.uv_scale);
|
||||
let mut uniforms = Uniforms::new(
|
||||
&Camera::default(),
|
||||
Rectangle::with_size(Size::new(width, rect.height)),
|
||||
false,
|
||||
);
|
||||
uniforms.eye_high = [0.0; 3];
|
||||
uniforms.eye_low = [0.0; 3];
|
||||
uniforms.view_rot = Mat4::IDENTITY;
|
||||
pipeline.upload_uniforms(&device, &queue, &uniforms);
|
||||
let u = 0.73;
|
||||
let ndc_x = 2.0 * u - 1.0;
|
||||
let wire = WireModel::solid(
|
||||
"alignment".into(),
|
||||
vec![[ndc_x, -0.8, 0.5], [ndc_x, 0.8, 0.5]],
|
||||
[1.0; 4],
|
||||
false,
|
||||
);
|
||||
pipeline.upload_preview_wires(&device, &queue, &[wire], &Default::default());
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
{
|
||||
let _clear = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &view,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
let clip = if parent_clip {
|
||||
Rectangle {
|
||||
x: placement.surface.x + 11,
|
||||
y: 64,
|
||||
width: placement.surface.width - 22,
|
||||
height: 96,
|
||||
}
|
||||
} else {
|
||||
placement.surface
|
||||
};
|
||||
pipeline.render(
|
||||
&mut encoder,
|
||||
&view,
|
||||
placement.raster,
|
||||
placement.surface,
|
||||
clip,
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
encoder.copy_texture_to_buffer(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &target,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &readback,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(256 * 4),
|
||||
rows_per_image: Some(256),
|
||||
},
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width: 256,
|
||||
height: 256,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
queue.submit([encoder.finish()]);
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
readback
|
||||
.slice(..)
|
||||
.map_async(wgpu::MapMode::Read, move |result| {
|
||||
tx.send(result).unwrap();
|
||||
});
|
||||
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
|
||||
rx.recv().unwrap().unwrap();
|
||||
{
|
||||
let bytes = readback.slice(..).get_mapped_range();
|
||||
let row = (rect.y + rect.height * 0.5) as usize;
|
||||
let expected = rect.x + u * rect.width;
|
||||
let row_has_wire = bytes[48 * 256 * 4..49 * 256 * 4]
|
||||
.chunks_exact(4)
|
||||
.any(|pixel| pixel[..3] != [0, 0, 0]);
|
||||
assert_eq!(
|
||||
row_has_wire, !parent_clip,
|
||||
"parent clip must exclude the wire at row 48"
|
||||
);
|
||||
let mut weight = 0.0;
|
||||
let mut center = 0.0;
|
||||
for column in (expected as usize - 4)..=(expected as usize + 4) {
|
||||
let value = bytes[(row * 256 + column) * 4] as f32;
|
||||
weight += value;
|
||||
center += (column as f32 + 0.5) * value;
|
||||
}
|
||||
assert!(weight > 0.0, "wire must produce pixels");
|
||||
assert!(
|
||||
(center / weight - expected).abs() < 0.25,
|
||||
"x={x}, width={width}, expected={expected}, actual={}",
|
||||
center / weight
|
||||
);
|
||||
}
|
||||
readback.unmap();
|
||||
}
|
||||
assert!(
|
||||
block_on(validation.pop()).is_none(),
|
||||
"GPU validation failed"
|
||||
);
|
||||
}
|
||||
let left_off = (-pos).max(0.0);
|
||||
let right_off = (pos + size - 1.0).max(0.0);
|
||||
let visible = (size - left_off - right_off).max(0.0);
|
||||
(left_off / size, visible / size)
|
||||
}
|
||||
|
||||
/// Apply a clip-space crop to `view_proj` so the sub-rect of the original
|
||||
|
||||
Reference in new issue
Block a user