Skip to main content

myth_render/
renderer.rs

1//! Rendering System
2//!
3//! The main [`Renderer`] struct orchestrating GPU rendering operations.
4
5use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
6
7use crate::core::binding::GlobalBindGroupCache;
8use crate::graph::composer::ComposerContext;
9use crate::graph::core::allocator::TransientPool;
10use crate::graph::core::arena::FrameArena;
11use crate::graph::core::graph::GraphStorage;
12use crate::graph::frame::RenderLists;
13#[cfg(feature = "debug_view")]
14use crate::graph::passes::DebugViewFeature;
15use crate::graph::passes::{
16    BloomFeature, BrdfLutFeature, CasFeature, FxaaFeature, IblComputeFeature, MsaaSyncFeature,
17    OpaqueFeature, PrepassFeature, ShadowFeature, SimpleForwardFeature, SkyboxFeature, SsaoFeature,
18    SsssFeature, TaaFeature, ToneMappingFeature, TransmissionCopyFeature, TransparentFeature,
19};
20use myth_assets::AssetServer;
21use myth_core::Result;
22use myth_scene::Scene;
23use myth_scene::camera::RenderCamera;
24
25use crate::core::{ResourceManager, WgpuContext};
26use crate::graph::{FrameComposer, RenderFrame};
27use crate::pipeline::PipelineCache;
28use crate::pipeline::ShaderManager;
29use crate::settings::{RenderPath, RendererInitConfig, RendererSettings};
30
31/// The main renderer responsible for GPU rendering operations.
32///
33/// The renderer manages the complete rendering pipeline including:
34/// - GPU context (device, queue, surface)
35/// - Resource management (buffers, textures, bind groups)
36/// - Pipeline caching (shader compilation, PSO creation)
37/// - Frame rendering (scene extraction, command submission)
38///
39/// # Lifecycle
40///
41/// 1. Create with [`Renderer::new`] (no GPU resources allocated)
42/// 2. Initialize GPU with [`Renderer::init`]
43/// 3. Render frames with [`Renderer::begin_frame`]
44/// 4. Clean up with [`Renderer::maybe_prune`]
45pub struct Renderer {
46    size: (u32, u32),
47    init_config: RendererInitConfig,
48    settings: RendererSettings,
49    context: Option<RendererState>,
50}
51
52/// Internal renderer state
53struct RendererState {
54    wgpu_ctx: WgpuContext,
55    resource_manager: ResourceManager,
56    pipeline_cache: PipelineCache,
57    shader_manager: ShaderManager,
58
59    render_frame: RenderFrame,
60    /// Render lists (separated from `render_frame` to avoid borrow conflicts)
61    render_lists: RenderLists,
62    // /// Frame blackboard (cross-pass transient data communication, cleared each frame)
63    // blackboard: FrameBlackboard,
64    global_bind_group_cache: GlobalBindGroupCache,
65
66    // ===== RDG (Declarative Render Graph) =====
67    pub(crate) graph_storage: GraphStorage,
68    // pub(crate) sampler_registry: SamplerRegistry,
69    pub(crate) transient_pool: TransientPool,
70    pub(crate) frame_arena: FrameArena,
71
72    // Post-processing passes
73    pub(crate) fxaa_pass: FxaaFeature,
74    pub(crate) taa_pass: TaaFeature,
75    pub(crate) cas_pass: CasFeature,
76    pub(crate) tone_map_pass: ToneMappingFeature,
77    pub(crate) bloom_pass: BloomFeature,
78    pub(crate) ssao_pass: SsaoFeature,
79
80    // Scene rendering passes
81    pub(crate) prepass: PrepassFeature,
82    pub(crate) opaque_pass: OpaqueFeature,
83    pub(crate) skybox_pass: SkyboxFeature,
84    pub(crate) transparent_pass: TransparentFeature,
85    pub(crate) transmission_copy_pass: TransmissionCopyFeature,
86    pub(crate) simple_forward_pass: SimpleForwardFeature,
87    pub(crate) ssss_pass: SsssFeature,
88    pub(crate) msaa_sync_pass: MsaaSyncFeature,
89
90    // Shadow + Compute passes (migrated from old system)
91    pub(crate) shadow_pass: ShadowFeature,
92    pub(crate) brdf_pass: BrdfLutFeature,
93    pub(crate) ibl_pass: IblComputeFeature,
94
95    // Debug view (compile-time gated)
96    #[cfg(feature = "debug_view")]
97    pub(crate) debug_view_pass: DebugViewFeature,
98}
99
100#[derive(Debug, Clone, Copy, Default)]
101pub struct FrameTime {
102    pub time: f32,
103    pub delta_time: f32,
104    pub frame_count: u64,
105}
106
107impl Renderer {
108    /// Phase 1: Create configuration (no GPU resources yet).
109    ///
110    /// This only stores the render settings. GPU resources are
111    /// allocated when [`init`](Self::init) is called.
112    ///
113    /// # Arguments
114    ///
115    /// * `init_config` - Static GPU/device configuration (consumed at init time)
116    /// * `settings` - Runtime rendering settings (can be changed later via [`update_settings`](Self::update_settings))
117    #[must_use]
118    pub fn new(init_config: RendererInitConfig, settings: RendererSettings) -> Self {
119        Self {
120            init_config,
121            settings,
122            context: None,
123            size: (0, 0),
124        }
125    }
126
127    /// Returns the current surface size in pixels as `(width, height)`.
128    #[inline]
129    #[must_use]
130    pub fn size(&self) -> (u32, u32) {
131        self.size
132    }
133
134    /// Phase 2: Initialize GPU context with window handle.
135    ///
136    /// This method:
137    /// 1. Creates the wgpu instance and adapter
138    /// 2. Requests a device with required features/limits
139    /// 3. Configures the surface for presentation
140    /// 4. Initializes resource manager and pipeline cache
141    pub async fn init<W>(&mut self, window: W, width: u32, height: u32) -> Result<()>
142    where
143        W: HasWindowHandle + HasDisplayHandle + Send + Sync + 'static,
144    {
145        if self.context.is_some() {
146            return Ok(());
147        }
148
149        self.size = (width, height);
150
151        // 1. Create WGPU context
152        let wgpu_ctx =
153            WgpuContext::new(window, &self.init_config, &self.settings, width, height).await?;
154
155        // 2. Initialize resource manager
156        let resource_manager = ResourceManager::new(
157            wgpu_ctx.device.clone(),
158            wgpu_ctx.queue.clone(),
159            self.settings.anisotropy_clamp,
160        );
161
162        // 3. Create render frame manager
163        let render_frame = RenderFrame::new();
164
165        // 5. Create global bind group cache
166        let global_bind_group_cache = GlobalBindGroupCache::new();
167
168        // let sampler_registry = SamplerRegistry::new(&wgpu_ctx.device);
169
170        // Shadow + compute passes (need device ref before wgpu_ctx moves)
171        let shadow_pass = ShadowFeature::new(&wgpu_ctx.device);
172        let brdf_pass = BrdfLutFeature::new(&wgpu_ctx.device);
173        let ibl_pass = IblComputeFeature::new(&wgpu_ctx.device);
174
175        // 6. Assemble state
176        self.context = Some(RendererState {
177            wgpu_ctx,
178            resource_manager,
179            pipeline_cache: PipelineCache::new(),
180            shader_manager: ShaderManager::new(),
181
182            render_frame,
183            render_lists: RenderLists::new(),
184            global_bind_group_cache,
185
186            // RDG
187            graph_storage: GraphStorage::new(),
188            // sampler_registry,
189            transient_pool: TransientPool::new(),
190            frame_arena: FrameArena::new(),
191            fxaa_pass: FxaaFeature::new(),
192            taa_pass: TaaFeature::new(),
193            cas_pass: CasFeature::new(),
194            tone_map_pass: ToneMappingFeature::new(),
195            bloom_pass: BloomFeature::new(),
196            ssao_pass: SsaoFeature::new(),
197
198            // RDG Scene Passes
199            prepass: PrepassFeature::new(),
200            opaque_pass: OpaqueFeature::new(),
201            skybox_pass: SkyboxFeature::new(),
202            transparent_pass: TransparentFeature::new(),
203            transmission_copy_pass: TransmissionCopyFeature::new(),
204            simple_forward_pass: SimpleForwardFeature::new(),
205            ssss_pass: SsssFeature::new(),
206            msaa_sync_pass: MsaaSyncFeature::new(),
207
208            // Shadow + Compute passes (migrated from old system)
209            shadow_pass,
210            brdf_pass,
211            ibl_pass,
212
213            #[cfg(feature = "debug_view")]
214            debug_view_pass: DebugViewFeature::new(),
215        });
216
217        log::info!("Renderer Initialized");
218        Ok(())
219    }
220
221    pub fn resize(&mut self, width: u32, height: u32) {
222        self.size = (width, height);
223        if let Some(state) = &mut self.context {
224            state.wgpu_ctx.resize(width, height);
225            // Invalidate all cached bind groups — texture views are now stale.
226            state.global_bind_group_cache.clear();
227        }
228    }
229
230    /// Begins building a new frame for rendering.
231    ///
232    /// Returns a [`FrameComposer`] that provides a chainable API for
233    /// configuring the render pipeline via custom pass hooks.
234    ///
235    /// # Usage
236    ///
237    /// ```rust,ignore
238    /// // Method 1: Use default built-in passes
239    /// if let Some(composer) = renderer.begin_frame(scene, camera, assets, time) {
240    ///     composer.render();
241    /// }
242    ///
243    /// // Method 2: With custom hooks (e.g., UI overlay)
244    /// if let Some(composer) = renderer.begin_frame(scene, camera, assets, time) {
245    ///     composer
246    ///         .add_custom_pass(HookStage::AfterPostProcess, |graph, bb| {
247    ///             ui_pass.target_tex = bb.surface_out;
248    ///             graph.add_pass(&mut ui_pass);
249    ///         })
250    ///         .render();
251    /// }
252    /// ```
253    ///
254    /// # Returns
255    ///
256    /// Returns `Some(FrameComposer)` if frame preparation succeeds,
257    /// or `None` if rendering should be skipped (e.g., window size is 0).
258    pub fn begin_frame<'a>(
259        &'a mut self,
260        scene: &'a mut Scene,
261        camera: &'a RenderCamera,
262        assets: &'a AssetServer,
263        frame_time: FrameTime,
264    ) -> Option<FrameComposer<'a>> {
265        if self.size.0 == 0 || self.size.1 == 0 {
266            return None;
267        }
268
269        let state = self.context.as_mut()?;
270
271        // ── Frame Arena Lifecycle ───────────────────────────────────────
272        // Reset the arena in O(1) — all previous PassNodes are trivially
273        // forgotten (no Drop needed).
274        state.frame_arena.reset();
275
276        // Advance the bind-group cache's frame counter for TTL tracking.
277        state.global_bind_group_cache.begin_frame();
278
279        // ── Phase 1: Extract scene, build shadow views, prepare global ──
280        let surface_size = state.wgpu_ctx.size();
281        state.render_frame.extract_and_prepare(
282            &mut state.resource_manager,
283            scene,
284            camera,
285            assets,
286            frame_time,
287            &mut state.render_lists,
288            surface_size,
289        );
290
291        let requested_msaa = camera.aa_mode.msaa_sample_count();
292        if state.wgpu_ctx.msaa_samples != requested_msaa {
293            state.wgpu_ctx.msaa_samples = requested_msaa;
294            state.wgpu_ctx.pipeline_settings_version += 1;
295        }
296
297        // ── Phase 2: Cull + sort + command generation ───────────────────
298        crate::graph::culling::cull_and_sort(
299            &state.render_frame.extracted_scene,
300            &state.render_frame.render_state,
301            &state.wgpu_ctx,
302            &mut state.resource_manager,
303            &mut state.pipeline_cache,
304            &mut state.shader_manager,
305            &mut state.render_lists,
306            camera,
307            assets,
308        );
309
310        // ── Phase 2.5: Feature extract & prepare ────────────────────────
311        //
312        // Resolve persistent GPU resources (pipelines, layouts, bind groups)
313        // BEFORE the render graph is built. This ensures all Features are
314        // fully prepared when their ephemeral PassNodes are created.
315        {
316            use crate::HDR_TEXTURE_FORMAT;
317            use crate::graph::core::context::ExtractContext;
318
319            let view_format = state.wgpu_ctx.surface_view_format;
320            let is_hf = state.wgpu_ctx.render_path.supports_post_processing();
321            let scene_id_val = scene.id();
322            let render_state_id = state.render_frame.render_state.id;
323            let global_state_key = (render_state_id, scene_id_val);
324
325            let ssao_enabled = scene.ssao.enabled && is_hf;
326            let needs_feature_id =
327                is_hf && (scene.screen_space.enable_sss || scene.screen_space.enable_ssr);
328            let needs_normal = ssao_enabled || needs_feature_id;
329            let needs_skybox = scene.background.needs_skybox_pass();
330            let bloom_enabled = scene.bloom.enabled && is_hf;
331
332            let mut extract_ctx = ExtractContext {
333                device: &state.wgpu_ctx.device,
334                queue: &state.wgpu_ctx.queue,
335                pipeline_cache: &mut state.pipeline_cache,
336                shader_manager: &mut state.shader_manager,
337                global_bind_group_cache: &mut state.global_bind_group_cache,
338                resource_manager: &mut state.resource_manager,
339                wgpu_ctx: &state.wgpu_ctx,
340                render_lists: &mut state.render_lists,
341                extracted_scene: &state.render_frame.extracted_scene,
342                render_state: &state.render_frame.render_state,
343                render_camera: camera,
344                assets,
345            };
346
347            // Always: compute + shadow
348            state.brdf_pass.extract_and_prepare(&mut extract_ctx);
349            state.ibl_pass.extract_and_prepare(&mut extract_ctx);
350            state.shadow_pass.extract_and_prepare(&mut extract_ctx);
351
352            // Skybox (both pipelines)
353            if needs_skybox {
354                let color_format = if is_hf {
355                    HDR_TEXTURE_FORMAT
356                } else {
357                    view_format
358                };
359                state.skybox_pass.extract_and_prepare(
360                    &mut extract_ctx,
361                    &scene.background.mode,
362                    &scene.background.uniforms,
363                    global_state_key,
364                    color_format,
365                );
366            }
367
368            if is_hf {
369                if let Some(taa_settins) = camera.aa_mode.taa_settings() {
370                    state.taa_pass.extract_and_prepare(
371                        &mut extract_ctx,
372                        taa_settins.feedback_weight,
373                        self.size,
374                        HDR_TEXTURE_FORMAT,
375                    );
376
377                    if taa_settins.sharpen_intensity > 0.0 {
378                        state.cas_pass.extract_and_prepare(
379                            &mut extract_ctx,
380                            taa_settins.sharpen_intensity,
381                            HDR_TEXTURE_FORMAT,
382                        );
383                    }
384                }
385
386                if let Some(fxaa_settings) = camera.aa_mode.fxaa_settings() {
387                    state.fxaa_pass.target_quality = fxaa_settings.quality();
388                    state
389                        .fxaa_pass
390                        .extract_and_prepare(&mut extract_ctx, view_format);
391                }
392
393                state.prepass.extract_and_prepare(
394                    &mut extract_ctx,
395                    needs_normal,
396                    needs_feature_id,
397                    camera.aa_mode.is_taa(),
398                );
399
400                if ssao_enabled {
401                    state
402                        .ssao_pass
403                        .extract_and_prepare(&mut extract_ctx, &scene.ssao.uniforms);
404                }
405
406                state.ssss_pass.extract_and_prepare(&mut extract_ctx);
407
408                // MSAA Sync — needed when SSSS modifies the resolved HDR
409                // buffer and subsequent passes re-enter the MSAA context.
410                let msaa = state.wgpu_ctx.msaa_samples;
411                let needs_specular = scene.screen_space.enable_sss;
412                if msaa > 1 && needs_specular {
413                    state
414                        .msaa_sync_pass
415                        .extract_and_prepare(&mut extract_ctx, msaa);
416                }
417
418                if bloom_enabled {
419                    state.bloom_pass.extract_and_prepare(
420                        &mut extract_ctx,
421                        &scene.bloom.upsample_uniforms,
422                        &scene.bloom.composite_uniforms,
423                    );
424                }
425
426                state.tone_map_pass.extract_and_prepare(
427                    &mut extract_ctx,
428                    scene.tone_mapping.mode,
429                    view_format,
430                    global_state_key,
431                    &scene.tone_mapping.uniforms,
432                    scene.tone_mapping.lut_texture,
433                );
434
435                // Debug View — prepare pipeline & uniforms when active
436                #[cfg(feature = "debug_view")]
437                {
438                    use crate::graph::render_state::DebugViewTarget;
439                    let target = state.render_frame.render_state.debug_view_target;
440                    if target != DebugViewTarget::None {
441                        state.debug_view_pass.extract_and_prepare(
442                            &mut extract_ctx,
443                            view_format,
444                            target.view_mode(),
445                        );
446                    }
447                }
448            }
449        }
450
451        // ── Phase 3: Build ComposerContext ──────────────────────────────
452        let ctx = ComposerContext {
453            wgpu_ctx: &mut state.wgpu_ctx,
454            resource_manager: &mut state.resource_manager,
455            pipeline_cache: &mut state.pipeline_cache,
456            shader_manager: &mut state.shader_manager,
457
458            extracted_scene: &state.render_frame.extracted_scene,
459            render_state: &state.render_frame.render_state,
460
461            global_bind_group_cache: &mut state.global_bind_group_cache,
462
463            render_lists: &mut state.render_lists,
464
465            // blackboard: &mut state.blackboard,
466            scene,
467            camera,
468            assets,
469            frame_time,
470
471            graph_storage: &mut state.graph_storage,
472            transient_pool: &mut state.transient_pool,
473            // sampler_registry: &mut state.sampler_registry,
474            frame_arena: &state.frame_arena,
475            fxaa_pass: &mut state.fxaa_pass,
476            taa_pass: &mut state.taa_pass,
477            cas_pass: &mut state.cas_pass,
478            tone_map_pass: &mut state.tone_map_pass,
479            bloom_pass: &mut state.bloom_pass,
480            ssao_pass: &mut state.ssao_pass,
481
482            prepass: &mut state.prepass,
483            opaque_pass: &mut state.opaque_pass,
484            skybox_pass: &mut state.skybox_pass,
485            transparent_pass: &mut state.transparent_pass,
486            transmission_copy_pass: &mut state.transmission_copy_pass,
487            simple_forward_pass: &mut state.simple_forward_pass,
488            ssss_pass: &mut state.ssss_pass,
489            msaa_sync_pass: &mut state.msaa_sync_pass,
490
491            shadow_pass: &mut state.shadow_pass,
492            brdf_pass: &mut state.brdf_pass,
493            ibl_pass: &mut state.ibl_pass,
494
495            #[cfg(feature = "debug_view")]
496            debug_view_pass: &mut state.debug_view_pass,
497        };
498
499        // Return FrameComposer, defer Surface acquisition to render() call
500        Some(FrameComposer::new(ctx, self.size))
501    }
502
503    /// Performs periodic resource cleanup.
504    ///
505    /// Should be called after each frame to release unused GPU resources.
506    /// Uses internal heuristics to avoid expensive cleanup every frame.
507    pub fn maybe_prune(&mut self) {
508        if let Some(state) = &mut self.context {
509            state.render_frame.maybe_prune(&mut state.resource_manager);
510            // Evict stale bind groups that haven't been touched recently.
511            state.global_bind_group_cache.garbage_collect();
512        }
513    }
514
515    // === Runtime Settings API ===
516
517    /// Returns the current [`RenderPath`].
518    #[inline]
519    pub fn render_path(&self) -> &RenderPath {
520        &self.settings.path
521    }
522
523    /// Returns a reference to the current runtime renderer settings.
524    #[inline]
525    pub fn settings(&self) -> &RendererSettings {
526        &self.settings
527    }
528
529    /// Returns a reference to the init-time configuration.
530    #[inline]
531    pub fn init_config(&self) -> &RendererInitConfig {
532        &self.init_config
533    }
534
535    /// Applies new runtime settings, performing an internal diff to update
536    /// only the parts that actually changed.
537    ///
538    /// This is the **single entry point** for all runtime configuration
539    /// changes. Callers (UI panels, scripting layers, etc.) should maintain
540    /// their own [`RendererSettings`] instance, mutate it, and pass it here.
541    pub fn update_settings(&mut self, new_settings: RendererSettings) {
542        if self.settings == new_settings {
543            return;
544        }
545
546        let old = std::mem::replace(&mut self.settings, new_settings);
547
548        if let Some(state) = &mut self.context {
549            // VSync
550            if old.vsync != self.settings.vsync {
551                state.wgpu_ctx.set_vsync(self.settings.vsync);
552            }
553
554            // Render path
555            if old.path != self.settings.path {
556                state.wgpu_ctx.render_path = self.settings.path;
557                state.wgpu_ctx.pipeline_settings_version += 1;
558                log::info!("RenderPath changed to {:?}", self.settings.path);
559            }
560
561            // Anisotropy
562            if old.anisotropy_clamp != self.settings.anisotropy_clamp {
563                state
564                    .resource_manager
565                    .sampler_registry
566                    .set_global_anisotropy(self.settings.anisotropy_clamp);
567                log::info!(
568                    "Anisotropy clamp changed to {}",
569                    self.settings.anisotropy_clamp
570                );
571            }
572        }
573    }
574
575    /// Switches the active render path at runtime.
576    ///
577    /// Convenience wrapper around [`update_settings`](Self::update_settings)
578    /// for changing only the render path.
579    pub fn set_render_path(&mut self, path: RenderPath) {
580        if self.settings.path != path {
581            let mut new = self.settings.clone();
582            new.path = path;
583            self.update_settings(new);
584        }
585    }
586
587    /// Sets the active debug view target.
588    ///
589    /// When set to anything other than `None`, the FrameComposer will
590    /// replace the post-process output with a fullscreen visualisation
591    /// of the selected intermediate texture (if available).
592    #[cfg(feature = "debug_view")]
593    pub fn set_debug_view_target(&mut self, target: crate::graph::render_state::DebugViewTarget) {
594        if let Some(state) = &mut self.context {
595            state.render_frame.render_state.debug_view_target = target;
596        }
597    }
598
599    /// Returns the current debug view target.
600    #[cfg(feature = "debug_view")]
601    pub fn debug_view_target(&self) -> crate::graph::render_state::DebugViewTarget {
602        self.context
603            .as_ref()
604            .map(|s| s.render_frame.render_state.debug_view_target)
605            .unwrap_or_default()
606    }
607
608    // === Public Methods: For External Plugins (e.g., UI Pass) ===
609
610    /// Returns a reference to the wgpu Device.
611    ///
612    /// Useful for external plugins to initialize GPU resources.
613    pub fn device(&self) -> Option<&wgpu::Device> {
614        self.context.as_ref().map(|s| &s.wgpu_ctx.device)
615    }
616
617    /// Returns a reference to the wgpu Queue.
618    ///
619    /// Useful for external plugins to submit commands.
620    pub fn queue(&self) -> Option<&wgpu::Queue> {
621        self.context.as_ref().map(|s| &s.wgpu_ctx.queue)
622    }
623
624    /// Returns the surface texture format.
625    ///
626    /// Useful for external plugins to configure render pipelines.
627    pub fn surface_format(&self) -> Option<wgpu::TextureFormat> {
628        self.context.as_ref().map(|s| s.wgpu_ctx.config.format)
629    }
630
631    /// Returns a reference to the `WgpuContext`.
632    ///
633    /// For external plugins that need access to low-level GPU resources.
634    /// Only available after renderer initialization.
635    pub fn wgpu_ctx(&self) -> Option<&WgpuContext> {
636        self.context.as_ref().map(|s| &s.wgpu_ctx)
637    }
638
639    pub fn dump_graph_mermaid(&self) -> Option<String> {
640        self.context
641            .as_ref()
642            .map(|s| s.graph_storage.dump_mermaid())
643    }
644}