Skip to main content

viewport_lib/renderer/render/
mod.rs

1use super::*;
2use wgpu::util::DeviceExt;
3
4/// Emit the 2D overlay draw calls shared by every paint path: SDF shapes,
5/// rects, labels, scalar bars, rulers, loading bars, and overlay images, in
6/// back-to-front order. Each block is guarded by its own prepared GPU data, so
7/// a path with no data for a given overlay skips it. Run after all scene
8/// content so the overlays sit on top. The overlay pipelines are format-neutral
9/// (no separate LDR/HDR variant), so this is shared verbatim across paths.
10macro_rules! emit_overlay_2d {
11    ($this:ident, $render_pass:ident) => {{
12        // SDF overlay shapes (drawn before rects and labels).
13        if let Some(ref sd) = $this.overlay_shape_gpu_data {
14            if sd.vertex_count > 0 {
15                if let Some(pipeline) = &$this.resources.overlay_shape.pipeline {
16                    if let Some(vbuf) = &sd.vertex_buf {
17                        $render_pass.set_pipeline(pipeline);
18                        $render_pass.set_vertex_buffer(0, vbuf.slice(..));
19                        $render_pass.draw(0..sd.vertex_count, 0..1);
20                    }
21                }
22            }
23            if !sd.tex_batches.is_empty() {
24                if let Some(pipeline) = &$this.resources.overlay_shape.tex_pipeline {
25                    $render_pass.set_pipeline(pipeline);
26                    for batch in &sd.tex_batches {
27                        $render_pass.set_bind_group(0, &batch.bind_group, &[]);
28                        $render_pass.set_vertex_buffer(0, batch.vertex_buf.slice(..));
29                        $render_pass.draw(0..batch.vertex_count, 0..1);
30                    }
31                }
32            }
33        }
34        // Overlay rects (drawn before labels so they act as backgrounds).
35        if let Some(ref rr) = $this.overlay_rect_gpu_data {
36            if let Some(pipeline) = &$this.resources.overlay_text.pipeline {
37                $render_pass.set_pipeline(pipeline);
38                $render_pass.set_bind_group(0, &rr.bind_group, &[]);
39                $render_pass.set_vertex_buffer(0, rr.vertex_buf.slice(..));
40                $render_pass.draw(0..rr.vertex_count, 0..1);
41            }
42        }
43        // Overlay labels (drawn after rects).
44        if let Some(ref ld) = $this.label_gpu_data {
45            if let Some(pipeline) = &$this.resources.overlay_text.pipeline {
46                $render_pass.set_pipeline(pipeline);
47                $render_pass.set_bind_group(0, &ld.bind_group, &[]);
48                $render_pass.set_vertex_buffer(0, ld.vertex_buf.slice(..));
49                $render_pass.draw(0..ld.vertex_count, 0..1);
50            }
51        }
52        // Scalar bars (drawn after labels).
53        if let Some(ref sb) = $this.scalar_bar_gpu_data {
54            if let Some(pipeline) = &$this.resources.overlay_text.pipeline {
55                $render_pass.set_pipeline(pipeline);
56                $render_pass.set_bind_group(0, &sb.bind_group, &[]);
57                $render_pass.set_vertex_buffer(0, sb.vertex_buf.slice(..));
58                $render_pass.draw(0..sb.vertex_count, 0..1);
59            }
60        }
61        // Rulers (drawn after scalar bars).
62        if let Some(ref rd) = $this.ruler_gpu_data {
63            if let Some(pipeline) = &$this.resources.overlay_text.pipeline {
64                $render_pass.set_pipeline(pipeline);
65                $render_pass.set_bind_group(0, &rd.bind_group, &[]);
66                $render_pass.set_vertex_buffer(0, rd.vertex_buf.slice(..));
67                $render_pass.draw(0..rd.vertex_count, 0..1);
68            }
69        }
70        // Loading bars (drawn after rulers).
71        if let Some(ref lb) = $this.loading_bar_gpu_data {
72            if let Some(pipeline) = &$this.resources.overlay_text.pipeline {
73                $render_pass.set_pipeline(pipeline);
74                $render_pass.set_bind_group(0, &lb.bind_group, &[]);
75                $render_pass.set_vertex_buffer(0, lb.vertex_buf.slice(..));
76                $render_pass.draw(0..lb.vertex_count, 0..1);
77            }
78        }
79        // Overlay images (drawn last, no depth test).
80        if !$this.overlay_image_gpu_data.is_empty() {
81            if let Some(pipeline) = &$this.resources.screen_image.pipeline {
82                $render_pass.set_pipeline(pipeline);
83                for gpu in &$this.overlay_image_gpu_data {
84                    $render_pass.set_bind_group(0, &gpu.bind_group, &[]);
85                    $render_pass.draw(0..6, 0..1);
86                }
87            }
88        }
89    }};
90}
91
92mod hdr_path;
93mod ldr_path;
94mod paint_direct;
95
96impl ViewportRenderer {
97    /// Render the scene into an intermediate dyn-res texture for the LDR callback
98    /// render path (e.g. eframe's `CallbackTrait`).
99    ///
100    /// Call from `CallbackTrait::prepare` after [`prepare`](Self::prepare), passing the
101    /// `egui_encoder`. If `current_render_scale < 1.0`, the full scene is drawn into a
102    /// scaled intermediate texture and `true` is returned. Call
103    /// [`paint_dyn_res_blit`](Self::paint_dyn_res_blit) from `CallbackTrait::paint`
104    /// instead of [`paint`](Self::paint).
105    ///
106    /// If scale is 1.0 or above, nothing is encoded and `false` is returned. Call
107    /// [`paint`](Self::paint) as normal.
108    ///
109    /// The `egui_encoder` is submitted before the surface render pass begins, so the
110    /// intermediate texture is fully written before the blit reads it.
111    pub(crate) fn prepare_ldr_dyn_res(
112        &mut self,
113        encoder: &mut wgpu::CommandEncoder,
114        device: &wgpu::Device,
115        frame: &FrameData,
116    ) -> bool {
117        if self.current_render_scale >= 1.0 - 0.001 {
118            return false;
119        }
120
121        let vp_idx = frame.camera.viewport_index;
122        let w = (frame.camera.viewport_size[0] as u32).max(1);
123        let h = (frame.camera.viewport_size[1] as u32).max(1);
124        let sw = ((w as f32 * self.current_render_scale) as u32).max(1);
125        let sh = ((h as f32 * self.current_render_scale) as u32).max(1);
126
127        self.ensure_dyn_res_target(device, vp_idx, [sw, sh], [w, h]);
128        self.resources.ensure_dyn_res_ds_pipeline(device);
129
130        let bg_colour = frame.viewport.background_colour.unwrap_or([
131            65.0 / 255.0,
132            65.0 / 255.0,
133            65.0 / 255.0,
134            1.0,
135        ]);
136
137        {
138            let slot = &self.viewport_slots[vp_idx];
139            let dr = slot.dyn_res.as_ref().unwrap();
140            let colour_view = &dr.colour_view;
141            let depth_view = &dr.depth_view;
142            let camera_bg = &slot.camera_bind_group;
143            let grid_bg = &slot.grid_bind_group;
144
145            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
146                label: Some("ldr_dyn_res_render_pass"),
147                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
148                    view: colour_view,
149                    resolve_target: None,
150                    ops: wgpu::Operations {
151                        load: wgpu::LoadOp::Clear(wgpu::Color {
152                            r: bg_colour[0] as f64,
153                            g: bg_colour[1] as f64,
154                            b: bg_colour[2] as f64,
155                            a: bg_colour[3] as f64,
156                        }),
157                        store: wgpu::StoreOp::Store,
158                    },
159                    depth_slice: None,
160                })],
161                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
162                    view: depth_view,
163                    depth_ops: Some(wgpu::Operations {
164                        load: wgpu::LoadOp::Clear(1.0),
165                        store: wgpu::StoreOp::Discard,
166                    }),
167                    stencil_ops: None,
168                }),
169                timestamp_writes: None,
170                occlusion_query_set: None,
171            });
172            emit_draw_calls!(
173                &self.resources,
174                &mut render_pass,
175                frame,
176                self.instancing.use_instancing,
177                &self.instancing.batches,
178                camera_bg,
179                grid_bg,
180                &self.compute_filter_results,
181                Some(slot),
182                &self.mesh_uniforms.wireframe_bind_groups,
183                &self.mesh_uniforms.bind_groups,
184                &self.prepared_surfaces
185            );
186            emit_scivis_draw_calls!(
187                &self.resources,
188                &mut render_pass,
189                &self.point_cloud_gpu_data,
190                &self.glyph_gpu_data,
191                &self.polyline_gpu_data,
192                &self.volume_gpu_data,
193                &self.streamtube_gpu_data,
194                camera_bg,
195                &self.tube_gpu_data,
196                &self.image_slice_gpu_data,
197                &self.tensor_glyph_gpu_data,
198                &self.ribbon_gpu_data,
199                &self.volume_surface_slice_gpu_data,
200                &self.sprite_gpu_data,
201                &self.mesh_instance_gpu_data,
202                false
203            );
204            // TransparentVolumeMesh boundary wireframe overlay.
205            if !self.mesh_uniforms.tvm_wireframe_draws.is_empty() {
206                if let Some(ref tvm_bg) = self.mesh_uniforms.tvm_wireframe_bg {
207                    render_pass.set_bind_group(0, camera_bg, &[]);
208                    for mesh_id in &self.mesh_uniforms.tvm_wireframe_draws {
209                        if let Some(mesh) = self.resources.mesh_store.get(*mesh_id) {
210                            render_pass.set_pipeline(&self.resources.wireframe_pipeline);
211                            render_pass.set_bind_group(
212                                2,
213                                &self.resources.deform.dummy_bind_group,
214                                &[],
215                            );
216                            render_pass.set_bind_group(1, tvm_bg, &[]);
217                            render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
218                            render_pass.set_index_buffer(
219                                mesh.edge_index_buffer.slice(..),
220                                wgpu::IndexFormat::Uint32,
221                            );
222                            render_pass.draw_indexed(0..mesh.edge_index_count, 0, 0..1);
223                        }
224                    }
225                }
226            }
227            // Implicit surface.
228            if !self.implicit_gpu_data.is_empty() {
229                if let Some(ref dual) = self.resources.implicit.pipeline {
230                    render_pass.set_pipeline(dual.for_format(false));
231                    render_pass.set_bind_group(0, camera_bg, &[]);
232                    for gpu in &self.implicit_gpu_data {
233                        render_pass.set_bind_group(1, &gpu.bind_group, &[]);
234                        render_pass.draw(0..6, 0..1);
235                    }
236                }
237            }
238            // GPU marching cubes indirect draw.
239            if !self.mc_gpu_data.is_empty() {
240                if let Some(ref dual) = self.resources.mc.surface_pipeline {
241                    render_pass.set_pipeline(dual.for_format(false));
242                    render_pass.set_bind_group(0, camera_bg, &[]);
243                    for mc in &self.mc_gpu_data {
244                        let vol = &self.resources.mc.volumes[mc.volume_idx];
245                        render_pass.set_bind_group(1, &mc.render_bg, &[]);
246                        for slab in &vol.slabs {
247                            render_pass.set_vertex_buffer(0, slab.vertex_buf.slice(..));
248                            render_pass.draw_indirect(&slab.indirect_buf, 0);
249                        }
250                    }
251                }
252            }
253            // Outline composite after all scene content.
254            emit_outline_composite!(&self.resources, &mut render_pass, Some(slot));
255            // Sub-object highlight (LDR path).
256            if let Some(sub_hl) = slot.sub_highlight.as_ref() {
257                if let (Some(fill_pl), Some(edge_pl), Some(sprite_pl)) = (
258                    &self.resources.sub_highlight.fill_ldr_pipeline,
259                    &self.resources.sub_highlight.edge_ldr_pipeline,
260                    &self.resources.sub_highlight.sprite_ldr_pipeline,
261                ) {
262                    if sub_hl.fill_vertex_count > 0 {
263                        render_pass.set_pipeline(fill_pl);
264                        render_pass.set_bind_group(0, camera_bg, &[]);
265                        render_pass.set_bind_group(1, &sub_hl.fill_bind_group, &[]);
266                        render_pass.set_vertex_buffer(0, sub_hl.fill_vertex_buf.slice(..));
267                        render_pass.draw(0..sub_hl.fill_vertex_count, 0..1);
268                    }
269                    if sub_hl.edge_segment_count > 0 {
270                        render_pass.set_pipeline(edge_pl);
271                        render_pass.set_bind_group(0, camera_bg, &[]);
272                        render_pass.set_bind_group(1, &sub_hl.edge_bind_group, &[]);
273                        render_pass.set_vertex_buffer(0, sub_hl.edge_vertex_buf.slice(..));
274                        render_pass.draw(0..6, 0..sub_hl.edge_segment_count);
275                    }
276                    if sub_hl.sprite_point_count > 0 {
277                        render_pass.set_pipeline(sprite_pl);
278                        render_pass.set_bind_group(0, camera_bg, &[]);
279                        render_pass.set_bind_group(1, &sub_hl.sprite_bind_group, &[]);
280                        render_pass.set_vertex_buffer(0, sub_hl.sprite_vertex_buf.slice(..));
281                        render_pass.draw(0..6, 0..sub_hl.sprite_point_count);
282                    }
283                }
284            }
285            // Screen-space image overlays.
286            if !self.screen_image_gpu_data.is_empty() {
287                if let Some(pipeline) = &self.resources.screen_image.pipeline {
288                    render_pass.set_pipeline(pipeline);
289                    for gpu in &self.screen_image_gpu_data {
290                        render_pass.set_bind_group(0, &gpu.bind_group, &[]);
291                        render_pass.draw(0..6, 0..1);
292                    }
293                }
294            }
295            emit_overlay_2d!(self, render_pass);
296        }
297
298        true
299    }
300
301    /// Blit the dyn-res intermediate texture into the provided render pass.
302    ///
303    /// Call from `CallbackTrait::paint` when
304    /// [`prepare_ldr_dyn_res`](Self::prepare_ldr_dyn_res) returned `true` for the same
305    /// frame. Emits a fullscreen upscale quad into `render_pass`.
306    pub(crate) fn paint_dyn_res_blit<'rp>(
307        &self,
308        render_pass: &mut wgpu::RenderPass<'rp>,
309        frame: &FrameData,
310    ) {
311        let vp_idx = frame.camera.viewport_index;
312        if let Some(dr) = self
313            .viewport_slots
314            .get(vp_idx)
315            .and_then(|s| s.dyn_res.as_ref())
316        {
317            if let Some(pipeline) = &self.resources.post.dyn_res_upscale_ds_pipeline {
318                render_pass.set_pipeline(pipeline);
319                render_pass.set_bind_group(0, &dr.upscale_bind_group, &[]);
320                render_pass.draw(0..3, 0..1);
321            }
322        }
323    }
324
325    /// Run the full HDR pipeline (OIT, EDL, tone-map) for the eframe callback model.
326    ///
327    /// This is the HDR counterpart of
328    /// [`prepare_ldr_dyn_res`](Self::prepare_ldr_dyn_res) for use when
329    /// `frame.effects.post_process.enabled` is `true`.
330    ///
331    /// Internally this method:
332    /// 1. Calls [`prepare`](Self::prepare) to upload uniforms and run the shadow pass.
333    /// 2. Ensures a per-viewport intermediate texture at the viewport's native resolution.
334    /// 3. Calls the full render pipeline (including OIT and EDL) into that texture.
335    ///
336    /// The returned [`wgpu::CommandBuffer`] must be returned from
337    /// `CallbackTrait::prepare` so eframe submits it **before** the egui render pass.
338    ///
339    /// Call [`paint_hdr_blit`](Self::paint_hdr_blit) from `CallbackTrait::paint` to
340    /// composite the intermediate texture into the egui render pass.
341    pub(crate) fn prepare_hdr_callback(
342        &mut self,
343        device: &wgpu::Device,
344        queue: &wgpu::Queue,
345        frame: &FrameData,
346    ) -> wgpu::CommandBuffer {
347        self.prepare(device, queue, frame);
348
349        let vp_idx = frame.camera.viewport_index;
350        // Intermediate texture must be at physical pixel size so it matches the
351        // HDR depth buffer allocated inside render_frame_internal (which also
352        // uses physical pixels). Using logical size here produces a mismatch on
353        // hidpi displays between the colour attachment (this texture) and the
354        // depth attachment (hdr_depth_view) in the grid/overlay passes.
355        let ppp = frame.camera.pixels_per_point;
356        let w = (frame.camera.viewport_size[0] * ppp).round() as u32;
357        let h = (frame.camera.viewport_size[1] * ppp).round() as u32;
358
359        // Ensure the blit pipeline (required by create_hdr_callback_target).
360        self.resources.ensure_dyn_res_pipeline(device);
361        self.resources.ensure_dyn_res_ds_pipeline(device);
362
363        // Create or resize the per-viewport intermediate texture.
364        self.ensure_viewport_slot(device, vp_idx);
365        let needs_create = match self.viewport_slots[vp_idx].hdr_callback.as_ref() {
366            None => true,
367            Some(t) => t.size != [w, h],
368        };
369        if needs_create {
370            let target = self.resources.create_hdr_callback_target(device, [w, h]);
371            self.viewport_slots[vp_idx].hdr_callback = Some(target);
372        }
373
374        // Create a fresh TextureView from the stored Texture.
375        // This owned view does not borrow viewport_slots, allowing the subsequent
376        // mutable call to render_frame_internal without a borrow conflict.
377        let output_view = self.viewport_slots[vp_idx]
378            .hdr_callback
379            .as_ref()
380            .unwrap()
381            .texture
382            .create_view(&wgpu::TextureViewDescriptor::default());
383
384        self.render_frame_internal(device, queue, &output_view, vp_idx, frame)
385    }
386
387    /// HDR encode for a single viewport in the multi-viewport eframe callback model.
388    ///
389    /// Like [`prepare_hdr_callback`](Self::prepare_hdr_callback) but skips the internal
390    /// [`prepare`](Self::prepare) call. The caller must have already called
391    /// [`prepare_scene`](Self::prepare_scene) and [`prepare_viewport`](Self::prepare_viewport)
392    /// for `id` before invoking this.
393    ///
394    /// Multi-viewport HDR sequence:
395    /// 1. Call `prepare_scene` once.
396    /// 2. Call `prepare_viewport` for each viewport.
397    /// 3. Call this method for each viewport; collect the returned `CommandBuffer`s.
398    /// 4. Return them from `CallbackTrait::prepare`.
399    ///
400    /// Call [`paint_hdr_blit`](Self::paint_hdr_blit) for each viewport from
401    /// `CallbackTrait::paint` with the scissor/viewport rect set first.
402    pub(crate) fn prepare_hdr_callback_viewport(
403        &mut self,
404        device: &wgpu::Device,
405        queue: &wgpu::Queue,
406        id: ViewportId,
407        frame: &FrameData,
408    ) -> wgpu::CommandBuffer {
409        let vp_idx = id.0;
410        let ppp = frame.camera.pixels_per_point;
411        let w = (frame.camera.viewport_size[0] * ppp).round() as u32;
412        let h = (frame.camera.viewport_size[1] * ppp).round() as u32;
413
414        self.resources.ensure_dyn_res_pipeline(device);
415        self.resources.ensure_dyn_res_ds_pipeline(device);
416
417        self.ensure_viewport_slot(device, vp_idx);
418        let needs_create = match self.viewport_slots[vp_idx].hdr_callback.as_ref() {
419            None => true,
420            Some(t) => t.size != [w, h],
421        };
422        if needs_create {
423            let target = self.resources.create_hdr_callback_target(device, [w, h]);
424            self.viewport_slots[vp_idx].hdr_callback = Some(target);
425        }
426
427        let output_view = self.viewport_slots[vp_idx]
428            .hdr_callback
429            .as_ref()
430            .unwrap()
431            .texture
432            .create_view(&wgpu::TextureViewDescriptor::default());
433
434        self.render_frame_internal(device, queue, &output_view, vp_idx, frame)
435    }
436
437    /// Blit the HDR intermediate texture into the egui render pass.
438    ///
439    /// Call from `CallbackTrait::paint` after
440    /// [`prepare_hdr_callback`](Self::prepare_hdr_callback) has been called for the
441    /// same frame and viewport. Emits a fullscreen triangle into `render_pass`.
442    pub(crate) fn paint_hdr_blit<'rp>(
443        &self,
444        render_pass: &mut wgpu::RenderPass<'rp>,
445        frame: &FrameData,
446    ) {
447        let vp_idx = frame.camera.viewport_index;
448        if let Some(hc) = self
449            .viewport_slots
450            .get(vp_idx)
451            .and_then(|s| s.hdr_callback.as_ref())
452        {
453            if let Some(pipeline) = &self.resources.post.dyn_res_upscale_ds_pipeline {
454                render_pass.set_pipeline(pipeline);
455                render_pass.set_bind_group(0, &hc.blit_bind_group, &[]);
456                render_pass.draw(0..3, 0..1);
457            }
458        }
459        // Shadow atlas viewer overlay.
460        if frame.effects.show_shadow_atlas {
461            render_pass.set_pipeline(&self.resources.shadow_atlas_viewer_pipeline);
462            render_pass.set_bind_group(0, &self.resources.shadow_atlas_viewer_bg, &[]);
463            render_pass.draw(0..6, 0..1);
464        }
465    }
466
467    /// Like [`paint_hdr_blit`](Self::paint_hdr_blit) but for render passes without a
468    /// depth-stencil attachment. Use this when you create the blit render pass yourself
469    /// (e.g. winit) and omit the depth attachment.
470    pub(crate) fn paint_hdr_blit_no_ds<'rp>(
471        &self,
472        render_pass: &mut wgpu::RenderPass<'rp>,
473        frame: &FrameData,
474    ) {
475        let vp_idx = frame.camera.viewport_index;
476        if let Some(hc) = self
477            .viewport_slots
478            .get(vp_idx)
479            .and_then(|s| s.hdr_callback.as_ref())
480        {
481            if let Some(pipeline) = &self.resources.post.dyn_res_upscale_pipeline {
482                render_pass.set_pipeline(pipeline);
483                render_pass.set_bind_group(0, &hc.blit_bind_group, &[]);
484                render_pass.draw(0..3, 0..1);
485            }
486        }
487    }
488
489    /// Unified prepare step for the eframe `CallbackTrait::prepare` method.
490    ///
491    /// Replaces manual `prepare` + `prepare_ldr_dyn_res` or `prepare_hdr_callback`
492    /// calls. Dispatches internally based on `frame.effects.post_process.enabled`:
493    ///
494    /// - HDR path (`post_process.enabled = true`): runs the full HDR pipeline (OIT,
495    ///   EDL, tone-map) and returns the resulting `CommandBuffer` for eframe to
496    ///   submit before the egui render pass.
497    /// - LDR path: calls `prepare`, and if dynamic resolution is active, encodes the
498    ///   scene into a separate `CommandBuffer` (also submitted before the render
499    ///   pass). Returns an empty `Vec` when dyn-res is inactive.
500    ///
501    /// Call [`paint_callback`](Self::paint_callback) from `CallbackTrait::paint`.
502    pub(crate) fn prepare_callback(
503        &mut self,
504        device: &wgpu::Device,
505        queue: &wgpu::Queue,
506        frame: &FrameData,
507    ) -> Vec<wgpu::CommandBuffer> {
508        if frame.effects.post_process.enabled {
509            let cb = self.prepare_hdr_callback(device, queue, frame);
510            vec![cb]
511        } else {
512            self.prepare(device, queue, frame);
513            if self.current_render_scale < 1.0 - 0.001 {
514                let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
515                    label: Some("ldr_dyn_res_callback_encoder"),
516                });
517                self.prepare_ldr_dyn_res(&mut encoder, device, frame);
518                vec![encoder.finish()]
519            } else {
520                Vec::new()
521            }
522        }
523    }
524
525    /// Unified paint step for the eframe `CallbackTrait::paint` method.
526    ///
527    /// Call after [`prepare_callback`](Self::prepare_callback) for the same frame.
528    /// Dispatches internally to `paint_hdr_blit`, `paint_dyn_res_blit`, or `paint`
529    /// based on which path `prepare_callback` activated.
530    pub(crate) fn paint_callback<'rp>(
531        &self,
532        render_pass: &mut wgpu::RenderPass<'rp>,
533        frame: &FrameData,
534    ) {
535        let vp_idx = frame.camera.viewport_index;
536        if frame.effects.post_process.enabled {
537            if self
538                .viewport_slots
539                .get(vp_idx)
540                .and_then(|s| s.hdr_callback.as_ref())
541                .is_some()
542            {
543                self.paint_hdr_blit(render_pass, frame);
544                return;
545            }
546        }
547        if self.current_render_scale < 1.0 - 0.001
548            && self
549                .viewport_slots
550                .get(vp_idx)
551                .and_then(|s| s.dyn_res.as_ref())
552                .is_some()
553        {
554            self.paint_dyn_res_blit(render_pass, frame);
555        } else {
556            self.paint_to(render_pass, frame);
557        }
558    }
559
560    /// High-level HDR render for a single viewport identified by `id`.
561    ///
562    /// Unlike [`render`](Self::render), this method does **not** call
563    /// [`prepare`](Self::prepare) internally.  The caller must have already called
564    /// [`prepare_scene`](Self::prepare_scene) and
565    /// [`prepare_viewport`](Self::prepare_viewport) for `id` before invoking this.
566    ///
567    /// This is the right entry point for multi-viewport frames:
568    /// 1. Call `prepare_scene` once.
569    /// 2. Call `prepare_viewport` for each viewport.
570    /// 3. Call `render_viewport` for each viewport with its own `output_view`.
571    ///
572    /// Returns a [`wgpu::CommandBuffer`] ready to submit.
573    pub(crate) fn render_viewport(
574        &mut self,
575        device: &wgpu::Device,
576        queue: &wgpu::Queue,
577        output_view: &wgpu::TextureView,
578        id: ViewportId,
579        frame: &FrameData,
580    ) -> wgpu::CommandBuffer {
581        self.render_frame_internal(device, queue, output_view, id.0, frame)
582    }
583
584    /// High-level HDR render method. Handles the full post-processing pipeline:
585    /// scene -> HDR texture -> (bloom) -> (SSAO) -> tone map -> output_view.
586    ///
587    /// When `frame.post_process.enabled` is false, falls back to a simple LDR render
588    /// pass targeting `output_view` directly.
589    ///
590    /// Returns a `CommandBuffer` ready to submit.
591    pub(crate) fn render(
592        &mut self,
593        device: &wgpu::Device,
594        queue: &wgpu::Queue,
595        output_view: &wgpu::TextureView,
596        frame: &FrameData,
597    ) -> wgpu::CommandBuffer {
598        // Always run prepare() to upload uniforms and run the shadow pass.
599        self.prepare(device, queue, frame);
600        self.render_frame_internal(
601            device,
602            queue,
603            output_view,
604            frame.camera.viewport_index,
605            frame,
606        )
607    }
608
609    /// Render-only path shared by `render()` and `render_viewport()`.
610    ///
611    /// `vp_idx` selects the per-viewport slot to use for camera/HDR state,
612    /// independent of `frame.camera.viewport_index`.
613    fn render_frame_internal(
614        &mut self,
615        device: &wgpu::Device,
616        queue: &wgpu::Queue,
617        output_view: &wgpu::TextureView,
618        vp_idx: usize,
619        frame: &FrameData,
620    ) -> wgpu::CommandBuffer {
621        let paint_start = std::time::Instant::now();
622        // Take the LOD-resolved surfaces from prepare (level mesh chosen, culled
623        // items hidden), then extend with the boundary draws contributed by
624        // opaque volume meshes (see the matching construction in `prepare.rs`).
625        // Reading the resolved list rather than the raw `frame.scene.surfaces`
626        // is what carries the LOD swap and cull into the HDR scene pass.
627        let scene_items_owned: Vec<SceneRenderItem> = {
628            let extra = frame
629                .scene
630                .volume_meshes
631                .iter()
632                .filter(|item| item.transparency.is_none())
633                .map(|item| item.to_render_item());
634            self.prepared_surfaces
635                .iter()
636                .cloned()
637                .chain(extra)
638                .collect()
639        };
640        let scene_items: &[SceneRenderItem] = &scene_items_owned;
641
642        let bg_colour = frame.viewport.background_colour.unwrap_or([
643            65.0 / 255.0,
644            65.0 / 255.0,
645            65.0 / 255.0,
646            1.0,
647        ]);
648        let ppp = frame.camera.pixels_per_point;
649        let w = (frame.camera.viewport_size[0] * ppp).round() as u32;
650        let h = (frame.camera.viewport_size[1] * ppp).round() as u32;
651
652        // Ensure per-viewport HDR targets. Provides a depth buffer for both LDR and HDR paths.
653        let ssaa_factor = frame.effects.post_process.ssaa_factor.max(1);
654        self.ensure_viewport_hdr(
655            device,
656            queue,
657            vp_idx,
658            w.max(1),
659            h.max(1),
660            ssaa_factor,
661            self.current_render_scale,
662        );
663
664        // Lazy-initialize GPU timestamp resources on first render call when supported.
665        if self.ts_query_set.is_none()
666            && device.features().contains(wgpu::Features::TIMESTAMP_QUERY)
667        {
668            // One begin/end timestamp pair per measured pass.
669            let ts_count = 2 * super::GPU_TS_SLOTS;
670            let ts_bytes = ts_count as u64 * 8;
671            self.ts_query_set = Some(device.create_query_set(&wgpu::QuerySetDescriptor {
672                label: Some("ts_query_set"),
673                ty: wgpu::QueryType::Timestamp,
674                count: ts_count,
675            }));
676            self.ts_resolve_buf = Some(device.create_buffer(&wgpu::BufferDescriptor {
677                label: Some("ts_resolve_buf"),
678                size: ts_bytes,
679                usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
680                mapped_at_creation: false,
681            }));
682            self.ts_staging_buf = Some(device.create_buffer(&wgpu::BufferDescriptor {
683                label: Some("ts_staging_buf"),
684                size: ts_bytes,
685                usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
686                mapped_at_creation: false,
687            }));
688            self.ts_period = queue.get_timestamp_period();
689            // Gap-filling skipped slots before resolve needs encoder-level
690            // timestamp writes. Without it we fall back to resolving the whole
691            // range, which is undefined for unwritten queries on some drivers.
692            self.ts_can_fill_gaps = device
693                .features()
694                .contains(wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS);
695        }
696
697        let cmd_buf = if !frame.effects.post_process.enabled {
698            self.render_frame_ldr(device, queue, output_view, vp_idx, frame, bg_colour, w, h)
699        } else {
700            self.render_frame_hdr(
701                device,
702                queue,
703                output_view,
704                vp_idx,
705                frame,
706                scene_items,
707                bg_colour,
708                w,
709                h,
710                ssaa_factor,
711            )
712        };
713        // CPU time spent encoding the paint pass (draw-call recording), separate
714        // from prepare. Latched so last_frame_stats() reflects it after render.
715        self.last_stats.cpu_paint_ms = paint_start.elapsed().as_secs_f32() * 1000.0;
716        cmd_buf
717    }
718
719    /// Render a frame into `output_view` and submit it, without reading anything
720    /// back. `output_view` must be a `RENDER_ATTACHMENT` view in the format the
721    /// renderer was created with, sized to `frame.camera.viewport_size`.
722    ///
723    /// Unlike [`render_offscreen`](Self::render_offscreen), this neither copies
724    /// the result to the CPU nor blocks on the GPU, so the caller can reuse one
725    /// target and drive many frames back to back. Intended for headless loops
726    /// (perf measurement, capture pipelines) that read results back through GPU
727    /// timestamps or their own copy on a cadence rather than every frame.
728    pub fn render_to_texture(
729        &mut self,
730        device: &wgpu::Device,
731        queue: &wgpu::Queue,
732        output_view: &wgpu::TextureView,
733        frame: &FrameData,
734    ) {
735        let cmd_buf = self.render(device, queue, output_view, frame);
736        queue.submit(std::iter::once(cmd_buf));
737    }
738
739    /// Render a frame to an offscreen texture and return raw RGBA bytes.
740    ///
741    /// Creates a temporary [`wgpu::Texture`] render target of the given dimensions,
742    /// runs all render passes (shadow, scene, post-processing) into it via
743    /// [`render()`](Self::render), then copies the result back to CPU memory.
744    ///
745    /// No OS window or [`wgpu::Surface`] is required. The caller is responsible for
746    /// initialising the wgpu adapter with `compatible_surface: None` and for
747    /// constructing a valid [`FrameData`] (including `viewport_size` matching
748    /// `width`/`height`).
749    ///
750    /// Returns `width * height * 4` bytes in RGBA8 layout. The caller encodes to
751    /// PNG/EXR independently : no image codec dependency in this crate.
752    pub fn render_offscreen(
753        &mut self,
754        device: &wgpu::Device,
755        queue: &wgpu::Queue,
756        frame: &FrameData,
757        width: u32,
758        height: u32,
759    ) -> Vec<u8> {
760        // 1. Create offscreen texture with RENDER_ATTACHMENT | COPY_SRC usage.
761        let target_format = self.resources.target_format;
762        let offscreen_texture = device.create_texture(&wgpu::TextureDescriptor {
763            label: Some("offscreen_target"),
764            size: wgpu::Extent3d {
765                width: width.max(1),
766                height: height.max(1),
767                depth_or_array_layers: 1,
768            },
769            mip_level_count: 1,
770            sample_count: 1,
771            dimension: wgpu::TextureDimension::D2,
772            format: target_format,
773            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
774            view_formats: &[],
775        });
776
777        // 2. Create a texture view for rendering into.
778        let output_view = offscreen_texture.create_view(&wgpu::TextureViewDescriptor::default());
779
780        // 3. render() calls ensure_viewport_hdr which provides the depth-stencil buffer
781        //    for both LDR and HDR paths, so no separate ensure_outline_target is needed.
782
783        // 4. Render the scene into the offscreen texture.
784        //    The caller must set `frame.camera.viewport_size` to `[width as f32, height as f32]`
785        //    and `frame.camera.render_camera.aspect` to `width as f32 / height as f32`
786        //    for correct HDR target allocation and scissor rects.
787        let cmd_buf = self.render(device, queue, &output_view, frame);
788        queue.submit(std::iter::once(cmd_buf));
789
790        // 5. Copy texture -> staging buffer (wgpu requires row alignment to 256 bytes).
791        let bytes_per_pixel = 4u32;
792        let unpadded_row = width * bytes_per_pixel;
793        let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
794        let padded_row = (unpadded_row + align - 1) & !(align - 1);
795        let buffer_size = (padded_row * height.max(1)) as u64;
796
797        let staging_buf = device.create_buffer(&wgpu::BufferDescriptor {
798            label: Some("offscreen_staging"),
799            size: buffer_size,
800            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
801            mapped_at_creation: false,
802        });
803
804        let mut copy_encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
805            label: Some("offscreen_copy_encoder"),
806        });
807        copy_encoder.copy_texture_to_buffer(
808            wgpu::TexelCopyTextureInfo {
809                texture: &offscreen_texture,
810                mip_level: 0,
811                origin: wgpu::Origin3d::ZERO,
812                aspect: wgpu::TextureAspect::All,
813            },
814            wgpu::TexelCopyBufferInfo {
815                buffer: &staging_buf,
816                layout: wgpu::TexelCopyBufferLayout {
817                    offset: 0,
818                    bytes_per_row: Some(padded_row),
819                    rows_per_image: Some(height.max(1)),
820                },
821            },
822            wgpu::Extent3d {
823                width: width.max(1),
824                height: height.max(1),
825                depth_or_array_layers: 1,
826            },
827        );
828        queue.submit(std::iter::once(copy_encoder.finish()));
829
830        // 6. Map buffer and extract tightly-packed RGBA pixels.
831        let (tx, rx) = std::sync::mpsc::channel();
832        staging_buf
833            .slice(..)
834            .map_async(wgpu::MapMode::Read, move |result| {
835                let _ = tx.send(result);
836            });
837        device
838            .poll(wgpu::PollType::Wait {
839                submission_index: None,
840                timeout: Some(std::time::Duration::from_secs(5)),
841            })
842            .unwrap();
843        let _ = rx.recv().unwrap_or(Err(wgpu::BufferAsyncError));
844
845        let mut pixels: Vec<u8> = Vec::with_capacity((width * height * 4) as usize);
846        {
847            let mapped = staging_buf.slice(..).get_mapped_range();
848            let data: &[u8] = &mapped;
849            if padded_row == unpadded_row {
850                // No padding : copy entire slice directly.
851                pixels.extend_from_slice(data);
852            } else {
853                // Strip row padding.
854                for row in 0..height as usize {
855                    let start = row * padded_row as usize;
856                    let end = start + unpadded_row as usize;
857                    pixels.extend_from_slice(&data[start..end]);
858                }
859            }
860        }
861        staging_buf.unmap();
862
863        // 7. Swizzle BGRA -> RGBA if the format stores bytes in BGRA order.
864        let is_bgra = matches!(
865            target_format,
866            wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb
867        );
868        if is_bgra {
869            for pixel in pixels.chunks_exact_mut(4) {
870                pixel.swap(0, 2); // B <-> R
871            }
872        }
873
874        pixels
875    }
876
877    // ------------------------------------------------------------------
878    // Backdrop blur helpers
879    // ------------------------------------------------------------------
880
881    /// Ensure the backdrop blur state textures exist at the right size.
882    fn ensure_backdrop_blur_state(&mut self, device: &wgpu::Device, w: u32, h: u32) {
883        let need_recreate = match &self.backdrop_blur_state {
884            Some(s) => s.size != [w, h] || s.format != self.resources.target_format,
885            None => true,
886        };
887        if !need_recreate {
888            return;
889        }
890
891        let format = self.resources.target_format;
892        let blur_w = (w / 2).max(1);
893        let blur_h = (h / 2).max(1);
894
895        let intermediate_texture = device.create_texture(&wgpu::TextureDescriptor {
896            label: Some("backdrop_intermediate"),
897            size: wgpu::Extent3d {
898                width: w,
899                height: h,
900                depth_or_array_layers: 1,
901            },
902            mip_level_count: 1,
903            sample_count: 1,
904            dimension: wgpu::TextureDimension::D2,
905            format,
906            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
907            view_formats: &[],
908        });
909        let intermediate_view = intermediate_texture.create_view(&Default::default());
910
911        let make_blur_tex = |label: &str| {
912            let t = device.create_texture(&wgpu::TextureDescriptor {
913                label: Some(label),
914                size: wgpu::Extent3d {
915                    width: blur_w,
916                    height: blur_h,
917                    depth_or_array_layers: 1,
918                },
919                mip_level_count: 1,
920                sample_count: 1,
921                dimension: wgpu::TextureDimension::D2,
922                format,
923                usage: wgpu::TextureUsages::RENDER_ATTACHMENT
924                    | wgpu::TextureUsages::TEXTURE_BINDING,
925                view_formats: &[],
926            });
927            let v = t.create_view(&Default::default());
928            (t, v)
929        };
930        let (blur_a_texture, blur_a_view) = make_blur_tex("backdrop_blur_a");
931        let (blur_b_texture, blur_b_view) = make_blur_tex("backdrop_blur_b");
932
933        self.backdrop_blur_state = Some(crate::resources::BackdropBlurState {
934            intermediate_texture,
935            intermediate_view,
936            blur_a_texture,
937            blur_a_view,
938            blur_b_texture,
939            blur_b_view,
940            size: [w, h],
941            format,
942        });
943    }
944
945    /// Run the backdrop blur pipeline: blit scene to half-res, then H blur, then V blur.
946    /// Returns the bind group that can be used to draw blur overlay shapes with the
947    /// texture pipeline.
948    fn run_backdrop_blur(
949        &self,
950        encoder: &mut wgpu::CommandEncoder,
951        device: &wgpu::Device,
952        _queue: &wgpu::Queue,
953        source_view: &wgpu::TextureView,
954        spread: f32,
955    ) -> wgpu::BindGroup {
956        let bs = self.backdrop_blur_state.as_ref().unwrap();
957        let blur_bgl = self.resources.backdrop_blur.bgl.as_ref().unwrap();
958        let blur_sampler = self.resources.backdrop_blur.sampler.as_ref().unwrap();
959        let blur_pipeline = self.resources.backdrop_blur.pipeline.as_ref().unwrap();
960        // Reuse dyn_res blit pipeline and BGL for the downsample pass.
961        let blit_pipeline = self
962            .resources
963            .post
964            .dyn_res_upscale_pipeline
965            .as_ref()
966            .unwrap();
967        let blit_bgl = self.resources.post.dyn_res_upscale_bgl.as_ref().unwrap();
968        let blit_sampler = self.resources.post.dyn_res_linear_sampler.as_ref().unwrap();
969
970        // Step 1: downsample source -> blur_a (half-res) using bilinear blit.
971        let downsample_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
972            label: Some("backdrop_downsample_bg"),
973            layout: blit_bgl,
974            entries: &[
975                wgpu::BindGroupEntry {
976                    binding: 0,
977                    resource: wgpu::BindingResource::TextureView(source_view),
978                },
979                wgpu::BindGroupEntry {
980                    binding: 1,
981                    resource: wgpu::BindingResource::Sampler(blit_sampler),
982                },
983            ],
984        });
985        {
986            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
987                label: Some("backdrop_downsample"),
988                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
989                    view: &bs.blur_a_view,
990                    resolve_target: None,
991                    ops: wgpu::Operations {
992                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
993                        store: wgpu::StoreOp::Store,
994                    },
995                    depth_slice: None,
996                })],
997                depth_stencil_attachment: None,
998                timestamp_writes: None,
999                occlusion_query_set: None,
1000            });
1001            pass.set_pipeline(blit_pipeline);
1002            pass.set_bind_group(0, &downsample_bg, &[]);
1003            pass.draw(0..3, 0..1);
1004        }
1005
1006        // Spread scaled for half-res: each texel covers 2 screen pixels.
1007        let effective_spread = (spread / 2.0).max(1.0);
1008
1009        // Step 2: horizontal blur: blur_a -> blur_b.
1010        let h_uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1011            label: Some("blur_h_uniform"),
1012            contents: bytemuck::cast_slice(&[1u32, effective_spread.to_bits(), 0u32, 0u32]),
1013            usage: wgpu::BufferUsages::UNIFORM,
1014        });
1015        let h_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1016            label: Some("blur_h_bg"),
1017            layout: blur_bgl,
1018            entries: &[
1019                wgpu::BindGroupEntry {
1020                    binding: 0,
1021                    resource: wgpu::BindingResource::TextureView(&bs.blur_a_view),
1022                },
1023                wgpu::BindGroupEntry {
1024                    binding: 1,
1025                    resource: wgpu::BindingResource::Sampler(blur_sampler),
1026                },
1027                wgpu::BindGroupEntry {
1028                    binding: 2,
1029                    resource: h_uniform.as_entire_binding(),
1030                },
1031            ],
1032        });
1033        {
1034            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1035                label: Some("backdrop_blur_h"),
1036                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1037                    view: &bs.blur_b_view,
1038                    resolve_target: None,
1039                    ops: wgpu::Operations {
1040                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1041                        store: wgpu::StoreOp::Store,
1042                    },
1043                    depth_slice: None,
1044                })],
1045                depth_stencil_attachment: None,
1046                timestamp_writes: None,
1047                occlusion_query_set: None,
1048            });
1049            pass.set_pipeline(blur_pipeline);
1050            pass.set_bind_group(0, &h_bg, &[]);
1051            pass.draw(0..3, 0..1);
1052        }
1053
1054        // Step 3: vertical blur: blur_b -> blur_a.
1055        let v_uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1056            label: Some("blur_v_uniform"),
1057            contents: bytemuck::cast_slice(&[0u32, effective_spread.to_bits(), 0u32, 0u32]),
1058            usage: wgpu::BufferUsages::UNIFORM,
1059        });
1060        let v_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1061            label: Some("blur_v_bg"),
1062            layout: blur_bgl,
1063            entries: &[
1064                wgpu::BindGroupEntry {
1065                    binding: 0,
1066                    resource: wgpu::BindingResource::TextureView(&bs.blur_b_view),
1067                },
1068                wgpu::BindGroupEntry {
1069                    binding: 1,
1070                    resource: wgpu::BindingResource::Sampler(blur_sampler),
1071                },
1072                wgpu::BindGroupEntry {
1073                    binding: 2,
1074                    resource: v_uniform.as_entire_binding(),
1075                },
1076            ],
1077        });
1078        {
1079            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1080                label: Some("backdrop_blur_v"),
1081                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1082                    view: &bs.blur_a_view,
1083                    resolve_target: None,
1084                    ops: wgpu::Operations {
1085                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1086                        store: wgpu::StoreOp::Store,
1087                    },
1088                    depth_slice: None,
1089                })],
1090                depth_stencil_attachment: None,
1091                timestamp_writes: None,
1092                occlusion_query_set: None,
1093            });
1094            pass.set_pipeline(blur_pipeline);
1095            pass.set_bind_group(0, &v_bg, &[]);
1096            pass.draw(0..3, 0..1);
1097        }
1098
1099        // Build the bind group for overlay shape drawing. Uses the overlay_shape_tex
1100        // bind group layout (texture + sampler) so blur shapes can be drawn with the
1101        // existing texture pipeline.
1102        let tex_bgl = self.resources.overlay_shape.tex_bgl.as_ref().unwrap();
1103        let tex_sampler = self.resources.overlay_shape.tex_sampler.as_ref().unwrap();
1104        device.create_bind_group(&wgpu::BindGroupDescriptor {
1105            label: Some("backdrop_blur_overlay_bg"),
1106            layout: tex_bgl,
1107            entries: &[
1108                wgpu::BindGroupEntry {
1109                    binding: 0,
1110                    resource: wgpu::BindingResource::TextureView(&bs.blur_a_view),
1111                },
1112                wgpu::BindGroupEntry {
1113                    binding: 1,
1114                    resource: wgpu::BindingResource::Sampler(tex_sampler),
1115                },
1116            ],
1117        })
1118    }
1119
1120    /// Returns true if the current frame has overlay shapes that need backdrop blur.
1121    fn has_backdrop_blur_shapes(&self) -> bool {
1122        self.overlay_shape_gpu_data
1123            .as_ref()
1124            .map_or(false, |sd| sd.blur_vertex_count > 0)
1125    }
1126
1127    /// Draw blur overlay shapes into the given render pass using the texture pipeline.
1128    fn draw_blur_shapes<'rp>(
1129        &'rp self,
1130        render_pass: &mut wgpu::RenderPass<'rp>,
1131        blur_bind_group: &'rp wgpu::BindGroup,
1132    ) {
1133        if let Some(ref sd) = self.overlay_shape_gpu_data {
1134            if sd.blur_vertex_count > 0 {
1135                if let (Some(pipeline), Some(vbuf)) = (
1136                    &self.resources.overlay_shape.tex_pipeline,
1137                    &sd.blur_vertex_buf,
1138                ) {
1139                    render_pass.set_pipeline(pipeline);
1140                    render_pass.set_bind_group(0, blur_bind_group, &[]);
1141                    render_pass.set_vertex_buffer(0, vbuf.slice(..));
1142                    render_pass.draw(0..sd.blur_vertex_count, 0..1);
1143                }
1144            }
1145        }
1146    }
1147}