Skip to main content

viewport_lib/renderer/
render.rs

1use super::*;
2use wgpu::util::DeviceExt;
3
4impl ViewportRenderer {
5    pub(crate) fn paint_to<'rp>(&self, render_pass: &mut wgpu::RenderPass<'rp>, frame: &FrameData) {
6        let vp_idx = frame.camera.viewport_index;
7        let camera_bg = self.viewport_camera_bind_group(vp_idx);
8        let grid_bg = self.viewport_grid_bind_group(vp_idx);
9        let vp_slot = self.viewport_slots.get(vp_idx);
10        emit_draw_calls!(
11            &self.resources,
12            &mut *render_pass,
13            frame,
14            self.use_instancing,
15            &self.instanced_batches,
16            camera_bg,
17            grid_bg,
18            &self.compute_filter_results,
19            vp_slot,
20            &self.wireframe_bind_groups
21        );
22        emit_scivis_draw_calls!(
23            &self.resources,
24            &mut *render_pass,
25            &self.point_cloud_gpu_data,
26            &self.glyph_gpu_data,
27            &self.polyline_gpu_data,
28            &self.volume_gpu_data,
29            &self.streamtube_gpu_data,
30            camera_bg,
31            &self.tube_gpu_data,
32            &self.image_slice_gpu_data,
33            &self.tensor_glyph_gpu_data,
34            &self.ribbon_gpu_data,
35            &self.volume_surface_slice_gpu_data,
36            &self.sprite_gpu_data,
37            false
38        );
39        // Gaussian splats (alpha-blended, back-to-front sorted, no depth write).
40        if !self.gaussian_splat_draw_data.is_empty() {
41            if let Some(ref dual) = self.resources.gaussian_splat_pipeline {
42                render_pass.set_pipeline(dual.for_format(false));
43                render_pass.set_bind_group(0, camera_bg, &[]);
44                for dd in &self.gaussian_splat_draw_data {
45                    if dd.wireframe {
46                        continue;
47                    }
48                    if let Some(set) = self.resources.gaussian_splat_store.get(dd.store_index) {
49                        if let Some(Some(vp_sort)) = set.viewport_sort.get(dd.viewport_index) {
50                            render_pass.set_bind_group(1, &vp_sort.render_bg, &[]);
51                            render_pass.draw(0..6, 0..dd.count);
52                        }
53                    }
54                }
55            }
56        }
57        // TransparentVolumeMesh boundary wireframe overlay.
58        if !self.tvm_wireframe_draws.is_empty() {
59            if let Some(ref tvm_bg) = self.tvm_wireframe_bg {
60                render_pass.set_bind_group(0, camera_bg, &[]);
61                for mesh_id in &self.tvm_wireframe_draws {
62                    if let Some(mesh) = self.resources.mesh_store.get(*mesh_id) {
63                        render_pass.set_pipeline(&self.resources.wireframe_pipeline);
64                        render_pass.set_bind_group(1, tvm_bg, &[]);
65                        render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
66                        render_pass.set_index_buffer(
67                            mesh.edge_index_buffer.slice(..),
68                            wgpu::IndexFormat::Uint32,
69                        );
70                        render_pass.draw_indexed(0..mesh.edge_index_count, 0, 0..1);
71                    }
72                }
73            }
74        }
75        // Phase 16 : GPU implicit surface (depth-writes enabled, LessEqual compare).
76        if !self.implicit_gpu_data.is_empty() {
77            if let Some(ref dual) = self.resources.implicit_pipeline {
78                render_pass.set_pipeline(dual.for_format(false));
79                render_pass.set_bind_group(0, camera_bg, &[]);
80                for gpu in &self.implicit_gpu_data {
81                    render_pass.set_bind_group(1, &gpu.bind_group, &[]);
82                    render_pass.draw(0..6, 0..1);
83                }
84            }
85        }
86        // Phase 17 : GPU marching cubes indirect draw.
87        if !self.mc_gpu_data.is_empty() {
88            render_pass.set_bind_group(0, camera_bg, &[]);
89            for mc in &self.mc_gpu_data {
90                let vol = &self.resources.mc_volumes[mc.volume_idx];
91                if mc.wireframe || frame.viewport.wireframe_mode {
92                    if let Some(ref dual) = self.resources.mc_wireframe_pipeline {
93                        render_pass.set_pipeline(dual.for_format(false));
94                        for (slab, wire_bg) in
95                            vol.slabs.iter().zip(mc.wire_slab_bgs.iter())
96                        {
97                            render_pass.set_bind_group(1, wire_bg, &[]);
98                            render_pass.draw_indirect(&slab.wire_indirect_buf, 0);
99                        }
100                    }
101                } else if let Some(ref dual) = self.resources.mc_surface_pipeline {
102                    render_pass.set_pipeline(dual.for_format(false));
103                    render_pass.set_bind_group(1, &mc.render_bg, &[]);
104                    for slab in &vol.slabs {
105                        render_pass.set_vertex_buffer(0, slab.vertex_buf.slice(..));
106                        render_pass.draw_indirect(&slab.indirect_buf, 0);
107                    }
108                }
109            }
110        }
111        // Outline composite after all scene content so translucent layers don't overdraw.
112        emit_outline_composite!(&self.resources, &mut *render_pass, vp_slot);
113        // Sub-object highlight (LDR path) : face fill, edge lines, vertex/point sprites.
114        if let Some(sub_hl) = self
115            .viewport_slots
116            .get(vp_idx)
117            .and_then(|s| s.sub_highlight.as_ref())
118        {
119            if let (Some(fill_pl), Some(edge_pl), Some(sprite_pl)) = (
120                &self.resources.sub_highlight_fill_ldr_pipeline,
121                &self.resources.sub_highlight_edge_ldr_pipeline,
122                &self.resources.sub_highlight_sprite_ldr_pipeline,
123            ) {
124                if sub_hl.fill_vertex_count > 0 {
125                    render_pass.set_pipeline(fill_pl);
126                    render_pass.set_bind_group(0, camera_bg, &[]);
127                    render_pass.set_bind_group(1, &sub_hl.fill_bind_group, &[]);
128                    render_pass.set_vertex_buffer(0, sub_hl.fill_vertex_buf.slice(..));
129                    render_pass.draw(0..sub_hl.fill_vertex_count, 0..1);
130                }
131                if sub_hl.edge_segment_count > 0 {
132                    render_pass.set_pipeline(edge_pl);
133                    render_pass.set_bind_group(0, camera_bg, &[]);
134                    render_pass.set_bind_group(1, &sub_hl.edge_bind_group, &[]);
135                    render_pass.set_vertex_buffer(0, sub_hl.edge_vertex_buf.slice(..));
136                    render_pass.draw(0..6, 0..sub_hl.edge_segment_count);
137                }
138                if sub_hl.sprite_point_count > 0 {
139                    render_pass.set_pipeline(sprite_pl);
140                    render_pass.set_bind_group(0, camera_bg, &[]);
141                    render_pass.set_bind_group(1, &sub_hl.sprite_bind_group, &[]);
142                    render_pass.set_vertex_buffer(0, sub_hl.sprite_vertex_buf.slice(..));
143                    render_pass.draw(0..6, 0..sub_hl.sprite_point_count);
144                }
145            }
146        }
147        // Phase 10B : screen-space image overlays (always on top, no depth test).
148        if !self.screen_image_gpu_data.is_empty() {
149            if let Some(pipeline) = &self.resources.screen_image_pipeline {
150                render_pass.set_pipeline(pipeline);
151                for gpu in &self.screen_image_gpu_data {
152                    render_pass.set_bind_group(0, &gpu.bind_group, &[]);
153                    render_pass.draw(0..6, 0..1);
154                }
155            }
156        }
157        // SDF overlay shapes (drawn before rects and labels).
158        if let Some(ref sd) = self.overlay_shape_gpu_data {
159            if sd.vertex_count > 0 {
160                if let Some(pipeline) = &self.resources.overlay_shape_pipeline {
161                    if let Some(vbuf) = &sd.vertex_buf {
162                        render_pass.set_pipeline(pipeline);
163                        render_pass.set_vertex_buffer(0, vbuf.slice(..));
164                        render_pass.draw(0..sd.vertex_count, 0..1);
165                    }
166                }
167            }
168            if !sd.tex_batches.is_empty() {
169                if let Some(pipeline) = &self.resources.overlay_shape_tex_pipeline {
170                    render_pass.set_pipeline(pipeline);
171                    for batch in &sd.tex_batches {
172                        render_pass.set_bind_group(0, &batch.bind_group, &[]);
173                        render_pass.set_vertex_buffer(0, batch.vertex_buf.slice(..));
174                        render_pass.draw(0..batch.vertex_count, 0..1);
175                    }
176                }
177            }
178        }
179        // Overlay rects (drawn before labels so they act as backgrounds).
180        if let Some(ref rr) = self.overlay_rect_gpu_data {
181            if let Some(pipeline) = &self.resources.overlay_text_pipeline {
182                render_pass.set_pipeline(pipeline);
183                render_pass.set_bind_group(0, &rr.bind_group, &[]);
184                render_pass.set_vertex_buffer(0, rr.vertex_buf.slice(..));
185                render_pass.draw(0..rr.vertex_count, 0..1);
186            }
187        }
188        // Overlay labels (always on top, after screen images).
189        if let Some(ref ld) = self.label_gpu_data {
190            if let Some(pipeline) = &self.resources.overlay_text_pipeline {
191                render_pass.set_pipeline(pipeline);
192                render_pass.set_bind_group(0, &ld.bind_group, &[]);
193                render_pass.set_vertex_buffer(0, ld.vertex_buf.slice(..));
194                render_pass.draw(0..ld.vertex_count, 0..1);
195            }
196        }
197        // Scalar bars (drawn after labels).
198        if let Some(ref sb) = self.scalar_bar_gpu_data {
199            if let Some(pipeline) = &self.resources.overlay_text_pipeline {
200                render_pass.set_pipeline(pipeline);
201                render_pass.set_bind_group(0, &sb.bind_group, &[]);
202                render_pass.set_vertex_buffer(0, sb.vertex_buf.slice(..));
203                render_pass.draw(0..sb.vertex_count, 0..1);
204            }
205        }
206        // Rulers (drawn after scalar bars).
207        if let Some(ref rd) = self.ruler_gpu_data {
208            if let Some(pipeline) = &self.resources.overlay_text_pipeline {
209                render_pass.set_pipeline(pipeline);
210                render_pass.set_bind_group(0, &rd.bind_group, &[]);
211                render_pass.set_vertex_buffer(0, rd.vertex_buf.slice(..));
212                render_pass.draw(0..rd.vertex_count, 0..1);
213            }
214        }
215        // Loading bars (drawn after rulers).
216        if let Some(ref lb) = self.loading_bar_gpu_data {
217            if let Some(pipeline) = &self.resources.overlay_text_pipeline {
218                render_pass.set_pipeline(pipeline);
219                render_pass.set_bind_group(0, &lb.bind_group, &[]);
220                render_pass.set_vertex_buffer(0, lb.vertex_buf.slice(..));
221                render_pass.draw(0..lb.vertex_count, 0..1);
222            }
223        }
224        // Phase 7 : overlay images (OverlayFrame, drawn last, no depth test).
225        if !self.overlay_image_gpu_data.is_empty() {
226            if let Some(pipeline) = &self.resources.screen_image_pipeline {
227                render_pass.set_pipeline(pipeline);
228                for gpu in &self.overlay_image_gpu_data {
229                    render_pass.set_bind_group(0, &gpu.bind_group, &[]);
230                    render_pass.draw(0..6, 0..1);
231                }
232            }
233        }
234    }
235
236    /// Render the scene into an intermediate dyn-res texture for the LDR callback
237    /// render path (e.g. eframe's `CallbackTrait`).
238    ///
239    /// Call from `CallbackTrait::prepare` after [`prepare`](Self::prepare), passing the
240    /// `egui_encoder`. If `current_render_scale < 1.0`, the full scene is drawn into a
241    /// scaled intermediate texture and `true` is returned. Call
242    /// [`paint_dyn_res_blit`](Self::paint_dyn_res_blit) from `CallbackTrait::paint`
243    /// instead of [`paint`](Self::paint).
244    ///
245    /// If scale is 1.0 or above, nothing is encoded and `false` is returned. Call
246    /// [`paint`](Self::paint) as normal.
247    ///
248    /// The `egui_encoder` is submitted before the surface render pass begins, so the
249    /// intermediate texture is fully written before the blit reads it.
250    pub(crate) fn prepare_ldr_dyn_res(
251        &mut self,
252        encoder: &mut wgpu::CommandEncoder,
253        device: &wgpu::Device,
254        frame: &FrameData,
255    ) -> bool {
256        if self.current_render_scale >= 1.0 - 0.001 {
257            return false;
258        }
259
260        let vp_idx = frame.camera.viewport_index;
261        let w = (frame.camera.viewport_size[0] as u32).max(1);
262        let h = (frame.camera.viewport_size[1] as u32).max(1);
263        let sw = ((w as f32 * self.current_render_scale) as u32).max(1);
264        let sh = ((h as f32 * self.current_render_scale) as u32).max(1);
265
266        self.ensure_dyn_res_target(device, vp_idx, [sw, sh], [w, h]);
267        self.resources.ensure_dyn_res_ds_pipeline(device);
268
269        let bg_colour = frame.viewport.background_colour.unwrap_or([
270            65.0 / 255.0,
271            65.0 / 255.0,
272            65.0 / 255.0,
273            1.0,
274        ]);
275
276        {
277            let slot = &self.viewport_slots[vp_idx];
278            let dr = slot.dyn_res.as_ref().unwrap();
279            let colour_view = &dr.colour_view;
280            let depth_view = &dr.depth_view;
281            let camera_bg = &slot.camera_bind_group;
282            let grid_bg = &slot.grid_bind_group;
283
284            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
285                label: Some("ldr_dyn_res_render_pass"),
286                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
287                    view: colour_view,
288                    resolve_target: None,
289                    ops: wgpu::Operations {
290                        load: wgpu::LoadOp::Clear(wgpu::Color {
291                            r: bg_colour[0] as f64,
292                            g: bg_colour[1] as f64,
293                            b: bg_colour[2] as f64,
294                            a: bg_colour[3] as f64,
295                        }),
296                        store: wgpu::StoreOp::Store,
297                    },
298                    depth_slice: None,
299                })],
300                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
301                    view: depth_view,
302                    depth_ops: Some(wgpu::Operations {
303                        load: wgpu::LoadOp::Clear(1.0),
304                        store: wgpu::StoreOp::Discard,
305                    }),
306                    stencil_ops: None,
307                }),
308                timestamp_writes: None,
309                occlusion_query_set: None,
310            });
311            emit_draw_calls!(
312                &self.resources,
313                &mut render_pass,
314                frame,
315                self.use_instancing,
316                &self.instanced_batches,
317                camera_bg,
318                grid_bg,
319                &self.compute_filter_results,
320                Some(slot),
321                &self.wireframe_bind_groups
322            );
323            emit_scivis_draw_calls!(
324                &self.resources,
325                &mut render_pass,
326                &self.point_cloud_gpu_data,
327                &self.glyph_gpu_data,
328                &self.polyline_gpu_data,
329                &self.volume_gpu_data,
330                &self.streamtube_gpu_data,
331                camera_bg,
332                &self.tube_gpu_data,
333                &self.image_slice_gpu_data,
334                &self.tensor_glyph_gpu_data,
335                &self.ribbon_gpu_data,
336                &self.volume_surface_slice_gpu_data,
337                &self.sprite_gpu_data,
338                false
339            );
340            // TransparentVolumeMesh boundary wireframe overlay.
341            if !self.tvm_wireframe_draws.is_empty() {
342                if let Some(ref tvm_bg) = self.tvm_wireframe_bg {
343                    render_pass.set_bind_group(0, camera_bg, &[]);
344                    for mesh_id in &self.tvm_wireframe_draws {
345                        if let Some(mesh) = self.resources.mesh_store.get(*mesh_id) {
346                            render_pass.set_pipeline(&self.resources.wireframe_pipeline);
347                            render_pass.set_bind_group(1, tvm_bg, &[]);
348                            render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
349                            render_pass.set_index_buffer(
350                                mesh.edge_index_buffer.slice(..),
351                                wgpu::IndexFormat::Uint32,
352                            );
353                            render_pass.draw_indexed(0..mesh.edge_index_count, 0, 0..1);
354                        }
355                    }
356                }
357            }
358            // Implicit surface.
359            if !self.implicit_gpu_data.is_empty() {
360                if let Some(ref dual) = self.resources.implicit_pipeline {
361                    render_pass.set_pipeline(dual.for_format(false));
362                    render_pass.set_bind_group(0, camera_bg, &[]);
363                    for gpu in &self.implicit_gpu_data {
364                        render_pass.set_bind_group(1, &gpu.bind_group, &[]);
365                        render_pass.draw(0..6, 0..1);
366                    }
367                }
368            }
369            // GPU marching cubes indirect draw.
370            if !self.mc_gpu_data.is_empty() {
371                if let Some(ref dual) = self.resources.mc_surface_pipeline {
372                    render_pass.set_pipeline(dual.for_format(false));
373                    render_pass.set_bind_group(0, camera_bg, &[]);
374                    for mc in &self.mc_gpu_data {
375                        let vol = &self.resources.mc_volumes[mc.volume_idx];
376                        render_pass.set_bind_group(1, &mc.render_bg, &[]);
377                        for slab in &vol.slabs {
378                            render_pass.set_vertex_buffer(0, slab.vertex_buf.slice(..));
379                            render_pass.draw_indirect(&slab.indirect_buf, 0);
380                        }
381                    }
382                }
383            }
384            // Outline composite after all scene content.
385            emit_outline_composite!(&self.resources, &mut render_pass, Some(slot));
386            // Sub-object highlight (LDR path).
387            if let Some(sub_hl) = slot.sub_highlight.as_ref() {
388                if let (Some(fill_pl), Some(edge_pl), Some(sprite_pl)) = (
389                    &self.resources.sub_highlight_fill_ldr_pipeline,
390                    &self.resources.sub_highlight_edge_ldr_pipeline,
391                    &self.resources.sub_highlight_sprite_ldr_pipeline,
392                ) {
393                    if sub_hl.fill_vertex_count > 0 {
394                        render_pass.set_pipeline(fill_pl);
395                        render_pass.set_bind_group(0, camera_bg, &[]);
396                        render_pass.set_bind_group(1, &sub_hl.fill_bind_group, &[]);
397                        render_pass.set_vertex_buffer(0, sub_hl.fill_vertex_buf.slice(..));
398                        render_pass.draw(0..sub_hl.fill_vertex_count, 0..1);
399                    }
400                    if sub_hl.edge_segment_count > 0 {
401                        render_pass.set_pipeline(edge_pl);
402                        render_pass.set_bind_group(0, camera_bg, &[]);
403                        render_pass.set_bind_group(1, &sub_hl.edge_bind_group, &[]);
404                        render_pass.set_vertex_buffer(0, sub_hl.edge_vertex_buf.slice(..));
405                        render_pass.draw(0..6, 0..sub_hl.edge_segment_count);
406                    }
407                    if sub_hl.sprite_point_count > 0 {
408                        render_pass.set_pipeline(sprite_pl);
409                        render_pass.set_bind_group(0, camera_bg, &[]);
410                        render_pass.set_bind_group(1, &sub_hl.sprite_bind_group, &[]);
411                        render_pass.set_vertex_buffer(0, sub_hl.sprite_vertex_buf.slice(..));
412                        render_pass.draw(0..6, 0..sub_hl.sprite_point_count);
413                    }
414                }
415            }
416            // Screen-space image overlays.
417            if !self.screen_image_gpu_data.is_empty() {
418                if let Some(pipeline) = &self.resources.screen_image_pipeline {
419                    render_pass.set_pipeline(pipeline);
420                    for gpu in &self.screen_image_gpu_data {
421                        render_pass.set_bind_group(0, &gpu.bind_group, &[]);
422                        render_pass.draw(0..6, 0..1);
423                    }
424                }
425            }
426            // SDF overlay shapes (drawn before rects and labels).
427            if let Some(ref sd) = self.overlay_shape_gpu_data {
428                if sd.vertex_count > 0 {
429                    if let Some(pipeline) = &self.resources.overlay_shape_pipeline {
430                        if let Some(vbuf) = &sd.vertex_buf {
431                            render_pass.set_pipeline(pipeline);
432                            render_pass.set_vertex_buffer(0, vbuf.slice(..));
433                            render_pass.draw(0..sd.vertex_count, 0..1);
434                        }
435                    }
436                }
437                if !sd.tex_batches.is_empty() {
438                    if let Some(pipeline) = &self.resources.overlay_shape_tex_pipeline {
439                        render_pass.set_pipeline(pipeline);
440                        for batch in &sd.tex_batches {
441                            render_pass.set_bind_group(0, &batch.bind_group, &[]);
442                            render_pass.set_vertex_buffer(0, batch.vertex_buf.slice(..));
443                            render_pass.draw(0..batch.vertex_count, 0..1);
444                        }
445                    }
446                }
447            }
448            // Overlay rects (drawn before labels so they act as backgrounds).
449            if let Some(ref rr) = self.overlay_rect_gpu_data {
450                if let Some(pipeline) = &self.resources.overlay_text_pipeline {
451                    render_pass.set_pipeline(pipeline);
452                    render_pass.set_bind_group(0, &rr.bind_group, &[]);
453                    render_pass.set_vertex_buffer(0, rr.vertex_buf.slice(..));
454                    render_pass.draw(0..rr.vertex_count, 0..1);
455                }
456            }
457            // Overlay labels.
458            if let Some(ref ld) = self.label_gpu_data {
459                if let Some(pipeline) = &self.resources.overlay_text_pipeline {
460                    render_pass.set_pipeline(pipeline);
461                    render_pass.set_bind_group(0, &ld.bind_group, &[]);
462                    render_pass.set_vertex_buffer(0, ld.vertex_buf.slice(..));
463                    render_pass.draw(0..ld.vertex_count, 0..1);
464                }
465            }
466            // Scalar bars.
467            if let Some(ref sb) = self.scalar_bar_gpu_data {
468                if let Some(pipeline) = &self.resources.overlay_text_pipeline {
469                    render_pass.set_pipeline(pipeline);
470                    render_pass.set_bind_group(0, &sb.bind_group, &[]);
471                    render_pass.set_vertex_buffer(0, sb.vertex_buf.slice(..));
472                    render_pass.draw(0..sb.vertex_count, 0..1);
473                }
474            }
475            // Rulers.
476            if let Some(ref rd) = self.ruler_gpu_data {
477                if let Some(pipeline) = &self.resources.overlay_text_pipeline {
478                    render_pass.set_pipeline(pipeline);
479                    render_pass.set_bind_group(0, &rd.bind_group, &[]);
480                    render_pass.set_vertex_buffer(0, rd.vertex_buf.slice(..));
481                    render_pass.draw(0..rd.vertex_count, 0..1);
482                }
483            }
484            // Loading bars.
485            if let Some(ref lb) = self.loading_bar_gpu_data {
486                if let Some(pipeline) = &self.resources.overlay_text_pipeline {
487                    render_pass.set_pipeline(pipeline);
488                    render_pass.set_bind_group(0, &lb.bind_group, &[]);
489                    render_pass.set_vertex_buffer(0, lb.vertex_buf.slice(..));
490                    render_pass.draw(0..lb.vertex_count, 0..1);
491                }
492            }
493            // Overlay images (drawn last).
494            if !self.overlay_image_gpu_data.is_empty() {
495                if let Some(pipeline) = &self.resources.screen_image_pipeline {
496                    render_pass.set_pipeline(pipeline);
497                    for gpu in &self.overlay_image_gpu_data {
498                        render_pass.set_bind_group(0, &gpu.bind_group, &[]);
499                        render_pass.draw(0..6, 0..1);
500                    }
501                }
502            }
503        }
504
505        true
506    }
507
508    /// Blit the dyn-res intermediate texture into the provided render pass.
509    ///
510    /// Call from `CallbackTrait::paint` when
511    /// [`prepare_ldr_dyn_res`](Self::prepare_ldr_dyn_res) returned `true` for the same
512    /// frame. Emits a fullscreen upscale quad into `render_pass`.
513    pub(crate) fn paint_dyn_res_blit<'rp>(
514        &self,
515        render_pass: &mut wgpu::RenderPass<'rp>,
516        frame: &FrameData,
517    ) {
518        let vp_idx = frame.camera.viewport_index;
519        if let Some(dr) = self
520            .viewport_slots
521            .get(vp_idx)
522            .and_then(|s| s.dyn_res.as_ref())
523        {
524            if let Some(pipeline) = &self.resources.dyn_res_upscale_ds_pipeline {
525                render_pass.set_pipeline(pipeline);
526                render_pass.set_bind_group(0, &dr.upscale_bind_group, &[]);
527                render_pass.draw(0..3, 0..1);
528            }
529        }
530    }
531
532    /// Run the full HDR pipeline (OIT, EDL, tone-map) for the eframe callback model.
533    ///
534    /// This is the HDR counterpart of
535    /// [`prepare_ldr_dyn_res`](Self::prepare_ldr_dyn_res) for use when
536    /// `frame.effects.post_process.enabled` is `true`.
537    ///
538    /// Internally this method:
539    /// 1. Calls [`prepare`](Self::prepare) to upload uniforms and run the shadow pass.
540    /// 2. Ensures a per-viewport intermediate texture at the viewport's native resolution.
541    /// 3. Calls the full render pipeline (including OIT and EDL) into that texture.
542    ///
543    /// The returned [`wgpu::CommandBuffer`] must be returned from
544    /// `CallbackTrait::prepare` so eframe submits it **before** the egui render pass.
545    ///
546    /// Call [`paint_hdr_blit`](Self::paint_hdr_blit) from `CallbackTrait::paint` to
547    /// composite the intermediate texture into the egui render pass.
548    pub(crate) fn prepare_hdr_callback(
549        &mut self,
550        device: &wgpu::Device,
551        queue: &wgpu::Queue,
552        frame: &FrameData,
553    ) -> wgpu::CommandBuffer {
554        self.prepare(device, queue, frame);
555
556        let vp_idx = frame.camera.viewport_index;
557        // Intermediate texture must be at physical pixel size so it matches the
558        // HDR depth buffer allocated inside render_frame_internal (which also
559        // uses physical pixels). Using logical size here produces a mismatch on
560        // hidpi displays between the colour attachment (this texture) and the
561        // depth attachment (hdr_depth_view) in the grid/overlay passes.
562        let ppp = frame.camera.pixels_per_point;
563        let w = (frame.camera.viewport_size[0] * ppp).round() as u32;
564        let h = (frame.camera.viewport_size[1] * ppp).round() as u32;
565
566        // Ensure the blit pipeline (required by create_hdr_callback_target).
567        self.resources.ensure_dyn_res_pipeline(device);
568        self.resources.ensure_dyn_res_ds_pipeline(device);
569
570        // Create or resize the per-viewport intermediate texture.
571        self.ensure_viewport_slot(device, vp_idx);
572        let needs_create = match self.viewport_slots[vp_idx].hdr_callback.as_ref() {
573            None => true,
574            Some(t) => t.size != [w, h],
575        };
576        if needs_create {
577            let target = self.resources.create_hdr_callback_target(device, [w, h]);
578            self.viewport_slots[vp_idx].hdr_callback = Some(target);
579        }
580
581        // Create a fresh TextureView from the stored Texture.
582        // This owned view does not borrow viewport_slots, allowing the subsequent
583        // mutable call to render_frame_internal without a borrow conflict.
584        let output_view = self.viewport_slots[vp_idx]
585            .hdr_callback
586            .as_ref()
587            .unwrap()
588            .texture
589            .create_view(&wgpu::TextureViewDescriptor::default());
590
591        self.render_frame_internal(device, queue, &output_view, vp_idx, frame)
592    }
593
594    /// Blit the HDR intermediate texture into the egui render pass.
595    ///
596    /// Call from `CallbackTrait::paint` after
597    /// [`prepare_hdr_callback`](Self::prepare_hdr_callback) has been called for the
598    /// same frame and viewport. Emits a fullscreen triangle into `render_pass`.
599    pub(crate) fn paint_hdr_blit<'rp>(&self, render_pass: &mut wgpu::RenderPass<'rp>, frame: &FrameData) {
600        let vp_idx = frame.camera.viewport_index;
601        if let Some(hc) = self
602            .viewport_slots
603            .get(vp_idx)
604            .and_then(|s| s.hdr_callback.as_ref())
605        {
606            if let Some(pipeline) = &self.resources.dyn_res_upscale_ds_pipeline {
607                render_pass.set_pipeline(pipeline);
608                render_pass.set_bind_group(0, &hc.blit_bind_group, &[]);
609                render_pass.draw(0..3, 0..1);
610            }
611        }
612    }
613
614    /// Unified prepare step for the eframe `CallbackTrait::prepare` method.
615    ///
616    /// Replaces manual `prepare` + `prepare_ldr_dyn_res` or `prepare_hdr_callback`
617    /// calls. Dispatches internally based on `frame.effects.post_process.enabled`:
618    ///
619    /// - HDR path (`post_process.enabled = true`): runs the full HDR pipeline (OIT,
620    ///   EDL, tone-map) and returns the resulting `CommandBuffer` for eframe to
621    ///   submit before the egui render pass.
622    /// - LDR path: calls `prepare`, and if dynamic resolution is active, encodes the
623    ///   scene into a separate `CommandBuffer` (also submitted before the render
624    ///   pass). Returns an empty `Vec` when dyn-res is inactive.
625    ///
626    /// Call [`paint_callback`](Self::paint_callback) from `CallbackTrait::paint`.
627    pub(crate) fn prepare_callback(
628        &mut self,
629        device: &wgpu::Device,
630        queue: &wgpu::Queue,
631        frame: &FrameData,
632    ) -> Vec<wgpu::CommandBuffer> {
633        if frame.effects.post_process.enabled {
634            let cb = self.prepare_hdr_callback(device, queue, frame);
635            vec![cb]
636        } else {
637            self.prepare(device, queue, frame);
638            if self.current_render_scale < 1.0 - 0.001 {
639                let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
640                    label: Some("ldr_dyn_res_callback_encoder"),
641                });
642                self.prepare_ldr_dyn_res(&mut encoder, device, frame);
643                vec![encoder.finish()]
644            } else {
645                Vec::new()
646            }
647        }
648    }
649
650    /// Unified paint step for the eframe `CallbackTrait::paint` method.
651    ///
652    /// Call after [`prepare_callback`](Self::prepare_callback) for the same frame.
653    /// Dispatches internally to `paint_hdr_blit`, `paint_dyn_res_blit`, or `paint`
654    /// based on which path `prepare_callback` activated.
655    pub(crate) fn paint_callback<'rp>(&self, render_pass: &mut wgpu::RenderPass<'rp>, frame: &FrameData) {
656        let vp_idx = frame.camera.viewport_index;
657        if frame.effects.post_process.enabled {
658            if self
659                .viewport_slots
660                .get(vp_idx)
661                .and_then(|s| s.hdr_callback.as_ref())
662                .is_some()
663            {
664                self.paint_hdr_blit(render_pass, frame);
665                return;
666            }
667        }
668        if self.current_render_scale < 1.0 - 0.001
669            && self
670                .viewport_slots
671                .get(vp_idx)
672                .and_then(|s| s.dyn_res.as_ref())
673                .is_some()
674        {
675            self.paint_dyn_res_blit(render_pass, frame);
676        } else {
677            self.paint_to(render_pass, frame);
678        }
679    }
680
681    /// High-level HDR render for a single viewport identified by `id`.
682    ///
683    /// Unlike [`render`](Self::render), this method does **not** call
684    /// [`prepare`](Self::prepare) internally.  The caller must have already called
685    /// [`prepare_scene`](Self::prepare_scene) and
686    /// [`prepare_viewport`](Self::prepare_viewport) for `id` before invoking this.
687    ///
688    /// This is the right entry point for multi-viewport frames:
689    /// 1. Call `prepare_scene` once.
690    /// 2. Call `prepare_viewport` for each viewport.
691    /// 3. Call `render_viewport` for each viewport with its own `output_view`.
692    ///
693    /// Returns a [`wgpu::CommandBuffer`] ready to submit.
694    pub(crate) fn render_viewport(
695        &mut self,
696        device: &wgpu::Device,
697        queue: &wgpu::Queue,
698        output_view: &wgpu::TextureView,
699        id: ViewportId,
700        frame: &FrameData,
701    ) -> wgpu::CommandBuffer {
702        self.render_frame_internal(device, queue, output_view, id.0, frame)
703    }
704
705    /// High-level HDR render method. Handles the full post-processing pipeline:
706    /// scene -> HDR texture -> (bloom) -> (SSAO) -> tone map -> output_view.
707    ///
708    /// When `frame.post_process.enabled` is false, falls back to a simple LDR render
709    /// pass targeting `output_view` directly.
710    ///
711    /// Returns a `CommandBuffer` ready to submit.
712    pub(crate) fn render(
713        &mut self,
714        device: &wgpu::Device,
715        queue: &wgpu::Queue,
716        output_view: &wgpu::TextureView,
717        frame: &FrameData,
718    ) -> wgpu::CommandBuffer {
719        // Always run prepare() to upload uniforms and run the shadow pass.
720        self.prepare(device, queue, frame);
721        self.render_frame_internal(
722            device,
723            queue,
724            output_view,
725            frame.camera.viewport_index,
726            frame,
727        )
728    }
729
730    /// Render-only path shared by `render()` and `render_viewport()`.
731    ///
732    /// `vp_idx` selects the per-viewport slot to use for camera/HDR state,
733    /// independent of `frame.camera.viewport_index`.
734    fn render_frame_internal(
735        &mut self,
736        device: &wgpu::Device,
737        queue: &wgpu::Queue,
738        output_view: &wgpu::TextureView,
739        vp_idx: usize,
740        frame: &FrameData,
741    ) -> wgpu::CommandBuffer {
742        // Read scene items from the surface submission.
743        let scene_items: &[SceneRenderItem] = match &frame.scene.surfaces {
744            SurfaceSubmission::Flat(items) => items.as_ref(),
745        };
746
747        let bg_colour = frame.viewport.background_colour.unwrap_or([
748            65.0 / 255.0,
749            65.0 / 255.0,
750            65.0 / 255.0,
751            1.0,
752        ]);
753        let ppp = frame.camera.pixels_per_point;
754        let w = (frame.camera.viewport_size[0] * ppp).round() as u32;
755        let h = (frame.camera.viewport_size[1] * ppp).round() as u32;
756
757        // Ensure per-viewport HDR targets. Provides a depth buffer for both LDR and HDR paths.
758        let ssaa_factor = frame.effects.post_process.ssaa_factor.max(1);
759        self.ensure_viewport_hdr(device, queue, vp_idx, w.max(1), h.max(1), ssaa_factor, self.current_render_scale);
760
761        // Phase 4 : lazy-initialize GPU timestamp resources on first render call when supported.
762        if self.ts_query_set.is_none()
763            && device.features().contains(wgpu::Features::TIMESTAMP_QUERY)
764        {
765            self.ts_query_set = Some(device.create_query_set(&wgpu::QuerySetDescriptor {
766                label: Some("ts_query_set"),
767                ty: wgpu::QueryType::Timestamp,
768                count: 2,
769            }));
770            self.ts_resolve_buf = Some(device.create_buffer(&wgpu::BufferDescriptor {
771                label: Some("ts_resolve_buf"),
772                size: 16,
773                usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
774                mapped_at_creation: false,
775            }));
776            self.ts_staging_buf = Some(device.create_buffer(&wgpu::BufferDescriptor {
777                label: Some("ts_staging_buf"),
778                size: 16,
779                usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
780                mapped_at_creation: false,
781            }));
782            self.ts_period = queue.get_timestamp_period();
783        }
784
785        if !frame.effects.post_process.enabled {
786            // LDR fallback. When dynamic resolution is active and render_scale < 1.0,
787            // draw into a scaled intermediate texture and upscale-blit to output_view.
788            // Otherwise render directly to output_view.
789            let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
790                label: Some("ldr_encoder"),
791            });
792
793            let use_dyn_res = self.current_render_scale < 1.0 - 0.001;
794            let needs_blur = self.has_backdrop_blur_shapes();
795
796            if use_dyn_res {
797                let sw = ((w as f32 * self.current_render_scale) as u32).max(1);
798                let sh = ((h as f32 * self.current_render_scale) as u32).max(1);
799                self.ensure_dyn_res_target(device, vp_idx, [sw, sh], [w.max(1), h.max(1)]);
800            }
801
802            // When blur backdrops are needed and we'd otherwise render directly
803            // to the surface (no dyn_res), force an intermediate so it can be
804            // sampled for the blur passes.
805            if needs_blur && !use_dyn_res {
806                self.ensure_backdrop_blur_state(device, w.max(1), h.max(1));
807            }
808
809            {
810                let slot = &self.viewport_slots[vp_idx];
811                let slot_hdr = slot.hdr.as_ref().expect(
812                    "HDR state missing in LDR path; ensure_viewport_hdr must have been called",
813                );
814                let camera_bg = &slot.camera_bind_group;
815                let grid_bg = &slot.grid_bind_group;
816                // Choose render target: dyn_res intermediate, backdrop intermediate, or output_view.
817                let (scene_colour_view, scene_depth_view): (&wgpu::TextureView, &wgpu::TextureView) =
818                    if use_dyn_res {
819                        let dr = slot.dyn_res.as_ref().unwrap();
820                        (&dr.colour_view, &dr.depth_view)
821                    } else if needs_blur {
822                        let bs = self.backdrop_blur_state.as_ref().unwrap();
823                        (&bs.intermediate_view, &slot_hdr.outline_depth_view)
824                    } else {
825                        (output_view, &slot_hdr.outline_depth_view)
826                    };
827                let ts_writes =
828                    self.ts_query_set
829                        .as_ref()
830                        .map(|qs| wgpu::RenderPassTimestampWrites {
831                            query_set: qs,
832                            beginning_of_pass_write_index: Some(0),
833                            end_of_pass_write_index: Some(1),
834                        });
835                let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
836                    label: Some("ldr_render_pass"),
837                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
838                        view: scene_colour_view,
839                        resolve_target: None,
840                        ops: wgpu::Operations {
841                            load: wgpu::LoadOp::Clear(wgpu::Color {
842                                r: bg_colour[0] as f64,
843                                g: bg_colour[1] as f64,
844                                b: bg_colour[2] as f64,
845                                a: bg_colour[3] as f64,
846                            }),
847                            store: wgpu::StoreOp::Store,
848                        },
849                        depth_slice: None,
850                    })],
851                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
852                        view: scene_depth_view,
853                        depth_ops: Some(wgpu::Operations {
854                            load: wgpu::LoadOp::Clear(1.0),
855                            store: wgpu::StoreOp::Discard,
856                        }),
857                        stencil_ops: None,
858                    }),
859                    timestamp_writes: ts_writes,
860                    occlusion_query_set: None,
861                });
862                emit_draw_calls!(
863                    &self.resources,
864                    &mut render_pass,
865                    frame,
866                    self.use_instancing,
867                    &self.instanced_batches,
868                    camera_bg,
869                    grid_bg,
870                    &self.compute_filter_results,
871                    Some(slot),
872                    &self.wireframe_bind_groups
873                );
874                emit_scivis_draw_calls!(
875                    &self.resources,
876                    &mut render_pass,
877                    &self.point_cloud_gpu_data,
878                    &self.glyph_gpu_data,
879                    &self.polyline_gpu_data,
880                    &self.volume_gpu_data,
881                    &self.streamtube_gpu_data,
882                    camera_bg,
883                    &self.tube_gpu_data,
884                    &self.image_slice_gpu_data,
885                    &self.tensor_glyph_gpu_data,
886                    &self.ribbon_gpu_data,
887                    &self.volume_surface_slice_gpu_data,
888                    &self.sprite_gpu_data,
889                    false
890                );
891                // TransparentVolumeMesh boundary wireframe overlay.
892                if !self.tvm_wireframe_draws.is_empty() {
893                    if let Some(ref tvm_bg) = self.tvm_wireframe_bg {
894                        render_pass.set_bind_group(0, camera_bg, &[]);
895                        for mesh_id in &self.tvm_wireframe_draws {
896                            if let Some(mesh) = self.resources.mesh_store.get(*mesh_id) {
897                                render_pass.set_pipeline(&self.resources.wireframe_pipeline);
898                                render_pass.set_bind_group(1, tvm_bg, &[]);
899                                render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
900                                render_pass.set_index_buffer(
901                                    mesh.edge_index_buffer.slice(..),
902                                    wgpu::IndexFormat::Uint32,
903                                );
904                                render_pass.draw_indexed(0..mesh.edge_index_count, 0, 0..1);
905                            }
906                        }
907                    }
908                }
909                // Phase 16 : GPU implicit surface.
910                if !self.implicit_gpu_data.is_empty() {
911                    if let Some(ref dual) = self.resources.implicit_pipeline {
912                        render_pass.set_pipeline(dual.for_format(false));
913                        render_pass.set_bind_group(0, camera_bg, &[]);
914                        for gpu in &self.implicit_gpu_data {
915                            render_pass.set_bind_group(1, &gpu.bind_group, &[]);
916                            render_pass.draw(0..6, 0..1);
917                        }
918                    }
919                }
920                // Phase 17 : GPU marching cubes indirect draw.
921                if !self.mc_gpu_data.is_empty() {
922                    if let Some(ref dual) = self.resources.mc_surface_pipeline {
923                        render_pass.set_pipeline(dual.for_format(false));
924                        render_pass.set_bind_group(0, camera_bg, &[]);
925                        for mc in &self.mc_gpu_data {
926                            let vol = &self.resources.mc_volumes[mc.volume_idx];
927                            render_pass.set_bind_group(1, &mc.render_bg, &[]);
928                            for slab in &vol.slabs {
929                                render_pass.set_vertex_buffer(0, slab.vertex_buf.slice(..));
930                                render_pass.draw_indirect(&slab.indirect_buf, 0);
931                            }
932                        }
933                    }
934                }
935                // Outline composite after all scene content.
936                emit_outline_composite!(&self.resources, &mut render_pass, Some(slot));
937                // Phase 10B / Phase 12 : screen-space image overlays.
938                // Regular items drawn with depth_compare: Always (always on top).
939                // Depth-composite items drawn with depth_compare: LessEqual (occluded by
940                // scene geometry whose depth was already written to the depth attachment).
941                if !self.screen_image_gpu_data.is_empty() {
942                    if let Some(overlay_pipeline) = &self.resources.screen_image_pipeline {
943                        let dc_pipeline = self.resources.screen_image_dc_pipeline.as_ref();
944                        for gpu in &self.screen_image_gpu_data {
945                            if let (Some(dc_bg), Some(dc_pipe)) =
946                                (&gpu.depth_bind_group, dc_pipeline)
947                            {
948                                render_pass.set_pipeline(dc_pipe);
949                                render_pass.set_bind_group(0, dc_bg, &[]);
950                            } else {
951                                render_pass.set_pipeline(overlay_pipeline);
952                                render_pass.set_bind_group(0, &gpu.bind_group, &[]);
953                            }
954                            render_pass.draw(0..6, 0..1);
955                        }
956                    }
957                }
958                // When blur backdrops are needed, skip overlays here. They'll
959                // be drawn in a second pass after the blur is applied.
960                if !needs_blur {
961                    // SDF overlay shapes (LDR fallback).
962                    if let Some(ref sd) = self.overlay_shape_gpu_data {
963                        if sd.vertex_count > 0 {
964                            if let Some(pipeline) = &self.resources.overlay_shape_pipeline {
965                                if let Some(vbuf) = &sd.vertex_buf {
966                                    render_pass.set_pipeline(pipeline);
967                                    render_pass.set_vertex_buffer(0, vbuf.slice(..));
968                                    render_pass.draw(0..sd.vertex_count, 0..1);
969                                }
970                            }
971                        }
972                        if !sd.tex_batches.is_empty() {
973                            if let Some(pipeline) = &self.resources.overlay_shape_tex_pipeline {
974                                render_pass.set_pipeline(pipeline);
975                                for batch in &sd.tex_batches {
976                                    render_pass.set_bind_group(0, &batch.bind_group, &[]);
977                                    render_pass.set_vertex_buffer(0, batch.vertex_buf.slice(..));
978                                    render_pass.draw(0..batch.vertex_count, 0..1);
979                                }
980                            }
981                        }
982                    }
983                    // Overlay rects (LDR fallback).
984                    if let Some(ref rr) = self.overlay_rect_gpu_data {
985                        if let Some(pipeline) = &self.resources.overlay_text_pipeline {
986                            render_pass.set_pipeline(pipeline);
987                            render_pass.set_bind_group(0, &rr.bind_group, &[]);
988                            render_pass.set_vertex_buffer(0, rr.vertex_buf.slice(..));
989                            render_pass.draw(0..rr.vertex_count, 0..1);
990                        }
991                    }
992                    // Overlay labels (LDR fallback: inside the same render pass).
993                    if let Some(ref ld) = self.label_gpu_data {
994                        if let Some(pipeline) = &self.resources.overlay_text_pipeline {
995                            render_pass.set_pipeline(pipeline);
996                            render_pass.set_bind_group(0, &ld.bind_group, &[]);
997                            render_pass.set_vertex_buffer(0, ld.vertex_buf.slice(..));
998                            render_pass.draw(0..ld.vertex_count, 0..1);
999                        }
1000                    }
1001                    // Scalar bars (LDR fallback).
1002                    if let Some(ref sb) = self.scalar_bar_gpu_data {
1003                        if let Some(pipeline) = &self.resources.overlay_text_pipeline {
1004                            render_pass.set_pipeline(pipeline);
1005                            render_pass.set_bind_group(0, &sb.bind_group, &[]);
1006                            render_pass.set_vertex_buffer(0, sb.vertex_buf.slice(..));
1007                            render_pass.draw(0..sb.vertex_count, 0..1);
1008                        }
1009                    }
1010                    // Rulers (LDR fallback).
1011                    if let Some(ref rd) = self.ruler_gpu_data {
1012                        if let Some(pipeline) = &self.resources.overlay_text_pipeline {
1013                            render_pass.set_pipeline(pipeline);
1014                            render_pass.set_bind_group(0, &rd.bind_group, &[]);
1015                            render_pass.set_vertex_buffer(0, rd.vertex_buf.slice(..));
1016                            render_pass.draw(0..rd.vertex_count, 0..1);
1017                        }
1018                    }
1019                    // Phase 7 : overlay images (OverlayFrame, LDR fallback, drawn last).
1020                    if !self.overlay_image_gpu_data.is_empty() {
1021                        if let Some(pipeline) = &self.resources.screen_image_pipeline {
1022                            render_pass.set_pipeline(pipeline);
1023                            for gpu in &self.overlay_image_gpu_data {
1024                                render_pass.set_bind_group(0, &gpu.bind_group, &[]);
1025                                render_pass.draw(0..6, 0..1);
1026                            }
1027                        }
1028                    }
1029                }
1030            }
1031            // -- End of scene render pass (dropped above). ---
1032
1033            // Backdrop blur: capture scene, run blur, then draw overlays in a
1034            // second render pass so blur shapes can sample the blurred result.
1035            if needs_blur {
1036                let spread = self.overlay_shape_gpu_data.as_ref()
1037                    .map(|d| d.max_blur_radius)
1038                    .unwrap_or(1.0);
1039                let blur_bg = {
1040                    let source = if use_dyn_res {
1041                        &self.viewport_slots[vp_idx].dyn_res.as_ref().unwrap().colour_view
1042                    } else {
1043                        &self.backdrop_blur_state.as_ref().unwrap().intermediate_view
1044                    };
1045                    self.run_backdrop_blur(&mut encoder, device, queue, source, spread)
1046                };
1047
1048                // Second render pass for overlays (Load to preserve scene content).
1049                let slot = &self.viewport_slots[vp_idx];
1050                let slot_hdr = slot.hdr.as_ref().unwrap();
1051                let overlay_colour_view: &wgpu::TextureView = if use_dyn_res {
1052                    &slot.dyn_res.as_ref().unwrap().colour_view
1053                } else {
1054                    &self.backdrop_blur_state.as_ref().unwrap().intermediate_view
1055                };
1056                let overlay_depth_view: &wgpu::TextureView = if use_dyn_res {
1057                    &slot.dyn_res.as_ref().unwrap().depth_view
1058                } else {
1059                    &slot_hdr.outline_depth_view
1060                };
1061                {
1062                    let mut overlay_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1063                        label: Some("ldr_overlay_blur_pass"),
1064                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1065                            view: overlay_colour_view,
1066                            resolve_target: None,
1067                            ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store },
1068                            depth_slice: None,
1069                        })],
1070                        depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1071                            view: overlay_depth_view,
1072                            depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Discard }),
1073                            stencil_ops: None,
1074                        }),
1075                        timestamp_writes: None,
1076                        occlusion_query_set: None,
1077                    });
1078                    // Draw blur backdrop shapes first.
1079                    self.draw_blur_shapes(&mut overlay_pass, &blur_bg);
1080                    // Then normal shapes.
1081                    if let Some(ref sd) = self.overlay_shape_gpu_data {
1082                        if sd.vertex_count > 0 {
1083                            if let Some(pipeline) = &self.resources.overlay_shape_pipeline {
1084                                if let Some(vbuf) = &sd.vertex_buf {
1085                                    overlay_pass.set_pipeline(pipeline);
1086                                    overlay_pass.set_vertex_buffer(0, vbuf.slice(..));
1087                                    overlay_pass.draw(0..sd.vertex_count, 0..1);
1088                                }
1089                            }
1090                        }
1091                        if !sd.tex_batches.is_empty() {
1092                            if let Some(pipeline) = &self.resources.overlay_shape_tex_pipeline {
1093                                overlay_pass.set_pipeline(pipeline);
1094                                for batch in &sd.tex_batches {
1095                                    overlay_pass.set_bind_group(0, &batch.bind_group, &[]);
1096                                    overlay_pass.set_vertex_buffer(0, batch.vertex_buf.slice(..));
1097                                    overlay_pass.draw(0..batch.vertex_count, 0..1);
1098                                }
1099                            }
1100                        }
1101                    }
1102                    if let Some(pipeline) = &self.resources.overlay_text_pipeline {
1103                        if let Some(ref rr) = self.overlay_rect_gpu_data {
1104                            overlay_pass.set_pipeline(pipeline);
1105                            overlay_pass.set_bind_group(0, &rr.bind_group, &[]);
1106                            overlay_pass.set_vertex_buffer(0, rr.vertex_buf.slice(..));
1107                            overlay_pass.draw(0..rr.vertex_count, 0..1);
1108                        }
1109                        if let Some(ref ld) = self.label_gpu_data {
1110                            overlay_pass.set_pipeline(pipeline);
1111                            overlay_pass.set_bind_group(0, &ld.bind_group, &[]);
1112                            overlay_pass.set_vertex_buffer(0, ld.vertex_buf.slice(..));
1113                            overlay_pass.draw(0..ld.vertex_count, 0..1);
1114                        }
1115                        if let Some(ref sb) = self.scalar_bar_gpu_data {
1116                            overlay_pass.set_pipeline(pipeline);
1117                            overlay_pass.set_bind_group(0, &sb.bind_group, &[]);
1118                            overlay_pass.set_vertex_buffer(0, sb.vertex_buf.slice(..));
1119                            overlay_pass.draw(0..sb.vertex_count, 0..1);
1120                        }
1121                        if let Some(ref rd) = self.ruler_gpu_data {
1122                            overlay_pass.set_pipeline(pipeline);
1123                            overlay_pass.set_bind_group(0, &rd.bind_group, &[]);
1124                            overlay_pass.set_vertex_buffer(0, rd.vertex_buf.slice(..));
1125                            overlay_pass.draw(0..rd.vertex_count, 0..1);
1126                        }
1127                    }
1128                    if !self.overlay_image_gpu_data.is_empty() {
1129                        if let Some(pipeline) = &self.resources.screen_image_pipeline {
1130                            overlay_pass.set_pipeline(pipeline);
1131                            for gpu in &self.overlay_image_gpu_data {
1132                                overlay_pass.set_bind_group(0, &gpu.bind_group, &[]);
1133                                overlay_pass.draw(0..6, 0..1);
1134                            }
1135                        }
1136                    }
1137                }
1138            }
1139
1140            // Phase 4 : resolve timestamp queries -> staging buffer.
1141            if let (Some(qs), Some(res_buf), Some(stg_buf)) = (
1142                self.ts_query_set.as_ref(),
1143                self.ts_resolve_buf.as_ref(),
1144                self.ts_staging_buf.as_ref(),
1145            ) {
1146                encoder.resolve_query_set(qs, 0..2, res_buf, 0);
1147                encoder.copy_buffer_to_buffer(res_buf, 0, stg_buf, 0, 16);
1148                self.ts_needs_readback = true;
1149            }
1150
1151            // Phase 3 : upscale blit from dyn_res intermediate to output_view.
1152            if use_dyn_res {
1153                let upscale_bg = &self.viewport_slots[vp_idx]
1154                    .dyn_res
1155                    .as_ref()
1156                    .unwrap()
1157                    .upscale_bind_group;
1158                let mut upscale_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1159                    label: Some("dyn_res_upscale_pass"),
1160                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1161                        view: output_view,
1162                        resolve_target: None,
1163                        ops: wgpu::Operations {
1164                            load: wgpu::LoadOp::Load,
1165                            store: wgpu::StoreOp::Store,
1166                        },
1167                        depth_slice: None,
1168                    })],
1169                    depth_stencil_attachment: None,
1170                    timestamp_writes: None,
1171                    occlusion_query_set: None,
1172                });
1173                if let Some(pipeline) = &self.resources.dyn_res_upscale_pipeline {
1174                    upscale_pass.set_pipeline(pipeline);
1175                    upscale_pass.set_bind_group(0, upscale_bg, &[]);
1176                    upscale_pass.draw(0..3, 0..1);
1177                }
1178            } else if needs_blur {
1179                // Blit backdrop intermediate to the output surface.
1180                let bs = self.backdrop_blur_state.as_ref().unwrap();
1181                let blit_bgl = self.resources.dyn_res_upscale_bgl.as_ref().unwrap();
1182                let blit_sampler = self.resources.dyn_res_linear_sampler.as_ref().unwrap();
1183                let blit_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1184                    label: Some("backdrop_blit_bg"),
1185                    layout: blit_bgl,
1186                    entries: &[
1187                        wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&bs.intermediate_view) },
1188                        wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(blit_sampler) },
1189                    ],
1190                });
1191                let mut blit_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1192                    label: Some("backdrop_blit_pass"),
1193                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1194                        view: output_view,
1195                        resolve_target: None,
1196                        ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store },
1197                        depth_slice: None,
1198                    })],
1199                    depth_stencil_attachment: None,
1200                    timestamp_writes: None,
1201                    occlusion_query_set: None,
1202                });
1203                if let Some(pipeline) = &self.resources.dyn_res_upscale_pipeline {
1204                    blit_pass.set_pipeline(pipeline);
1205                    blit_pass.set_bind_group(0, &blit_bg, &[]);
1206                    blit_pass.draw(0..3, 0..1);
1207                }
1208            }
1209
1210            return encoder.finish();
1211        }
1212
1213        // HDR path.
1214        let pp = &frame.effects.post_process;
1215
1216        let hdr_clear_rgb = [
1217            bg_colour[0].powf(2.2),
1218            bg_colour[1].powf(2.2),
1219            bg_colour[2].powf(2.2),
1220        ];
1221
1222        // Upload tone map uniform into the per-viewport buffer.
1223        let mode = match pp.tone_mapping {
1224            crate::renderer::ToneMapping::Reinhard => 0u32,
1225            crate::renderer::ToneMapping::Aces => 1u32,
1226            crate::renderer::ToneMapping::KhronosNeutral => 2u32,
1227        };
1228        let tm_uniform = crate::resources::ToneMapUniform {
1229            exposure: pp.exposure,
1230            mode,
1231            bloom_enabled: if pp.bloom { 1 } else { 0 },
1232            ssao_enabled: if pp.ssao { 1 } else { 0 },
1233            contact_shadows_enabled: if pp.contact_shadows { 1 } else { 0 },
1234            edl_enabled: if pp.edl_enabled { 1 } else { 0 },
1235            edl_radius: pp.edl_radius,
1236            edl_strength: pp.edl_strength,
1237            background_colour: bg_colour,
1238            near_plane: frame.camera.render_camera.near,
1239            far_plane: frame.camera.render_camera.far,
1240            lic_enabled: if frame.scene.lic_items.is_empty() {
1241                0
1242            } else {
1243                1
1244            },
1245            lic_strength: frame
1246                .scene
1247                .lic_items
1248                .first()
1249                .map(|i| i.config.strength)
1250                .unwrap_or(0.5),
1251        };
1252        {
1253            let hdr = self.viewport_slots[vp_idx].hdr.as_ref().unwrap();
1254            queue.write_buffer(
1255                &hdr.tone_map_uniform_buf,
1256                0,
1257                bytemuck::cast_slice(&[tm_uniform]),
1258            );
1259
1260            // Upload SSAO uniform if needed.
1261            if pp.ssao {
1262                let proj = frame.camera.render_camera.projection;
1263                let inv_proj = proj.inverse();
1264                let ssao_uniform = crate::resources::SsaoUniform {
1265                    inv_proj: inv_proj.to_cols_array_2d(),
1266                    proj: proj.to_cols_array_2d(),
1267                    radius: 0.5,
1268                    bias: 0.025,
1269                    _pad: [0.0; 2],
1270                };
1271                queue.write_buffer(
1272                    &hdr.ssao_uniform_buf,
1273                    0,
1274                    bytemuck::cast_slice(&[ssao_uniform]),
1275                );
1276            }
1277
1278            // Upload contact shadow uniform if needed.
1279            if pp.contact_shadows {
1280                let proj = frame.camera.render_camera.projection;
1281                let inv_proj = proj.inverse();
1282                let light_dir_world: glam::Vec3 =
1283                    if let Some(l) = frame.effects.lighting.lights.first() {
1284                        match l.kind {
1285                            LightKind::Directional { direction } => {
1286                                glam::Vec3::from(direction).normalize()
1287                            }
1288                            LightKind::Spot { direction, .. } => {
1289                                glam::Vec3::from(direction).normalize()
1290                            }
1291                            _ => glam::Vec3::new(0.0, -1.0, 0.0),
1292                        }
1293                    } else {
1294                        glam::Vec3::new(0.0, -1.0, 0.0)
1295                    };
1296                let view = frame.camera.render_camera.view;
1297                let light_dir_view = view.transform_vector3(light_dir_world).normalize();
1298                let world_up_view = view.transform_vector3(glam::Vec3::Z).normalize();
1299                let cs_uniform = crate::resources::ContactShadowUniform {
1300                    inv_proj: inv_proj.to_cols_array_2d(),
1301                    proj: proj.to_cols_array_2d(),
1302                    light_dir_view: [light_dir_view.x, light_dir_view.y, light_dir_view.z, 0.0],
1303                    world_up_view: [world_up_view.x, world_up_view.y, world_up_view.z, 0.0],
1304                    params: [
1305                        pp.contact_shadow_max_distance,
1306                        pp.contact_shadow_steps as f32,
1307                        pp.contact_shadow_thickness,
1308                        0.0,
1309                    ],
1310                };
1311                queue.write_buffer(
1312                    &hdr.contact_shadow_uniform_buf,
1313                    0,
1314                    bytemuck::cast_slice(&[cs_uniform]),
1315                );
1316            }
1317
1318            // Upload bloom uniform if needed.
1319            if pp.bloom {
1320                let bloom_u = crate::resources::BloomUniform {
1321                    threshold: pp.bloom_threshold,
1322                    intensity: pp.bloom_intensity,
1323                    horizontal: 0,
1324                    _pad: 0,
1325                };
1326                queue.write_buffer(&hdr.bloom_uniform_buf, 0, bytemuck::cast_slice(&[bloom_u]));
1327            }
1328        }
1329
1330        // Upload DoF uniform when enabled.
1331        if pp.dof_enabled {
1332            let (w, h) = {
1333                let hdr = self.viewport_slots[vp_idx].hdr.as_ref().unwrap();
1334                (hdr.scene_size[0] as f32, hdr.scene_size[1] as f32)
1335            };
1336            let dof_uniform = crate::resources::DofUniform {
1337                focal_distance: pp.dof_focal_distance,
1338                focal_range: pp.dof_focal_range,
1339                max_blur_radius: pp.dof_max_blur_radius,
1340                near_plane: frame.camera.render_camera.near,
1341                far_plane: frame.camera.render_camera.far,
1342                viewport_width: w,
1343                viewport_height: h,
1344                _pad: 0.0,
1345            };
1346            let hdr = self.viewport_slots[vp_idx].hdr.as_ref().unwrap();
1347            queue.write_buffer(
1348                &hdr.dof_uniform_buf,
1349                0,
1350                bytemuck::cast_slice(&[dof_uniform]),
1351            );
1352        }
1353
1354        // Rebuild tone-map bind group with correct bloom/AO/DoF texture views.
1355        {
1356            let hdr = self.viewport_slots[vp_idx].hdr.as_mut().unwrap();
1357            self.resources.rebuild_tone_map_bind_group(
1358                device,
1359                hdr,
1360                pp.bloom,
1361                pp.ssao,
1362                pp.contact_shadows,
1363                !frame.scene.lic_items.is_empty(),
1364                pp.dof_enabled,
1365            );
1366        }
1367
1368        // -----------------------------------------------------------------------
1369        // Pre-allocate OIT targets if any transparent items exist.
1370        // Must happen before camera_bg is borrowed (borrow-checker constraint).
1371        // -----------------------------------------------------------------------
1372        {
1373            let needs_oit = if self.use_instancing && !self.instanced_batches.is_empty() {
1374                self.instanced_batches.iter().any(|b| b.is_transparent)
1375            } else {
1376                scene_items
1377                    .iter()
1378                    .any(|i| !i.appearance.hidden && i.appearance.opacity < 1.0)
1379            } || frame
1380                .scene
1381                .transparent_volume_meshes
1382                .iter()
1383                .any(|i| !i.appearance.hidden);
1384            if needs_oit {
1385                let hdr = self.viewport_slots[vp_idx].hdr.as_mut().unwrap();
1386                let [sw, sh] = hdr.scene_size;
1387                self.resources
1388                    .ensure_viewport_oit(device, hdr, sw, sh);
1389            }
1390        }
1391
1392        // -----------------------------------------------------------------------
1393        // Build the command encoder.
1394        // -----------------------------------------------------------------------
1395        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1396            label: Some("hdr_encoder"),
1397        });
1398
1399        // Per-viewport camera bind group and HDR state for the HDR path.
1400        let slot = &self.viewport_slots[vp_idx];
1401        let camera_bg = &slot.camera_bind_group;
1402        let slot_hdr = slot.hdr.as_ref().expect(
1403            "HDR state missing; ensure_viewport_hdr must be called before render_frame_internal",
1404        );
1405
1406        // -----------------------------------------------------------------------
1407        // HDR scene pass: render geometry into the HDR texture.
1408        // -----------------------------------------------------------------------
1409        {
1410            // Use SSAA target if enabled, otherwise render directly to hdr_texture.
1411            let use_ssaa = ssaa_factor > 1
1412                && slot_hdr.ssaa_colour_view.is_some()
1413                && slot_hdr.ssaa_depth_view.is_some();
1414            let scene_colour_view = if use_ssaa {
1415                slot_hdr.ssaa_colour_view.as_ref().unwrap()
1416            } else {
1417                &slot_hdr.hdr_view
1418            };
1419            let scene_depth_view = if use_ssaa {
1420                slot_hdr.ssaa_depth_view.as_ref().unwrap()
1421            } else {
1422                &slot_hdr.hdr_depth_view
1423            };
1424
1425            let clear_wgpu = wgpu::Color {
1426                r: hdr_clear_rgb[0] as f64,
1427                g: hdr_clear_rgb[1] as f64,
1428                b: hdr_clear_rgb[2] as f64,
1429                // Clear alpha to 0.0 so OIT composite can signal presence via alpha > 0.
1430                // Background pixels remain at alpha=0 and are detected in tone_map.wgsl.
1431                a: 0.0,
1432            };
1433
1434            let hdr_ts_writes =
1435                self.ts_query_set
1436                    .as_ref()
1437                    .map(|qs| wgpu::RenderPassTimestampWrites {
1438                        query_set: qs,
1439                        beginning_of_pass_write_index: Some(0),
1440                        end_of_pass_write_index: Some(1),
1441                    });
1442            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1443                label: Some("hdr_scene_pass"),
1444                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1445                    view: scene_colour_view,
1446                    resolve_target: None,
1447                    ops: wgpu::Operations {
1448                        load: wgpu::LoadOp::Clear(clear_wgpu),
1449                        store: wgpu::StoreOp::Store,
1450                    },
1451                    depth_slice: None,
1452                })],
1453                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1454                    view: scene_depth_view,
1455                    depth_ops: Some(wgpu::Operations {
1456                        load: wgpu::LoadOp::Clear(1.0),
1457                        store: wgpu::StoreOp::Store,
1458                    }),
1459                    stencil_ops: Some(wgpu::Operations {
1460                        load: wgpu::LoadOp::Clear(0),
1461                        store: wgpu::StoreOp::Store,
1462                    }),
1463                }),
1464                timestamp_writes: hdr_ts_writes,
1465                occlusion_query_set: None,
1466            });
1467
1468            let resources = &self.resources;
1469            render_pass.set_bind_group(0, camera_bg, &[]);
1470
1471            // Check skybox eligibility early; drawn after all opaques below.
1472            let show_skybox = frame
1473                .effects
1474                .environment
1475                .as_ref()
1476                .is_some_and(|e| e.show_skybox)
1477                && resources.ibl_skybox_view.is_some();
1478
1479            let use_instancing = self.use_instancing;
1480            let batches = &self.instanced_batches;
1481            let compute_filter_results = &self.compute_filter_results;
1482
1483            if !scene_items.is_empty() {
1484                if use_instancing && !batches.is_empty() {
1485                    let excluded_items: Vec<&SceneRenderItem> = scene_items
1486                        .iter()
1487                        .filter(|item| {
1488                            !item.appearance.hidden
1489                                && (item.active_attribute.is_some()
1490                                    || item.material.is_two_sided()
1491                                    || item.material.matcap_id.is_some())
1492                                && resources.mesh_store.get(item.mesh_id).is_some()
1493                        })
1494                        .collect();
1495
1496                    // Separate opaque and transparent batches.
1497                    // Carry the global batch index (position in `batches`) alongside each batch
1498                    // so draw_indexed_indirect can compute the correct buffer offset.
1499                    let mut opaque_batches: Vec<(usize, &InstancedBatch)> = Vec::new();
1500                    let mut transparent_batches: Vec<(usize, &InstancedBatch)> = Vec::new();
1501                    for (batch_global_idx, batch) in batches.iter().enumerate() {
1502                        if batch.is_transparent {
1503                            transparent_batches.push((batch_global_idx, batch));
1504                        } else {
1505                            opaque_batches.push((batch_global_idx, batch));
1506                        }
1507                    }
1508
1509                    if !opaque_batches.is_empty() && !frame.viewport.wireframe_mode {
1510                        let use_indirect = self.gpu_culling_enabled
1511                            && resources.hdr_solid_instanced_cull_pipeline.is_some()
1512                            && resources.indirect_args_buf.is_some();
1513
1514                        if use_indirect {
1515                            if let (Some(pipeline), Some(indirect_buf)) = (
1516                                &resources.hdr_solid_instanced_cull_pipeline,
1517                                &resources.indirect_args_buf,
1518                            ) {
1519                                render_pass.set_pipeline(pipeline);
1520                                for (batch_global_idx, batch) in &opaque_batches {
1521                                    let Some(mesh) = resources.mesh_store.get(batch.mesh_id) else {
1522                                        continue;
1523                                    };
1524                                    let mat_key = (
1525                                        batch.texture_id.unwrap_or(u64::MAX),
1526                                        batch.normal_map_id.unwrap_or(u64::MAX),
1527                                        batch.ao_map_id.unwrap_or(u64::MAX),
1528                                    );
1529                                    let Some(inst_tex_bg) =
1530                                        resources.instance_cull_bind_groups.get(&mat_key)
1531                                    else {
1532                                        continue;
1533                                    };
1534                                    render_pass.set_bind_group(1, inst_tex_bg, &[]);
1535                                    render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1536                                    render_pass.set_index_buffer(
1537                                        mesh.index_buffer.slice(..),
1538                                        wgpu::IndexFormat::Uint32,
1539                                    );
1540                                    // Each DrawIndexedIndirect entry is 20 bytes; index by global
1541                                    // batch position so the offset matches write_indirect_args output.
1542                                    render_pass.draw_indexed_indirect(
1543                                        indirect_buf,
1544                                        *batch_global_idx as u64 * 20,
1545                                    );
1546                                }
1547                            }
1548                        } else if let Some(ref pipeline) = resources.hdr_solid_instanced_pipeline {
1549                            render_pass.set_pipeline(pipeline);
1550                            for (_, batch) in &opaque_batches {
1551                                let Some(mesh) = resources.mesh_store.get(batch.mesh_id) else {
1552                                    continue;
1553                                };
1554                                let mat_key = (
1555                                    batch.texture_id.unwrap_or(u64::MAX),
1556                                    batch.normal_map_id.unwrap_or(u64::MAX),
1557                                    batch.ao_map_id.unwrap_or(u64::MAX),
1558                                );
1559                                let Some(inst_tex_bg) =
1560                                    resources.instance_bind_groups.get(&mat_key)
1561                                else {
1562                                    continue;
1563                                };
1564                                render_pass.set_bind_group(1, inst_tex_bg, &[]);
1565                                render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1566                                render_pass.set_index_buffer(
1567                                    mesh.index_buffer.slice(..),
1568                                    wgpu::IndexFormat::Uint32,
1569                                );
1570                                render_pass.draw_indexed(
1571                                    0..mesh.index_count,
1572                                    0,
1573                                    batch.instance_offset
1574                                        ..batch.instance_offset + batch.instance_count,
1575                                );
1576                            }
1577                        }
1578                    }
1579
1580                    // NOTE: transparent_batches are now rendered in the OIT pass below,
1581                    // not in the HDR scene pass. This block intentionally left empty.
1582                    let _ = &transparent_batches; // suppress unused warning
1583
1584                    if frame.viewport.wireframe_mode {
1585                        if let Some(ref hdr_wf) = resources.hdr_wireframe_pipeline {
1586                            let mut wf_idx = 0usize;
1587                            for item in scene_items {
1588                                if item.appearance.hidden {
1589                                    continue;
1590                                }
1591                                let Some(mesh) = resources.mesh_store.get(item.mesh_id) else {
1592                                    continue;
1593                                };
1594                                let skin_bg = item.skin_instance.and_then(|inst| {
1595                                    resources.skin_instance_bind_group(item.mesh_id, inst)
1596                                });
1597                                if let (Some(bg), Some(hdr_skinned_wf)) =
1598                                    (skin_bg, resources.hdr_skinned_wireframe_pipeline.as_ref())
1599                                {
1600                                    render_pass.set_pipeline(hdr_skinned_wf);
1601                                    render_pass.set_bind_group(2, bg, &[]);
1602                                } else {
1603                                    render_pass.set_pipeline(hdr_wf);
1604                                }
1605                                let bg = self
1606                                    .wireframe_bind_groups
1607                                    .get(wf_idx)
1608                                    .unwrap_or(&mesh.object_bind_group);
1609                                render_pass.set_bind_group(1, bg, &[]);
1610                                render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1611                                render_pass.set_index_buffer(
1612                                    mesh.edge_index_buffer.slice(..),
1613                                    wgpu::IndexFormat::Uint32,
1614                                );
1615                                render_pass.draw_indexed(0..mesh.edge_index_count, 0, 0..1);
1616                                wf_idx += 1;
1617                            }
1618                        }
1619                    } else if let (Some(hdr_solid), Some(hdr_solid_two_sided)) = (
1620                        &resources.hdr_solid_pipeline,
1621                        &resources.hdr_solid_two_sided_pipeline,
1622                    ) {
1623                        // Only opaque excluded items are drawn in the scene pass; transparent
1624                        // excluded items go to the OIT pass below. LDR draws all excluded
1625                        // items inline (including transparent ones) using the transparent
1626                        // pipeline -- an intentional divergence since HDR uses OIT for
1627                        // transparency throughout.
1628                        for item in excluded_items
1629                            .into_iter()
1630                            .filter(|item| item.appearance.opacity >= 1.0 && !item.material.is_blend())
1631                        {
1632                            let Some(mesh) = resources.mesh_store.get(item.mesh_id) else {
1633                                continue;
1634                            };
1635                            let skin_bg = item.skin_instance.and_then(|inst| {
1636                                resources.skin_instance_bind_group(item.mesh_id, inst)
1637                            });
1638                            if let (Some(bg), Some(hdr_skinned_pl)) = (
1639                                skin_bg,
1640                                if item.material.is_two_sided() {
1641                                    resources.hdr_skinned_solid_two_sided_pipeline.as_ref()
1642                                } else {
1643                                    resources.hdr_skinned_solid_pipeline.as_ref()
1644                                },
1645                            ) {
1646                                render_pass.set_pipeline(hdr_skinned_pl);
1647                                render_pass.set_bind_group(2, bg, &[]);
1648                            } else {
1649                                let pipeline = if item.material.is_two_sided() {
1650                                    hdr_solid_two_sided
1651                                } else {
1652                                    hdr_solid
1653                                };
1654                                render_pass.set_pipeline(pipeline);
1655                            }
1656                            render_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
1657                            render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1658                            let filter = compute_filter_results
1659                                .iter()
1660                                .find(|r| r.mesh_id == item.mesh_id);
1661                            if let Some(fr) = filter {
1662                                render_pass.set_index_buffer(
1663                                    fr.index_buffer.slice(..),
1664                                    wgpu::IndexFormat::Uint32,
1665                                );
1666                                render_pass.draw_indexed(0..fr.index_count, 0, 0..1);
1667                            } else {
1668                                render_pass.set_index_buffer(
1669                                    mesh.index_buffer.slice(..),
1670                                    wgpu::IndexFormat::Uint32,
1671                                );
1672                                render_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1673                            }
1674                        }
1675                    }
1676
1677                    // Normal-line overlays for instanced items with show_normals set.
1678                    // Instanced batch draws skip per-item logic, so these are drawn
1679                    // here after all batches finish.
1680                    if let Some(hdr_wf) = &resources.hdr_wireframe_pipeline {
1681                        for item in scene_items.iter().filter(|i| i.show_normals && !i.appearance.hidden) {
1682                            let Some(mesh) = resources.mesh_store.get(item.mesh_id) else {
1683                                continue;
1684                            };
1685                            if let Some(ref nl_buf) = mesh.normal_line_buffer {
1686                                if mesh.normal_line_count > 0 {
1687                                    render_pass.set_pipeline(hdr_wf);
1688                                    render_pass.set_bind_group(1, &mesh.normal_bind_group, &[]);
1689                                    render_pass.set_vertex_buffer(0, nl_buf.slice(..));
1690                                    render_pass.draw(0..mesh.normal_line_count, 0..1);
1691                                }
1692                            }
1693                        }
1694                    }
1695                } else {
1696                    // Per-object path.
1697                    let eye = glam::Vec3::from(frame.camera.render_camera.eye_position);
1698                    let dist_from_eye = |item: &&SceneRenderItem| -> f32 {
1699                        let pos =
1700                            glam::Vec3::new(item.model[3][0], item.model[3][1], item.model[3][2]);
1701                        (pos - eye).length()
1702                    };
1703
1704                    let mut opaque: Vec<&SceneRenderItem> = Vec::new();
1705                    let mut transparent: Vec<&SceneRenderItem> = Vec::new();
1706                    for item in scene_items {
1707                        if item.appearance.hidden || resources.mesh_store.get(item.mesh_id).is_none() {
1708                            continue;
1709                        }
1710                        if item.appearance.opacity < 1.0 || item.material.is_blend() {
1711                            transparent.push(item);
1712                        } else {
1713                            opaque.push(item);
1714                        }
1715                    }
1716                    opaque.sort_by(|a, b| {
1717                        dist_from_eye(a)
1718                            .partial_cmp(&dist_from_eye(b))
1719                            .unwrap_or(std::cmp::Ordering::Equal)
1720                    });
1721                    transparent.sort_by(|a, b| {
1722                        dist_from_eye(b)
1723                            .partial_cmp(&dist_from_eye(a))
1724                            .unwrap_or(std::cmp::Ordering::Equal)
1725                    });
1726
1727                    let draw_item_hdr =
1728                        |render_pass: &mut wgpu::RenderPass<'_>,
1729                         item: &SceneRenderItem,
1730                         solid_pl: &wgpu::RenderPipeline,
1731                         trans_pl: &wgpu::RenderPipeline,
1732                         wf_pl: &wgpu::RenderPipeline| {
1733                            let mesh = resources.mesh_store.get(item.mesh_id).unwrap();
1734                            // mesh.object_bind_group (group 1) already carries the object uniform
1735                            // and the correct texture views.
1736                            render_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
1737
1738                            // Skinned routing: if the mesh has a skin sidecar
1739                            // and this item's instance has a palette uploaded,
1740                            // switch to the skinned pipeline variant and bind
1741                            // group 2. Falls back to the static path otherwise.
1742                            let skin_bg = item.skin_instance.and_then(|inst| {
1743                                resources.skin_instance_bind_group(item.mesh_id, inst)
1744                            });
1745                            let is_face_attr = item.active_attribute.as_ref().map_or(false, |a| {
1746                                matches!(
1747                                    a.kind,
1748                                    crate::resources::AttributeKind::Face
1749                                        | crate::resources::AttributeKind::FaceColour
1750                                        | crate::resources::AttributeKind::Halfedge
1751                                        | crate::resources::AttributeKind::Corner
1752                                )
1753                            });
1754                            if frame.viewport.wireframe_mode {
1755                                if let (Some(bg), Some(hdr_skinned_wf)) =
1756                                    (skin_bg, resources.hdr_skinned_wireframe_pipeline.as_ref())
1757                                {
1758                                    render_pass.set_pipeline(hdr_skinned_wf);
1759                                    render_pass.set_bind_group(2, bg, &[]);
1760                                } else {
1761                                    render_pass.set_pipeline(wf_pl);
1762                                }
1763                                render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1764                                render_pass.set_index_buffer(
1765                                    mesh.edge_index_buffer.slice(..),
1766                                    wgpu::IndexFormat::Uint32,
1767                                );
1768                                render_pass.draw_indexed(0..mesh.edge_index_count, 0, 0..1);
1769                            } else if is_face_attr {
1770                                if let Some(ref fvb) = mesh.face_vertex_buffer {
1771                                    let pl = if item.appearance.opacity < 1.0 {
1772                                        trans_pl
1773                                    } else {
1774                                        solid_pl
1775                                    };
1776                                    render_pass.set_pipeline(pl);
1777                                    render_pass.set_vertex_buffer(0, fvb.slice(..));
1778                                    render_pass.draw(0..mesh.index_count, 0..1);
1779                                }
1780                            } else {
1781                                let filter = compute_filter_results
1782                                    .iter()
1783                                    .find(|r| r.mesh_id == item.mesh_id);
1784                                let pl = if item.appearance.opacity < 1.0 {
1785                                    trans_pl
1786                                } else {
1787                                    solid_pl
1788                                };
1789                                let is_blended = item.appearance.opacity < 1.0
1790                                    || item.material.is_blend();
1791                                let hdr_skinned_pl = if is_blended {
1792                                    resources.hdr_skinned_transparent_pipeline.as_ref()
1793                                } else if item.material.is_two_sided() {
1794                                    resources.hdr_skinned_solid_two_sided_pipeline.as_ref()
1795                                } else {
1796                                    resources.hdr_skinned_solid_pipeline.as_ref()
1797                                };
1798                                if let (Some(bg), Some(hdr_skinned)) = (skin_bg, hdr_skinned_pl)
1799                                {
1800                                    render_pass.set_pipeline(hdr_skinned);
1801                                    render_pass.set_bind_group(2, bg, &[]);
1802                                } else {
1803                                    render_pass.set_pipeline(pl);
1804                                }
1805                                render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1806                                if let Some(fr) = filter {
1807                                    render_pass.set_index_buffer(
1808                                        fr.index_buffer.slice(..),
1809                                        wgpu::IndexFormat::Uint32,
1810                                    );
1811                                    render_pass.draw_indexed(0..fr.index_count, 0, 0..1);
1812                                } else {
1813                                    render_pass.set_index_buffer(
1814                                        mesh.index_buffer.slice(..),
1815                                        wgpu::IndexFormat::Uint32,
1816                                    );
1817                                    render_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1818                                }
1819                            }
1820                            if item.show_normals {
1821                                if let Some(ref nl_buf) = mesh.normal_line_buffer {
1822                                    if mesh.normal_line_count > 0 {
1823                                        render_pass.set_pipeline(wf_pl);
1824                                        render_pass.set_bind_group(
1825                                            1,
1826                                            &mesh.normal_bind_group,
1827                                            &[],
1828                                        );
1829                                        render_pass.set_vertex_buffer(0, nl_buf.slice(..));
1830                                        render_pass.draw(0..mesh.normal_line_count, 0..1);
1831                                    }
1832                                }
1833                            }
1834                        };
1835
1836                    // NOTE: only opaque items are drawn here. Transparent items are
1837                    // routed to the OIT pass below.
1838                    let _ = &transparent; // suppress unused warning
1839                    if let (
1840                        Some(hdr_solid),
1841                        Some(hdr_solid_two_sided),
1842                        Some(hdr_trans),
1843                        Some(hdr_wf),
1844                    ) = (
1845                        &resources.hdr_solid_pipeline,
1846                        &resources.hdr_solid_two_sided_pipeline,
1847                        &resources.hdr_transparent_pipeline,
1848                        &resources.hdr_wireframe_pipeline,
1849                    ) {
1850                        for item in &opaque {
1851                            let solid_pl = if item.material.is_two_sided() {
1852                                hdr_solid_two_sided
1853                            } else {
1854                                hdr_solid
1855                            };
1856                            draw_item_hdr(&mut render_pass, item, solid_pl, hdr_trans, hdr_wf);
1857                        }
1858                    }
1859                }
1860            }
1861
1862            // Cap fill pass (HDR path : section view cross-section fill).
1863            if !slot.cap_buffers.is_empty() {
1864                if let Some(ref hdr_overlay) = resources.hdr_overlay_pipeline {
1865                    render_pass.set_pipeline(hdr_overlay);
1866                    render_pass.set_bind_group(0, camera_bg, &[]);
1867                    for (vbuf, ibuf, idx_count, _ubuf, bg) in &slot.cap_buffers {
1868                        render_pass.set_bind_group(1, bg, &[]);
1869                        render_pass.set_vertex_buffer(0, vbuf.slice(..));
1870                        render_pass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint32);
1871                        render_pass.draw_indexed(0..*idx_count, 0, 0..1);
1872                    }
1873                }
1874            }
1875
1876            // SciVis Phase B+D+M8+M: point cloud, glyph, polyline, volume, streamtube (HDR path).
1877            emit_scivis_draw_calls!(
1878                &self.resources,
1879                &mut render_pass,
1880                &self.point_cloud_gpu_data,
1881                &self.glyph_gpu_data,
1882                &self.polyline_gpu_data,
1883                &self.volume_gpu_data,
1884                &self.streamtube_gpu_data,
1885                camera_bg,
1886                &self.tube_gpu_data,
1887                &self.image_slice_gpu_data,
1888                &self.tensor_glyph_gpu_data,
1889                &self.ribbon_gpu_data,
1890                &self.volume_surface_slice_gpu_data,
1891                &self.sprite_gpu_data,
1892                true
1893            );
1894
1895            // Phase 16 : GPU implicit surface (HDR path, before skybox).
1896            if !self.implicit_gpu_data.is_empty() {
1897                if let Some(ref dual) = self.resources.implicit_pipeline {
1898                    render_pass.set_pipeline(dual.for_format(true));
1899                    render_pass.set_bind_group(0, camera_bg, &[]);
1900                    for gpu in &self.implicit_gpu_data {
1901                        render_pass.set_bind_group(1, &gpu.bind_group, &[]);
1902                        render_pass.draw(0..6, 0..1);
1903                    }
1904                }
1905            }
1906            // Phase 17 : GPU marching cubes indirect draw (HDR path).
1907            if !self.mc_gpu_data.is_empty() {
1908                render_pass.set_bind_group(0, camera_bg, &[]);
1909                for mc in &self.mc_gpu_data {
1910                    let vol = &self.resources.mc_volumes[mc.volume_idx];
1911                    if mc.wireframe || frame.viewport.wireframe_mode {
1912                        if let Some(ref dual) = self.resources.mc_wireframe_pipeline {
1913                            render_pass.set_pipeline(dual.for_format(true));
1914                            for (slab, wire_bg) in
1915                                vol.slabs.iter().zip(mc.wire_slab_bgs.iter())
1916                            {
1917                                render_pass.set_bind_group(1, wire_bg, &[]);
1918                                render_pass.draw_indirect(&slab.wire_indirect_buf, 0);
1919                            }
1920                        }
1921                    } else if let Some(ref dual) = self.resources.mc_surface_pipeline {
1922                        render_pass.set_pipeline(dual.for_format(true));
1923                        render_pass.set_bind_group(1, &mc.render_bg, &[]);
1924                        for slab in &vol.slabs {
1925                            render_pass.set_vertex_buffer(0, slab.vertex_buf.slice(..));
1926                            render_pass.draw_indirect(&slab.indirect_buf, 0);
1927                        }
1928                    }
1929                }
1930            }
1931
1932            // Gaussian splats (HDR path).
1933            if !self.gaussian_splat_draw_data.is_empty() {
1934                if let Some(ref dual) = self.resources.gaussian_splat_pipeline {
1935                    render_pass.set_pipeline(dual.for_format(true));
1936                    render_pass.set_bind_group(0, camera_bg, &[]);
1937                    for dd in &self.gaussian_splat_draw_data {
1938                        if dd.wireframe {
1939                            continue;
1940                        }
1941                        if let Some(set) = self.resources.gaussian_splat_store.get(dd.store_index) {
1942                            if let Some(Some(vp_sort)) = set.viewport_sort.get(dd.viewport_index) {
1943                                render_pass.set_bind_group(1, &vp_sort.render_bg, &[]);
1944                                render_pass.draw(0..6, 0..dd.count);
1945                            }
1946                        }
1947                    }
1948                }
1949            }
1950            // TransparentVolumeMesh boundary wireframe overlay (HDR path).
1951            if !self.tvm_wireframe_draws.is_empty() {
1952                if let (Some(tvm_bg), Some(hdr_wf)) = (&self.tvm_wireframe_bg, &resources.hdr_wireframe_pipeline) {
1953                    for mesh_id in &self.tvm_wireframe_draws {
1954                        if let Some(mesh) = resources.mesh_store.get(*mesh_id) {
1955                            render_pass.set_pipeline(hdr_wf);
1956                            render_pass.set_bind_group(1, tvm_bg, &[]);
1957                            render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1958                            render_pass.set_index_buffer(
1959                                mesh.edge_index_buffer.slice(..),
1960                                wgpu::IndexFormat::Uint32,
1961                            );
1962                            render_pass.draw_indexed(0..mesh.edge_index_count, 0, 0..1);
1963                        }
1964                    }
1965                }
1966            }
1967
1968            // Draw skybox last among opaques : only uncovered sky pixels pass depth == 1.0.
1969            if show_skybox {
1970                render_pass.set_bind_group(0, camera_bg, &[]);
1971                render_pass.set_pipeline(&resources.skybox_pipeline);
1972                render_pass.draw(0..3, 0..1);
1973            }
1974        }
1975
1976        // -----------------------------------------------------------------------
1977        // SSAA resolve pass: downsample supersampled scene -> hdr_texture.
1978        // Only runs when ssaa_factor > 1 and the resolve pipeline is available.
1979        // -----------------------------------------------------------------------
1980        if ssaa_factor > 1 {
1981            let slot_hdr = self.viewport_slots[vp_idx].hdr.as_ref().unwrap();
1982            if let (Some(pipeline), Some(bg)) = (
1983                &self.resources.ssaa_resolve_pipeline,
1984                &slot_hdr.ssaa_resolve_bind_group,
1985            ) {
1986                let mut resolve_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1987                    label: Some("ssaa_resolve_pass"),
1988                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1989                        view: &slot_hdr.hdr_view,
1990                        resolve_target: None,
1991                        ops: wgpu::Operations {
1992                            load: wgpu::LoadOp::Load,
1993                            store: wgpu::StoreOp::Store,
1994                        },
1995                        depth_slice: None,
1996                    })],
1997                    depth_stencil_attachment: None,
1998                    timestamp_writes: None,
1999                    occlusion_query_set: None,
2000                });
2001                resolve_pass.set_pipeline(pipeline);
2002                resolve_pass.set_bind_group(0, bg, &[]);
2003                resolve_pass.draw(0..3, 0..1);
2004            }
2005        }
2006
2007        // -----------------------------------------------------------------------
2008        // Sub-object highlight pass: face fill, edge lines, vertex sprites.
2009        // Runs after opaque geometry (depth buffer is ready) and before OIT so
2010        // highlights are not occluded by opaque surfaces.
2011        // -----------------------------------------------------------------------
2012        if let Some(sub_hl) = self.viewport_slots[vp_idx].sub_highlight.as_ref() {
2013            let resources = &self.resources;
2014            if let (Some(fill_pl), Some(edge_pl), Some(sprite_pl)) = (
2015                &resources.sub_highlight_fill_pipeline,
2016                &resources.sub_highlight_edge_pipeline,
2017                &resources.sub_highlight_sprite_pipeline,
2018            ) {
2019                let slot_hdr = self.viewport_slots[vp_idx].hdr.as_ref().unwrap();
2020                let camera_bg = &self.viewport_slots[vp_idx].camera_bind_group;
2021                let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2022                    label: Some("sub_highlight_pass"),
2023                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2024                        view: &slot_hdr.hdr_view,
2025                        resolve_target: None,
2026                        ops: wgpu::Operations {
2027                            load: wgpu::LoadOp::Load,
2028                            store: wgpu::StoreOp::Store,
2029                        },
2030                        depth_slice: None,
2031                    })],
2032                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2033                        view: &slot_hdr.hdr_depth_view,
2034                        depth_ops: Some(wgpu::Operations {
2035                            load: wgpu::LoadOp::Load,
2036                            // Store even though depth_write_enabled=false on all
2037                            // sub-highlight pipelines: the values are unchanged, but
2038                            // StoreOp::Discard would invalidate the tile on Metal and
2039                            // cause subsequent passes (tone_map, grid, screen images)
2040                            // to read 0.0, making the background go black.
2041                            store: wgpu::StoreOp::Store,
2042                        }),
2043                        stencil_ops: None,
2044                    }),
2045                    timestamp_writes: None,
2046                    occlusion_query_set: None,
2047                });
2048
2049                if sub_hl.fill_vertex_count > 0 {
2050                    pass.set_pipeline(fill_pl);
2051                    pass.set_bind_group(0, camera_bg, &[]);
2052                    pass.set_bind_group(1, &sub_hl.fill_bind_group, &[]);
2053                    pass.set_vertex_buffer(0, sub_hl.fill_vertex_buf.slice(..));
2054                    pass.draw(0..sub_hl.fill_vertex_count, 0..1);
2055                }
2056                if sub_hl.edge_segment_count > 0 {
2057                    pass.set_pipeline(edge_pl);
2058                    pass.set_bind_group(0, camera_bg, &[]);
2059                    pass.set_bind_group(1, &sub_hl.edge_bind_group, &[]);
2060                    pass.set_vertex_buffer(0, sub_hl.edge_vertex_buf.slice(..));
2061                    pass.draw(0..6, 0..sub_hl.edge_segment_count);
2062                }
2063                if sub_hl.sprite_point_count > 0 {
2064                    pass.set_pipeline(sprite_pl);
2065                    pass.set_bind_group(0, camera_bg, &[]);
2066                    pass.set_bind_group(1, &sub_hl.sprite_bind_group, &[]);
2067                    pass.set_vertex_buffer(0, sub_hl.sprite_vertex_buf.slice(..));
2068                    pass.draw(0..6, 0..sub_hl.sprite_point_count);
2069                }
2070            }
2071        }
2072
2073        // -----------------------------------------------------------------------
2074        // OIT pass: render transparent items into accum + reveal textures.
2075        // Completely skipped when no transparent items exist (zero overhead).
2076        // -----------------------------------------------------------------------
2077        let has_transparent = if self.use_instancing && !self.instanced_batches.is_empty() {
2078            // Transparent instanced batches go through OIT. Transparent excluded items
2079            // (two-sided, active-attribute, matcap) are not in any instanced batch, so
2080            // they must also be checked here -- otherwise the OIT pass is skipped and
2081            // those items are invisible.
2082            self.instanced_batches.iter().any(|b| b.is_transparent)
2083                || scene_items.iter().any(|i| {
2084                    !i.appearance.hidden
2085                        && (i.appearance.opacity < 1.0 || i.material.is_blend())
2086                        && (i.active_attribute.is_some()
2087                            || i.material.is_two_sided()
2088                            || i.material.matcap_id.is_some())
2089                })
2090        } else {
2091            scene_items
2092                .iter()
2093                .any(|i| !i.appearance.hidden && i.appearance.opacity < 1.0)
2094        } || frame
2095            .scene
2096            .transparent_volume_meshes
2097            .iter()
2098            .any(|i| !i.appearance.hidden);
2099
2100        if has_transparent {
2101            // OIT targets already allocated in the pre-pass above.
2102            if let (Some(accum_view), Some(reveal_view)) = (
2103                slot_hdr.oit_accum_view.as_ref(),
2104                slot_hdr.oit_reveal_view.as_ref(),
2105            ) {
2106                let hdr_depth_view = &slot_hdr.hdr_depth_view;
2107                // Clear accum to (0,0,0,0), reveal to 1.0 (no contribution yet).
2108                let mut oit_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2109                    label: Some("oit_pass"),
2110                    color_attachments: &[
2111                        Some(wgpu::RenderPassColorAttachment {
2112                            view: accum_view,
2113                            resolve_target: None,
2114                            ops: wgpu::Operations {
2115                                load: wgpu::LoadOp::Clear(wgpu::Color {
2116                                    r: 0.0,
2117                                    g: 0.0,
2118                                    b: 0.0,
2119                                    a: 0.0,
2120                                }),
2121                                store: wgpu::StoreOp::Store,
2122                            },
2123                            depth_slice: None,
2124                        }),
2125                        Some(wgpu::RenderPassColorAttachment {
2126                            view: reveal_view,
2127                            resolve_target: None,
2128                            ops: wgpu::Operations {
2129                                load: wgpu::LoadOp::Clear(wgpu::Color {
2130                                    r: 1.0,
2131                                    g: 1.0,
2132                                    b: 1.0,
2133                                    a: 1.0,
2134                                }),
2135                                store: wgpu::StoreOp::Store,
2136                            },
2137                            depth_slice: None,
2138                        }),
2139                    ],
2140                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2141                        view: hdr_depth_view,
2142                        depth_ops: Some(wgpu::Operations {
2143                            load: wgpu::LoadOp::Load, // reuse opaque depth
2144                            store: wgpu::StoreOp::Store,
2145                        }),
2146                        stencil_ops: None,
2147                    }),
2148                    timestamp_writes: None,
2149                    occlusion_query_set: None,
2150                });
2151
2152                oit_pass.set_bind_group(0, camera_bg, &[]);
2153
2154                if self.use_instancing && !self.instanced_batches.is_empty() {
2155                    let use_indirect_oit = self.gpu_culling_enabled
2156                        && self.resources.oit_instanced_cull_pipeline.is_some()
2157                        && self.resources.indirect_args_buf.is_some();
2158
2159                    if use_indirect_oit {
2160                        if let (Some(pipeline), Some(indirect_buf)) = (
2161                            &self.resources.oit_instanced_cull_pipeline,
2162                            &self.resources.indirect_args_buf,
2163                        ) {
2164                            oit_pass.set_pipeline(pipeline);
2165                            for (batch_global_idx, batch) in
2166                                self.instanced_batches.iter().enumerate()
2167                            {
2168                                if !batch.is_transparent {
2169                                    continue;
2170                                }
2171                                let Some(mesh) = self.resources.mesh_store.get(batch.mesh_id)
2172                                else {
2173                                    continue;
2174                                };
2175                                let mat_key = (
2176                                    batch.texture_id.unwrap_or(u64::MAX),
2177                                    batch.normal_map_id.unwrap_or(u64::MAX),
2178                                    batch.ao_map_id.unwrap_or(u64::MAX),
2179                                );
2180                                let Some(inst_tex_bg) =
2181                                    self.resources.instance_cull_bind_groups.get(&mat_key)
2182                                else {
2183                                    continue;
2184                                };
2185                                oit_pass.set_bind_group(1, inst_tex_bg, &[]);
2186                                oit_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
2187                                oit_pass.set_index_buffer(
2188                                    mesh.index_buffer.slice(..),
2189                                    wgpu::IndexFormat::Uint32,
2190                                );
2191                                oit_pass.draw_indexed_indirect(
2192                                    indirect_buf,
2193                                    batch_global_idx as u64 * 20,
2194                                );
2195                            }
2196                        }
2197                    } else if let Some(ref pipeline) = self.resources.oit_instanced_pipeline {
2198                        oit_pass.set_pipeline(pipeline);
2199                        for batch in &self.instanced_batches {
2200                            if !batch.is_transparent {
2201                                continue;
2202                            }
2203                            let Some(mesh) = self.resources.mesh_store.get(batch.mesh_id) else {
2204                                continue;
2205                            };
2206                            let mat_key = (
2207                                batch.texture_id.unwrap_or(u64::MAX),
2208                                batch.normal_map_id.unwrap_or(u64::MAX),
2209                                batch.ao_map_id.unwrap_or(u64::MAX),
2210                            );
2211                            let Some(inst_tex_bg) =
2212                                self.resources.instance_bind_groups.get(&mat_key)
2213                            else {
2214                                continue;
2215                            };
2216                            oit_pass.set_bind_group(1, inst_tex_bg, &[]);
2217                            oit_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
2218                            oit_pass.set_index_buffer(
2219                                mesh.index_buffer.slice(..),
2220                                wgpu::IndexFormat::Uint32,
2221                            );
2222                            oit_pass.draw_indexed(
2223                                0..mesh.index_count,
2224                                0,
2225                                batch.instance_offset..batch.instance_offset + batch.instance_count,
2226                            );
2227                        }
2228                    }
2229
2230                    // Transparent excluded items (two-sided, active attribute, matcap) are not
2231                    // in any instanced batch, so the instanced OIT loop above skips them.
2232                    // Render them here individually so they are not invisible at opacity < 1.
2233                    if let Some(ref pipeline) = self.resources.oit_pipeline {
2234                        oit_pass.set_pipeline(pipeline);
2235                        let mut last_skinned = false;
2236                        for item in scene_items {
2237                            if item.appearance.hidden || (item.appearance.opacity >= 1.0 && !item.material.is_blend()) {
2238                                continue;
2239                            }
2240                            let skin_bg = item.skin_instance.and_then(|inst| {
2241                                self.resources.skin_instance_bind_group(item.mesh_id, inst)
2242                            });
2243                            if skin_bg.is_none()
2244                                && item.active_attribute.is_none()
2245                                && !item.material.is_two_sided()
2246                                && item.material.matcap_id.is_none()
2247                            {
2248                                continue;
2249                            }
2250                            let Some(mesh) = self.resources.mesh_store.get(item.mesh_id) else {
2251                                continue;
2252                            };
2253                            let want_skinned = skin_bg.is_some()
2254                                && self.resources.skinned_oit_pipeline.is_some();
2255                            if want_skinned != last_skinned {
2256                                if want_skinned {
2257                                    oit_pass.set_pipeline(
2258                                        self.resources.skinned_oit_pipeline.as_ref().unwrap(),
2259                                    );
2260                                } else {
2261                                    oit_pass.set_pipeline(pipeline);
2262                                }
2263                                last_skinned = want_skinned;
2264                            }
2265                            oit_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
2266                            if let Some(bg) = skin_bg {
2267                                oit_pass.set_bind_group(2, bg, &[]);
2268                            }
2269                            oit_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
2270                            oit_pass.set_index_buffer(
2271                                mesh.index_buffer.slice(..),
2272                                wgpu::IndexFormat::Uint32,
2273                            );
2274                            oit_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
2275                        }
2276                    }
2277                } else if let Some(ref pipeline) = self.resources.oit_pipeline {
2278                    oit_pass.set_pipeline(pipeline);
2279                    let mut last_skinned = false;
2280                    for item in scene_items {
2281                        if item.appearance.hidden || item.appearance.opacity >= 1.0 {
2282                            continue;
2283                        }
2284                        let Some(mesh) = self.resources.mesh_store.get(item.mesh_id) else {
2285                            continue;
2286                        };
2287                        let skin_bg = item.skin_instance.and_then(|inst| {
2288                            self.resources.skin_instance_bind_group(item.mesh_id, inst)
2289                        });
2290                        let want_skinned = skin_bg.is_some()
2291                            && self.resources.skinned_oit_pipeline.is_some();
2292                        if want_skinned != last_skinned {
2293                            if want_skinned {
2294                                oit_pass.set_pipeline(
2295                                    self.resources.skinned_oit_pipeline.as_ref().unwrap(),
2296                                );
2297                            } else {
2298                                oit_pass.set_pipeline(pipeline);
2299                            }
2300                            last_skinned = want_skinned;
2301                        }
2302                        oit_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
2303                        if let Some(bg) = skin_bg {
2304                            oit_pass.set_bind_group(2, bg, &[]);
2305                        }
2306                        oit_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
2307                        oit_pass.set_index_buffer(
2308                            mesh.index_buffer.slice(..),
2309                            wgpu::IndexFormat::Uint32,
2310                        );
2311                        oit_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
2312                    }
2313                }
2314
2315                // -----------------------------------------------------------
2316                // Projected tetrahedra transparent volume meshes (Phase 6).
2317                // -----------------------------------------------------------
2318                if !frame.scene.transparent_volume_meshes.is_empty() {
2319                    self.resources.ensure_pt_pipeline(device);
2320                    if let Some(pipeline) = self.resources.pt_pipeline.as_ref() {
2321                        oit_pass.set_pipeline(pipeline);
2322                        oit_pass.set_bind_group(0, camera_bg, &[]);
2323                        for item in &frame.scene.transparent_volume_meshes {
2324                            if item.appearance.hidden {
2325                                continue;
2326                            }
2327                            if item.appearance.wireframe || frame.viewport.wireframe_mode {
2328                                continue;
2329                            }
2330                            let Some(gpu) = self.resources.projected_tet_store.get(item.id.0)
2331                            else {
2332                                continue;
2333                            };
2334                            let (scalar_min, scalar_max) =
2335                                item.scalar_range.unwrap_or(gpu.scalar_range);
2336                            let uniform = crate::resources::ProjectedTetUniform {
2337                                density: item.density,
2338                                scalar_min,
2339                                scalar_max,
2340                                threshold_min: item.threshold_min,
2341                                threshold_max: item.threshold_max,
2342                                _pad: 0.0,
2343                            };
2344                            queue.write_buffer(
2345                                &gpu.uniform_buffer,
2346                                0,
2347                                bytemuck::bytes_of(&uniform),
2348                            );
2349                            for chunk in &gpu.chunks {
2350                                oit_pass.set_bind_group(1, &chunk.bind_group, &[]);
2351                                oit_pass.draw(0..6, 0..chunk.tet_count);
2352                            }
2353                        }
2354                    }
2355                }
2356            }
2357        }
2358
2359        // -----------------------------------------------------------------------
2360        // OIT composite pass: blend accum/reveal into HDR buffer.
2361        // Only executes when transparent items were present.
2362        // -----------------------------------------------------------------------
2363        if has_transparent {
2364            if let (Some(pipeline), Some(bg)) = (
2365                self.resources.oit_composite_pipeline.as_ref(),
2366                slot_hdr.oit_composite_bind_group.as_ref(),
2367            ) {
2368                let hdr_view = &slot_hdr.hdr_view;
2369                let mut composite_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2370                    label: Some("oit_composite_pass"),
2371                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2372                        view: hdr_view,
2373                        resolve_target: None,
2374                        ops: wgpu::Operations {
2375                            load: wgpu::LoadOp::Load,
2376                            store: wgpu::StoreOp::Store,
2377                        },
2378                        depth_slice: None,
2379                    })],
2380                    depth_stencil_attachment: None,
2381                    timestamp_writes: None,
2382                    occlusion_query_set: None,
2383                });
2384                composite_pass.set_pipeline(pipeline);
2385                composite_pass.set_bind_group(0, bg, &[]);
2386                composite_pass.draw(0..3, 0..1);
2387            }
2388        }
2389
2390        // -----------------------------------------------------------------------
2391        // Phase 4: Surface LIC passes.
2392        // Pass 1: render each LIC mesh into lic_vector_texture (Rgba8Unorm).
2393        // Pass 2: advect fullscreen triangle into lic_output_texture (R8Unorm).
2394        // -----------------------------------------------------------------------
2395        if !self.lic_gpu_data.is_empty() {
2396            if let (Some(surface_pipeline), Some(advect_pipeline)) = (
2397                self.resources.lic_surface_pipeline.as_ref(),
2398                self.resources.lic_advect_pipeline.as_ref(),
2399            ) {
2400                let camera_bg = &slot.camera_bind_group;
2401                // Pass 1: surface vector pass (clears lic_vector_texture first).
2402                {
2403                    let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2404                        label: Some("lic_surface_pass"),
2405                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2406                            view: &slot_hdr.lic_vector_view,
2407                            resolve_target: None,
2408                            ops: wgpu::Operations {
2409                                load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
2410                                store: wgpu::StoreOp::Store,
2411                            },
2412                            depth_slice: None,
2413                        })],
2414                        depth_stencil_attachment: None,
2415                        timestamp_writes: None,
2416                        occlusion_query_set: None,
2417                    });
2418                    pass.set_pipeline(surface_pipeline);
2419                    pass.set_bind_group(0, camera_bg, &[]);
2420                    for gpu in &self.lic_gpu_data {
2421                        let Some(mesh) = self.resources.mesh_store.get(gpu.mesh_id) else {
2422                            continue;
2423                        };
2424                        let Some(vec_buf) =
2425                            mesh.vector_attribute_buffers.get(&gpu.vector_attribute)
2426                        else {
2427                            continue;
2428                        };
2429                        pass.set_bind_group(1, &gpu.bind_group, &[]);
2430                        pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
2431                        pass.set_vertex_buffer(1, vec_buf.slice(..));
2432                        pass.set_index_buffer(
2433                            mesh.index_buffer.slice(..),
2434                            wgpu::IndexFormat::Uint32,
2435                        );
2436                        pass.draw_indexed(0..mesh.index_count, 0, 0..1);
2437                    }
2438                }
2439                // Pass 2: advect pass (fullscreen, writes LIC intensity to lic_output_texture).
2440                {
2441                    let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2442                        label: Some("lic_advect_pass"),
2443                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2444                            view: &slot_hdr.lic_output_view,
2445                            resolve_target: None,
2446                            ops: wgpu::Operations {
2447                                load: wgpu::LoadOp::Clear(wgpu::Color {
2448                                    r: 0.5,
2449                                    g: 0.0,
2450                                    b: 0.0,
2451                                    a: 1.0,
2452                                }),
2453                                store: wgpu::StoreOp::Store,
2454                            },
2455                            depth_slice: None,
2456                        })],
2457                        depth_stencil_attachment: None,
2458                        timestamp_writes: None,
2459                        occlusion_query_set: None,
2460                    });
2461                    pass.set_pipeline(advect_pipeline);
2462                    pass.set_bind_group(0, &slot_hdr.lic_advect_bind_group, &[]);
2463                    pass.draw(0..3, 0..1);
2464                }
2465            }
2466        }
2467
2468        // -----------------------------------------------------------------------
2469        // Outline composite pass (HDR path): blit offscreen outline onto hdr_view.
2470        // Runs after the HDR scene pass (which has depth+stencil) in a separate
2471        // pass with no depth attachment, so the composite pipeline is compatible.
2472        // -----------------------------------------------------------------------
2473        if !slot.outline_object_buffers.is_empty()
2474            || !slot.splat_outline_buffers.is_empty()
2475            || !slot.streamtube_outline_items.is_empty()
2476            || !slot.tube_outline_items.is_empty()
2477            || !slot.ribbon_outline_items.is_empty()
2478            || !slot.polyline_outline_indices.is_empty()
2479            || !slot.volume_outline_indices.is_empty()
2480            || !slot.glyph_outline_indices.is_empty()
2481            || !slot.tensor_glyph_outline_indices.is_empty()
2482            || !slot.sprite_outline_indices.is_empty()
2483            || !slot.raw_geom_outline_buffers.is_empty()
2484            || !slot.screen_rect_outline_buffers.is_empty()
2485            || !slot.implicit_outline_indices.is_empty()
2486            || !slot.mc_outline_data.is_empty()
2487        {
2488            // Prefer the HDR-format pipeline; fall back to LDR single-sample.
2489            let hdr_pipeline = self
2490                .resources
2491                .outline_composite_pipeline_hdr
2492                .as_ref()
2493                .or(self.resources.outline_composite_pipeline_single.as_ref());
2494            if let Some(pipeline) = hdr_pipeline {
2495                let bg = &slot_hdr.outline_composite_bind_group;
2496                let hdr_view = &slot_hdr.hdr_view;
2497                let hdr_depth_view = &slot_hdr.hdr_depth_view;
2498                let mut outline_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2499                    label: Some("hdr_outline_composite_pass"),
2500                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2501                        view: hdr_view,
2502                        resolve_target: None,
2503                        ops: wgpu::Operations {
2504                            load: wgpu::LoadOp::Load,
2505                            store: wgpu::StoreOp::Store,
2506                        },
2507                        depth_slice: None,
2508                    })],
2509                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2510                        view: hdr_depth_view,
2511                        depth_ops: Some(wgpu::Operations {
2512                            load: wgpu::LoadOp::Load,
2513                            store: wgpu::StoreOp::Store,
2514                        }),
2515                        stencil_ops: None,
2516                    }),
2517                    timestamp_writes: None,
2518                    occlusion_query_set: None,
2519                });
2520                outline_pass.set_pipeline(pipeline);
2521                outline_pass.set_bind_group(0, bg, &[]);
2522                outline_pass.draw(0..3, 0..1);
2523            }
2524        }
2525
2526        // Phase 5 : effect throttling. Flag was computed in prepare() so that
2527        // FrameStats reports exactly what fired rather than an approximation.
2528        let throttle_effects = self.degradation_effects_throttled;
2529
2530        // -----------------------------------------------------------------------
2531        // SSAO pass.
2532        // -----------------------------------------------------------------------
2533        if pp.ssao && !throttle_effects {
2534            if let Some(ssao_pipeline) = &self.resources.ssao_pipeline {
2535                {
2536                    let mut ssao_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2537                        label: Some("ssao_pass"),
2538                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2539                            view: &slot_hdr.ssao_view,
2540                            resolve_target: None,
2541                            ops: wgpu::Operations {
2542                                load: wgpu::LoadOp::Clear(wgpu::Color::WHITE),
2543                                store: wgpu::StoreOp::Store,
2544                            },
2545                            depth_slice: None,
2546                        })],
2547                        depth_stencil_attachment: None,
2548                        timestamp_writes: None,
2549                        occlusion_query_set: None,
2550                    });
2551                    ssao_pass.set_pipeline(ssao_pipeline);
2552                    ssao_pass.set_bind_group(0, &slot_hdr.ssao_bg, &[]);
2553                    ssao_pass.draw(0..3, 0..1);
2554                }
2555
2556                // SSAO blur pass.
2557                if let Some(ssao_blur_pipeline) = &self.resources.ssao_blur_pipeline {
2558                    let mut ssao_blur_pass =
2559                        encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2560                            label: Some("ssao_blur_pass"),
2561                            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2562                                view: &slot_hdr.ssao_blur_view,
2563                                resolve_target: None,
2564                                ops: wgpu::Operations {
2565                                    load: wgpu::LoadOp::Clear(wgpu::Color::WHITE),
2566                                    store: wgpu::StoreOp::Store,
2567                                },
2568                                depth_slice: None,
2569                            })],
2570                            depth_stencil_attachment: None,
2571                            timestamp_writes: None,
2572                            occlusion_query_set: None,
2573                        });
2574                    ssao_blur_pass.set_pipeline(ssao_blur_pipeline);
2575                    ssao_blur_pass.set_bind_group(0, &slot_hdr.ssao_blur_bg, &[]);
2576                    ssao_blur_pass.draw(0..3, 0..1);
2577                }
2578            }
2579        }
2580
2581        // -----------------------------------------------------------------------
2582        // Contact shadow pass.
2583        // -----------------------------------------------------------------------
2584        if pp.contact_shadows && !throttle_effects {
2585            if let Some(cs_pipeline) = &self.resources.contact_shadow_pipeline {
2586                let mut cs_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2587                    label: Some("contact_shadow_pass"),
2588                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2589                        view: &slot_hdr.contact_shadow_view,
2590                        resolve_target: None,
2591                        depth_slice: None,
2592                        ops: wgpu::Operations {
2593                            load: wgpu::LoadOp::Clear(wgpu::Color::WHITE),
2594                            store: wgpu::StoreOp::Store,
2595                        },
2596                    })],
2597                    depth_stencil_attachment: None,
2598                    timestamp_writes: None,
2599                    occlusion_query_set: None,
2600                });
2601                cs_pass.set_pipeline(cs_pipeline);
2602                cs_pass.set_bind_group(0, &slot_hdr.contact_shadow_bg, &[]);
2603                cs_pass.draw(0..3, 0..1);
2604            }
2605        }
2606
2607        // -----------------------------------------------------------------------
2608        // Bloom passes.
2609        // -----------------------------------------------------------------------
2610        if pp.bloom && !throttle_effects {
2611            // Threshold pass: extract bright pixels into bloom_threshold_texture.
2612            if let Some(bloom_threshold_pipeline) = &self.resources.bloom_threshold_pipeline {
2613                {
2614                    let mut threshold_pass =
2615                        encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2616                            label: Some("bloom_threshold_pass"),
2617                            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2618                                view: &slot_hdr.bloom_threshold_view,
2619                                resolve_target: None,
2620                                ops: wgpu::Operations {
2621                                    load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
2622                                    store: wgpu::StoreOp::Store,
2623                                },
2624                                depth_slice: None,
2625                            })],
2626                            depth_stencil_attachment: None,
2627                            timestamp_writes: None,
2628                            occlusion_query_set: None,
2629                        });
2630                    threshold_pass.set_pipeline(bloom_threshold_pipeline);
2631                    threshold_pass.set_bind_group(0, &slot_hdr.bloom_threshold_bg, &[]);
2632                    threshold_pass.draw(0..3, 0..1);
2633                }
2634
2635                // 4 ping-pong H+V blur passes for a wide glow.
2636                // Pass 1: threshold -> ping -> pong. Passes 2-4: pong -> ping -> pong.
2637                if let Some(blur_pipeline) = &self.resources.bloom_blur_pipeline {
2638                    let blur_h_bg = &slot_hdr.bloom_blur_h_bg;
2639                    let blur_h_pong_bg = &slot_hdr.bloom_blur_h_pong_bg;
2640                    let blur_v_bg = &slot_hdr.bloom_blur_v_bg;
2641                    let bloom_ping_view = &slot_hdr.bloom_ping_view;
2642                    let bloom_pong_view = &slot_hdr.bloom_pong_view;
2643                    const BLUR_ITERATIONS: usize = 4;
2644                    for i in 0..BLUR_ITERATIONS {
2645                        // H pass: pass 0 reads threshold, subsequent passes read pong.
2646                        let h_bg = if i == 0 { blur_h_bg } else { blur_h_pong_bg };
2647                        {
2648                            let mut h_pass =
2649                                encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2650                                    label: Some("bloom_blur_h_pass"),
2651                                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2652                                        view: bloom_ping_view,
2653                                        resolve_target: None,
2654                                        ops: wgpu::Operations {
2655                                            load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
2656                                            store: wgpu::StoreOp::Store,
2657                                        },
2658                                        depth_slice: None,
2659                                    })],
2660                                    depth_stencil_attachment: None,
2661                                    timestamp_writes: None,
2662                                    occlusion_query_set: None,
2663                                });
2664                            h_pass.set_pipeline(blur_pipeline);
2665                            h_pass.set_bind_group(0, h_bg, &[]);
2666                            h_pass.draw(0..3, 0..1);
2667                        }
2668                        // V pass: ping -> pong.
2669                        {
2670                            let mut v_pass =
2671                                encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2672                                    label: Some("bloom_blur_v_pass"),
2673                                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2674                                        view: bloom_pong_view,
2675                                        resolve_target: None,
2676                                        ops: wgpu::Operations {
2677                                            load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
2678                                            store: wgpu::StoreOp::Store,
2679                                        },
2680                                        depth_slice: None,
2681                                    })],
2682                                    depth_stencil_attachment: None,
2683                                    timestamp_writes: None,
2684                                    occlusion_query_set: None,
2685                                });
2686                            v_pass.set_pipeline(blur_pipeline);
2687                            v_pass.set_bind_group(0, blur_v_bg, &[]);
2688                            v_pass.draw(0..3, 0..1);
2689                        }
2690                    }
2691                }
2692            }
2693        }
2694
2695        // -----------------------------------------------------------------------
2696        // Depth of field pass: HDR + depth -> dof_texture (when enabled).
2697        // -----------------------------------------------------------------------
2698        if pp.dof_enabled && !throttle_effects {
2699            if let Some(dof_pipeline) = &self.resources.dof_pipeline {
2700                let mut dof_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2701                    label: Some("dof_pass"),
2702                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2703                        view: &slot_hdr.dof_view,
2704                        resolve_target: None,
2705                        depth_slice: None,
2706                        ops: wgpu::Operations {
2707                            load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
2708                            store: wgpu::StoreOp::Store,
2709                        },
2710                    })],
2711                    depth_stencil_attachment: None,
2712                    timestamp_writes: None,
2713                    occlusion_query_set: None,
2714                });
2715                dof_pass.set_pipeline(dof_pipeline);
2716                dof_pass.set_bind_group(0, &slot_hdr.dof_bg, &[]);
2717                dof_pass.draw(0..3, 0..1);
2718            }
2719        }
2720
2721        // -----------------------------------------------------------------------
2722        // Tone map pass: HDR + bloom + AO -> tone-mapped LDR.
2723        //
2724        // When render_scale < 1.0 the entire post-process chain runs at scene
2725        // resolution. The result lands in upscale_view (scene-res) and is then
2726        // upscale-blitted to output_view at native resolution.
2727        // -----------------------------------------------------------------------
2728        let use_fxaa = pp.fxaa;
2729        let use_hdr_upscale = slot_hdr.upscale_bind_group.is_some();
2730        if let Some(tone_map_pipeline) = &self.resources.tone_map_pipeline {
2731            let tone_target: &wgpu::TextureView = if use_fxaa {
2732                &slot_hdr.fxaa_view
2733            } else if use_hdr_upscale {
2734                slot_hdr.upscale_view.as_ref().unwrap()
2735            } else {
2736                output_view
2737            };
2738            let mut tone_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2739                label: Some("tone_map_pass"),
2740                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2741                    view: tone_target,
2742                    resolve_target: None,
2743                    ops: wgpu::Operations {
2744                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
2745                        store: wgpu::StoreOp::Store,
2746                    },
2747                    depth_slice: None,
2748                })],
2749                depth_stencil_attachment: None,
2750                timestamp_writes: None,
2751                occlusion_query_set: None,
2752            });
2753            tone_pass.set_pipeline(tone_map_pipeline);
2754            tone_pass.set_bind_group(0, &slot_hdr.tone_map_bind_group, &[]);
2755            tone_pass.draw(0..3, 0..1);
2756        }
2757
2758        // -----------------------------------------------------------------------
2759        // FXAA pass: fxaa_texture -> upscale_view (scaled) or output_view (1:1).
2760        // -----------------------------------------------------------------------
2761        if use_fxaa {
2762            if let Some(fxaa_pipeline) = &self.resources.fxaa_pipeline {
2763                let fxaa_target: &wgpu::TextureView = if use_hdr_upscale {
2764                    slot_hdr.upscale_view.as_ref().unwrap()
2765                } else {
2766                    output_view
2767                };
2768                let mut fxaa_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2769                    label: Some("fxaa_pass"),
2770                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2771                        view: fxaa_target,
2772                        resolve_target: None,
2773                        ops: wgpu::Operations {
2774                            load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
2775                            store: wgpu::StoreOp::Store,
2776                        },
2777                        depth_slice: None,
2778                    })],
2779                    depth_stencil_attachment: None,
2780                    timestamp_writes: None,
2781                    occlusion_query_set: None,
2782                });
2783                fxaa_pass.set_pipeline(fxaa_pipeline);
2784                fxaa_pass.set_bind_group(0, &slot_hdr.fxaa_bind_group, &[]);
2785                fxaa_pass.draw(0..3, 0..1);
2786            }
2787        }
2788
2789        // -----------------------------------------------------------------------
2790        // HDR upscale pass: blit scene-resolution post-processed output to native
2791        // output_view. Only runs when render_scale < 1.0.
2792        // -----------------------------------------------------------------------
2793        if use_hdr_upscale {
2794            let slot_hdr = self.viewport_slots[vp_idx].hdr.as_ref().unwrap();
2795            if let Some(upscale_bg) = &slot_hdr.upscale_bind_group {
2796                if let Some(pipeline) = &self.resources.dyn_res_upscale_pipeline {
2797                    let mut upscale_pass =
2798                        encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2799                            label: Some("hdr_upscale_pass"),
2800                            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2801                                view: output_view,
2802                                resolve_target: None,
2803                                ops: wgpu::Operations {
2804                                    load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
2805                                    store: wgpu::StoreOp::Store,
2806                                },
2807                                depth_slice: None,
2808                            })],
2809                            depth_stencil_attachment: None,
2810                            timestamp_writes: None,
2811                            occlusion_query_set: None,
2812                        });
2813                    upscale_pass.set_pipeline(pipeline);
2814                    upscale_pass.set_bind_group(0, upscale_bg, &[]);
2815                    upscale_pass.draw(0..3, 0..1);
2816                }
2817            }
2818        }
2819
2820        // Depth blit pass: when render_scale < 1.0, the scene depth texture is
2821        // smaller than the output surface. Copy it to output_depth_texture (native
2822        // resolution) so the post-tone-map passes below can attach output_depth_view
2823        // alongside output_view without a size mismatch. Skipped when render_scale
2824        // is 1.0 (output_depth_view is just a second view of hdr_depth_texture).
2825        {
2826            let slot_hdr = self.viewport_slots[vp_idx].hdr.as_ref().unwrap();
2827            if let Some(blit_bg) = &slot_hdr.depth_blit_bind_group {
2828                if let Some(blit_pipeline) = &self.resources.depth_blit_pipeline {
2829                    let mut blit_pass =
2830                        encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2831                            label: Some("depth_blit_pass"),
2832                            color_attachments: &[],
2833                            depth_stencil_attachment: Some(
2834                                wgpu::RenderPassDepthStencilAttachment {
2835                                    view: &slot_hdr.output_depth_view,
2836                                    depth_ops: Some(wgpu::Operations {
2837                                        load: wgpu::LoadOp::Clear(1.0),
2838                                        store: wgpu::StoreOp::Store,
2839                                    }),
2840                                    stencil_ops: None,
2841                                },
2842                            ),
2843                            timestamp_writes: None,
2844                            occlusion_query_set: None,
2845                        });
2846                    blit_pass.set_pipeline(blit_pipeline);
2847                    blit_pass.set_bind_group(0, blit_bg, &[]);
2848                    blit_pass.draw(0..3, 0..1);
2849                }
2850            }
2851        }
2852
2853        // Grid pass (HDR path): draw the existing analytical grid on the final
2854        // output after tone mapping / FXAA, reusing the scene depth buffer so
2855        // scene geometry still occludes the grid exactly as in the LDR path.
2856        if frame.viewport.show_grid {
2857            let slot = &self.viewport_slots[vp_idx];
2858            let slot_hdr = slot.hdr.as_ref().unwrap();
2859            let grid_bg = &slot.grid_bind_group;
2860            let mut grid_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2861                label: Some("hdr_grid_pass"),
2862                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2863                    view: output_view,
2864                    resolve_target: None,
2865                    ops: wgpu::Operations {
2866                        load: wgpu::LoadOp::Load,
2867                        store: wgpu::StoreOp::Store,
2868                    },
2869                    depth_slice: None,
2870                })],
2871                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2872                    view: &slot_hdr.output_depth_view,
2873                    depth_ops: Some(wgpu::Operations {
2874                        load: wgpu::LoadOp::Load,
2875                        store: wgpu::StoreOp::Store,
2876                    }),
2877                    stencil_ops: None,
2878                }),
2879                timestamp_writes: None,
2880                occlusion_query_set: None,
2881            });
2882            grid_pass.set_pipeline(&self.resources.grid_pipeline);
2883            grid_pass.set_bind_group(0, grid_bg, &[]);
2884            grid_pass.draw(0..3, 0..1);
2885        }
2886
2887        // Ground plane pass (HDR path): drawn after grid, before editor overlays.
2888        // Uses the scene depth buffer for correct occlusion against geometry.
2889        if !matches!(
2890            frame.effects.ground_plane.mode,
2891            crate::renderer::types::GroundPlaneMode::None
2892        ) {
2893            let slot = &self.viewport_slots[vp_idx];
2894            let slot_hdr = slot.hdr.as_ref().unwrap();
2895            let mut gp_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2896                label: Some("hdr_ground_plane_pass"),
2897                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2898                    view: output_view,
2899                    resolve_target: None,
2900                    ops: wgpu::Operations {
2901                        load: wgpu::LoadOp::Load,
2902                        store: wgpu::StoreOp::Store,
2903                    },
2904                    depth_slice: None,
2905                })],
2906                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2907                    view: &slot_hdr.output_depth_view,
2908                    depth_ops: Some(wgpu::Operations {
2909                        load: wgpu::LoadOp::Load,
2910                        store: wgpu::StoreOp::Store,
2911                    }),
2912                    stencil_ops: None,
2913                }),
2914                timestamp_writes: None,
2915                occlusion_query_set: None,
2916            });
2917            gp_pass.set_pipeline(&self.resources.ground_plane_pipeline);
2918            gp_pass.set_bind_group(0, &self.resources.ground_plane_bind_group, &[]);
2919            gp_pass.draw(0..3, 0..1);
2920        }
2921
2922        // Phase 10B / Phase 12 : screen-space image overlay pass (HDR path).
2923        // Must run before the editor overlay and axes passes because those
2924        // discard hdr_depth_view (StoreOp::Discard). The DC pipeline compares
2925        // per-pixel image depth against the scene depth buffer; if the buffer
2926        // has been discarded, Metal returns zeros and all DC fragments fail.
2927        // Plain overlay items (depth_compare: Always) are unaffected by depth,
2928        // but ordering them here keeps depth-composite correct.
2929        if !self.screen_image_gpu_data.is_empty() {
2930            if let Some(overlay_pipeline) = &self.resources.screen_image_pipeline {
2931                let slot_hdr = self.viewport_slots[vp_idx].hdr.as_ref().unwrap();
2932                let dc_pipeline = self.resources.screen_image_dc_pipeline.as_ref();
2933                let mut img_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2934                    label: Some("screen_image_pass"),
2935                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2936                        view: output_view,
2937                        resolve_target: None,
2938                        ops: wgpu::Operations {
2939                            load: wgpu::LoadOp::Load,
2940                            store: wgpu::StoreOp::Store,
2941                        },
2942                        depth_slice: None,
2943                    })],
2944                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2945                        view: &slot_hdr.output_depth_view,
2946                        depth_ops: Some(wgpu::Operations {
2947                            load: wgpu::LoadOp::Load,
2948                            store: wgpu::StoreOp::Discard,
2949                        }),
2950                        stencil_ops: None,
2951                    }),
2952                    timestamp_writes: None,
2953                    occlusion_query_set: None,
2954                });
2955                for gpu in &self.screen_image_gpu_data {
2956                    if let (Some(dc_bg), Some(dc_pipe)) = (&gpu.depth_bind_group, dc_pipeline) {
2957                        img_pass.set_pipeline(dc_pipe);
2958                        img_pass.set_bind_group(0, dc_bg, &[]);
2959                    } else {
2960                        img_pass.set_pipeline(overlay_pipeline);
2961                        img_pass.set_bind_group(0, &gpu.bind_group, &[]);
2962                    }
2963                    img_pass.draw(0..6, 0..1);
2964                }
2965            }
2966        }
2967
2968        // Editor overlay pass (HDR path): draw viewport/editor overlays on the
2969        // final output after tone mapping / FXAA, reusing the scene depth
2970        // buffer so depth-tested helpers still behave correctly.
2971        {
2972            let slot = &self.viewport_slots[vp_idx];
2973            let slot_hdr = slot.hdr.as_ref().unwrap();
2974            let has_editor_overlays = (frame.interaction.gizmo_model.is_some()
2975                && slot.gizmo_index_count > 0)
2976                || !slot.constraint_line_buffers.is_empty()
2977                || !slot.clip_plane_fill_buffers.is_empty()
2978                || !slot.clip_plane_line_buffers.is_empty()
2979                || !slot.xray_object_buffers.is_empty();
2980            if has_editor_overlays {
2981                let camera_bg = &slot.camera_bind_group;
2982                let mut overlay_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2983                    label: Some("hdr_editor_overlay_pass"),
2984                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2985                        view: output_view,
2986                        resolve_target: None,
2987                        ops: wgpu::Operations {
2988                            load: wgpu::LoadOp::Load,
2989                            store: wgpu::StoreOp::Store,
2990                        },
2991                        depth_slice: None,
2992                    })],
2993                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2994                        view: &slot_hdr.output_depth_view,
2995                        depth_ops: Some(wgpu::Operations {
2996                            load: wgpu::LoadOp::Load,
2997                            store: wgpu::StoreOp::Discard,
2998                        }),
2999                        stencil_ops: None,
3000                    }),
3001                    timestamp_writes: None,
3002                    occlusion_query_set: None,
3003                });
3004
3005                if frame.interaction.gizmo_model.is_some() && slot.gizmo_index_count > 0 {
3006                    overlay_pass.set_pipeline(&self.resources.gizmo_pipeline);
3007                    overlay_pass.set_bind_group(0, camera_bg, &[]);
3008                    overlay_pass.set_bind_group(1, &slot.gizmo_bind_group, &[]);
3009                    overlay_pass.set_vertex_buffer(0, slot.gizmo_vertex_buffer.slice(..));
3010                    overlay_pass.set_index_buffer(
3011                        slot.gizmo_index_buffer.slice(..),
3012                        wgpu::IndexFormat::Uint32,
3013                    );
3014                    overlay_pass.draw_indexed(0..slot.gizmo_index_count, 0, 0..1);
3015                }
3016
3017                if !slot.constraint_line_buffers.is_empty() {
3018                    overlay_pass.set_pipeline(&self.resources.overlay_line_pipeline);
3019                    overlay_pass.set_bind_group(0, camera_bg, &[]);
3020                    for (vbuf, ibuf, index_count, _ubuf, bg) in &slot.constraint_line_buffers {
3021                        overlay_pass.set_bind_group(1, bg, &[]);
3022                        overlay_pass.set_vertex_buffer(0, vbuf.slice(..));
3023                        overlay_pass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint32);
3024                        overlay_pass.draw_indexed(0..*index_count, 0, 0..1);
3025                    }
3026                }
3027
3028                if !slot.clip_plane_fill_buffers.is_empty() {
3029                    overlay_pass.set_pipeline(&self.resources.overlay_pipeline);
3030                    overlay_pass.set_bind_group(0, camera_bg, &[]);
3031                    for (vbuf, ibuf, idx_count, _ubuf, bg) in &slot.clip_plane_fill_buffers {
3032                        overlay_pass.set_bind_group(1, bg, &[]);
3033                        overlay_pass.set_vertex_buffer(0, vbuf.slice(..));
3034                        overlay_pass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint32);
3035                        overlay_pass.draw_indexed(0..*idx_count, 0, 0..1);
3036                    }
3037                }
3038
3039                if !slot.clip_plane_line_buffers.is_empty() {
3040                    overlay_pass.set_pipeline(&self.resources.overlay_line_pipeline);
3041                    overlay_pass.set_bind_group(0, camera_bg, &[]);
3042                    for (vbuf, ibuf, idx_count, _ubuf, bg) in &slot.clip_plane_line_buffers {
3043                        overlay_pass.set_bind_group(1, bg, &[]);
3044                        overlay_pass.set_vertex_buffer(0, vbuf.slice(..));
3045                        overlay_pass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint32);
3046                        overlay_pass.draw_indexed(0..*idx_count, 0, 0..1);
3047                    }
3048                }
3049
3050                if !slot.xray_object_buffers.is_empty() {
3051                    overlay_pass.set_pipeline(&self.resources.xray_pipeline);
3052                    overlay_pass.set_bind_group(0, camera_bg, &[]);
3053                    for (mesh_id, _buf, bg) in &slot.xray_object_buffers {
3054                        let Some(mesh) = self.resources.mesh_store.get(*mesh_id) else {
3055                            continue;
3056                        };
3057                        overlay_pass.set_bind_group(1, bg, &[]);
3058                        overlay_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
3059                        overlay_pass.set_index_buffer(
3060                            mesh.index_buffer.slice(..),
3061                            wgpu::IndexFormat::Uint32,
3062                        );
3063                        overlay_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
3064                    }
3065                }
3066            }
3067        }
3068
3069        // Axes indicator pass (HDR path): draw in screen space on the final
3070        // output after tone mapping / FXAA so it stays visible in PBR mode.
3071        if frame.viewport.show_axes_indicator {
3072            let slot = &self.viewport_slots[vp_idx];
3073            if slot.axes_vertex_count > 0 {
3074                let slot_hdr = slot.hdr.as_ref().unwrap();
3075                let mut axes_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3076                    label: Some("hdr_axes_pass"),
3077                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
3078                        view: output_view,
3079                        resolve_target: None,
3080                        ops: wgpu::Operations {
3081                            load: wgpu::LoadOp::Load,
3082                            store: wgpu::StoreOp::Store,
3083                        },
3084                        depth_slice: None,
3085                    })],
3086                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
3087                        view: &slot_hdr.output_depth_view,
3088                        depth_ops: Some(wgpu::Operations {
3089                            load: wgpu::LoadOp::Load,
3090                            store: wgpu::StoreOp::Discard,
3091                        }),
3092                        stencil_ops: None,
3093                    }),
3094                    timestamp_writes: None,
3095                    occlusion_query_set: None,
3096                });
3097                axes_pass.set_pipeline(&self.resources.axes_pipeline);
3098                axes_pass.set_vertex_buffer(0, slot.axes_vertex_buffer.slice(..));
3099                axes_pass.draw(0..slot.axes_vertex_count, 0..1);
3100            }
3101        }
3102
3103        // Overlay shapes, rects, labels, scalar bars, rulers, and overlay images (HDR path): drawn last.
3104        let has_overlay = self.overlay_shape_gpu_data.is_some()
3105            || self.overlay_rect_gpu_data.is_some()
3106            || self.label_gpu_data.is_some()
3107            || self.scalar_bar_gpu_data.is_some()
3108            || self.ruler_gpu_data.is_some()
3109            || self.loading_bar_gpu_data.is_some()
3110            || !self.overlay_image_gpu_data.is_empty();
3111
3112        // HDR backdrop blur: the tonemapped scene is on an intermediate so we
3113        // can sample it for the blur. When blur is needed we redirect the
3114        // tonemapped output to a managed intermediate, blur it, then draw
3115        // overlays there and blit to output_view at the end.
3116        let needs_hdr_blur = self.has_backdrop_blur_shapes();
3117        let hdr_blur_bg: Option<wgpu::BindGroup> = if needs_hdr_blur && has_overlay {
3118            self.ensure_backdrop_blur_state(device, w.max(1), h.max(1));
3119            // Blit output_view content to the intermediate so we have a
3120            // samplable copy. We already have the tonemapped result on
3121            // output_view. Unfortunately surface textures can't be sampled,
3122            // so we blit to our intermediate first.
3123            //
3124            // Actually for HDR: the tone-map wrote to output_view (or
3125            // upscale_view). We need the scene in a samplable texture. The
3126            // HDR colour texture (hdr_colour_view) is samplable but it's HDR.
3127            // For simplicity, use the HDR colour texture as the blur source;
3128            // the blur result will be HDR-ish but clamped by the LDR target
3129            // format of the blur textures. This looks acceptable in practice.
3130            let slot_hdr = self.viewport_slots[vp_idx].hdr.as_ref().unwrap();
3131            let source = &slot_hdr.hdr_view;
3132            let spread = self.overlay_shape_gpu_data.as_ref()
3133                .map(|d| d.max_blur_radius)
3134                .unwrap_or(8.0);
3135            Some(self.run_backdrop_blur(&mut encoder, device, queue, source, spread))
3136        } else {
3137            None
3138        };
3139
3140        if has_overlay {
3141            let hdr_depth_view = &self.viewport_slots[vp_idx]
3142                .hdr
3143                .as_ref()
3144                .unwrap()
3145                .output_depth_view;
3146            let mut overlay_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3147                label: Some("overlay_pass"),
3148                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
3149                    view: output_view,
3150                    resolve_target: None,
3151                    ops: wgpu::Operations {
3152                        load: wgpu::LoadOp::Load,
3153                        store: wgpu::StoreOp::Store,
3154                    },
3155                    depth_slice: None,
3156                })],
3157                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
3158                    view: hdr_depth_view,
3159                    depth_ops: Some(wgpu::Operations {
3160                        load: wgpu::LoadOp::Load,
3161                        store: wgpu::StoreOp::Discard,
3162                    }),
3163                    stencil_ops: None,
3164                }),
3165                timestamp_writes: None,
3166                occlusion_query_set: None,
3167            });
3168            // Blur backdrop shapes drawn first (behind normal shapes).
3169            if let Some(ref bg) = hdr_blur_bg {
3170                self.draw_blur_shapes(&mut overlay_pass, bg);
3171            }
3172            // SDF overlay shapes (HDR path).
3173            if let Some(ref sd) = self.overlay_shape_gpu_data {
3174                if sd.vertex_count > 0 {
3175                    if let Some(pipeline) = &self.resources.overlay_shape_pipeline {
3176                        if let Some(vbuf) = &sd.vertex_buf {
3177                            overlay_pass.set_pipeline(pipeline);
3178                            overlay_pass.set_vertex_buffer(0, vbuf.slice(..));
3179                            overlay_pass.draw(0..sd.vertex_count, 0..1);
3180                        }
3181                    }
3182                }
3183                if !sd.tex_batches.is_empty() {
3184                    if let Some(pipeline) = &self.resources.overlay_shape_tex_pipeline {
3185                        overlay_pass.set_pipeline(pipeline);
3186                        for batch in &sd.tex_batches {
3187                            overlay_pass.set_bind_group(0, &batch.bind_group, &[]);
3188                            overlay_pass.set_vertex_buffer(0, batch.vertex_buf.slice(..));
3189                            overlay_pass.draw(0..batch.vertex_count, 0..1);
3190                        }
3191                    }
3192                }
3193            }
3194            if let Some(pipeline) = &self.resources.overlay_text_pipeline {
3195                overlay_pass.set_pipeline(pipeline);
3196                if let Some(ref rr) = self.overlay_rect_gpu_data {
3197                    overlay_pass.set_bind_group(0, &rr.bind_group, &[]);
3198                    overlay_pass.set_vertex_buffer(0, rr.vertex_buf.slice(..));
3199                    overlay_pass.draw(0..rr.vertex_count, 0..1);
3200                }
3201                if let Some(ref ld) = self.label_gpu_data {
3202                    overlay_pass.set_bind_group(0, &ld.bind_group, &[]);
3203                    overlay_pass.set_vertex_buffer(0, ld.vertex_buf.slice(..));
3204                    overlay_pass.draw(0..ld.vertex_count, 0..1);
3205                }
3206                if let Some(ref sb) = self.scalar_bar_gpu_data {
3207                    overlay_pass.set_bind_group(0, &sb.bind_group, &[]);
3208                    overlay_pass.set_vertex_buffer(0, sb.vertex_buf.slice(..));
3209                    overlay_pass.draw(0..sb.vertex_count, 0..1);
3210                }
3211                if let Some(ref rd) = self.ruler_gpu_data {
3212                    overlay_pass.set_bind_group(0, &rd.bind_group, &[]);
3213                    overlay_pass.set_vertex_buffer(0, rd.vertex_buf.slice(..));
3214                    overlay_pass.draw(0..rd.vertex_count, 0..1);
3215                }
3216                if let Some(ref lb) = self.loading_bar_gpu_data {
3217                    overlay_pass.set_bind_group(0, &lb.bind_group, &[]);
3218                    overlay_pass.set_vertex_buffer(0, lb.vertex_buf.slice(..));
3219                    overlay_pass.draw(0..lb.vertex_count, 0..1);
3220                }
3221            }
3222            // Phase 7 : overlay images drawn last inside the overlay pass.
3223            if !self.overlay_image_gpu_data.is_empty() {
3224                if let Some(pipeline) = &self.resources.screen_image_pipeline {
3225                    overlay_pass.set_pipeline(pipeline);
3226                    for gpu in &self.overlay_image_gpu_data {
3227                        overlay_pass.set_bind_group(0, &gpu.bind_group, &[]);
3228                        overlay_pass.draw(0..6, 0..1);
3229                    }
3230                }
3231            }
3232        }
3233
3234        // Phase 4 : resolve timestamp queries -> staging buffer (HDR path).
3235        if let (Some(qs), Some(res_buf), Some(stg_buf)) = (
3236            self.ts_query_set.as_ref(),
3237            self.ts_resolve_buf.as_ref(),
3238            self.ts_staging_buf.as_ref(),
3239        ) {
3240            encoder.resolve_query_set(qs, 0..2, res_buf, 0);
3241            encoder.copy_buffer_to_buffer(res_buf, 0, stg_buf, 0, 16);
3242            self.ts_needs_readback = true;
3243        }
3244
3245        encoder.finish()
3246    }
3247
3248    /// Render a frame to an offscreen texture and return raw RGBA bytes.
3249    ///
3250    /// Creates a temporary [`wgpu::Texture`] render target of the given dimensions,
3251    /// runs all render passes (shadow, scene, post-processing) into it via
3252    /// [`render()`](Self::render), then copies the result back to CPU memory.
3253    ///
3254    /// No OS window or [`wgpu::Surface`] is required. The caller is responsible for
3255    /// initialising the wgpu adapter with `compatible_surface: None` and for
3256    /// constructing a valid [`FrameData`] (including `viewport_size` matching
3257    /// `width`/`height`).
3258    ///
3259    /// Returns `width * height * 4` bytes in RGBA8 layout. The caller encodes to
3260    /// PNG/EXR independently : no image codec dependency in this crate.
3261    pub fn render_offscreen(
3262        &mut self,
3263        device: &wgpu::Device,
3264        queue: &wgpu::Queue,
3265        frame: &FrameData,
3266        width: u32,
3267        height: u32,
3268    ) -> Vec<u8> {
3269        // 1. Create offscreen texture with RENDER_ATTACHMENT | COPY_SRC usage.
3270        let target_format = self.resources.target_format;
3271        let offscreen_texture = device.create_texture(&wgpu::TextureDescriptor {
3272            label: Some("offscreen_target"),
3273            size: wgpu::Extent3d {
3274                width: width.max(1),
3275                height: height.max(1),
3276                depth_or_array_layers: 1,
3277            },
3278            mip_level_count: 1,
3279            sample_count: 1,
3280            dimension: wgpu::TextureDimension::D2,
3281            format: target_format,
3282            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
3283            view_formats: &[],
3284        });
3285
3286        // 2. Create a texture view for rendering into.
3287        let output_view = offscreen_texture.create_view(&wgpu::TextureViewDescriptor::default());
3288
3289        // 3. render() calls ensure_viewport_hdr which provides the depth-stencil buffer
3290        //    for both LDR and HDR paths, so no separate ensure_outline_target is needed.
3291
3292        // 4. Render the scene into the offscreen texture.
3293        //    The caller must set `frame.camera.viewport_size` to `[width as f32, height as f32]`
3294        //    and `frame.camera.render_camera.aspect` to `width as f32 / height as f32`
3295        //    for correct HDR target allocation and scissor rects.
3296        let cmd_buf = self.render(device, queue, &output_view, frame);
3297        queue.submit(std::iter::once(cmd_buf));
3298
3299        // 5. Copy texture -> staging buffer (wgpu requires row alignment to 256 bytes).
3300        let bytes_per_pixel = 4u32;
3301        let unpadded_row = width * bytes_per_pixel;
3302        let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
3303        let padded_row = (unpadded_row + align - 1) & !(align - 1);
3304        let buffer_size = (padded_row * height.max(1)) as u64;
3305
3306        let staging_buf = device.create_buffer(&wgpu::BufferDescriptor {
3307            label: Some("offscreen_staging"),
3308            size: buffer_size,
3309            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
3310            mapped_at_creation: false,
3311        });
3312
3313        let mut copy_encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
3314            label: Some("offscreen_copy_encoder"),
3315        });
3316        copy_encoder.copy_texture_to_buffer(
3317            wgpu::TexelCopyTextureInfo {
3318                texture: &offscreen_texture,
3319                mip_level: 0,
3320                origin: wgpu::Origin3d::ZERO,
3321                aspect: wgpu::TextureAspect::All,
3322            },
3323            wgpu::TexelCopyBufferInfo {
3324                buffer: &staging_buf,
3325                layout: wgpu::TexelCopyBufferLayout {
3326                    offset: 0,
3327                    bytes_per_row: Some(padded_row),
3328                    rows_per_image: Some(height.max(1)),
3329                },
3330            },
3331            wgpu::Extent3d {
3332                width: width.max(1),
3333                height: height.max(1),
3334                depth_or_array_layers: 1,
3335            },
3336        );
3337        queue.submit(std::iter::once(copy_encoder.finish()));
3338
3339        // 6. Map buffer and extract tightly-packed RGBA pixels.
3340        let (tx, rx) = std::sync::mpsc::channel();
3341        staging_buf
3342            .slice(..)
3343            .map_async(wgpu::MapMode::Read, move |result| {
3344                let _ = tx.send(result);
3345            });
3346        device
3347            .poll(wgpu::PollType::Wait {
3348                submission_index: None,
3349                timeout: Some(std::time::Duration::from_secs(5)),
3350            })
3351            .unwrap();
3352        let _ = rx.recv().unwrap_or(Err(wgpu::BufferAsyncError));
3353
3354        let mut pixels: Vec<u8> = Vec::with_capacity((width * height * 4) as usize);
3355        {
3356            let mapped = staging_buf.slice(..).get_mapped_range();
3357            let data: &[u8] = &mapped;
3358            if padded_row == unpadded_row {
3359                // No padding : copy entire slice directly.
3360                pixels.extend_from_slice(data);
3361            } else {
3362                // Strip row padding.
3363                for row in 0..height as usize {
3364                    let start = row * padded_row as usize;
3365                    let end = start + unpadded_row as usize;
3366                    pixels.extend_from_slice(&data[start..end]);
3367                }
3368            }
3369        }
3370        staging_buf.unmap();
3371
3372        // 7. Swizzle BGRA -> RGBA if the format stores bytes in BGRA order.
3373        let is_bgra = matches!(
3374            target_format,
3375            wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb
3376        );
3377        if is_bgra {
3378            for pixel in pixels.chunks_exact_mut(4) {
3379                pixel.swap(0, 2); // B ↔ R
3380            }
3381        }
3382
3383        pixels
3384    }
3385
3386    // ------------------------------------------------------------------
3387    // Backdrop blur helpers
3388    // ------------------------------------------------------------------
3389
3390    /// Ensure the backdrop blur state textures exist at the right size.
3391    fn ensure_backdrop_blur_state(
3392        &mut self,
3393        device: &wgpu::Device,
3394        w: u32,
3395        h: u32,
3396    ) {
3397        let need_recreate = match &self.backdrop_blur_state {
3398            Some(s) => s.size != [w, h] || s.format != self.resources.target_format,
3399            None => true,
3400        };
3401        if !need_recreate {
3402            return;
3403        }
3404
3405        let format = self.resources.target_format;
3406        let blur_w = (w / 2).max(1);
3407        let blur_h = (h / 2).max(1);
3408
3409        let intermediate_texture = device.create_texture(&wgpu::TextureDescriptor {
3410            label: Some("backdrop_intermediate"),
3411            size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
3412            mip_level_count: 1,
3413            sample_count: 1,
3414            dimension: wgpu::TextureDimension::D2,
3415            format,
3416            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
3417            view_formats: &[],
3418        });
3419        let intermediate_view = intermediate_texture.create_view(&Default::default());
3420
3421        let make_blur_tex = |label: &str| {
3422            let t = device.create_texture(&wgpu::TextureDescriptor {
3423                label: Some(label),
3424                size: wgpu::Extent3d { width: blur_w, height: blur_h, depth_or_array_layers: 1 },
3425                mip_level_count: 1,
3426                sample_count: 1,
3427                dimension: wgpu::TextureDimension::D2,
3428                format,
3429                usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
3430                view_formats: &[],
3431            });
3432            let v = t.create_view(&Default::default());
3433            (t, v)
3434        };
3435        let (blur_a_texture, blur_a_view) = make_blur_tex("backdrop_blur_a");
3436        let (blur_b_texture, blur_b_view) = make_blur_tex("backdrop_blur_b");
3437
3438        self.backdrop_blur_state = Some(crate::resources::BackdropBlurState {
3439            intermediate_texture,
3440            intermediate_view,
3441            blur_a_texture,
3442            blur_a_view,
3443            blur_b_texture,
3444            blur_b_view,
3445            size: [w, h],
3446            format,
3447        });
3448    }
3449
3450    /// Run the backdrop blur pipeline: blit scene to half-res, then H blur, then V blur.
3451    /// Returns the bind group that can be used to draw blur overlay shapes with the
3452    /// texture pipeline.
3453    fn run_backdrop_blur(
3454        &self,
3455        encoder: &mut wgpu::CommandEncoder,
3456        device: &wgpu::Device,
3457        _queue: &wgpu::Queue,
3458        source_view: &wgpu::TextureView,
3459        spread: f32,
3460    ) -> wgpu::BindGroup {
3461        let bs = self.backdrop_blur_state.as_ref().unwrap();
3462        let blur_bgl = self.resources.backdrop_blur_bgl.as_ref().unwrap();
3463        let blur_sampler = self.resources.backdrop_blur_sampler.as_ref().unwrap();
3464        let blur_pipeline = self.resources.backdrop_blur_pipeline.as_ref().unwrap();
3465        // Reuse dyn_res blit pipeline and BGL for the downsample pass.
3466        let blit_pipeline = self.resources.dyn_res_upscale_pipeline.as_ref().unwrap();
3467        let blit_bgl = self.resources.dyn_res_upscale_bgl.as_ref().unwrap();
3468        let blit_sampler = self.resources.dyn_res_linear_sampler.as_ref().unwrap();
3469
3470        // Step 1: downsample source -> blur_a (half-res) using bilinear blit.
3471        let downsample_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
3472            label: Some("backdrop_downsample_bg"),
3473            layout: blit_bgl,
3474            entries: &[
3475                wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(source_view) },
3476                wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(blit_sampler) },
3477            ],
3478        });
3479        {
3480            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3481                label: Some("backdrop_downsample"),
3482                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
3483                    view: &bs.blur_a_view,
3484                    resolve_target: None,
3485                    ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store },
3486                    depth_slice: None,
3487                })],
3488                depth_stencil_attachment: None,
3489                timestamp_writes: None,
3490                occlusion_query_set: None,
3491            });
3492            pass.set_pipeline(blit_pipeline);
3493            pass.set_bind_group(0, &downsample_bg, &[]);
3494            pass.draw(0..3, 0..1);
3495        }
3496
3497        // Spread scaled for half-res: each texel covers 2 screen pixels.
3498        let effective_spread = (spread / 2.0).max(1.0);
3499
3500        // Step 2: horizontal blur: blur_a -> blur_b.
3501        let h_uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3502            label: Some("blur_h_uniform"),
3503            contents: bytemuck::cast_slice(&[1u32, effective_spread.to_bits(), 0u32, 0u32]),
3504            usage: wgpu::BufferUsages::UNIFORM,
3505        });
3506        let h_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
3507            label: Some("blur_h_bg"),
3508            layout: blur_bgl,
3509            entries: &[
3510                wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&bs.blur_a_view) },
3511                wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(blur_sampler) },
3512                wgpu::BindGroupEntry { binding: 2, resource: h_uniform.as_entire_binding() },
3513            ],
3514        });
3515        {
3516            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3517                label: Some("backdrop_blur_h"),
3518                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
3519                    view: &bs.blur_b_view,
3520                    resolve_target: None,
3521                    ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store },
3522                    depth_slice: None,
3523                })],
3524                depth_stencil_attachment: None,
3525                timestamp_writes: None,
3526                occlusion_query_set: None,
3527            });
3528            pass.set_pipeline(blur_pipeline);
3529            pass.set_bind_group(0, &h_bg, &[]);
3530            pass.draw(0..3, 0..1);
3531        }
3532
3533        // Step 3: vertical blur: blur_b -> blur_a.
3534        let v_uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3535            label: Some("blur_v_uniform"),
3536            contents: bytemuck::cast_slice(&[0u32, effective_spread.to_bits(), 0u32, 0u32]),
3537            usage: wgpu::BufferUsages::UNIFORM,
3538        });
3539        let v_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
3540            label: Some("blur_v_bg"),
3541            layout: blur_bgl,
3542            entries: &[
3543                wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&bs.blur_b_view) },
3544                wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(blur_sampler) },
3545                wgpu::BindGroupEntry { binding: 2, resource: v_uniform.as_entire_binding() },
3546            ],
3547        });
3548        {
3549            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3550                label: Some("backdrop_blur_v"),
3551                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
3552                    view: &bs.blur_a_view,
3553                    resolve_target: None,
3554                    ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store },
3555                    depth_slice: None,
3556                })],
3557                depth_stencil_attachment: None,
3558                timestamp_writes: None,
3559                occlusion_query_set: None,
3560            });
3561            pass.set_pipeline(blur_pipeline);
3562            pass.set_bind_group(0, &v_bg, &[]);
3563            pass.draw(0..3, 0..1);
3564        }
3565
3566        // Build the bind group for overlay shape drawing. Uses the overlay_shape_tex
3567        // bind group layout (texture + sampler) so blur shapes can be drawn with the
3568        // existing texture pipeline.
3569        let tex_bgl = self.resources.overlay_shape_tex_bgl.as_ref().unwrap();
3570        let tex_sampler = self.resources.overlay_shape_tex_sampler.as_ref().unwrap();
3571        device.create_bind_group(&wgpu::BindGroupDescriptor {
3572            label: Some("backdrop_blur_overlay_bg"),
3573            layout: tex_bgl,
3574            entries: &[
3575                wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&bs.blur_a_view) },
3576                wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(tex_sampler) },
3577            ],
3578        })
3579    }
3580
3581    /// Returns true if the current frame has overlay shapes that need backdrop blur.
3582    fn has_backdrop_blur_shapes(&self) -> bool {
3583        self.overlay_shape_gpu_data
3584            .as_ref()
3585            .map_or(false, |sd| sd.blur_vertex_count > 0)
3586    }
3587
3588    /// Draw blur overlay shapes into the given render pass using the texture pipeline.
3589    fn draw_blur_shapes<'rp>(
3590        &'rp self,
3591        render_pass: &mut wgpu::RenderPass<'rp>,
3592        blur_bind_group: &'rp wgpu::BindGroup,
3593    ) {
3594        if let Some(ref sd) = self.overlay_shape_gpu_data {
3595            if sd.blur_vertex_count > 0 {
3596                if let (Some(pipeline), Some(vbuf)) = (
3597                    &self.resources.overlay_shape_tex_pipeline,
3598                    &sd.blur_vertex_buf,
3599                ) {
3600                    render_pass.set_pipeline(pipeline);
3601                    render_pass.set_bind_group(0, blur_bind_group, &[]);
3602                    render_pass.set_vertex_buffer(0, vbuf.slice(..));
3603                    render_pass.draw(0..sd.blur_vertex_count, 0..1);
3604                }
3605            }
3606        }
3607    }
3608}