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(
|
pipeline.render(
|
||||||
&mut encoder,
|
&mut encoder,
|
||||||
&target,
|
&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 },
|
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
|
||||||
[0.0, 0.0, 0.0, 1.0],
|
[0.0, 0.0, 0.0, 1.0],
|
||||||
false,
|
false,
|
||||||
@@ -635,7 +636,8 @@ fn test_thick_and_tapered_arc_gpu_rendering() {
|
|||||||
pipeline.render(
|
pipeline.render(
|
||||||
&mut encoder,
|
&mut encoder,
|
||||||
&target,
|
&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 },
|
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
|
||||||
[0.0, 0.0, 0.0, 1.0],
|
[0.0, 0.0, 0.0, 1.0],
|
||||||
false,
|
false,
|
||||||
@@ -740,7 +742,8 @@ fn test_tilted_3d_donut_and_thick_arc_gpu_rendering() {
|
|||||||
pipeline.render(
|
pipeline.render(
|
||||||
&mut encoder,
|
&mut encoder,
|
||||||
&target,
|
&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 },
|
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
|
||||||
[0.0, 0.0, 0.0, 1.0],
|
[0.0, 0.0, 0.0, 1.0],
|
||||||
false,
|
false,
|
||||||
@@ -855,7 +858,8 @@ fn test_selected_ellipse_overlay() {
|
|||||||
pipeline.render(
|
pipeline.render(
|
||||||
&mut encoder,
|
&mut encoder,
|
||||||
&target,
|
&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 },
|
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
|
||||||
[0.0, 0.0, 0.0, 1.0],
|
[0.0, 0.0, 0.0, 1.0],
|
||||||
false,
|
false,
|
||||||
@@ -926,7 +930,8 @@ fn test_pline_arc_switch_preview_and_render() {
|
|||||||
pipeline.render(
|
pipeline.render(
|
||||||
&mut encoder,
|
&mut encoder,
|
||||||
&target,
|
&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 },
|
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
|
||||||
[0.0, 0.0, 0.0, 1.0],
|
[0.0, 0.0, 0.0, 1.0],
|
||||||
false,
|
false,
|
||||||
|
|||||||
+15
-19
@@ -201,6 +201,7 @@ pub struct Pipeline {
|
|||||||
/// geometry passes render at this size; the blit UV is scaled by
|
/// 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 / alloc_size` so it samples only the filled region.
|
||||||
depth_texture_size: Size<u32>,
|
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
|
/// Actual allocated size of the depth / MSAA / resolve textures. Rounded
|
||||||
/// up from the requested size to a coarse grid so a divider drag (which
|
/// 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
|
/// changes the pane size a few pixels every frame) doesn't recreate these
|
||||||
@@ -2321,6 +2322,7 @@ impl Pipeline {
|
|||||||
wipeout_bgl1,
|
wipeout_bgl1,
|
||||||
image_bgl1,
|
image_bgl1,
|
||||||
depth_texture_size: Size::new(1, 1),
|
depth_texture_size: Size::new(1, 1),
|
||||||
|
viewport: None,
|
||||||
// (0, 0) forces the first `ensure_depth_texture` to allocate at the
|
// (0, 0) forces the first `ensure_depth_texture` to allocate at the
|
||||||
// real rounded size — the constructor textures above are placeholders.
|
// real rounded size — the constructor textures above are placeholders.
|
||||||
alloc_size: Size::new(0, 0),
|
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
|
/// Render geometry at its fractional pixel rectangle and blit the resolve
|
||||||
/// MSAA / resolve textures are this size) and blit the resulting resolve
|
|
||||||
/// to `surface_dest` on the swap-chain. The UV crop is read from the
|
/// to `surface_dest` on the swap-chain. The UV crop is read from the
|
||||||
/// blit uniform buffer (written by `upload_blit_uv` during `prepare`)
|
/// blit uniform buffer (written by `upload_blit_uv` during `prepare`)
|
||||||
/// so a viewport that hangs off the canvas still composites the correct
|
/// so a viewport that hangs off the canvas still composites the correct
|
||||||
@@ -3977,19 +3978,14 @@ analytic={:.1} regular={:.1} blocks={:.1}",
|
|||||||
&self,
|
&self,
|
||||||
encoder: &mut wgpu::CommandEncoder,
|
encoder: &mut wgpu::CommandEncoder,
|
||||||
target: &wgpu::TextureView,
|
target: &wgpu::TextureView,
|
||||||
vp_size: Size<u32>,
|
raster: Rectangle,
|
||||||
surface_dest: Rectangle<u32>,
|
surface_dest: Rectangle<u32>,
|
||||||
|
surface_clip: Rectangle<u32>,
|
||||||
bg_color: [f32; 4],
|
bg_color: [f32; 4],
|
||||||
mesh_wireframe: bool,
|
mesh_wireframe: bool,
|
||||||
hidden_line: bool,
|
hidden_line: bool,
|
||||||
show_3d_edges: 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 msaa = &self.msaa_view;
|
||||||
let [r, g, b, a] = bg_color;
|
let [r, g, b, a] = bg_color;
|
||||||
let clear_color = if self.clip_boundary.is_some() || self.skip_background {
|
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,
|
occlusion_query_set: None,
|
||||||
multiview_mask: None,
|
multiview_mask: None,
|
||||||
});
|
});
|
||||||
// MSAA texture is clip-bounds-sized, so viewport starts at (0, 0).
|
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
|
||||||
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
|
|
||||||
// Stamp the viewport clip boundary into the just-cleared stencil
|
// Stamp the viewport clip boundary into the just-cleared stencil
|
||||||
// (interior → 1) before any content draws, so every pass below can
|
// (interior → 1) before any content draws, so every pass below can
|
||||||
// clip to the shape with reference 1.
|
// clip to the shape with reference 1.
|
||||||
@@ -4155,7 +4150,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
|
|||||||
occlusion_query_set: None,
|
occlusion_query_set: None,
|
||||||
multiview_mask: 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_pipeline(&self.image_pipeline);
|
||||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||||
pass.set_stencil_reference(stencil_ref);
|
pass.set_stencil_reference(stencil_ref);
|
||||||
@@ -4192,7 +4187,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
|
|||||||
occlusion_query_set: None,
|
occlusion_query_set: None,
|
||||||
multiview_mask: 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_bind_group(0, &self.uniform_bind_group, &[]);
|
||||||
pass.set_stencil_reference(stencil_ref);
|
pass.set_stencil_reference(stencil_ref);
|
||||||
// Four draw paths share this pass:
|
// Four draw paths share this pass:
|
||||||
@@ -4491,7 +4486,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
|
|||||||
occlusion_query_set: None,
|
occlusion_query_set: None,
|
||||||
multiview_mask: 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_bind_group(0, &self.uniform_bind_group, &[]);
|
||||||
pass.set_stencil_reference(stencil_ref);
|
pass.set_stencil_reference(stencil_ref);
|
||||||
if !fill.chunks_3d.is_empty() {
|
if !fill.chunks_3d.is_empty() {
|
||||||
@@ -4562,7 +4557,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
|
|||||||
occlusion_query_set: None,
|
occlusion_query_set: None,
|
||||||
multiview_mask: 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_pipeline(&self.wire_pipeline);
|
||||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||||
pass.set_stencil_reference(stencil_ref);
|
pass.set_stencil_reference(stencil_ref);
|
||||||
@@ -4605,7 +4600,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
|
|||||||
occlusion_query_set: None,
|
occlusion_query_set: None,
|
||||||
multiview_mask: 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_pipeline(&self.wire_pipeline);
|
||||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||||
// In a filled-with-edges mode the mesh outline edges frame the shaded
|
// 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,
|
occlusion_query_set: None,
|
||||||
multiview_mask: 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_pipeline(&self.text_pipeline);
|
||||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||||
pass.set_stencil_reference(stencil_ref);
|
pass.set_stencil_reference(stencil_ref);
|
||||||
@@ -4809,7 +4804,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
|
|||||||
occlusion_query_set: None,
|
occlusion_query_set: None,
|
||||||
multiview_mask: 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_pipeline(&self.wipeout_pipeline);
|
||||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||||
pass.set_stencil_reference(stencil_ref);
|
pass.set_stencil_reference(stencil_ref);
|
||||||
@@ -4859,7 +4854,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
|
|||||||
occlusion_query_set: None,
|
occlusion_query_set: None,
|
||||||
multiview_mask: 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_bind_group(0, &self.uniform_bind_group, &[]);
|
||||||
pass.set_stencil_reference(stencil_ref);
|
pass.set_stencil_reference(stencil_ref);
|
||||||
if !self.gpu_selected_wires.is_empty() {
|
if !self.gpu_selected_wires.is_empty() {
|
||||||
@@ -5002,6 +4997,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
|
|||||||
1.0,
|
1.0,
|
||||||
);
|
);
|
||||||
pass.set_pipeline(&self.blit_pipeline);
|
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.set_bind_group(0, &self.blit_bind_group, &[]);
|
||||||
pass.draw(0..6, 0..1);
|
pass.draw(0..6, 0..1);
|
||||||
}
|
}
|
||||||
|
|||||||
+316
-59
@@ -395,32 +395,23 @@ impl shader::Primitive for Primitive {
|
|||||||
inner.slot_id = vp.instance_id;
|
inner.slot_id = vp.instance_id;
|
||||||
inner.forget_cached_keys();
|
inner.forget_cached_keys();
|
||||||
}
|
}
|
||||||
// The MSAA / depth / resolve textures are always sized to the
|
// Round texture coverage, retaining fractional raster placement.
|
||||||
// FULL viewport rectangle (not the on-canvas-visible portion)
|
let placement = physical_viewport(
|
||||||
// so the camera matrices render at consistent aspect / scale.
|
Rectangle {
|
||||||
// The blit step picks the visible sub-rectangle out via the
|
x: (bounds.x + vp.screen_rect.x * bounds.width) * scale,
|
||||||
// shader's UV crop uniform, which lets partially off-canvas
|
y: (bounds.y + vp.screen_rect.y * bounds.height) * scale,
|
||||||
// viewports composite to their visible surface area without
|
width: vp.screen_rect.width * bounds.width * scale,
|
||||||
// drift.
|
height: vp.screen_rect.height * bounds.height * scale,
|
||||||
let clip_size = Size::new(
|
},
|
||||||
(vp.screen_rect.width * bounds.width * scale).ceil().max(1.0) as u32,
|
viewport.physical_size(),
|
||||||
(vp.screen_rect.height * bounds.height * scale).ceil().max(1.0) as u32,
|
|
||||||
);
|
);
|
||||||
|
let clip_size = placement.size;
|
||||||
|
inner.viewport = Some(placement);
|
||||||
inner.ensure_depth_texture(device, clip_size);
|
inner.ensure_depth_texture(device, clip_size);
|
||||||
let viewcube_side =
|
let viewcube_side =
|
||||||
(crate::scene::VIEWCUBE_RENDER_PX.ceil() * scale).ceil().max(1.0) as u32;
|
(crate::scene::VIEWCUBE_RENDER_PX.ceil() * scale).ceil().max(1.0) as u32;
|
||||||
inner
|
inner.viewcube.ensure_depth_texture(device, Size::new(viewcube_side, viewcube_side));
|
||||||
.viewcube
|
inner.upload_blit_uv(queue, placement.uv_offset, placement.uv_scale);
|
||||||
.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.upload_background_images(
|
inner.upload_background_images(
|
||||||
device,
|
device,
|
||||||
queue,
|
queue,
|
||||||
@@ -440,7 +431,7 @@ impl shader::Primitive for Primitive {
|
|||||||
// drawing size. The ViewCube is excluded from the signature and
|
// drawing size. The ViewCube is excluded from the signature and
|
||||||
// keeps updating in its own always-on pass, so cube hover still
|
// keeps updating in its own always-on pass, so cube hover still
|
||||||
// tracks while the scene is cached.
|
// 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
|
// REDRAW bypass: pin render_sig to force a full pass this frame
|
||||||
// even if the signature is otherwise unchanged. The request is
|
// even if the signature is otherwise unchanged. The request is
|
||||||
// one-shot per viewport (consumed by the builder that produced
|
// one-shot per viewport (consumed by the builder that produced
|
||||||
@@ -1311,8 +1302,6 @@ retained_contributors={}",
|
|||||||
) {
|
) {
|
||||||
let nav_render_started = iced::time::Instant::now();
|
let nav_render_started = iced::time::Instant::now();
|
||||||
pipeline.frame_rendered.store(true, std::sync::atomic::Ordering::Relaxed);
|
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_right = clip.x + clip.width;
|
||||||
let clip_bottom = clip.y + clip.height;
|
let clip_bottom = clip.y + clip.height;
|
||||||
for vp in &self.viewports {
|
for vp in &self.viewports {
|
||||||
@@ -1322,27 +1311,25 @@ retained_contributors={}",
|
|||||||
let Some(inner) = pipeline.inners.get(*slot) else {
|
let Some(inner) = pipeline.inners.get(*slot) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
// Where the viewport would land on the surface in absolute
|
let Some(placement) = inner.viewport else {
|
||||||
// pixels (i32 because either edge may stick off the canvas).
|
continue;
|
||||||
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 surface_dest = placement.surface;
|
||||||
let vp_full_w = (vp.screen_rect.width * cw).max(1.0) as i32;
|
let left = surface_dest.x.max(clip.x);
|
||||||
let vp_full_h = (vp.screen_rect.height * ch).max(1.0) as i32;
|
let top = surface_dest.y.max(clip.y);
|
||||||
// Intersect with the surface clip — that's the slice we blit.
|
let surface_clip = Rectangle {
|
||||||
let dest_x = vp_full_x.max(clip.x as i32);
|
x: left,
|
||||||
let dest_y = vp_full_y.max(clip.y as i32);
|
y: top,
|
||||||
let dest_right = (vp_full_x + vp_full_w).min(clip_right as i32);
|
width: (surface_dest.x + surface_dest.width).min(clip_right).saturating_sub(left),
|
||||||
let dest_bottom = (vp_full_y + vp_full_h).min(clip_bottom as i32);
|
height: (surface_dest.y + surface_dest.height).min(clip_bottom).saturating_sub(top),
|
||||||
if dest_right <= dest_x || dest_bottom <= dest_y {
|
};
|
||||||
|
if surface_clip.width == 0 || surface_clip.height == 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let surface_dest = Rectangle {
|
let vp_full_x = surface_dest.x as i32;
|
||||||
x: dest_x as u32,
|
let vp_full_y = surface_dest.y as i32;
|
||||||
y: dest_y as u32,
|
let vp_full_w = surface_dest.width as i32;
|
||||||
width: (dest_right - dest_x) as u32,
|
let vp_full_h = surface_dest.height as i32;
|
||||||
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);
|
|
||||||
// `mesh_fill` is false for Wireframe 2D / Wireframe 3D — flip
|
// `mesh_fill` is false for Wireframe 2D / Wireframe 3D — flip
|
||||||
// the draw path so meshes use the wireframe pipeline + the
|
// the draw path so meshes use the wireframe pipeline + the
|
||||||
// pre-built triangle-edge index buffer.
|
// pre-built triangle-edge index buffer.
|
||||||
@@ -1350,8 +1337,9 @@ retained_contributors={}",
|
|||||||
inner.render(
|
inner.render(
|
||||||
encoder,
|
encoder,
|
||||||
target,
|
target,
|
||||||
vp_size,
|
placement.raster,
|
||||||
surface_dest,
|
surface_dest,
|
||||||
|
surface_clip,
|
||||||
self.bg_color,
|
self.bg_color,
|
||||||
mesh_wireframe,
|
mesh_wireframe,
|
||||||
vp.hidden_line,
|
vp.hidden_line,
|
||||||
@@ -1363,6 +1351,8 @@ retained_contributors={}",
|
|||||||
// rejects negative origins, and a clamped cube would scale
|
// rejects negative origins, and a clamped cube would scale
|
||||||
// distortedly. The active viewport is normally fully visible.
|
// distortedly. The active viewport is normally fully visible.
|
||||||
if vp.show_viewcube
|
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_x >= clip.x as i32
|
||||||
&& vp_full_y >= clip.y as i32
|
&& vp_full_y >= clip.y as i32
|
||||||
&& vp_full_x + vp_full_w <= clip_right 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
|
/// live preview IS included (its coordinates), so a rubber-band tracking the
|
||||||
/// cursor still renders, and the frame where the preview clears erases it
|
/// cursor still renders, and the frame where the preview clears erases it
|
||||||
/// instead of freezing the last overlay on screen.
|
/// 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};
|
use std::hash::{Hash, Hasher};
|
||||||
let mut h = rustc_hash::FxHasher::default();
|
let mut h = rustc_hash::FxHasher::default();
|
||||||
// Camera + per-view shading flags all live in the uniforms (view_rot, eye
|
// 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.
|
// frame (skip_hatch flips false) re-renders with hatches and re-caches.
|
||||||
vp.skip_hatch.hash(&mut h);
|
vp.skip_hatch.hash(&mut h);
|
||||||
vp.skip_background.hash(&mut h);
|
vp.skip_background.hash(&mut h);
|
||||||
clip_w.hash(&mut h);
|
placement.size.width.hash(&mut h);
|
||||||
clip_h.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
|
// Live overlay (command preview / interim / grip drag). Small — a handful
|
||||||
// of wires — so hashing its coordinates is cheap and catches the endpoint
|
// of wires — so hashing its coordinates is cheap and catches the endpoint
|
||||||
// moving with the cursor as well as the preview appearing / clearing.
|
// 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()
|
h.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// On-canvas-visible UV crop on one axis. `pos` and `size` are in the
|
#[derive(Clone, Copy)]
|
||||||
/// shader widget's normalized 0..1 coords. Returns `(uv_offset, uv_scale)`
|
pub(in crate::scene) struct PhysicalViewport {
|
||||||
/// applied as `actual_uv = quad_uv * uv_scale + uv_offset` in the blit
|
size: Size<u32>,
|
||||||
/// shader — identity `(0.0, 1.0)` for fully on-canvas viewports.
|
raster: Rectangle,
|
||||||
fn uv_crop_axis(pos: f32, size: f32) -> (f32, f32) {
|
surface: Rectangle<u32>,
|
||||||
if size <= 0.0 {
|
uv_offset: [f32; 2],
|
||||||
return (0.0, 1.0);
|
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
|
/// Apply a clip-space crop to `view_proj` so the sub-rect of the original
|
||||||
|
|||||||
Reference in new issue
Block a user