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 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-08 18:20:27 +03:00
1 parent b3c8c0a2f1
commit 54a29409e7
1 file changed
+44 -6
+44 -6
View File
@@ -90,7 +90,16 @@ pub struct Pipeline {
/// analogue of the selected-wire xray overlay). Rebuilt on selection change.
text_highlight_vbuf: Option<wgpu::Buffer>,
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>,
/// 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,
/// 4× MSAA color buffer for the main drawing passes.
msaa_view: wgpu::TextureView,
@@ -1167,6 +1176,9 @@ impl Pipeline {
hatch_batched_bgl1,
image_bgl1,
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,
msaa_view,
resolve_view,
@@ -1630,10 +1642,21 @@ impl Pipeline {
/// Write the blit shader's UV crop uniform. Call in `prepare` (the only
/// 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]) {
// 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(
&self.blit_uniform_buffer,
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 ─────────────────────
// Both msaa_view and resolve_view are sized to clip_bounds, so the
// resolve does NOT touch any pixels outside the shader widget's area.
// ── Resolve MSAA → resolve texture ────────────────────────────────
// Both are the same (rounded `alloc_size`) offscreen texture, so the
// 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 {
label: Some("msaa.resolve_pass"),
@@ -2314,7 +2339,12 @@ impl Pipeline {
}
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);
self.depth_view = depth_tex.create_view(&wgpu::TextureViewDescriptor::default());
let msaa_tex = create_msaa_texture(device, size, self.surface_format);
@@ -2340,11 +2370,19 @@ impl Pipeline {
],
});
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:
/// diagonal > 200 px → 0 (HIGH)
/// diagonal 50–200 px → 1 (MID)