forked from 1b-agent/opencadstudio-upstream
fix(viewport): clip MSAA to canvas, crop view_proj instead
The previous off-canvas fix sized each per-viewport MSAA / depth /
resolve texture to the full viewport rect, then UV-cropped the blit.
With deep paper-space zoom that rect can grow past wgpu's 8192-pixel
texture limit and the program panics in `Device::create_texture`
("Dimension Y value 9493 exceeds the limit of 8192").
`viewport_data_for` now clips the screen rect to the canvas before
returning the `ViewportData`, so the per-viewport textures are bounded
by the surface (≤ canvas size, always within the GPU limit). The
camera frustum is still built at the *full* vp aspect (so the world it
projects matches the viewport entity), and a new `crop_view_proj`
post-multiplies the standard view_proj by a clip-space transform that
remaps the visible sub-rect to NDC `[-1, 1]^2` — the off-center ortho
that lets the visible-sized MSAA carry the correct portion of the
view. `viewport_size` is set to the visible size for the wire shader's
screen-space line extrusion; `world_per_pixel` is crop-invariant (the
`vs` factor cancels) so the value `Uniforms::new` computes from the
full bounds remains correct.
Viewports whose screen rect doesn't intersect the canvas at all return
`None` and are filtered out before any inner pipeline is allocated.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
1 parent
3d24dd5168
commit
2b5db8501b
1 file changed
+78
-13
+78
-13
@@ -339,6 +339,32 @@ fn uv_crop_axis(pos: f32, size: f32) -> (f32, f32) {
|
||||
(left_off / size, visible / size)
|
||||
}
|
||||
|
||||
/// Apply a clip-space crop to `view_proj` so the sub-rect of the original
|
||||
/// view defined by UV offset `(uo, vo)` + scale `(us, vs)` is remapped to
|
||||
/// NDC `[-1, 1]^2`. Identity transform when the sub-rect is the whole
|
||||
/// view (`uo=vo=0`, `us=vs=1`). Used by viewports that hang off the
|
||||
/// canvas — the camera frustum stays at full-vp aspect, but only the
|
||||
/// visible portion lands in the MSAA target.
|
||||
fn crop_view_proj(view_proj: glam::Mat4, uo: f32, vo: f32, us: f32, vs: f32) -> glam::Mat4 {
|
||||
// Build the matrix that maps the visible clip-space sub-rect
|
||||
// x ∈ [2uo - 1, 2(uo+us) - 1]
|
||||
// y ∈ [1 - 2(vo+vs), 1 - 2vo]
|
||||
// back to NDC [-1, 1]^2. (Texture v is top-down → camera y flips.)
|
||||
let us = us.max(1e-6);
|
||||
let vs = vs.max(1e-6);
|
||||
let sx = 1.0 / us;
|
||||
let sy = 1.0 / vs;
|
||||
let tx = (1.0 - 2.0 * uo - us) / us;
|
||||
let ty = -(1.0 - 2.0 * vo - vs) / vs;
|
||||
let crop = glam::Mat4::from_cols_array(&[
|
||||
sx, 0.0, 0.0, 0.0, // col 0
|
||||
0.0, sy, 0.0, 0.0, // col 1
|
||||
0.0, 0.0, 1.0, 0.0, // col 2
|
||||
tx, ty, 0.0, 1.0, // col 3
|
||||
]);
|
||||
crop * view_proj
|
||||
}
|
||||
|
||||
// ── Render-style helpers (impl Scene) ────────────────────────────────────
|
||||
|
||||
impl Scene {
|
||||
@@ -510,7 +536,7 @@ impl Scene {
|
||||
let bg_color = [0.0, 0.0, 0.0, 0.0];
|
||||
let viewports: Vec<ViewportData> = instances
|
||||
.iter()
|
||||
.map(|inst| self.viewport_data_for(inst, canvas, hover_region))
|
||||
.filter_map(|inst| self.viewport_data_for(inst, canvas, hover_region))
|
||||
.collect();
|
||||
// Empty viewports → blit nothing. The widget container's
|
||||
// background (model bg or the PaperCanvas widget below) stays
|
||||
@@ -527,10 +553,33 @@ impl Scene {
|
||||
inst: &ViewportInstance,
|
||||
canvas: (f32, f32),
|
||||
hover_region: Option<usize>,
|
||||
) -> ViewportData {
|
||||
) -> Option<ViewportData> {
|
||||
let flags = render_mode_flags(inst.render_mode);
|
||||
let view_wireframe = !flags.face3d_fill;
|
||||
|
||||
// Clip the viewport rect to the canvas; size the per-viewport MSAA
|
||||
// / depth / resolve textures to that visible portion. Sizing them
|
||||
// to the full vp rect would blow past wgpu's per-dimension texture
|
||||
// limit (8192 on common GPUs) once paper-space zoom grows the rect
|
||||
// far enough off the canvas.
|
||||
let full = inst.screen_rect;
|
||||
if full.width <= 0.0 || full.height <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let visible_x = full.x.max(0.0);
|
||||
let visible_y = full.y.max(0.0);
|
||||
let visible_x_end = (full.x + full.width).min(canvas.0);
|
||||
let visible_y_end = (full.y + full.height).min(canvas.1);
|
||||
let visible_w = (visible_x_end - visible_x).max(0.0);
|
||||
let visible_h = (visible_y_end - visible_y).max(0.0);
|
||||
if visible_w < 1.0 || visible_h < 1.0 {
|
||||
return None;
|
||||
}
|
||||
let uo = ((visible_x - full.x) / full.width).clamp(0.0, 1.0);
|
||||
let vo = ((visible_y - full.y) / full.height).clamp(0.0, 1.0);
|
||||
let us = (visible_w / full.width).clamp(0.0, 1.0);
|
||||
let vs = (visible_h / full.height).clamp(0.0, 1.0);
|
||||
|
||||
let base_arc = if inst.handle == acadrust::Handle::NULL {
|
||||
self.entity_wires_arc()
|
||||
} else {
|
||||
@@ -548,25 +597,41 @@ impl Scene {
|
||||
Arc::new(v)
|
||||
};
|
||||
|
||||
// Camera uniforms use the viewport's pixel size for aspect / ortho.
|
||||
let bounds_px = Rectangle {
|
||||
// Build the camera at the *full* viewport's aspect so the ortho
|
||||
// frustum matches what the viewport entity stores, then post-
|
||||
// multiply by a clip-space "zoom into the visible sub-rect" that
|
||||
// maps the visible portion to NDC [-1, 1]. Geometry passes
|
||||
// rasterize into a visible-sized MSAA, so `viewport_size` (used
|
||||
// by the wire shader to extrude line thickness in screen pixels)
|
||||
// must be the visible size — but `world_per_pixel` is invariant
|
||||
// under cropping (full_h cancels with vs) so the value computed
|
||||
// from the full bounds is the one we want.
|
||||
let full_bounds = Rectangle {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: inst.screen_rect.width.max(1.0),
|
||||
height: inst.screen_rect.height.max(1.0),
|
||||
width: full.width.max(1.0),
|
||||
height: full.height.max(1.0),
|
||||
};
|
||||
let mut uniforms =
|
||||
Uniforms::new(&inst.camera, bounds_px, self.document.header.lineweight_display);
|
||||
Uniforms::new(&inst.camera, full_bounds, self.document.header.lineweight_display);
|
||||
uniforms.view_proj = crop_view_proj(uniforms.view_proj, uo, vo, us, vs);
|
||||
uniforms.viewport_size = [visible_w, visible_h];
|
||||
uniforms.flat_shade = if flags.flat_shade { 1.0 } else { 0.0 };
|
||||
|
||||
// `screen_rect` carries the *visible* sub-rectangle in normalized
|
||||
// canvas coords — that's what `Pipeline::prepare` uses to size
|
||||
// the per-viewport textures and what `Primitive::render` uses to
|
||||
// pick the surface destination. The UV crop uniform reads as
|
||||
// identity here, since the texture already covers exactly the
|
||||
// visible portion.
|
||||
let screen_rect = Rectangle {
|
||||
x: inst.screen_rect.x / canvas.0,
|
||||
y: inst.screen_rect.y / canvas.1,
|
||||
width: inst.screen_rect.width / canvas.0,
|
||||
height: inst.screen_rect.height / canvas.1,
|
||||
x: visible_x / canvas.0,
|
||||
y: visible_y / canvas.1,
|
||||
width: visible_w / canvas.0,
|
||||
height: visible_h / canvas.1,
|
||||
};
|
||||
|
||||
ViewportData {
|
||||
Some(ViewportData {
|
||||
wires: all_wires,
|
||||
face3d_wires: Arc::new(face3d_wires),
|
||||
hatches: self.hatch_models_arc(),
|
||||
@@ -586,7 +651,7 @@ impl Scene {
|
||||
geometry_epoch: self.geometry_epoch,
|
||||
camera_generation: self.camera_generation,
|
||||
screen_rect,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Update viewcube hover state from cursor position within `bounds`.
|
||||
|
||||
Reference in new issue
Block a user