Skip to main content

viewport_lib/renderer/
render.rs

1use super::*;
2
3impl ViewportRenderer {
4    /// Issue draw calls for the viewport. Call inside a `wgpu::RenderPass`.
5    ///
6    /// This method requires a `'static` render pass (as provided by egui's
7    /// `CallbackTrait`). For non-static render passes (iced, manual wgpu),
8    /// use [`paint_to`](Self::paint_to).
9    pub fn paint(&self, render_pass: &mut wgpu::RenderPass<'static>, frame: &FrameData) {
10        let vp_idx = frame.camera.viewport_index;
11        let camera_bg = self.viewport_camera_bind_group(vp_idx);
12        let grid_bg = self.viewport_grid_bind_group(vp_idx);
13        let vp_slot = self.viewport_slots.get(vp_idx);
14        emit_draw_calls!(
15            &self.resources,
16            &mut *render_pass,
17            frame,
18            self.use_instancing,
19            &self.instanced_batches,
20            camera_bg,
21            grid_bg,
22            &self.compute_filter_results,
23            vp_slot
24        );
25        emit_scivis_draw_calls!(
26            &self.resources,
27            &mut *render_pass,
28            &self.point_cloud_gpu_data,
29            &self.glyph_gpu_data,
30            &self.polyline_gpu_data,
31            &self.volume_gpu_data,
32            &self.streamtube_gpu_data,
33            camera_bg,
34            &self.tube_gpu_data,
35            &self.image_slice_gpu_data,
36            &self.tensor_glyph_gpu_data,
37            &self.ribbon_gpu_data
38        );
39        // Phase 16 : GPU implicit surface (depth-writes enabled, LessEqual compare).
40        if !self.implicit_gpu_data.is_empty() {
41            if let Some(pipeline) = &self.resources.implicit_pipeline {
42                render_pass.set_pipeline(pipeline);
43                render_pass.set_bind_group(0, camera_bg, &[]);
44                for gpu in &self.implicit_gpu_data {
45                    render_pass.set_bind_group(1, &gpu.bind_group, &[]);
46                    render_pass.draw(0..6, 0..1);
47                }
48            }
49        }
50        // Phase 17 : GPU marching cubes indirect draw.
51        if !self.mc_gpu_data.is_empty() {
52            if let Some(pipeline) = &self.resources.mc_surface_pipeline {
53                render_pass.set_pipeline(pipeline);
54                render_pass.set_bind_group(0, camera_bg, &[]);
55                for mc in &self.mc_gpu_data {
56                    let vol = &self.resources.mc_volumes[mc.volume_idx];
57                    render_pass.set_bind_group(1, &mc.render_bg, &[]);
58                    for slab in &vol.slabs {
59                        render_pass.set_vertex_buffer(0, slab.vertex_buf.slice(..));
60                        render_pass.draw_indirect(&slab.indirect_buf, 0);
61                    }
62                }
63            }
64        }
65        // Sub-object highlight (LDR path) : face fill, edge lines, vertex/point sprites.
66        if let Some(sub_hl) = self.viewport_slots.get(vp_idx).and_then(|s| s.sub_highlight.as_ref()) {
67            if let (Some(fill_pl), Some(edge_pl), Some(sprite_pl)) = (
68                &self.resources.sub_highlight_fill_ldr_pipeline,
69                &self.resources.sub_highlight_edge_ldr_pipeline,
70                &self.resources.sub_highlight_sprite_ldr_pipeline,
71            ) {
72                if sub_hl.fill_vertex_count > 0 {
73                    render_pass.set_pipeline(fill_pl);
74                    render_pass.set_bind_group(0, camera_bg, &[]);
75                    render_pass.set_bind_group(1, &sub_hl.fill_bind_group, &[]);
76                    render_pass.set_vertex_buffer(0, sub_hl.fill_vertex_buf.slice(..));
77                    render_pass.draw(0..sub_hl.fill_vertex_count, 0..1);
78                }
79                if sub_hl.edge_segment_count > 0 {
80                    render_pass.set_pipeline(edge_pl);
81                    render_pass.set_bind_group(0, camera_bg, &[]);
82                    render_pass.set_bind_group(1, &sub_hl.edge_bind_group, &[]);
83                    render_pass.set_vertex_buffer(0, sub_hl.edge_vertex_buf.slice(..));
84                    render_pass.draw(0..6, 0..sub_hl.edge_segment_count);
85                }
86                if sub_hl.sprite_point_count > 0 {
87                    render_pass.set_pipeline(sprite_pl);
88                    render_pass.set_bind_group(0, camera_bg, &[]);
89                    render_pass.set_bind_group(1, &sub_hl.sprite_bind_group, &[]);
90                    render_pass.set_vertex_buffer(0, sub_hl.sprite_vertex_buf.slice(..));
91                    render_pass.draw(0..6, 0..sub_hl.sprite_point_count);
92                }
93            }
94        }
95        // Phase 10B : screen-space image overlays (always on top, no depth test).
96        if !self.screen_image_gpu_data.is_empty() {
97            if let Some(pipeline) = &self.resources.screen_image_pipeline {
98                render_pass.set_pipeline(pipeline);
99                for gpu in &self.screen_image_gpu_data {
100                    render_pass.set_bind_group(0, &gpu.bind_group, &[]);
101                    render_pass.draw(0..6, 0..1);
102                }
103            }
104        }
105        // Overlay labels (always on top, after screen images).
106        if let Some(ref ld) = self.label_gpu_data {
107            if let Some(pipeline) = &self.resources.overlay_text_pipeline {
108                render_pass.set_pipeline(pipeline);
109                render_pass.set_bind_group(0, &ld.bind_group, &[]);
110                render_pass.set_vertex_buffer(0, ld.vertex_buf.slice(..));
111                render_pass.draw(0..ld.vertex_count, 0..1);
112            }
113        }
114        // Scalar bars (drawn after labels).
115        if let Some(ref sb) = self.scalar_bar_gpu_data {
116            if let Some(pipeline) = &self.resources.overlay_text_pipeline {
117                render_pass.set_pipeline(pipeline);
118                render_pass.set_bind_group(0, &sb.bind_group, &[]);
119                render_pass.set_vertex_buffer(0, sb.vertex_buf.slice(..));
120                render_pass.draw(0..sb.vertex_count, 0..1);
121            }
122        }
123        // Rulers (drawn after scalar bars).
124        if let Some(ref rd) = self.ruler_gpu_data {
125            if let Some(pipeline) = &self.resources.overlay_text_pipeline {
126                render_pass.set_pipeline(pipeline);
127                render_pass.set_bind_group(0, &rd.bind_group, &[]);
128                render_pass.set_vertex_buffer(0, rd.vertex_buf.slice(..));
129                render_pass.draw(0..rd.vertex_count, 0..1);
130            }
131        }
132        // Loading bars (drawn after rulers).
133        if let Some(ref lb) = self.loading_bar_gpu_data {
134            if let Some(pipeline) = &self.resources.overlay_text_pipeline {
135                render_pass.set_pipeline(pipeline);
136                render_pass.set_bind_group(0, &lb.bind_group, &[]);
137                render_pass.set_vertex_buffer(0, lb.vertex_buf.slice(..));
138                render_pass.draw(0..lb.vertex_count, 0..1);
139            }
140        }
141        // Phase 7 : overlay images (OverlayFrame, drawn last, no depth test).
142        if !self.overlay_image_gpu_data.is_empty() {
143            if let Some(pipeline) = &self.resources.screen_image_pipeline {
144                render_pass.set_pipeline(pipeline);
145                for gpu in &self.overlay_image_gpu_data {
146                    render_pass.set_bind_group(0, &gpu.bind_group, &[]);
147                    render_pass.draw(0..6, 0..1);
148                }
149            }
150        }
151    }
152
153    /// Issue draw calls into a render pass with any lifetime.
154    ///
155    /// Identical to [`paint`](Self::paint) but accepts a render pass with a
156    /// non-`'static` lifetime, making it usable from iced, raw wgpu, or any
157    /// framework that creates its own render pass.
158    pub fn paint_to<'rp>(&'rp self, render_pass: &mut wgpu::RenderPass<'rp>, frame: &FrameData) {
159        let vp_idx = frame.camera.viewport_index;
160        let camera_bg = self.viewport_camera_bind_group(vp_idx);
161        let grid_bg = self.viewport_grid_bind_group(vp_idx);
162        let vp_slot = self.viewport_slots.get(vp_idx);
163        emit_draw_calls!(
164            &self.resources,
165            &mut *render_pass,
166            frame,
167            self.use_instancing,
168            &self.instanced_batches,
169            camera_bg,
170            grid_bg,
171            &self.compute_filter_results,
172            vp_slot
173        );
174        emit_scivis_draw_calls!(
175            &self.resources,
176            &mut *render_pass,
177            &self.point_cloud_gpu_data,
178            &self.glyph_gpu_data,
179            &self.polyline_gpu_data,
180            &self.volume_gpu_data,
181            &self.streamtube_gpu_data,
182            camera_bg,
183            &self.tube_gpu_data,
184            &self.image_slice_gpu_data,
185            &self.tensor_glyph_gpu_data,
186            &self.ribbon_gpu_data
187        );
188        // Phase 16 : GPU implicit surface (depth-writes enabled, LessEqual compare).
189        if !self.implicit_gpu_data.is_empty() {
190            if let Some(pipeline) = &self.resources.implicit_pipeline {
191                render_pass.set_pipeline(pipeline);
192                render_pass.set_bind_group(0, camera_bg, &[]);
193                for gpu in &self.implicit_gpu_data {
194                    render_pass.set_bind_group(1, &gpu.bind_group, &[]);
195                    render_pass.draw(0..6, 0..1);
196                }
197            }
198        }
199        // Phase 17 : GPU marching cubes indirect draw.
200        if !self.mc_gpu_data.is_empty() {
201            if let Some(pipeline) = &self.resources.mc_surface_pipeline {
202                render_pass.set_pipeline(pipeline);
203                render_pass.set_bind_group(0, camera_bg, &[]);
204                for mc in &self.mc_gpu_data {
205                    let vol = &self.resources.mc_volumes[mc.volume_idx];
206                    render_pass.set_bind_group(1, &mc.render_bg, &[]);
207                    for slab in &vol.slabs {
208                        render_pass.set_vertex_buffer(0, slab.vertex_buf.slice(..));
209                        render_pass.draw_indirect(&slab.indirect_buf, 0);
210                    }
211                }
212            }
213        }
214        // Sub-object highlight (LDR path) : face fill, edge lines, vertex/point sprites.
215        if let Some(sub_hl) = self.viewport_slots.get(vp_idx).and_then(|s| s.sub_highlight.as_ref()) {
216            if let (Some(fill_pl), Some(edge_pl), Some(sprite_pl)) = (
217                &self.resources.sub_highlight_fill_ldr_pipeline,
218                &self.resources.sub_highlight_edge_ldr_pipeline,
219                &self.resources.sub_highlight_sprite_ldr_pipeline,
220            ) {
221                if sub_hl.fill_vertex_count > 0 {
222                    render_pass.set_pipeline(fill_pl);
223                    render_pass.set_bind_group(0, camera_bg, &[]);
224                    render_pass.set_bind_group(1, &sub_hl.fill_bind_group, &[]);
225                    render_pass.set_vertex_buffer(0, sub_hl.fill_vertex_buf.slice(..));
226                    render_pass.draw(0..sub_hl.fill_vertex_count, 0..1);
227                }
228                if sub_hl.edge_segment_count > 0 {
229                    render_pass.set_pipeline(edge_pl);
230                    render_pass.set_bind_group(0, camera_bg, &[]);
231                    render_pass.set_bind_group(1, &sub_hl.edge_bind_group, &[]);
232                    render_pass.set_vertex_buffer(0, sub_hl.edge_vertex_buf.slice(..));
233                    render_pass.draw(0..6, 0..sub_hl.edge_segment_count);
234                }
235                if sub_hl.sprite_point_count > 0 {
236                    render_pass.set_pipeline(sprite_pl);
237                    render_pass.set_bind_group(0, camera_bg, &[]);
238                    render_pass.set_bind_group(1, &sub_hl.sprite_bind_group, &[]);
239                    render_pass.set_vertex_buffer(0, sub_hl.sprite_vertex_buf.slice(..));
240                    render_pass.draw(0..6, 0..sub_hl.sprite_point_count);
241                }
242            }
243        }
244        // Phase 10B : screen-space image overlays (always on top, no depth test).
245        if !self.screen_image_gpu_data.is_empty() {
246            if let Some(pipeline) = &self.resources.screen_image_pipeline {
247                render_pass.set_pipeline(pipeline);
248                for gpu in &self.screen_image_gpu_data {
249                    render_pass.set_bind_group(0, &gpu.bind_group, &[]);
250                    render_pass.draw(0..6, 0..1);
251                }
252            }
253        }
254        // Overlay labels (always on top, after screen images).
255        if let Some(ref ld) = self.label_gpu_data {
256            if let Some(pipeline) = &self.resources.overlay_text_pipeline {
257                render_pass.set_pipeline(pipeline);
258                render_pass.set_bind_group(0, &ld.bind_group, &[]);
259                render_pass.set_vertex_buffer(0, ld.vertex_buf.slice(..));
260                render_pass.draw(0..ld.vertex_count, 0..1);
261            }
262        }
263        // Scalar bars (drawn after labels).
264        if let Some(ref sb) = self.scalar_bar_gpu_data {
265            if let Some(pipeline) = &self.resources.overlay_text_pipeline {
266                render_pass.set_pipeline(pipeline);
267                render_pass.set_bind_group(0, &sb.bind_group, &[]);
268                render_pass.set_vertex_buffer(0, sb.vertex_buf.slice(..));
269                render_pass.draw(0..sb.vertex_count, 0..1);
270            }
271        }
272        // Rulers (drawn after scalar bars).
273        if let Some(ref rd) = self.ruler_gpu_data {
274            if let Some(pipeline) = &self.resources.overlay_text_pipeline {
275                render_pass.set_pipeline(pipeline);
276                render_pass.set_bind_group(0, &rd.bind_group, &[]);
277                render_pass.set_vertex_buffer(0, rd.vertex_buf.slice(..));
278                render_pass.draw(0..rd.vertex_count, 0..1);
279            }
280        }
281        // Loading bars (drawn after rulers).
282        if let Some(ref lb) = self.loading_bar_gpu_data {
283            if let Some(pipeline) = &self.resources.overlay_text_pipeline {
284                render_pass.set_pipeline(pipeline);
285                render_pass.set_bind_group(0, &lb.bind_group, &[]);
286                render_pass.set_vertex_buffer(0, lb.vertex_buf.slice(..));
287                render_pass.draw(0..lb.vertex_count, 0..1);
288            }
289        }
290        // Phase 7 : overlay images (OverlayFrame, drawn last, no depth test).
291        if !self.overlay_image_gpu_data.is_empty() {
292            if let Some(pipeline) = &self.resources.screen_image_pipeline {
293                render_pass.set_pipeline(pipeline);
294                for gpu in &self.overlay_image_gpu_data {
295                    render_pass.set_bind_group(0, &gpu.bind_group, &[]);
296                    render_pass.draw(0..6, 0..1);
297                }
298            }
299        }
300    }
301
302    /// Render the scene into an intermediate dyn-res texture for the LDR callback
303    /// render path (e.g. eframe's `CallbackTrait`).
304    ///
305    /// Call from `CallbackTrait::prepare` after [`prepare`](Self::prepare), passing the
306    /// `egui_encoder`. If `current_render_scale < 1.0`, the full scene is drawn into a
307    /// scaled intermediate texture and `true` is returned. Call
308    /// [`paint_dyn_res_blit`](Self::paint_dyn_res_blit) from `CallbackTrait::paint`
309    /// instead of [`paint`](Self::paint).
310    ///
311    /// If scale is 1.0 or above, nothing is encoded and `false` is returned. Call
312    /// [`paint`](Self::paint) as normal.
313    ///
314    /// The `egui_encoder` is submitted before the surface render pass begins, so the
315    /// intermediate texture is fully written before the blit reads it.
316    pub fn prepare_ldr_dyn_res(
317        &mut self,
318        encoder: &mut wgpu::CommandEncoder,
319        device: &wgpu::Device,
320        frame: &FrameData,
321    ) -> bool {
322        if self.current_render_scale >= 1.0 - 0.001 {
323            return false;
324        }
325
326        let vp_idx = frame.camera.viewport_index;
327        let w = (frame.camera.viewport_size[0] as u32).max(1);
328        let h = (frame.camera.viewport_size[1] as u32).max(1);
329        let sw = ((w as f32 * self.current_render_scale) as u32).max(1);
330        let sh = ((h as f32 * self.current_render_scale) as u32).max(1);
331
332        self.ensure_dyn_res_target(device, vp_idx, [sw, sh], [w, h]);
333        self.resources.ensure_dyn_res_ds_pipeline(device);
334
335        let bg_color = frame.viewport.background_color.unwrap_or([
336            65.0 / 255.0,
337            65.0 / 255.0,
338            65.0 / 255.0,
339            1.0,
340        ]);
341
342        {
343            let slot = &self.viewport_slots[vp_idx];
344            let dr = slot.dyn_res.as_ref().unwrap();
345            let color_view = &dr.color_view;
346            let depth_view = &dr.depth_view;
347            let camera_bg = &slot.camera_bind_group;
348            let grid_bg = &slot.grid_bind_group;
349
350            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
351                label: Some("ldr_dyn_res_render_pass"),
352                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
353                    view: color_view,
354                    resolve_target: None,
355                    ops: wgpu::Operations {
356                        load: wgpu::LoadOp::Clear(wgpu::Color {
357                            r: bg_color[0] as f64,
358                            g: bg_color[1] as f64,
359                            b: bg_color[2] as f64,
360                            a: bg_color[3] as f64,
361                        }),
362                        store: wgpu::StoreOp::Store,
363                    },
364                    depth_slice: None,
365                })],
366                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
367                    view: depth_view,
368                    depth_ops: Some(wgpu::Operations {
369                        load: wgpu::LoadOp::Clear(1.0),
370                        store: wgpu::StoreOp::Discard,
371                    }),
372                    stencil_ops: None,
373                }),
374                timestamp_writes: None,
375                occlusion_query_set: None,
376            });
377            emit_draw_calls!(
378                &self.resources,
379                &mut render_pass,
380                frame,
381                self.use_instancing,
382                &self.instanced_batches,
383                camera_bg,
384                grid_bg,
385                &self.compute_filter_results,
386                Some(slot)
387            );
388            emit_scivis_draw_calls!(
389                &self.resources,
390                &mut render_pass,
391                &self.point_cloud_gpu_data,
392                &self.glyph_gpu_data,
393                &self.polyline_gpu_data,
394                &self.volume_gpu_data,
395                &self.streamtube_gpu_data,
396                camera_bg,
397                &self.tube_gpu_data,
398                &self.image_slice_gpu_data,
399                &self.tensor_glyph_gpu_data,
400                &self.ribbon_gpu_data
401            );
402            // Implicit surface.
403            if !self.implicit_gpu_data.is_empty() {
404                if let Some(pipeline) = &self.resources.implicit_pipeline {
405                    render_pass.set_pipeline(pipeline);
406                    render_pass.set_bind_group(0, camera_bg, &[]);
407                    for gpu in &self.implicit_gpu_data {
408                        render_pass.set_bind_group(1, &gpu.bind_group, &[]);
409                        render_pass.draw(0..6, 0..1);
410                    }
411                }
412            }
413            // GPU marching cubes indirect draw.
414            if !self.mc_gpu_data.is_empty() {
415                if let Some(pipeline) = &self.resources.mc_surface_pipeline {
416                    render_pass.set_pipeline(pipeline);
417                    render_pass.set_bind_group(0, camera_bg, &[]);
418                    for mc in &self.mc_gpu_data {
419                        let vol = &self.resources.mc_volumes[mc.volume_idx];
420                        render_pass.set_bind_group(1, &mc.render_bg, &[]);
421                        for slab in &vol.slabs {
422                            render_pass.set_vertex_buffer(0, slab.vertex_buf.slice(..));
423                            render_pass.draw_indirect(&slab.indirect_buf, 0);
424                        }
425                    }
426                }
427            }
428            // Sub-object highlight (LDR path).
429            if let Some(sub_hl) = slot.sub_highlight.as_ref() {
430                if let (Some(fill_pl), Some(edge_pl), Some(sprite_pl)) = (
431                    &self.resources.sub_highlight_fill_ldr_pipeline,
432                    &self.resources.sub_highlight_edge_ldr_pipeline,
433                    &self.resources.sub_highlight_sprite_ldr_pipeline,
434                ) {
435                    if sub_hl.fill_vertex_count > 0 {
436                        render_pass.set_pipeline(fill_pl);
437                        render_pass.set_bind_group(0, camera_bg, &[]);
438                        render_pass.set_bind_group(1, &sub_hl.fill_bind_group, &[]);
439                        render_pass.set_vertex_buffer(0, sub_hl.fill_vertex_buf.slice(..));
440                        render_pass.draw(0..sub_hl.fill_vertex_count, 0..1);
441                    }
442                    if sub_hl.edge_segment_count > 0 {
443                        render_pass.set_pipeline(edge_pl);
444                        render_pass.set_bind_group(0, camera_bg, &[]);
445                        render_pass.set_bind_group(1, &sub_hl.edge_bind_group, &[]);
446                        render_pass.set_vertex_buffer(0, sub_hl.edge_vertex_buf.slice(..));
447                        render_pass.draw(0..6, 0..sub_hl.edge_segment_count);
448                    }
449                    if sub_hl.sprite_point_count > 0 {
450                        render_pass.set_pipeline(sprite_pl);
451                        render_pass.set_bind_group(0, camera_bg, &[]);
452                        render_pass.set_bind_group(1, &sub_hl.sprite_bind_group, &[]);
453                        render_pass.set_vertex_buffer(0, sub_hl.sprite_vertex_buf.slice(..));
454                        render_pass.draw(0..6, 0..sub_hl.sprite_point_count);
455                    }
456                }
457            }
458            // Screen-space image overlays.
459            if !self.screen_image_gpu_data.is_empty() {
460                if let Some(pipeline) = &self.resources.screen_image_pipeline {
461                    render_pass.set_pipeline(pipeline);
462                    for gpu in &self.screen_image_gpu_data {
463                        render_pass.set_bind_group(0, &gpu.bind_group, &[]);
464                        render_pass.draw(0..6, 0..1);
465                    }
466                }
467            }
468            // Overlay labels.
469            if let Some(ref ld) = self.label_gpu_data {
470                if let Some(pipeline) = &self.resources.overlay_text_pipeline {
471                    render_pass.set_pipeline(pipeline);
472                    render_pass.set_bind_group(0, &ld.bind_group, &[]);
473                    render_pass.set_vertex_buffer(0, ld.vertex_buf.slice(..));
474                    render_pass.draw(0..ld.vertex_count, 0..1);
475                }
476            }
477            // Scalar bars.
478            if let Some(ref sb) = self.scalar_bar_gpu_data {
479                if let Some(pipeline) = &self.resources.overlay_text_pipeline {
480                    render_pass.set_pipeline(pipeline);
481                    render_pass.set_bind_group(0, &sb.bind_group, &[]);
482                    render_pass.set_vertex_buffer(0, sb.vertex_buf.slice(..));
483                    render_pass.draw(0..sb.vertex_count, 0..1);
484                }
485            }
486            // Rulers.
487            if let Some(ref rd) = self.ruler_gpu_data {
488                if let Some(pipeline) = &self.resources.overlay_text_pipeline {
489                    render_pass.set_pipeline(pipeline);
490                    render_pass.set_bind_group(0, &rd.bind_group, &[]);
491                    render_pass.set_vertex_buffer(0, rd.vertex_buf.slice(..));
492                    render_pass.draw(0..rd.vertex_count, 0..1);
493                }
494            }
495            // Loading bars.
496            if let Some(ref lb) = self.loading_bar_gpu_data {
497                if let Some(pipeline) = &self.resources.overlay_text_pipeline {
498                    render_pass.set_pipeline(pipeline);
499                    render_pass.set_bind_group(0, &lb.bind_group, &[]);
500                    render_pass.set_vertex_buffer(0, lb.vertex_buf.slice(..));
501                    render_pass.draw(0..lb.vertex_count, 0..1);
502                }
503            }
504            // Overlay images (drawn last).
505            if !self.overlay_image_gpu_data.is_empty() {
506                if let Some(pipeline) = &self.resources.screen_image_pipeline {
507                    render_pass.set_pipeline(pipeline);
508                    for gpu in &self.overlay_image_gpu_data {
509                        render_pass.set_bind_group(0, &gpu.bind_group, &[]);
510                        render_pass.draw(0..6, 0..1);
511                    }
512                }
513            }
514        }
515
516        true
517    }
518
519    /// Blit the dyn-res intermediate texture into the provided render pass.
520    ///
521    /// Call from `CallbackTrait::paint` when
522    /// [`prepare_ldr_dyn_res`](Self::prepare_ldr_dyn_res) returned `true` for the same
523    /// frame. Emits a fullscreen upscale quad into `render_pass`.
524    pub fn paint_dyn_res_blit(
525        &self,
526        render_pass: &mut wgpu::RenderPass<'static>,
527        frame: &FrameData,
528    ) {
529        let vp_idx = frame.camera.viewport_index;
530        if let Some(dr) = self.viewport_slots.get(vp_idx).and_then(|s| s.dyn_res.as_ref()) {
531            if let Some(pipeline) = &self.resources.dyn_res_upscale_ds_pipeline {
532                render_pass.set_pipeline(pipeline);
533                render_pass.set_bind_group(0, &dr.upscale_bind_group, &[]);
534                render_pass.draw(0..3, 0..1);
535            }
536        }
537    }
538
539    /// High-level HDR render for a single viewport identified by `id`.
540    ///
541    /// Unlike [`render`](Self::render), this method does **not** call
542    /// [`prepare`](Self::prepare) internally.  The caller must have already called
543    /// [`prepare_scene`](Self::prepare_scene) and
544    /// [`prepare_viewport`](Self::prepare_viewport) for `id` before invoking this.
545    ///
546    /// This is the right entry point for multi-viewport frames:
547    /// 1. Call `prepare_scene` once.
548    /// 2. Call `prepare_viewport` for each viewport.
549    /// 3. Call `render_viewport` for each viewport with its own `output_view`.
550    ///
551    /// Returns a [`wgpu::CommandBuffer`] ready to submit.
552    pub fn render_viewport(
553        &mut self,
554        device: &wgpu::Device,
555        queue: &wgpu::Queue,
556        output_view: &wgpu::TextureView,
557        id: ViewportId,
558        frame: &FrameData,
559    ) -> wgpu::CommandBuffer {
560        self.render_frame_internal(device, queue, output_view, id.0, frame)
561    }
562
563    /// High-level HDR render method. Handles the full post-processing pipeline:
564    /// scene -> HDR texture -> (bloom) -> (SSAO) -> tone map -> output_view.
565    ///
566    /// When `frame.post_process.enabled` is false, falls back to a simple LDR render
567    /// pass targeting `output_view` directly.
568    ///
569    /// Returns a `CommandBuffer` ready to submit.
570    pub fn render(
571        &mut self,
572        device: &wgpu::Device,
573        queue: &wgpu::Queue,
574        output_view: &wgpu::TextureView,
575        frame: &FrameData,
576    ) -> wgpu::CommandBuffer {
577        // Always run prepare() to upload uniforms and run the shadow pass.
578        self.prepare(device, queue, frame);
579        self.render_frame_internal(
580            device,
581            queue,
582            output_view,
583            frame.camera.viewport_index,
584            frame,
585        )
586    }
587
588    /// Render-only path shared by `render()` and `render_viewport()`.
589    ///
590    /// `vp_idx` selects the per-viewport slot to use for camera/HDR state,
591    /// independent of `frame.camera.viewport_index`.
592    fn render_frame_internal(
593        &mut self,
594        device: &wgpu::Device,
595        queue: &wgpu::Queue,
596        output_view: &wgpu::TextureView,
597        vp_idx: usize,
598        frame: &FrameData,
599    ) -> wgpu::CommandBuffer {
600        // Read scene items from the surface submission.
601        let scene_items: &[SceneRenderItem] = match &frame.scene.surfaces {
602            SurfaceSubmission::Flat(items) => items.as_ref(),
603        };
604
605        let bg_color = frame.viewport.background_color.unwrap_or([
606            65.0 / 255.0,
607            65.0 / 255.0,
608            65.0 / 255.0,
609            1.0,
610        ]);
611        let w = frame.camera.viewport_size[0] as u32;
612        let h = frame.camera.viewport_size[1] as u32;
613
614        // Ensure per-viewport HDR targets. Provides a depth buffer for both LDR and HDR paths.
615        let ssaa_factor = frame.effects.post_process.ssaa_factor.max(1);
616        self.ensure_viewport_hdr(device, queue, vp_idx, w.max(1), h.max(1), ssaa_factor);
617
618        // Phase 4 : lazy-initialize GPU timestamp resources on first render call when supported.
619        if self.ts_query_set.is_none()
620            && device.features().contains(wgpu::Features::TIMESTAMP_QUERY)
621        {
622            self.ts_query_set = Some(device.create_query_set(&wgpu::QuerySetDescriptor {
623                label: Some("ts_query_set"),
624                ty: wgpu::QueryType::Timestamp,
625                count: 2,
626            }));
627            self.ts_resolve_buf = Some(device.create_buffer(&wgpu::BufferDescriptor {
628                label: Some("ts_resolve_buf"),
629                size: 16,
630                usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
631                mapped_at_creation: false,
632            }));
633            self.ts_staging_buf = Some(device.create_buffer(&wgpu::BufferDescriptor {
634                label: Some("ts_staging_buf"),
635                size: 16,
636                usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
637                mapped_at_creation: false,
638            }));
639            self.ts_period = queue.get_timestamp_period();
640        }
641
642        if !frame.effects.post_process.enabled {
643            // LDR fallback. When dynamic resolution is active and render_scale < 1.0,
644            // draw into a scaled intermediate texture and upscale-blit to output_view.
645            // Otherwise render directly to output_view.
646            let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
647                label: Some("ldr_encoder"),
648            });
649
650            let use_dyn_res = self.current_render_scale < 1.0 - 0.001;
651
652            if use_dyn_res {
653                let sw = ((w as f32 * self.current_render_scale) as u32).max(1);
654                let sh = ((h as f32 * self.current_render_scale) as u32).max(1);
655                self.ensure_dyn_res_target(device, vp_idx, [sw, sh], [w.max(1), h.max(1)]);
656            }
657
658            {
659                let slot = &self.viewport_slots[vp_idx];
660                let slot_hdr = slot.hdr.as_ref().unwrap();
661                let camera_bg = &slot.camera_bind_group;
662                let grid_bg = &slot.grid_bind_group;
663                // Choose render target: dyn_res intermediate or directly output_view.
664                let (scene_color_view, scene_depth_view): (&wgpu::TextureView, &wgpu::TextureView) =
665                    if use_dyn_res {
666                        let dr = slot.dyn_res.as_ref().unwrap();
667                        (&dr.color_view, &dr.depth_view)
668                    } else {
669                        (output_view, &slot_hdr.outline_depth_view)
670                    };
671                let ts_writes = self.ts_query_set.as_ref().map(|qs| {
672                    wgpu::RenderPassTimestampWrites {
673                        query_set: qs,
674                        beginning_of_pass_write_index: Some(0),
675                        end_of_pass_write_index: Some(1),
676                    }
677                });
678                let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
679                    label: Some("ldr_render_pass"),
680                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
681                        view: scene_color_view,
682                        resolve_target: None,
683                        ops: wgpu::Operations {
684                            load: wgpu::LoadOp::Clear(wgpu::Color {
685                                r: bg_color[0] as f64,
686                                g: bg_color[1] as f64,
687                                b: bg_color[2] as f64,
688                                a: bg_color[3] as f64,
689                            }),
690                            store: wgpu::StoreOp::Store,
691                        },
692                        depth_slice: None,
693                    })],
694                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
695                        view: scene_depth_view,
696                        depth_ops: Some(wgpu::Operations {
697                            load: wgpu::LoadOp::Clear(1.0),
698                            store: wgpu::StoreOp::Discard,
699                        }),
700                        stencil_ops: None,
701                    }),
702                    timestamp_writes: ts_writes,
703                    occlusion_query_set: None,
704                });
705                emit_draw_calls!(
706                    &self.resources,
707                    &mut render_pass,
708                    frame,
709                    self.use_instancing,
710                    &self.instanced_batches,
711                    camera_bg,
712                    grid_bg,
713                    &self.compute_filter_results,
714                    Some(slot)
715                );
716                emit_scivis_draw_calls!(
717                    &self.resources,
718                    &mut render_pass,
719                    &self.point_cloud_gpu_data,
720                    &self.glyph_gpu_data,
721                    &self.polyline_gpu_data,
722                    &self.volume_gpu_data,
723                    &self.streamtube_gpu_data,
724                    camera_bg,
725                    &self.tube_gpu_data,
726                    &self.image_slice_gpu_data,
727                    &self.tensor_glyph_gpu_data,
728                    &self.ribbon_gpu_data
729                );
730                // Phase 16 : GPU implicit surface.
731                if !self.implicit_gpu_data.is_empty() {
732                    if let Some(pipeline) = &self.resources.implicit_pipeline {
733                        render_pass.set_pipeline(pipeline);
734                        render_pass.set_bind_group(0, camera_bg, &[]);
735                        for gpu in &self.implicit_gpu_data {
736                            render_pass.set_bind_group(1, &gpu.bind_group, &[]);
737                            render_pass.draw(0..6, 0..1);
738                        }
739                    }
740                }
741                // Phase 17 : GPU marching cubes indirect draw.
742                if !self.mc_gpu_data.is_empty() {
743                    if let Some(pipeline) = &self.resources.mc_surface_pipeline {
744                        render_pass.set_pipeline(pipeline);
745                        render_pass.set_bind_group(0, camera_bg, &[]);
746                        for mc in &self.mc_gpu_data {
747                            let vol = &self.resources.mc_volumes[mc.volume_idx];
748                            render_pass.set_bind_group(1, &mc.render_bg, &[]);
749                            for slab in &vol.slabs {
750                                render_pass.set_vertex_buffer(0, slab.vertex_buf.slice(..));
751                                render_pass.draw_indirect(&slab.indirect_buf, 0);
752                            }
753                        }
754                    }
755                }
756                // Phase 10B / Phase 12 : screen-space image overlays.
757                // Regular items drawn with depth_compare: Always (always on top).
758                // Depth-composite items drawn with depth_compare: LessEqual (occluded by
759                // scene geometry whose depth was already written to the depth attachment).
760                if !self.screen_image_gpu_data.is_empty() {
761                    if let Some(overlay_pipeline) = &self.resources.screen_image_pipeline {
762                        let dc_pipeline = self.resources.screen_image_dc_pipeline.as_ref();
763                        for gpu in &self.screen_image_gpu_data {
764                            if let (Some(dc_bg), Some(dc_pipe)) =
765                                (&gpu.depth_bind_group, dc_pipeline)
766                            {
767                                render_pass.set_pipeline(dc_pipe);
768                                render_pass.set_bind_group(0, dc_bg, &[]);
769                            } else {
770                                render_pass.set_pipeline(overlay_pipeline);
771                                render_pass.set_bind_group(0, &gpu.bind_group, &[]);
772                            }
773                            render_pass.draw(0..6, 0..1);
774                        }
775                    }
776                }
777                // Overlay labels (LDR fallback: inside the same render pass).
778                if let Some(ref ld) = self.label_gpu_data {
779                    if let Some(pipeline) = &self.resources.overlay_text_pipeline {
780                        render_pass.set_pipeline(pipeline);
781                        render_pass.set_bind_group(0, &ld.bind_group, &[]);
782                        render_pass.set_vertex_buffer(0, ld.vertex_buf.slice(..));
783                        render_pass.draw(0..ld.vertex_count, 0..1);
784                    }
785                }
786                // Scalar bars (LDR fallback).
787                if let Some(ref sb) = self.scalar_bar_gpu_data {
788                    if let Some(pipeline) = &self.resources.overlay_text_pipeline {
789                        render_pass.set_pipeline(pipeline);
790                        render_pass.set_bind_group(0, &sb.bind_group, &[]);
791                        render_pass.set_vertex_buffer(0, sb.vertex_buf.slice(..));
792                        render_pass.draw(0..sb.vertex_count, 0..1);
793                    }
794                }
795                // Rulers (LDR fallback).
796                if let Some(ref rd) = self.ruler_gpu_data {
797                    if let Some(pipeline) = &self.resources.overlay_text_pipeline {
798                        render_pass.set_pipeline(pipeline);
799                        render_pass.set_bind_group(0, &rd.bind_group, &[]);
800                        render_pass.set_vertex_buffer(0, rd.vertex_buf.slice(..));
801                        render_pass.draw(0..rd.vertex_count, 0..1);
802                    }
803                }
804                // Phase 7 : overlay images (OverlayFrame, LDR fallback, drawn last).
805                if !self.overlay_image_gpu_data.is_empty() {
806                    if let Some(pipeline) = &self.resources.screen_image_pipeline {
807                        render_pass.set_pipeline(pipeline);
808                        for gpu in &self.overlay_image_gpu_data {
809                            render_pass.set_bind_group(0, &gpu.bind_group, &[]);
810                            render_pass.draw(0..6, 0..1);
811                        }
812                    }
813                }
814            }
815
816            // Phase 4 : resolve timestamp queries -> staging buffer.
817            if let (Some(qs), Some(res_buf), Some(stg_buf)) = (
818                self.ts_query_set.as_ref(),
819                self.ts_resolve_buf.as_ref(),
820                self.ts_staging_buf.as_ref(),
821            ) {
822                encoder.resolve_query_set(qs, 0..2, res_buf, 0);
823                encoder.copy_buffer_to_buffer(res_buf, 0, stg_buf, 0, 16);
824                self.ts_needs_readback = true;
825            }
826
827            // Phase 3 : upscale blit from dyn_res intermediate to output_view.
828            if use_dyn_res {
829                let upscale_bg =
830                    &self.viewport_slots[vp_idx].dyn_res.as_ref().unwrap().upscale_bind_group;
831                let mut upscale_pass =
832                    encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
833                        label: Some("dyn_res_upscale_pass"),
834                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
835                            view: output_view,
836                            resolve_target: None,
837                            ops: wgpu::Operations {
838                                load: wgpu::LoadOp::Load,
839                                store: wgpu::StoreOp::Store,
840                            },
841                            depth_slice: None,
842                        })],
843                        depth_stencil_attachment: None,
844                        timestamp_writes: None,
845                        occlusion_query_set: None,
846                    });
847                if let Some(pipeline) = &self.resources.dyn_res_upscale_pipeline {
848                    upscale_pass.set_pipeline(pipeline);
849                    upscale_pass.set_bind_group(0, upscale_bg, &[]);
850                    upscale_pass.draw(0..3, 0..1);
851                }
852            }
853
854            return encoder.finish();
855        }
856
857        // HDR path.
858        let pp = &frame.effects.post_process;
859
860        let hdr_clear_rgb = [
861            bg_color[0].powf(2.2),
862            bg_color[1].powf(2.2),
863            bg_color[2].powf(2.2),
864        ];
865
866        // Upload tone map uniform into the per-viewport buffer.
867        let mode = match pp.tone_mapping {
868            crate::renderer::ToneMapping::Reinhard => 0u32,
869            crate::renderer::ToneMapping::Aces => 1u32,
870            crate::renderer::ToneMapping::KhronosNeutral => 2u32,
871        };
872        let tm_uniform = crate::resources::ToneMapUniform {
873            exposure: pp.exposure,
874            mode,
875            bloom_enabled: if pp.bloom { 1 } else { 0 },
876            ssao_enabled: if pp.ssao { 1 } else { 0 },
877            contact_shadows_enabled: if pp.contact_shadows { 1 } else { 0 },
878            edl_enabled: if pp.edl_enabled { 1 } else { 0 },
879            edl_radius: pp.edl_radius,
880            edl_strength: pp.edl_strength,
881            background_color: bg_color,
882            near_plane: frame.camera.render_camera.near,
883            far_plane: frame.camera.render_camera.far,
884            lic_enabled: if frame.scene.lic_items.is_empty() { 0 } else { 1 },
885            lic_strength: frame.scene.lic_items.first().map(|i| i.config.strength).unwrap_or(0.5),
886        };
887        {
888            let hdr = self.viewport_slots[vp_idx].hdr.as_ref().unwrap();
889            queue.write_buffer(
890                &hdr.tone_map_uniform_buf,
891                0,
892                bytemuck::cast_slice(&[tm_uniform]),
893            );
894
895            // Upload SSAO uniform if needed.
896            if pp.ssao {
897                let proj = frame.camera.render_camera.projection;
898                let inv_proj = proj.inverse();
899                let ssao_uniform = crate::resources::SsaoUniform {
900                    inv_proj: inv_proj.to_cols_array_2d(),
901                    proj: proj.to_cols_array_2d(),
902                    radius: 0.5,
903                    bias: 0.025,
904                    _pad: [0.0; 2],
905                };
906                queue.write_buffer(
907                    &hdr.ssao_uniform_buf,
908                    0,
909                    bytemuck::cast_slice(&[ssao_uniform]),
910                );
911            }
912
913            // Upload contact shadow uniform if needed.
914            if pp.contact_shadows {
915                let proj = frame.camera.render_camera.projection;
916                let inv_proj = proj.inverse();
917                let light_dir_world: glam::Vec3 =
918                    if let Some(l) = frame.effects.lighting.lights.first() {
919                        match l.kind {
920                            LightKind::Directional { direction } => {
921                                glam::Vec3::from(direction).normalize()
922                            }
923                            LightKind::Spot { direction, .. } => {
924                                glam::Vec3::from(direction).normalize()
925                            }
926                            _ => glam::Vec3::new(0.0, -1.0, 0.0),
927                        }
928                    } else {
929                        glam::Vec3::new(0.0, -1.0, 0.0)
930                    };
931                let view = frame.camera.render_camera.view;
932                let light_dir_view = view.transform_vector3(light_dir_world).normalize();
933                let world_up_view = view.transform_vector3(glam::Vec3::Z).normalize();
934                let cs_uniform = crate::resources::ContactShadowUniform {
935                    inv_proj: inv_proj.to_cols_array_2d(),
936                    proj: proj.to_cols_array_2d(),
937                    light_dir_view: [light_dir_view.x, light_dir_view.y, light_dir_view.z, 0.0],
938                    world_up_view: [world_up_view.x, world_up_view.y, world_up_view.z, 0.0],
939                    params: [
940                        pp.contact_shadow_max_distance,
941                        pp.contact_shadow_steps as f32,
942                        pp.contact_shadow_thickness,
943                        0.0,
944                    ],
945                };
946                queue.write_buffer(
947                    &hdr.contact_shadow_uniform_buf,
948                    0,
949                    bytemuck::cast_slice(&[cs_uniform]),
950                );
951            }
952
953            // Upload bloom uniform if needed.
954            if pp.bloom {
955                let bloom_u = crate::resources::BloomUniform {
956                    threshold: pp.bloom_threshold,
957                    intensity: pp.bloom_intensity,
958                    horizontal: 0,
959                    _pad: 0,
960                };
961                queue.write_buffer(&hdr.bloom_uniform_buf, 0, bytemuck::cast_slice(&[bloom_u]));
962            }
963        }
964
965        // Upload DoF uniform when enabled.
966        if pp.dof_enabled {
967            let (w, h) = {
968                let hdr = self.viewport_slots[vp_idx].hdr.as_ref().unwrap();
969                (hdr.size[0] as f32, hdr.size[1] as f32)
970            };
971            let dof_uniform = crate::resources::DofUniform {
972                focal_distance: pp.dof_focal_distance,
973                focal_range: pp.dof_focal_range,
974                max_blur_radius: pp.dof_max_blur_radius,
975                near_plane: frame.camera.render_camera.near,
976                far_plane: frame.camera.render_camera.far,
977                viewport_width: w,
978                viewport_height: h,
979                _pad: 0.0,
980            };
981            let hdr = self.viewport_slots[vp_idx].hdr.as_ref().unwrap();
982            queue.write_buffer(
983                &hdr.dof_uniform_buf,
984                0,
985                bytemuck::cast_slice(&[dof_uniform]),
986            );
987        }
988
989        // Rebuild tone-map bind group with correct bloom/AO/DoF texture views.
990        {
991            let hdr = self.viewport_slots[vp_idx].hdr.as_mut().unwrap();
992            self.resources.rebuild_tone_map_bind_group(
993                device,
994                hdr,
995                pp.bloom,
996                pp.ssao,
997                pp.contact_shadows,
998                !frame.scene.lic_items.is_empty(),
999                pp.dof_enabled,
1000            );
1001        }
1002
1003        // -----------------------------------------------------------------------
1004        // Pre-allocate OIT targets if any transparent items exist.
1005        // Must happen before camera_bg is borrowed (borrow-checker constraint).
1006        // -----------------------------------------------------------------------
1007        {
1008            let needs_oit = if self.use_instancing && !self.instanced_batches.is_empty() {
1009                self.instanced_batches.iter().any(|b| b.is_transparent)
1010            } else {
1011                scene_items
1012                    .iter()
1013                    .any(|i| i.visible && i.material.opacity < 1.0)
1014            } || frame.scene.transparent_volume_meshes.iter().any(|i| i.visible);
1015            if needs_oit {
1016                let hdr = self.viewport_slots[vp_idx].hdr.as_mut().unwrap();
1017                self.resources
1018                    .ensure_viewport_oit(device, hdr, w.max(1), h.max(1));
1019            }
1020        }
1021
1022        // -----------------------------------------------------------------------
1023        // Build the command encoder.
1024        // -----------------------------------------------------------------------
1025        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1026            label: Some("hdr_encoder"),
1027        });
1028
1029        // Per-viewport camera bind group and HDR state for the HDR path.
1030        let slot = &self.viewport_slots[vp_idx];
1031        let camera_bg = &slot.camera_bind_group;
1032        let slot_hdr = slot.hdr.as_ref().unwrap();
1033
1034        // -----------------------------------------------------------------------
1035        // HDR scene pass: render geometry into the HDR texture.
1036        // -----------------------------------------------------------------------
1037        {
1038            // Use SSAA target if enabled, otherwise render directly to hdr_texture.
1039            let use_ssaa = ssaa_factor > 1
1040                && slot_hdr.ssaa_color_view.is_some()
1041                && slot_hdr.ssaa_depth_view.is_some();
1042            let scene_color_view = if use_ssaa {
1043                slot_hdr.ssaa_color_view.as_ref().unwrap()
1044            } else {
1045                &slot_hdr.hdr_view
1046            };
1047            let scene_depth_view = if use_ssaa {
1048                slot_hdr.ssaa_depth_view.as_ref().unwrap()
1049            } else {
1050                &slot_hdr.hdr_depth_view
1051            };
1052
1053            let clear_wgpu = wgpu::Color {
1054                r: hdr_clear_rgb[0] as f64,
1055                g: hdr_clear_rgb[1] as f64,
1056                b: hdr_clear_rgb[2] as f64,
1057                // Clear alpha to 0.0 so OIT composite can signal presence via alpha > 0.
1058                // Background pixels remain at alpha=0 and are detected in tone_map.wgsl.
1059                a: 0.0,
1060            };
1061
1062            let hdr_ts_writes = self.ts_query_set.as_ref().map(|qs| {
1063                wgpu::RenderPassTimestampWrites {
1064                    query_set: qs,
1065                    beginning_of_pass_write_index: Some(0),
1066                    end_of_pass_write_index: Some(1),
1067                }
1068            });
1069            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1070                label: Some("hdr_scene_pass"),
1071                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1072                    view: scene_color_view,
1073                    resolve_target: None,
1074                    ops: wgpu::Operations {
1075                        load: wgpu::LoadOp::Clear(clear_wgpu),
1076                        store: wgpu::StoreOp::Store,
1077                    },
1078                    depth_slice: None,
1079                })],
1080                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1081                    view: scene_depth_view,
1082                    depth_ops: Some(wgpu::Operations {
1083                        load: wgpu::LoadOp::Clear(1.0),
1084                        store: wgpu::StoreOp::Store,
1085                    }),
1086                    stencil_ops: Some(wgpu::Operations {
1087                        load: wgpu::LoadOp::Clear(0),
1088                        store: wgpu::StoreOp::Store,
1089                    }),
1090                }),
1091                timestamp_writes: hdr_ts_writes,
1092                occlusion_query_set: None,
1093            });
1094
1095            let resources = &self.resources;
1096            render_pass.set_bind_group(0, camera_bg, &[]);
1097
1098            // Check skybox eligibility early; drawn after all opaques below.
1099            let show_skybox = frame
1100                .effects
1101                .environment
1102                .as_ref()
1103                .is_some_and(|e| e.show_skybox)
1104                && resources.ibl_skybox_view.is_some();
1105
1106            let use_instancing = self.use_instancing;
1107            let batches = &self.instanced_batches;
1108
1109            if !scene_items.is_empty() {
1110                if use_instancing && !batches.is_empty() {
1111                    let excluded_items: Vec<&SceneRenderItem> = scene_items
1112                        .iter()
1113                        .filter(|item| {
1114                            item.visible
1115                                && (item.active_attribute.is_some()
1116                                    || item.material.is_two_sided()
1117                                    || item.material.matcap_id.is_some())
1118                                && resources
1119                                    .mesh_store
1120                                    .get(item.mesh_id)
1121                                    .is_some()
1122                        })
1123                        .collect();
1124
1125                    // Separate opaque and transparent batches.
1126                    // Carry the global batch index (position in `batches`) alongside each batch
1127                    // so draw_indexed_indirect can compute the correct buffer offset.
1128                    let mut opaque_batches: Vec<(usize, &InstancedBatch)> = Vec::new();
1129                    let mut transparent_batches: Vec<(usize, &InstancedBatch)> = Vec::new();
1130                    for (batch_global_idx, batch) in batches.iter().enumerate() {
1131                        if batch.is_transparent {
1132                            transparent_batches.push((batch_global_idx, batch));
1133                        } else {
1134                            opaque_batches.push((batch_global_idx, batch));
1135                        }
1136                    }
1137
1138                    if !opaque_batches.is_empty() && !frame.viewport.wireframe_mode {
1139                        let use_indirect = self.gpu_culling_enabled
1140                            && resources.hdr_solid_instanced_cull_pipeline.is_some()
1141                            && resources.indirect_args_buf.is_some();
1142
1143                        if use_indirect {
1144                            if let (
1145                                Some(pipeline),
1146                                Some(indirect_buf),
1147                            ) = (
1148                                &resources.hdr_solid_instanced_cull_pipeline,
1149                                &resources.indirect_args_buf,
1150                            ) {
1151                                render_pass.set_pipeline(pipeline);
1152                                for (batch_global_idx, batch) in &opaque_batches {
1153                                    let Some(mesh) = resources.mesh_store.get(batch.mesh_id)
1154                                    else {
1155                                        continue;
1156                                    };
1157                                    let mat_key = (
1158                                        batch.texture_id.unwrap_or(u64::MAX),
1159                                        batch.normal_map_id.unwrap_or(u64::MAX),
1160                                        batch.ao_map_id.unwrap_or(u64::MAX),
1161                                    );
1162                                    let Some(inst_tex_bg) =
1163                                        resources.instance_cull_bind_groups.get(&mat_key)
1164                                    else {
1165                                        continue;
1166                                    };
1167                                    render_pass.set_bind_group(1, inst_tex_bg, &[]);
1168                                    render_pass.set_vertex_buffer(
1169                                        0,
1170                                        mesh.vertex_buffer.slice(..),
1171                                    );
1172                                    render_pass.set_index_buffer(
1173                                        mesh.index_buffer.slice(..),
1174                                        wgpu::IndexFormat::Uint32,
1175                                    );
1176                                    // Each DrawIndexedIndirect entry is 20 bytes; index by global
1177                                    // batch position so the offset matches write_indirect_args output.
1178                                    render_pass.draw_indexed_indirect(
1179                                        indirect_buf,
1180                                        *batch_global_idx as u64 * 20,
1181                                    );
1182                                }
1183                            }
1184                        } else if let Some(ref pipeline) = resources.hdr_solid_instanced_pipeline {
1185                            render_pass.set_pipeline(pipeline);
1186                            for (_, batch) in &opaque_batches {
1187                                let Some(mesh) = resources
1188                                    .mesh_store
1189                                    .get(batch.mesh_id)
1190                                else {
1191                                    continue;
1192                                };
1193                                let mat_key = (
1194                                    batch.texture_id.unwrap_or(u64::MAX),
1195                                    batch.normal_map_id.unwrap_or(u64::MAX),
1196                                    batch.ao_map_id.unwrap_or(u64::MAX),
1197                                );
1198                                let Some(inst_tex_bg) =
1199                                    resources.instance_bind_groups.get(&mat_key)
1200                                else {
1201                                    continue;
1202                                };
1203                                render_pass.set_bind_group(1, inst_tex_bg, &[]);
1204                                render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1205                                render_pass.set_index_buffer(
1206                                    mesh.index_buffer.slice(..),
1207                                    wgpu::IndexFormat::Uint32,
1208                                );
1209                                render_pass.draw_indexed(
1210                                    0..mesh.index_count,
1211                                    0,
1212                                    batch.instance_offset
1213                                        ..batch.instance_offset + batch.instance_count,
1214                                );
1215                            }
1216                        }
1217                    }
1218
1219                    // NOTE: transparent_batches are now rendered in the OIT pass below,
1220                    // not in the HDR scene pass. This block intentionally left empty.
1221                    let _ = &transparent_batches; // suppress unused warning
1222
1223                    if frame.viewport.wireframe_mode {
1224                        if let Some(ref hdr_wf) = resources.hdr_wireframe_pipeline {
1225                            render_pass.set_pipeline(hdr_wf);
1226                            for item in scene_items {
1227                                if !item.visible {
1228                                    continue;
1229                                }
1230                                let Some(mesh) = resources
1231                                    .mesh_store
1232                                    .get(item.mesh_id)
1233                                else {
1234                                    continue;
1235                                };
1236                                render_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
1237                                render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1238                                render_pass.set_index_buffer(
1239                                    mesh.edge_index_buffer.slice(..),
1240                                    wgpu::IndexFormat::Uint32,
1241                                );
1242                                render_pass.draw_indexed(0..mesh.edge_index_count, 0, 0..1);
1243                            }
1244                        }
1245                    } else if let (Some(hdr_solid), Some(hdr_solid_two_sided)) = (
1246                        &resources.hdr_solid_pipeline,
1247                        &resources.hdr_solid_two_sided_pipeline,
1248                    ) {
1249                        for item in excluded_items
1250                            .into_iter()
1251                            .filter(|item| item.material.opacity >= 1.0)
1252                        {
1253                            let Some(mesh) = resources
1254                                .mesh_store
1255                                .get(item.mesh_id)
1256                            else {
1257                                continue;
1258                            };
1259                            let pipeline = if item.material.is_two_sided() {
1260                                hdr_solid_two_sided
1261                            } else {
1262                                hdr_solid
1263                            };
1264                            render_pass.set_pipeline(pipeline);
1265                            render_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
1266                            render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1267                            render_pass.set_index_buffer(
1268                                mesh.index_buffer.slice(..),
1269                                wgpu::IndexFormat::Uint32,
1270                            );
1271                            render_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1272                        }
1273                    }
1274                } else {
1275                    // Per-object path.
1276                    let eye = glam::Vec3::from(frame.camera.render_camera.eye_position);
1277                    let dist_from_eye = |item: &&SceneRenderItem| -> f32 {
1278                        let pos =
1279                            glam::Vec3::new(item.model[3][0], item.model[3][1], item.model[3][2]);
1280                        (pos - eye).length()
1281                    };
1282
1283                    let mut opaque: Vec<&SceneRenderItem> = Vec::new();
1284                    let mut transparent: Vec<&SceneRenderItem> = Vec::new();
1285                    for item in scene_items {
1286                        if !item.visible
1287                            || resources
1288                                .mesh_store
1289                                .get(item.mesh_id)
1290                                .is_none()
1291                        {
1292                            continue;
1293                        }
1294                        if item.material.opacity < 1.0 {
1295                            transparent.push(item);
1296                        } else {
1297                            opaque.push(item);
1298                        }
1299                    }
1300                    opaque.sort_by(|a, b| {
1301                        dist_from_eye(a)
1302                            .partial_cmp(&dist_from_eye(b))
1303                            .unwrap_or(std::cmp::Ordering::Equal)
1304                    });
1305                    transparent.sort_by(|a, b| {
1306                        dist_from_eye(b)
1307                            .partial_cmp(&dist_from_eye(a))
1308                            .unwrap_or(std::cmp::Ordering::Equal)
1309                    });
1310
1311                    let draw_item_hdr =
1312                        |render_pass: &mut wgpu::RenderPass<'_>,
1313                         item: &SceneRenderItem,
1314                         solid_pl: &wgpu::RenderPipeline,
1315                         trans_pl: &wgpu::RenderPipeline,
1316                         wf_pl: &wgpu::RenderPipeline| {
1317                            let mesh = resources
1318                                .mesh_store
1319                                .get(item.mesh_id)
1320                                .unwrap();
1321                            // mesh.object_bind_group (group 1) already carries the object uniform
1322                            // and the correct texture views.
1323                            render_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
1324                            let is_face_attr = item.active_attribute.as_ref().map_or(false, |a| {
1325                                matches!(
1326                                    a.kind,
1327                                    crate::resources::AttributeKind::Face
1328                                        | crate::resources::AttributeKind::FaceColor
1329                                        | crate::resources::AttributeKind::Halfedge
1330                                        | crate::resources::AttributeKind::Corner
1331                                )
1332                            });
1333                            if frame.viewport.wireframe_mode {
1334                                render_pass.set_pipeline(wf_pl);
1335                                render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1336                                render_pass.set_index_buffer(
1337                                    mesh.edge_index_buffer.slice(..),
1338                                    wgpu::IndexFormat::Uint32,
1339                                );
1340                                render_pass.draw_indexed(0..mesh.edge_index_count, 0, 0..1);
1341                            } else if is_face_attr {
1342                                if let Some(ref fvb) = mesh.face_vertex_buffer {
1343                                    let pl = if item.material.opacity < 1.0 {
1344                                        trans_pl
1345                                    } else {
1346                                        solid_pl
1347                                    };
1348                                    render_pass.set_pipeline(pl);
1349                                    render_pass.set_vertex_buffer(0, fvb.slice(..));
1350                                    render_pass.draw(0..mesh.index_count, 0..1);
1351                                }
1352                            } else if item.material.opacity < 1.0 {
1353                                render_pass.set_pipeline(trans_pl);
1354                                render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1355                                render_pass.set_index_buffer(
1356                                    mesh.index_buffer.slice(..),
1357                                    wgpu::IndexFormat::Uint32,
1358                                );
1359                                render_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1360                            } else {
1361                                render_pass.set_pipeline(solid_pl);
1362                                render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1363                                render_pass.set_index_buffer(
1364                                    mesh.index_buffer.slice(..),
1365                                    wgpu::IndexFormat::Uint32,
1366                                );
1367                                render_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1368                            }
1369                        };
1370
1371                    // NOTE: only opaque items are drawn here. Transparent items are
1372                    // routed to the OIT pass below.
1373                    let _ = &transparent; // suppress unused warning
1374                    if let (
1375                        Some(hdr_solid),
1376                        Some(hdr_solid_two_sided),
1377                        Some(hdr_trans),
1378                        Some(hdr_wf),
1379                    ) = (
1380                        &resources.hdr_solid_pipeline,
1381                        &resources.hdr_solid_two_sided_pipeline,
1382                        &resources.hdr_transparent_pipeline,
1383                        &resources.hdr_wireframe_pipeline,
1384                    ) {
1385                        for item in &opaque {
1386                            let solid_pl = if item.material.is_two_sided() {
1387                                hdr_solid_two_sided
1388                            } else {
1389                                hdr_solid
1390                            };
1391                            draw_item_hdr(&mut render_pass, item, solid_pl, hdr_trans, hdr_wf);
1392                        }
1393                    }
1394                }
1395            }
1396
1397            // Cap fill pass (HDR path : section view cross-section fill).
1398            if !slot.cap_buffers.is_empty() {
1399                if let Some(ref hdr_overlay) = resources.hdr_overlay_pipeline {
1400                    render_pass.set_pipeline(hdr_overlay);
1401                    render_pass.set_bind_group(0, camera_bg, &[]);
1402                    for (vbuf, ibuf, idx_count, _ubuf, bg) in &slot.cap_buffers {
1403                        render_pass.set_bind_group(1, bg, &[]);
1404                        render_pass.set_vertex_buffer(0, vbuf.slice(..));
1405                        render_pass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint32);
1406                        render_pass.draw_indexed(0..*idx_count, 0, 0..1);
1407                    }
1408                }
1409            }
1410
1411            // SciVis Phase B+D+M8+M: point cloud, glyph, polyline, volume, streamtube (HDR path).
1412            emit_scivis_draw_calls!(
1413                &self.resources,
1414                &mut render_pass,
1415                &self.point_cloud_gpu_data,
1416                &self.glyph_gpu_data,
1417                &self.polyline_gpu_data,
1418                &self.volume_gpu_data,
1419                &self.streamtube_gpu_data,
1420                camera_bg,
1421                &self.tube_gpu_data,
1422                &self.image_slice_gpu_data,
1423                &self.tensor_glyph_gpu_data,
1424                &self.ribbon_gpu_data
1425            );
1426
1427            // Phase 16 : GPU implicit surface (HDR path, before skybox).
1428            if !self.implicit_gpu_data.is_empty() {
1429                if let Some(pipeline) = &self.resources.implicit_pipeline {
1430                    render_pass.set_pipeline(pipeline);
1431                    render_pass.set_bind_group(0, camera_bg, &[]);
1432                    for gpu in &self.implicit_gpu_data {
1433                        render_pass.set_bind_group(1, &gpu.bind_group, &[]);
1434                        render_pass.draw(0..6, 0..1);
1435                    }
1436                }
1437            }
1438            // Phase 17 : GPU marching cubes indirect draw (HDR path).
1439            if !self.mc_gpu_data.is_empty() {
1440                if let Some(pipeline) = &self.resources.mc_surface_pipeline {
1441                    render_pass.set_pipeline(pipeline);
1442                    render_pass.set_bind_group(0, camera_bg, &[]);
1443                    for mc in &self.mc_gpu_data {
1444                        let vol = &self.resources.mc_volumes[mc.volume_idx];
1445                        render_pass.set_bind_group(1, &mc.render_bg, &[]);
1446                        for slab in &vol.slabs {
1447                            render_pass.set_vertex_buffer(0, slab.vertex_buf.slice(..));
1448                            render_pass.draw_indirect(&slab.indirect_buf, 0);
1449                        }
1450                    }
1451                }
1452            }
1453
1454            // Draw skybox last among opaques : only uncovered sky pixels pass depth == 1.0.
1455            if show_skybox {
1456                render_pass.set_bind_group(0, camera_bg, &[]);
1457                render_pass.set_pipeline(&resources.skybox_pipeline);
1458                render_pass.draw(0..3, 0..1);
1459            }
1460        }
1461
1462        // -----------------------------------------------------------------------
1463        // SSAA resolve pass: downsample supersampled scene -> hdr_texture.
1464        // Only runs when ssaa_factor > 1 and the resolve pipeline is available.
1465        // -----------------------------------------------------------------------
1466        if ssaa_factor > 1 {
1467            let slot_hdr = self.viewport_slots[vp_idx].hdr.as_ref().unwrap();
1468            if let (Some(pipeline), Some(bg)) = (
1469                &self.resources.ssaa_resolve_pipeline,
1470                &slot_hdr.ssaa_resolve_bind_group,
1471            ) {
1472                let mut resolve_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1473                    label: Some("ssaa_resolve_pass"),
1474                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1475                        view: &slot_hdr.hdr_view,
1476                        resolve_target: None,
1477                        ops: wgpu::Operations {
1478                            load: wgpu::LoadOp::Load,
1479                            store: wgpu::StoreOp::Store,
1480                        },
1481                        depth_slice: None,
1482                    })],
1483                    depth_stencil_attachment: None,
1484                    timestamp_writes: None,
1485                    occlusion_query_set: None,
1486                });
1487                resolve_pass.set_pipeline(pipeline);
1488                resolve_pass.set_bind_group(0, bg, &[]);
1489                resolve_pass.draw(0..3, 0..1);
1490            }
1491        }
1492
1493        // -----------------------------------------------------------------------
1494        // Sub-object highlight pass: face fill, edge lines, vertex sprites.
1495        // Runs after opaque geometry (depth buffer is ready) and before OIT so
1496        // highlights are not occluded by opaque surfaces.
1497        // -----------------------------------------------------------------------
1498        if let Some(sub_hl) = self.viewport_slots[vp_idx].sub_highlight.as_ref() {
1499            let resources = &self.resources;
1500            if let (Some(fill_pl), Some(edge_pl), Some(sprite_pl)) = (
1501                &resources.sub_highlight_fill_pipeline,
1502                &resources.sub_highlight_edge_pipeline,
1503                &resources.sub_highlight_sprite_pipeline,
1504            ) {
1505                let slot_hdr = self.viewport_slots[vp_idx].hdr.as_ref().unwrap();
1506                let camera_bg = &self.viewport_slots[vp_idx].camera_bind_group;
1507                let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1508                    label: Some("sub_highlight_pass"),
1509                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1510                        view: &slot_hdr.hdr_view,
1511                        resolve_target: None,
1512                        ops: wgpu::Operations {
1513                            load: wgpu::LoadOp::Load,
1514                            store: wgpu::StoreOp::Store,
1515                        },
1516                        depth_slice: None,
1517                    })],
1518                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1519                        view: &slot_hdr.hdr_depth_view,
1520                        depth_ops: Some(wgpu::Operations {
1521                            load: wgpu::LoadOp::Load,
1522                            store: wgpu::StoreOp::Discard,
1523                        }),
1524                        stencil_ops: None,
1525                    }),
1526                    timestamp_writes: None,
1527                    occlusion_query_set: None,
1528                });
1529
1530                if sub_hl.fill_vertex_count > 0 {
1531                    pass.set_pipeline(fill_pl);
1532                    pass.set_bind_group(0, camera_bg, &[]);
1533                    pass.set_bind_group(1, &sub_hl.fill_bind_group, &[]);
1534                    pass.set_vertex_buffer(0, sub_hl.fill_vertex_buf.slice(..));
1535                    pass.draw(0..sub_hl.fill_vertex_count, 0..1);
1536                }
1537                if sub_hl.edge_segment_count > 0 {
1538                    pass.set_pipeline(edge_pl);
1539                    pass.set_bind_group(0, camera_bg, &[]);
1540                    pass.set_bind_group(1, &sub_hl.edge_bind_group, &[]);
1541                    pass.set_vertex_buffer(0, sub_hl.edge_vertex_buf.slice(..));
1542                    pass.draw(0..6, 0..sub_hl.edge_segment_count);
1543                }
1544                if sub_hl.sprite_point_count > 0 {
1545                    pass.set_pipeline(sprite_pl);
1546                    pass.set_bind_group(0, camera_bg, &[]);
1547                    pass.set_bind_group(1, &sub_hl.sprite_bind_group, &[]);
1548                    pass.set_vertex_buffer(0, sub_hl.sprite_vertex_buf.slice(..));
1549                    pass.draw(0..6, 0..sub_hl.sprite_point_count);
1550                }
1551            }
1552        }
1553
1554        // -----------------------------------------------------------------------
1555        // OIT pass: render transparent items into accum + reveal textures.
1556        // Completely skipped when no transparent items exist (zero overhead).
1557        // -----------------------------------------------------------------------
1558        let has_transparent = if self.use_instancing && !self.instanced_batches.is_empty() {
1559            self.instanced_batches.iter().any(|b| b.is_transparent)
1560        } else {
1561            scene_items
1562                .iter()
1563                .any(|i| i.visible && i.material.opacity < 1.0)
1564        } || frame.scene.transparent_volume_meshes.iter().any(|i| i.visible);
1565
1566        if has_transparent {
1567            // OIT targets already allocated in the pre-pass above.
1568            if let (Some(accum_view), Some(reveal_view)) = (
1569                slot_hdr.oit_accum_view.as_ref(),
1570                slot_hdr.oit_reveal_view.as_ref(),
1571            ) {
1572                let hdr_depth_view = &slot_hdr.hdr_depth_view;
1573                // Clear accum to (0,0,0,0), reveal to 1.0 (no contribution yet).
1574                let mut oit_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1575                    label: Some("oit_pass"),
1576                    color_attachments: &[
1577                        Some(wgpu::RenderPassColorAttachment {
1578                            view: accum_view,
1579                            resolve_target: None,
1580                            ops: wgpu::Operations {
1581                                load: wgpu::LoadOp::Clear(wgpu::Color {
1582                                    r: 0.0,
1583                                    g: 0.0,
1584                                    b: 0.0,
1585                                    a: 0.0,
1586                                }),
1587                                store: wgpu::StoreOp::Store,
1588                            },
1589                            depth_slice: None,
1590                        }),
1591                        Some(wgpu::RenderPassColorAttachment {
1592                            view: reveal_view,
1593                            resolve_target: None,
1594                            ops: wgpu::Operations {
1595                                load: wgpu::LoadOp::Clear(wgpu::Color {
1596                                    r: 1.0,
1597                                    g: 1.0,
1598                                    b: 1.0,
1599                                    a: 1.0,
1600                                }),
1601                                store: wgpu::StoreOp::Store,
1602                            },
1603                            depth_slice: None,
1604                        }),
1605                    ],
1606                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1607                        view: hdr_depth_view,
1608                        depth_ops: Some(wgpu::Operations {
1609                            load: wgpu::LoadOp::Load, // reuse opaque depth
1610                            store: wgpu::StoreOp::Store,
1611                        }),
1612                        stencil_ops: None,
1613                    }),
1614                    timestamp_writes: None,
1615                    occlusion_query_set: None,
1616                });
1617
1618                oit_pass.set_bind_group(0, camera_bg, &[]);
1619
1620                if self.use_instancing && !self.instanced_batches.is_empty() {
1621                    let use_indirect_oit = self.gpu_culling_enabled
1622                        && self.resources.oit_instanced_cull_pipeline.is_some()
1623                        && self.resources.indirect_args_buf.is_some();
1624
1625                    if use_indirect_oit {
1626                        if let (
1627                            Some(pipeline),
1628                            Some(indirect_buf),
1629                        ) = (
1630                            &self.resources.oit_instanced_cull_pipeline,
1631                            &self.resources.indirect_args_buf,
1632                        ) {
1633                            oit_pass.set_pipeline(pipeline);
1634                            for (batch_global_idx, batch) in
1635                                self.instanced_batches.iter().enumerate()
1636                            {
1637                                if !batch.is_transparent {
1638                                    continue;
1639                                }
1640                                let Some(mesh) =
1641                                    self.resources.mesh_store.get(batch.mesh_id)
1642                                else {
1643                                    continue;
1644                                };
1645                                let mat_key = (
1646                                    batch.texture_id.unwrap_or(u64::MAX),
1647                                    batch.normal_map_id.unwrap_or(u64::MAX),
1648                                    batch.ao_map_id.unwrap_or(u64::MAX),
1649                                );
1650                                let Some(inst_tex_bg) =
1651                                    self.resources.instance_cull_bind_groups.get(&mat_key)
1652                                else {
1653                                    continue;
1654                                };
1655                                oit_pass.set_bind_group(1, inst_tex_bg, &[]);
1656                                oit_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1657                                oit_pass.set_index_buffer(
1658                                    mesh.index_buffer.slice(..),
1659                                    wgpu::IndexFormat::Uint32,
1660                                );
1661                                oit_pass.draw_indexed_indirect(
1662                                    indirect_buf,
1663                                    batch_global_idx as u64 * 20,
1664                                );
1665                            }
1666                        }
1667                    } else if let Some(ref pipeline) = self.resources.oit_instanced_pipeline {
1668                        oit_pass.set_pipeline(pipeline);
1669                        for batch in &self.instanced_batches {
1670                            if !batch.is_transparent {
1671                                continue;
1672                            }
1673                            let Some(mesh) = self
1674                                .resources
1675                                .mesh_store
1676                                .get(batch.mesh_id)
1677                            else {
1678                                continue;
1679                            };
1680                            let mat_key = (
1681                                batch.texture_id.unwrap_or(u64::MAX),
1682                                batch.normal_map_id.unwrap_or(u64::MAX),
1683                                batch.ao_map_id.unwrap_or(u64::MAX),
1684                            );
1685                            let Some(inst_tex_bg) =
1686                                self.resources.instance_bind_groups.get(&mat_key)
1687                            else {
1688                                continue;
1689                            };
1690                            oit_pass.set_bind_group(1, inst_tex_bg, &[]);
1691                            oit_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1692                            oit_pass.set_index_buffer(
1693                                mesh.index_buffer.slice(..),
1694                                wgpu::IndexFormat::Uint32,
1695                            );
1696                            oit_pass.draw_indexed(
1697                                0..mesh.index_count,
1698                                0,
1699                                batch.instance_offset..batch.instance_offset + batch.instance_count,
1700                            );
1701                        }
1702                    }
1703                } else if let Some(ref pipeline) = self.resources.oit_pipeline {
1704                    oit_pass.set_pipeline(pipeline);
1705                    for item in scene_items {
1706                        if !item.visible || item.material.opacity >= 1.0 {
1707                            continue;
1708                        }
1709                        let Some(mesh) = self
1710                            .resources
1711                            .mesh_store
1712                            .get(item.mesh_id)
1713                        else {
1714                            continue;
1715                        };
1716                        oit_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
1717                        oit_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1718                        oit_pass.set_index_buffer(
1719                            mesh.index_buffer.slice(..),
1720                            wgpu::IndexFormat::Uint32,
1721                        );
1722                        oit_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1723                    }
1724                }
1725
1726                // -----------------------------------------------------------
1727                // Projected tetrahedra transparent volume meshes (Phase 6).
1728                // -----------------------------------------------------------
1729                if !frame.scene.transparent_volume_meshes.is_empty() {
1730                    self.resources.ensure_pt_pipeline(device);
1731                    if let Some(pipeline) = self.resources.pt_pipeline.as_ref() {
1732                        oit_pass.set_pipeline(pipeline);
1733                        oit_pass.set_bind_group(0, camera_bg, &[]);
1734                        for item in &frame.scene.transparent_volume_meshes {
1735                            if !item.visible {
1736                                continue;
1737                            }
1738                            let Some(gpu) =
1739                                self.resources.projected_tet_store.get(item.id.0)
1740                            else {
1741                                continue;
1742                            };
1743                            let (scalar_min, scalar_max) =
1744                                item.scalar_range.unwrap_or(gpu.scalar_range);
1745                            let uniform = crate::resources::ProjectedTetUniform {
1746                                density: item.density,
1747                                scalar_min,
1748                                scalar_max,
1749                                _pad: 0.0,
1750                            };
1751                            queue.write_buffer(
1752                                &gpu.uniform_buffer,
1753                                0,
1754                                bytemuck::bytes_of(&uniform),
1755                            );
1756                            oit_pass.set_bind_group(1, &gpu.bind_group, &[]);
1757                            oit_pass.draw(0..6, 0..gpu.tet_count);
1758                        }
1759                    }
1760                }
1761            }
1762        }
1763
1764        // -----------------------------------------------------------------------
1765        // OIT composite pass: blend accum/reveal into HDR buffer.
1766        // Only executes when transparent items were present.
1767        // -----------------------------------------------------------------------
1768        if has_transparent {
1769            if let (Some(pipeline), Some(bg)) = (
1770                self.resources.oit_composite_pipeline.as_ref(),
1771                slot_hdr.oit_composite_bind_group.as_ref(),
1772            ) {
1773                let hdr_view = &slot_hdr.hdr_view;
1774                let mut composite_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1775                    label: Some("oit_composite_pass"),
1776                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1777                        view: hdr_view,
1778                        resolve_target: None,
1779                        ops: wgpu::Operations {
1780                            load: wgpu::LoadOp::Load,
1781                            store: wgpu::StoreOp::Store,
1782                        },
1783                        depth_slice: None,
1784                    })],
1785                    depth_stencil_attachment: None,
1786                    timestamp_writes: None,
1787                    occlusion_query_set: None,
1788                });
1789                composite_pass.set_pipeline(pipeline);
1790                composite_pass.set_bind_group(0, bg, &[]);
1791                composite_pass.draw(0..3, 0..1);
1792            }
1793        }
1794
1795        // -----------------------------------------------------------------------
1796        // Phase 4: Surface LIC passes.
1797        // Pass 1: render each LIC mesh into lic_vector_texture (Rgba8Unorm).
1798        // Pass 2: advect fullscreen triangle into lic_output_texture (R8Unorm).
1799        // -----------------------------------------------------------------------
1800        if !self.lic_gpu_data.is_empty() {
1801            if let (Some(surface_pipeline), Some(advect_pipeline)) = (
1802                self.resources.lic_surface_pipeline.as_ref(),
1803                self.resources.lic_advect_pipeline.as_ref(),
1804            ) {
1805                let camera_bg = &slot.camera_bind_group;
1806                // Pass 1: surface vector pass (clears lic_vector_texture first).
1807                {
1808                    let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1809                        label: Some("lic_surface_pass"),
1810                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1811                            view: &slot_hdr.lic_vector_view,
1812                            resolve_target: None,
1813                            ops: wgpu::Operations {
1814                                load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1815                                store: wgpu::StoreOp::Store,
1816                            },
1817                            depth_slice: None,
1818                        })],
1819                        depth_stencil_attachment: None,
1820                        timestamp_writes: None,
1821                        occlusion_query_set: None,
1822                    });
1823                    pass.set_pipeline(surface_pipeline);
1824                    pass.set_bind_group(0, camera_bg, &[]);
1825                    for gpu in &self.lic_gpu_data {
1826                        let Some(mesh) = self.resources.mesh_store.get(gpu.mesh_id) else {
1827                            continue;
1828                        };
1829                        let Some(vec_buf) = mesh.vector_attribute_buffers.get(&gpu.vector_attribute) else {
1830                            continue;
1831                        };
1832                        pass.set_bind_group(1, &gpu.bind_group, &[]);
1833                        pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1834                        pass.set_vertex_buffer(1, vec_buf.slice(..));
1835                        pass.set_index_buffer(
1836                            mesh.index_buffer.slice(..),
1837                            wgpu::IndexFormat::Uint32,
1838                        );
1839                        pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1840                    }
1841                }
1842                // Pass 2: advect pass (fullscreen, writes LIC intensity to lic_output_texture).
1843                {
1844                    let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1845                        label: Some("lic_advect_pass"),
1846                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1847                            view: &slot_hdr.lic_output_view,
1848                            resolve_target: None,
1849                            ops: wgpu::Operations {
1850                                load: wgpu::LoadOp::Clear(wgpu::Color {
1851                                    r: 0.5,
1852                                    g: 0.0,
1853                                    b: 0.0,
1854                                    a: 1.0,
1855                                }),
1856                                store: wgpu::StoreOp::Store,
1857                            },
1858                            depth_slice: None,
1859                        })],
1860                        depth_stencil_attachment: None,
1861                        timestamp_writes: None,
1862                        occlusion_query_set: None,
1863                    });
1864                    pass.set_pipeline(advect_pipeline);
1865                    pass.set_bind_group(0, &slot_hdr.lic_advect_bind_group, &[]);
1866                    pass.draw(0..3, 0..1);
1867                }
1868            }
1869        }
1870
1871        // -----------------------------------------------------------------------
1872        // Outline composite pass (HDR path): blit offscreen outline onto hdr_view.
1873        // Runs after the HDR scene pass (which has depth+stencil) in a separate
1874        // pass with no depth attachment, so the composite pipeline is compatible.
1875        // -----------------------------------------------------------------------
1876        if !slot.outline_object_buffers.is_empty() {
1877            // Prefer the HDR-format pipeline; fall back to LDR single-sample.
1878            let hdr_pipeline = self
1879                .resources
1880                .outline_composite_pipeline_hdr
1881                .as_ref()
1882                .or(self.resources.outline_composite_pipeline_single.as_ref());
1883            if let Some(pipeline) = hdr_pipeline {
1884                let bg = &slot_hdr.outline_composite_bind_group;
1885                let hdr_view = &slot_hdr.hdr_view;
1886                let hdr_depth_view = &slot_hdr.hdr_depth_view;
1887                let mut outline_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1888                    label: Some("hdr_outline_composite_pass"),
1889                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1890                        view: hdr_view,
1891                        resolve_target: None,
1892                        ops: wgpu::Operations {
1893                            load: wgpu::LoadOp::Load,
1894                            store: wgpu::StoreOp::Store,
1895                        },
1896                        depth_slice: None,
1897                    })],
1898                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1899                        view: hdr_depth_view,
1900                        depth_ops: Some(wgpu::Operations {
1901                            load: wgpu::LoadOp::Load,
1902                            store: wgpu::StoreOp::Store,
1903                        }),
1904                        stencil_ops: None,
1905                    }),
1906                    timestamp_writes: None,
1907                    occlusion_query_set: None,
1908                });
1909                outline_pass.set_pipeline(pipeline);
1910                outline_pass.set_bind_group(0, bg, &[]);
1911                outline_pass.draw(0..3, 0..1);
1912            }
1913        }
1914
1915        // Phase 5 : effect throttling. Flag was computed in prepare() so that
1916        // FrameStats reports exactly what fired rather than an approximation.
1917        let throttle_effects = self.degradation_effects_throttled;
1918
1919        // -----------------------------------------------------------------------
1920        // SSAO pass.
1921        // -----------------------------------------------------------------------
1922        if pp.ssao && !throttle_effects {
1923            if let Some(ssao_pipeline) = &self.resources.ssao_pipeline {
1924                {
1925                    let mut ssao_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1926                        label: Some("ssao_pass"),
1927                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1928                            view: &slot_hdr.ssao_view,
1929                            resolve_target: None,
1930                            ops: wgpu::Operations {
1931                                load: wgpu::LoadOp::Clear(wgpu::Color::WHITE),
1932                                store: wgpu::StoreOp::Store,
1933                            },
1934                            depth_slice: None,
1935                        })],
1936                        depth_stencil_attachment: None,
1937                        timestamp_writes: None,
1938                        occlusion_query_set: None,
1939                    });
1940                    ssao_pass.set_pipeline(ssao_pipeline);
1941                    ssao_pass.set_bind_group(0, &slot_hdr.ssao_bg, &[]);
1942                    ssao_pass.draw(0..3, 0..1);
1943                }
1944
1945                // SSAO blur pass.
1946                if let Some(ssao_blur_pipeline) = &self.resources.ssao_blur_pipeline {
1947                    let mut ssao_blur_pass =
1948                        encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1949                            label: Some("ssao_blur_pass"),
1950                            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1951                                view: &slot_hdr.ssao_blur_view,
1952                                resolve_target: None,
1953                                ops: wgpu::Operations {
1954                                    load: wgpu::LoadOp::Clear(wgpu::Color::WHITE),
1955                                    store: wgpu::StoreOp::Store,
1956                                },
1957                                depth_slice: None,
1958                            })],
1959                            depth_stencil_attachment: None,
1960                            timestamp_writes: None,
1961                            occlusion_query_set: None,
1962                        });
1963                    ssao_blur_pass.set_pipeline(ssao_blur_pipeline);
1964                    ssao_blur_pass.set_bind_group(0, &slot_hdr.ssao_blur_bg, &[]);
1965                    ssao_blur_pass.draw(0..3, 0..1);
1966                }
1967            }
1968        }
1969
1970        // -----------------------------------------------------------------------
1971        // Contact shadow pass.
1972        // -----------------------------------------------------------------------
1973        if pp.contact_shadows && !throttle_effects {
1974            if let Some(cs_pipeline) = &self.resources.contact_shadow_pipeline {
1975                let mut cs_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1976                    label: Some("contact_shadow_pass"),
1977                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1978                        view: &slot_hdr.contact_shadow_view,
1979                        resolve_target: None,
1980                        depth_slice: None,
1981                        ops: wgpu::Operations {
1982                            load: wgpu::LoadOp::Clear(wgpu::Color::WHITE),
1983                            store: wgpu::StoreOp::Store,
1984                        },
1985                    })],
1986                    depth_stencil_attachment: None,
1987                    timestamp_writes: None,
1988                    occlusion_query_set: None,
1989                });
1990                cs_pass.set_pipeline(cs_pipeline);
1991                cs_pass.set_bind_group(0, &slot_hdr.contact_shadow_bg, &[]);
1992                cs_pass.draw(0..3, 0..1);
1993            }
1994        }
1995
1996        // -----------------------------------------------------------------------
1997        // Bloom passes.
1998        // -----------------------------------------------------------------------
1999        if pp.bloom && !throttle_effects {
2000            // Threshold pass: extract bright pixels into bloom_threshold_texture.
2001            if let Some(bloom_threshold_pipeline) = &self.resources.bloom_threshold_pipeline {
2002                {
2003                    let mut threshold_pass =
2004                        encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2005                            label: Some("bloom_threshold_pass"),
2006                            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2007                                view: &slot_hdr.bloom_threshold_view,
2008                                resolve_target: None,
2009                                ops: wgpu::Operations {
2010                                    load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
2011                                    store: wgpu::StoreOp::Store,
2012                                },
2013                                depth_slice: None,
2014                            })],
2015                            depth_stencil_attachment: None,
2016                            timestamp_writes: None,
2017                            occlusion_query_set: None,
2018                        });
2019                    threshold_pass.set_pipeline(bloom_threshold_pipeline);
2020                    threshold_pass.set_bind_group(0, &slot_hdr.bloom_threshold_bg, &[]);
2021                    threshold_pass.draw(0..3, 0..1);
2022                }
2023
2024                // 4 ping-pong H+V blur passes for a wide glow.
2025                // Pass 1: threshold -> ping -> pong. Passes 2-4: pong -> ping -> pong.
2026                if let Some(blur_pipeline) = &self.resources.bloom_blur_pipeline {
2027                    let blur_h_bg = &slot_hdr.bloom_blur_h_bg;
2028                    let blur_h_pong_bg = &slot_hdr.bloom_blur_h_pong_bg;
2029                    let blur_v_bg = &slot_hdr.bloom_blur_v_bg;
2030                    let bloom_ping_view = &slot_hdr.bloom_ping_view;
2031                    let bloom_pong_view = &slot_hdr.bloom_pong_view;
2032                    const BLUR_ITERATIONS: usize = 4;
2033                    for i in 0..BLUR_ITERATIONS {
2034                        // H pass: pass 0 reads threshold, subsequent passes read pong.
2035                        let h_bg = if i == 0 { blur_h_bg } else { blur_h_pong_bg };
2036                        {
2037                            let mut h_pass =
2038                                encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2039                                    label: Some("bloom_blur_h_pass"),
2040                                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2041                                        view: bloom_ping_view,
2042                                        resolve_target: None,
2043                                        ops: wgpu::Operations {
2044                                            load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
2045                                            store: wgpu::StoreOp::Store,
2046                                        },
2047                                        depth_slice: None,
2048                                    })],
2049                                    depth_stencil_attachment: None,
2050                                    timestamp_writes: None,
2051                                    occlusion_query_set: None,
2052                                });
2053                            h_pass.set_pipeline(blur_pipeline);
2054                            h_pass.set_bind_group(0, h_bg, &[]);
2055                            h_pass.draw(0..3, 0..1);
2056                        }
2057                        // V pass: ping -> pong.
2058                        {
2059                            let mut v_pass =
2060                                encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2061                                    label: Some("bloom_blur_v_pass"),
2062                                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2063                                        view: bloom_pong_view,
2064                                        resolve_target: None,
2065                                        ops: wgpu::Operations {
2066                                            load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
2067                                            store: wgpu::StoreOp::Store,
2068                                        },
2069                                        depth_slice: None,
2070                                    })],
2071                                    depth_stencil_attachment: None,
2072                                    timestamp_writes: None,
2073                                    occlusion_query_set: None,
2074                                });
2075                            v_pass.set_pipeline(blur_pipeline);
2076                            v_pass.set_bind_group(0, blur_v_bg, &[]);
2077                            v_pass.draw(0..3, 0..1);
2078                        }
2079                    }
2080                }
2081            }
2082        }
2083
2084        // -----------------------------------------------------------------------
2085        // Depth of field pass: HDR + depth -> dof_texture (when enabled).
2086        // -----------------------------------------------------------------------
2087        if pp.dof_enabled && !throttle_effects {
2088            if let Some(dof_pipeline) = &self.resources.dof_pipeline {
2089                let mut dof_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2090                    label: Some("dof_pass"),
2091                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2092                        view: &slot_hdr.dof_view,
2093                        resolve_target: None,
2094                        depth_slice: None,
2095                        ops: wgpu::Operations {
2096                            load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
2097                            store: wgpu::StoreOp::Store,
2098                        },
2099                    })],
2100                    depth_stencil_attachment: None,
2101                    timestamp_writes: None,
2102                    occlusion_query_set: None,
2103                });
2104                dof_pass.set_pipeline(dof_pipeline);
2105                dof_pass.set_bind_group(0, &slot_hdr.dof_bg, &[]);
2106                dof_pass.draw(0..3, 0..1);
2107            }
2108        }
2109
2110        // -----------------------------------------------------------------------
2111        // Tone map pass: HDR + bloom + AO -> (fxaa_texture if FXAA) or output_view.
2112        // -----------------------------------------------------------------------
2113        let use_fxaa = pp.fxaa;
2114        if let Some(tone_map_pipeline) = &self.resources.tone_map_pipeline {
2115            let tone_target: &wgpu::TextureView = if use_fxaa {
2116                &slot_hdr.fxaa_view
2117            } else {
2118                output_view
2119            };
2120            let mut tone_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2121                label: Some("tone_map_pass"),
2122                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2123                    view: tone_target,
2124                    resolve_target: None,
2125                    ops: wgpu::Operations {
2126                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
2127                        store: wgpu::StoreOp::Store,
2128                    },
2129                    depth_slice: None,
2130                })],
2131                depth_stencil_attachment: None,
2132                timestamp_writes: None,
2133                occlusion_query_set: None,
2134            });
2135            tone_pass.set_pipeline(tone_map_pipeline);
2136            tone_pass.set_bind_group(0, &slot_hdr.tone_map_bind_group, &[]);
2137            tone_pass.draw(0..3, 0..1);
2138        }
2139
2140        // -----------------------------------------------------------------------
2141        // FXAA pass: fxaa_texture -> output_view (only when FXAA is enabled).
2142        // -----------------------------------------------------------------------
2143        if use_fxaa {
2144            if let Some(fxaa_pipeline) = &self.resources.fxaa_pipeline {
2145                let mut fxaa_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2146                    label: Some("fxaa_pass"),
2147                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2148                        view: output_view,
2149                        resolve_target: None,
2150                        ops: wgpu::Operations {
2151                            load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
2152                            store: wgpu::StoreOp::Store,
2153                        },
2154                        depth_slice: None,
2155                    })],
2156                    depth_stencil_attachment: None,
2157                    timestamp_writes: None,
2158                    occlusion_query_set: None,
2159                });
2160                fxaa_pass.set_pipeline(fxaa_pipeline);
2161                fxaa_pass.set_bind_group(0, &slot_hdr.fxaa_bind_group, &[]);
2162                fxaa_pass.draw(0..3, 0..1);
2163            }
2164        }
2165
2166        // Grid pass (HDR path): draw the existing analytical grid on the final
2167        // output after tone mapping / FXAA, reusing the scene depth buffer so
2168        // scene geometry still occludes the grid exactly as in the LDR path.
2169        if frame.viewport.show_grid {
2170            let slot = &self.viewport_slots[vp_idx];
2171            let slot_hdr = slot.hdr.as_ref().unwrap();
2172            let grid_bg = &slot.grid_bind_group;
2173            let mut grid_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2174                label: Some("hdr_grid_pass"),
2175                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2176                    view: output_view,
2177                    resolve_target: None,
2178                    ops: wgpu::Operations {
2179                        load: wgpu::LoadOp::Load,
2180                        store: wgpu::StoreOp::Store,
2181                    },
2182                    depth_slice: None,
2183                })],
2184                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2185                    view: &slot_hdr.hdr_depth_view,
2186                    depth_ops: Some(wgpu::Operations {
2187                        load: wgpu::LoadOp::Load,
2188                        store: wgpu::StoreOp::Store,
2189                    }),
2190                    stencil_ops: None,
2191                }),
2192                timestamp_writes: None,
2193                occlusion_query_set: None,
2194            });
2195            grid_pass.set_pipeline(&self.resources.grid_pipeline);
2196            grid_pass.set_bind_group(0, grid_bg, &[]);
2197            grid_pass.draw(0..3, 0..1);
2198        }
2199
2200        // Ground plane pass (HDR path): drawn after grid, before editor overlays.
2201        // Uses the scene depth buffer for correct occlusion against geometry.
2202        if !matches!(
2203            frame.effects.ground_plane.mode,
2204            crate::renderer::types::GroundPlaneMode::None
2205        ) {
2206            let slot = &self.viewport_slots[vp_idx];
2207            let slot_hdr = slot.hdr.as_ref().unwrap();
2208            let mut gp_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2209                label: Some("hdr_ground_plane_pass"),
2210                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2211                    view: output_view,
2212                    resolve_target: None,
2213                    ops: wgpu::Operations {
2214                        load: wgpu::LoadOp::Load,
2215                        store: wgpu::StoreOp::Store,
2216                    },
2217                    depth_slice: None,
2218                })],
2219                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2220                    view: &slot_hdr.hdr_depth_view,
2221                    depth_ops: Some(wgpu::Operations {
2222                        load: wgpu::LoadOp::Load,
2223                        store: wgpu::StoreOp::Store,
2224                    }),
2225                    stencil_ops: None,
2226                }),
2227                timestamp_writes: None,
2228                occlusion_query_set: None,
2229            });
2230            gp_pass.set_pipeline(&self.resources.ground_plane_pipeline);
2231            gp_pass.set_bind_group(0, &self.resources.ground_plane_bind_group, &[]);
2232            gp_pass.draw(0..3, 0..1);
2233        }
2234
2235        // Editor overlay pass (HDR path): draw viewport/editor overlays on the
2236        // final output after tone mapping / FXAA, reusing the scene depth
2237        // buffer so depth-tested helpers still behave correctly.
2238        {
2239            let slot = &self.viewport_slots[vp_idx];
2240            let slot_hdr = slot.hdr.as_ref().unwrap();
2241            let has_editor_overlays = (frame.interaction.gizmo_model.is_some()
2242                && slot.gizmo_index_count > 0)
2243                || !slot.constraint_line_buffers.is_empty()
2244                || !slot.clip_plane_fill_buffers.is_empty()
2245                || !slot.clip_plane_line_buffers.is_empty()
2246                || !slot.xray_object_buffers.is_empty();
2247            if has_editor_overlays {
2248                let camera_bg = &slot.camera_bind_group;
2249                let mut overlay_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2250                    label: Some("hdr_editor_overlay_pass"),
2251                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2252                        view: output_view,
2253                        resolve_target: None,
2254                        ops: wgpu::Operations {
2255                            load: wgpu::LoadOp::Load,
2256                            store: wgpu::StoreOp::Store,
2257                        },
2258                        depth_slice: None,
2259                    })],
2260                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2261                        view: &slot_hdr.hdr_depth_view,
2262                        depth_ops: Some(wgpu::Operations {
2263                            load: wgpu::LoadOp::Load,
2264                            store: wgpu::StoreOp::Discard,
2265                        }),
2266                        stencil_ops: None,
2267                    }),
2268                    timestamp_writes: None,
2269                    occlusion_query_set: None,
2270                });
2271
2272                if frame.interaction.gizmo_model.is_some() && slot.gizmo_index_count > 0 {
2273                    overlay_pass.set_pipeline(&self.resources.gizmo_pipeline);
2274                    overlay_pass.set_bind_group(0, camera_bg, &[]);
2275                    overlay_pass.set_bind_group(1, &slot.gizmo_bind_group, &[]);
2276                    overlay_pass.set_vertex_buffer(0, slot.gizmo_vertex_buffer.slice(..));
2277                    overlay_pass.set_index_buffer(
2278                        slot.gizmo_index_buffer.slice(..),
2279                        wgpu::IndexFormat::Uint32,
2280                    );
2281                    overlay_pass.draw_indexed(0..slot.gizmo_index_count, 0, 0..1);
2282                }
2283
2284                if !slot.constraint_line_buffers.is_empty() {
2285                    overlay_pass.set_pipeline(&self.resources.overlay_line_pipeline);
2286                    overlay_pass.set_bind_group(0, camera_bg, &[]);
2287                    for (vbuf, ibuf, index_count, _ubuf, bg) in &slot.constraint_line_buffers {
2288                        overlay_pass.set_bind_group(1, bg, &[]);
2289                        overlay_pass.set_vertex_buffer(0, vbuf.slice(..));
2290                        overlay_pass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint32);
2291                        overlay_pass.draw_indexed(0..*index_count, 0, 0..1);
2292                    }
2293                }
2294
2295                if !slot.clip_plane_fill_buffers.is_empty() {
2296                    overlay_pass.set_pipeline(&self.resources.overlay_pipeline);
2297                    overlay_pass.set_bind_group(0, camera_bg, &[]);
2298                    for (vbuf, ibuf, idx_count, _ubuf, bg) in &slot.clip_plane_fill_buffers {
2299                        overlay_pass.set_bind_group(1, bg, &[]);
2300                        overlay_pass.set_vertex_buffer(0, vbuf.slice(..));
2301                        overlay_pass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint32);
2302                        overlay_pass.draw_indexed(0..*idx_count, 0, 0..1);
2303                    }
2304                }
2305
2306                if !slot.clip_plane_line_buffers.is_empty() {
2307                    overlay_pass.set_pipeline(&self.resources.overlay_line_pipeline);
2308                    overlay_pass.set_bind_group(0, camera_bg, &[]);
2309                    for (vbuf, ibuf, idx_count, _ubuf, bg) in &slot.clip_plane_line_buffers {
2310                        overlay_pass.set_bind_group(1, bg, &[]);
2311                        overlay_pass.set_vertex_buffer(0, vbuf.slice(..));
2312                        overlay_pass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint32);
2313                        overlay_pass.draw_indexed(0..*idx_count, 0, 0..1);
2314                    }
2315                }
2316
2317                if !slot.xray_object_buffers.is_empty() {
2318                    overlay_pass.set_pipeline(&self.resources.xray_pipeline);
2319                    overlay_pass.set_bind_group(0, camera_bg, &[]);
2320                    for (mesh_id, _buf, bg) in &slot.xray_object_buffers {
2321                        let Some(mesh) = self
2322                            .resources
2323                            .mesh_store
2324                            .get(*mesh_id)
2325                        else {
2326                            continue;
2327                        };
2328                        overlay_pass.set_bind_group(1, bg, &[]);
2329                        overlay_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
2330                        overlay_pass.set_index_buffer(
2331                            mesh.index_buffer.slice(..),
2332                            wgpu::IndexFormat::Uint32,
2333                        );
2334                        overlay_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
2335                    }
2336                }
2337            }
2338        }
2339
2340        // Axes indicator pass (HDR path): draw in screen space on the final
2341        // output after tone mapping / FXAA so it stays visible in PBR mode.
2342        if frame.viewport.show_axes_indicator {
2343            let slot = &self.viewport_slots[vp_idx];
2344            if slot.axes_vertex_count > 0 {
2345                let slot_hdr = slot.hdr.as_ref().unwrap();
2346                let mut axes_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2347                    label: Some("hdr_axes_pass"),
2348                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2349                        view: output_view,
2350                        resolve_target: None,
2351                        ops: wgpu::Operations {
2352                            load: wgpu::LoadOp::Load,
2353                            store: wgpu::StoreOp::Store,
2354                        },
2355                        depth_slice: None,
2356                    })],
2357                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2358                        view: &slot_hdr.hdr_depth_view,
2359                        depth_ops: Some(wgpu::Operations {
2360                            load: wgpu::LoadOp::Load,
2361                            store: wgpu::StoreOp::Discard,
2362                        }),
2363                        stencil_ops: None,
2364                    }),
2365                    timestamp_writes: None,
2366                    occlusion_query_set: None,
2367                });
2368                axes_pass.set_pipeline(&self.resources.axes_pipeline);
2369                axes_pass.set_vertex_buffer(0, slot.axes_vertex_buffer.slice(..));
2370                axes_pass.draw(0..slot.axes_vertex_count, 0..1);
2371            }
2372        }
2373
2374        // Phase 10B / Phase 12 : screen-space image overlay pass (HDR path).
2375        // Drawn after axes so overlays are always on top of everything.
2376        // Regular items use depth_compare: Always; depth-composite items use LessEqual.
2377        if !self.screen_image_gpu_data.is_empty() {
2378            if let Some(overlay_pipeline) = &self.resources.screen_image_pipeline {
2379                let slot_hdr = self.viewport_slots[vp_idx].hdr.as_ref().unwrap();
2380                let dc_pipeline = self.resources.screen_image_dc_pipeline.as_ref();
2381                let mut img_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2382                    label: Some("screen_image_pass"),
2383                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2384                        view: output_view,
2385                        resolve_target: None,
2386                        ops: wgpu::Operations {
2387                            load: wgpu::LoadOp::Load,
2388                            store: wgpu::StoreOp::Store,
2389                        },
2390                        depth_slice: None,
2391                    })],
2392                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2393                        view: &slot_hdr.hdr_depth_view,
2394                        depth_ops: Some(wgpu::Operations {
2395                            load: wgpu::LoadOp::Load,
2396                            store: wgpu::StoreOp::Discard,
2397                        }),
2398                        stencil_ops: None,
2399                    }),
2400                    timestamp_writes: None,
2401                    occlusion_query_set: None,
2402                });
2403                for gpu in &self.screen_image_gpu_data {
2404                    if let (Some(dc_bg), Some(dc_pipe)) = (&gpu.depth_bind_group, dc_pipeline) {
2405                        img_pass.set_pipeline(dc_pipe);
2406                        img_pass.set_bind_group(0, dc_bg, &[]);
2407                    } else {
2408                        img_pass.set_pipeline(overlay_pipeline);
2409                        img_pass.set_bind_group(0, &gpu.bind_group, &[]);
2410                    }
2411                    img_pass.draw(0..6, 0..1);
2412                }
2413            }
2414        }
2415
2416        // Overlay labels, scalar bars, rulers, and overlay images (HDR path): drawn last.
2417        let has_overlay = self.label_gpu_data.is_some()
2418            || self.scalar_bar_gpu_data.is_some()
2419            || self.ruler_gpu_data.is_some()
2420            || self.loading_bar_gpu_data.is_some()
2421            || !self.overlay_image_gpu_data.is_empty();
2422        if has_overlay {
2423            let hdr_depth_view =
2424                &self.viewport_slots[vp_idx].hdr.as_ref().unwrap().hdr_depth_view;
2425            let mut overlay_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2426                label: Some("overlay_pass"),
2427                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2428                    view: output_view,
2429                    resolve_target: None,
2430                    ops: wgpu::Operations {
2431                        load: wgpu::LoadOp::Load,
2432                        store: wgpu::StoreOp::Store,
2433                    },
2434                    depth_slice: None,
2435                })],
2436                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2437                    view: hdr_depth_view,
2438                    depth_ops: Some(wgpu::Operations {
2439                        load: wgpu::LoadOp::Load,
2440                        store: wgpu::StoreOp::Discard,
2441                    }),
2442                    stencil_ops: None,
2443                }),
2444                timestamp_writes: None,
2445                occlusion_query_set: None,
2446            });
2447            if let Some(pipeline) = &self.resources.overlay_text_pipeline {
2448                overlay_pass.set_pipeline(pipeline);
2449                if let Some(ref ld) = self.label_gpu_data {
2450                    overlay_pass.set_bind_group(0, &ld.bind_group, &[]);
2451                    overlay_pass.set_vertex_buffer(0, ld.vertex_buf.slice(..));
2452                    overlay_pass.draw(0..ld.vertex_count, 0..1);
2453                }
2454                if let Some(ref sb) = self.scalar_bar_gpu_data {
2455                    overlay_pass.set_bind_group(0, &sb.bind_group, &[]);
2456                    overlay_pass.set_vertex_buffer(0, sb.vertex_buf.slice(..));
2457                    overlay_pass.draw(0..sb.vertex_count, 0..1);
2458                }
2459                if let Some(ref rd) = self.ruler_gpu_data {
2460                    overlay_pass.set_bind_group(0, &rd.bind_group, &[]);
2461                    overlay_pass.set_vertex_buffer(0, rd.vertex_buf.slice(..));
2462                    overlay_pass.draw(0..rd.vertex_count, 0..1);
2463                }
2464                if let Some(ref lb) = self.loading_bar_gpu_data {
2465                    overlay_pass.set_bind_group(0, &lb.bind_group, &[]);
2466                    overlay_pass.set_vertex_buffer(0, lb.vertex_buf.slice(..));
2467                    overlay_pass.draw(0..lb.vertex_count, 0..1);
2468                }
2469            }
2470            // Phase 7 : overlay images drawn last inside the overlay pass.
2471            if !self.overlay_image_gpu_data.is_empty() {
2472                if let Some(pipeline) = &self.resources.screen_image_pipeline {
2473                    overlay_pass.set_pipeline(pipeline);
2474                    for gpu in &self.overlay_image_gpu_data {
2475                        overlay_pass.set_bind_group(0, &gpu.bind_group, &[]);
2476                        overlay_pass.draw(0..6, 0..1);
2477                    }
2478                }
2479            }
2480        }
2481
2482        // Phase 4 : resolve timestamp queries -> staging buffer (HDR path).
2483        if let (Some(qs), Some(res_buf), Some(stg_buf)) = (
2484            self.ts_query_set.as_ref(),
2485            self.ts_resolve_buf.as_ref(),
2486            self.ts_staging_buf.as_ref(),
2487        ) {
2488            encoder.resolve_query_set(qs, 0..2, res_buf, 0);
2489            encoder.copy_buffer_to_buffer(res_buf, 0, stg_buf, 0, 16);
2490            self.ts_needs_readback = true;
2491        }
2492
2493        encoder.finish()
2494    }
2495
2496    /// Render a frame to an offscreen texture and return raw RGBA bytes.
2497    ///
2498    /// Creates a temporary [`wgpu::Texture`] render target of the given dimensions,
2499    /// runs all render passes (shadow, scene, post-processing) into it via
2500    /// [`render()`](Self::render), then copies the result back to CPU memory.
2501    ///
2502    /// No OS window or [`wgpu::Surface`] is required. The caller is responsible for
2503    /// initialising the wgpu adapter with `compatible_surface: None` and for
2504    /// constructing a valid [`FrameData`] (including `viewport_size` matching
2505    /// `width`/`height`).
2506    ///
2507    /// Returns `width * height * 4` bytes in RGBA8 layout. The caller encodes to
2508    /// PNG/EXR independently : no image codec dependency in this crate.
2509    pub fn render_offscreen(
2510        &mut self,
2511        device: &wgpu::Device,
2512        queue: &wgpu::Queue,
2513        frame: &FrameData,
2514        width: u32,
2515        height: u32,
2516    ) -> Vec<u8> {
2517        // 1. Create offscreen texture with RENDER_ATTACHMENT | COPY_SRC usage.
2518        let target_format = self.resources.target_format;
2519        let offscreen_texture = device.create_texture(&wgpu::TextureDescriptor {
2520            label: Some("offscreen_target"),
2521            size: wgpu::Extent3d {
2522                width: width.max(1),
2523                height: height.max(1),
2524                depth_or_array_layers: 1,
2525            },
2526            mip_level_count: 1,
2527            sample_count: 1,
2528            dimension: wgpu::TextureDimension::D2,
2529            format: target_format,
2530            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
2531            view_formats: &[],
2532        });
2533
2534        // 2. Create a texture view for rendering into.
2535        let output_view = offscreen_texture.create_view(&wgpu::TextureViewDescriptor::default());
2536
2537        // 3. render() calls ensure_viewport_hdr which provides the depth-stencil buffer
2538        //    for both LDR and HDR paths, so no separate ensure_outline_target is needed.
2539
2540        // 4. Render the scene into the offscreen texture.
2541        //    The caller must set `frame.camera.viewport_size` to `[width as f32, height as f32]`
2542        //    and `frame.camera.render_camera.aspect` to `width as f32 / height as f32`
2543        //    for correct HDR target allocation and scissor rects.
2544        let cmd_buf = self.render(device, queue, &output_view, frame);
2545        queue.submit(std::iter::once(cmd_buf));
2546
2547        // 5. Copy texture -> staging buffer (wgpu requires row alignment to 256 bytes).
2548        let bytes_per_pixel = 4u32;
2549        let unpadded_row = width * bytes_per_pixel;
2550        let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
2551        let padded_row = (unpadded_row + align - 1) & !(align - 1);
2552        let buffer_size = (padded_row * height.max(1)) as u64;
2553
2554        let staging_buf = device.create_buffer(&wgpu::BufferDescriptor {
2555            label: Some("offscreen_staging"),
2556            size: buffer_size,
2557            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2558            mapped_at_creation: false,
2559        });
2560
2561        let mut copy_encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
2562            label: Some("offscreen_copy_encoder"),
2563        });
2564        copy_encoder.copy_texture_to_buffer(
2565            wgpu::TexelCopyTextureInfo {
2566                texture: &offscreen_texture,
2567                mip_level: 0,
2568                origin: wgpu::Origin3d::ZERO,
2569                aspect: wgpu::TextureAspect::All,
2570            },
2571            wgpu::TexelCopyBufferInfo {
2572                buffer: &staging_buf,
2573                layout: wgpu::TexelCopyBufferLayout {
2574                    offset: 0,
2575                    bytes_per_row: Some(padded_row),
2576                    rows_per_image: Some(height.max(1)),
2577                },
2578            },
2579            wgpu::Extent3d {
2580                width: width.max(1),
2581                height: height.max(1),
2582                depth_or_array_layers: 1,
2583            },
2584        );
2585        queue.submit(std::iter::once(copy_encoder.finish()));
2586
2587        // 6. Map buffer and extract tightly-packed RGBA pixels.
2588        let (tx, rx) = std::sync::mpsc::channel();
2589        staging_buf
2590            .slice(..)
2591            .map_async(wgpu::MapMode::Read, move |result| {
2592                let _ = tx.send(result);
2593            });
2594        device
2595            .poll(wgpu::PollType::Wait {
2596                submission_index: None,
2597                timeout: Some(std::time::Duration::from_secs(5)),
2598            })
2599            .unwrap();
2600        let _ = rx.recv().unwrap_or(Err(wgpu::BufferAsyncError));
2601
2602        let mut pixels: Vec<u8> = Vec::with_capacity((width * height * 4) as usize);
2603        {
2604            let mapped = staging_buf.slice(..).get_mapped_range();
2605            let data: &[u8] = &mapped;
2606            if padded_row == unpadded_row {
2607                // No padding : copy entire slice directly.
2608                pixels.extend_from_slice(data);
2609            } else {
2610                // Strip row padding.
2611                for row in 0..height as usize {
2612                    let start = row * padded_row as usize;
2613                    let end = start + unpadded_row as usize;
2614                    pixels.extend_from_slice(&data[start..end]);
2615                }
2616            }
2617        }
2618        staging_buf.unmap();
2619
2620        // 7. Swizzle BGRA -> RGBA if the format stores bytes in BGRA order.
2621        let is_bgra = matches!(
2622            target_format,
2623            wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb
2624        );
2625        if is_bgra {
2626            for pixel in pixels.chunks_exact_mut(4) {
2627                pixel.swap(0, 2); // B ↔ R
2628            }
2629        }
2630
2631        pixels
2632    }
2633}