fix(web): grow-only viewport textures to stop Windows-Firefox divider-drag freeze (#191)
Dragging a model-tile pane_grid divider changed each pane's pixel size every frame, and `ensure_depth_texture` recreated the depth + 4×MSAA + resolve textures on any size delta. On Windows Firefox (WebGL2 → ANGLE → D3D11) this per-frame texture realloc hangs the D3D driver — the same hang class as the native DX12 lock in #120. Linux Firefox (native GL) and Chromium/Edge tolerate the churn, which is why the freeze is specific to Windows Firefox. Round the depth/MSAA/resolve allocation up to a 128px grid and only recreate when the rounded size changes, so a live drag reallocates at most once per 128px instead of every frame. Geometry passes still render at the real size into the [0,0,render] corner of the larger texture, and the blit UV is scaled by render/alloc so it samples exactly the filled region — visually a no-op on every platform. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b3c8c0a2f1
commit
54a29409e7
1 changed files with 44 additions and 6 deletions
|
|
@ -90,7 +90,16 @@ pub struct Pipeline {
|
||||||
/// analogue of the selected-wire xray overlay). Rebuilt on selection change.
|
/// analogue of the selected-wire xray overlay). Rebuilt on selection change.
|
||||||
text_highlight_vbuf: Option<wgpu::Buffer>,
|
text_highlight_vbuf: Option<wgpu::Buffer>,
|
||||||
text_highlight_vcount: u32,
|
text_highlight_vcount: u32,
|
||||||
|
/// Last requested render size (the full viewport rect, in pixels). The
|
||||||
|
/// 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>,
|
depth_texture_size: Size<u32>,
|
||||||
|
/// 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
|
||||||
|
/// textures every frame. Recreation is what hangs the ANGLE → D3D11 path
|
||||||
|
/// on Windows-Firefox (issue #191); grow-only bucketing makes it rare.
|
||||||
|
alloc_size: Size<u32>,
|
||||||
depth_view: wgpu::TextureView,
|
depth_view: wgpu::TextureView,
|
||||||
/// 4× MSAA color buffer for the main drawing passes.
|
/// 4× MSAA color buffer for the main drawing passes.
|
||||||
msaa_view: wgpu::TextureView,
|
msaa_view: wgpu::TextureView,
|
||||||
|
|
@ -1167,6 +1176,9 @@ impl Pipeline {
|
||||||
hatch_batched_bgl1,
|
hatch_batched_bgl1,
|
||||||
image_bgl1,
|
image_bgl1,
|
||||||
depth_texture_size: Size::new(1, 1),
|
depth_texture_size: Size::new(1, 1),
|
||||||
|
// (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),
|
||||||
depth_view,
|
depth_view,
|
||||||
msaa_view,
|
msaa_view,
|
||||||
resolve_view,
|
resolve_view,
|
||||||
|
|
@ -1630,10 +1642,21 @@ impl Pipeline {
|
||||||
/// Write the blit shader's UV crop uniform. Call in `prepare` (the only
|
/// Write the blit shader's UV crop uniform. Call in `prepare` (the only
|
||||||
/// place with a `&Queue`) — `render` then just submits the draw call.
|
/// place with a `&Queue`) — `render` then just submits the draw call.
|
||||||
pub fn upload_blit_uv(&self, queue: &wgpu::Queue, uv_offset: [f32; 2], uv_scale: [f32; 2]) {
|
pub fn upload_blit_uv(&self, queue: &wgpu::Queue, uv_offset: [f32; 2], uv_scale: [f32; 2]) {
|
||||||
|
// The geometry passes render at `depth_texture_size` into the top-left
|
||||||
|
// corner of the (possibly larger) `alloc_size` resolve texture, so the
|
||||||
|
// rendered image occupies UV [0, render/alloc]. Scale the incoming crop
|
||||||
|
// — computed in full-image-normalized units — down into that region.
|
||||||
|
let fx = self.depth_texture_size.width as f32 / self.alloc_size.width.max(1) as f32;
|
||||||
|
let fy = self.depth_texture_size.height as f32 / self.alloc_size.height.max(1) as f32;
|
||||||
queue.write_buffer(
|
queue.write_buffer(
|
||||||
&self.blit_uniform_buffer,
|
&self.blit_uniform_buffer,
|
||||||
0,
|
0,
|
||||||
bytemuck::cast_slice(&[uv_offset[0], uv_offset[1], uv_scale[0], uv_scale[1]]),
|
bytemuck::cast_slice(&[
|
||||||
|
uv_offset[0] * fx,
|
||||||
|
uv_offset[1] * fy,
|
||||||
|
uv_scale[0] * fx,
|
||||||
|
uv_scale[1] * fy,
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2255,9 +2278,11 @@ impl Pipeline {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Resolve MSAA → clip-sized resolve texture ─────────────────────
|
// ── Resolve MSAA → resolve texture ────────────────────────────────
|
||||||
// Both msaa_view and resolve_view are sized to clip_bounds, so the
|
// Both are the same (rounded `alloc_size`) offscreen texture, so the
|
||||||
// resolve does NOT touch any pixels outside the shader widget's area.
|
// resolve never touches the surface. Only the drawn [0, render_size]
|
||||||
|
// corner holds content; the rounded border stays the cleared bg color
|
||||||
|
// and is never sampled (the blit UV is scaled to render/alloc).
|
||||||
{
|
{
|
||||||
let _resolve = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
let _resolve = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
label: Some("msaa.resolve_pass"),
|
label: Some("msaa.resolve_pass"),
|
||||||
|
|
@ -2314,7 +2339,12 @@ impl Pipeline {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ensure_depth_texture(&mut self, device: &wgpu::Device, size: Size<u32>) {
|
pub fn ensure_depth_texture(&mut self, device: &wgpu::Device, size: Size<u32>) {
|
||||||
if self.depth_texture_size != size {
|
// Record the requested render size every frame (the blit UV scale reads
|
||||||
|
// it); only reallocate the textures when the *rounded* size changes.
|
||||||
|
self.depth_texture_size = size;
|
||||||
|
let alloc = Size::new(round_up_tex(size.width), round_up_tex(size.height));
|
||||||
|
if self.alloc_size != alloc {
|
||||||
|
let size = alloc;
|
||||||
let depth_tex = create_depth_texture(device, size);
|
let depth_tex = create_depth_texture(device, size);
|
||||||
self.depth_view = depth_tex.create_view(&wgpu::TextureViewDescriptor::default());
|
self.depth_view = depth_tex.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
let msaa_tex = create_msaa_texture(device, size, self.surface_format);
|
let msaa_tex = create_msaa_texture(device, size, self.surface_format);
|
||||||
|
|
@ -2340,11 +2370,19 @@ impl Pipeline {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
self.resolve_view = resolve_view;
|
self.resolve_view = resolve_view;
|
||||||
self.depth_texture_size = size;
|
self.alloc_size = alloc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Round a texture dimension up to a coarse grid so a live divider drag
|
||||||
|
/// doesn't recreate the depth / MSAA / resolve textures on every frame. See
|
||||||
|
/// `alloc_size` — this is the mitigation for the Windows-Firefox freeze (#191).
|
||||||
|
fn round_up_tex(n: u32) -> u32 {
|
||||||
|
const GRID: u32 = 128;
|
||||||
|
((n.max(1) + GRID - 1) / GRID) * GRID
|
||||||
|
}
|
||||||
|
|
||||||
/// Project a mesh's `world_aabb` and pick a LOD slot:
|
/// Project a mesh's `world_aabb` and pick a LOD slot:
|
||||||
/// diagonal > 200 px → 0 (HIGH)
|
/// diagonal > 200 px → 0 (HIGH)
|
||||||
/// diagonal 50–200 px → 1 (MID)
|
/// diagonal 50–200 px → 1 (MID)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue