Skip to main content

myth_render/core/gpu/
environment.rs

1use wgpu::TextureViewDimension;
2
3use myth_assets::AssetServer;
4use myth_resources::texture::TextureSource;
5
6use crate::core::gpu::ResourceState;
7
8use super::{ResourceManager, generate_gpu_resource_id};
9
10const EQUIRECT_CUBE_SIZE: u32 = 1024;
11pub const PMREM_SIZE: u32 = 512;
12pub const BRDF_LUT_SIZE: u32 = 128;
13
14/// How the environment source needs to be processed by `IBLComputePass`.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum CubeSourceType {
17    /// 2D equirectangular HDR → `equirect_to_cube` + `mipmap_gen` + PMREM
18    Equirectangular,
19    /// Cube map without mipmaps → blit to owned cube + `mipmap_gen` + PMREM
20    CubeNoMipmaps,
21    /// Cube map with mipmaps → PMREM only (uses source cube directly)
22    CubeWithMipmaps,
23}
24
25/// GPU-side environment map resources.
26///
27/// Created during `resolve_gpu_environment` (prepare phase) and written by
28/// `IBLComputePass` (compute phase). The pass only writes into pre-created
29/// textures; it never creates or removes cache entries.
30#[derive(Debug)]
31pub struct GpuEnvironment {
32    /// Version of the source texture when this entry was last (re)created
33    pub source_version: u32,
34    /// Whether compute pass needs to (re)generate the textures
35    pub needs_compute: bool,
36    /// How the source needs to be processed
37    pub source_type: CubeSourceType,
38    /// Cube texture (owned; created when source is 2D or cube without mipmaps)
39    pub cube_texture: Option<wgpu::Texture>,
40    /// PMREM texture (always owned)
41    pub pmrem_texture: wgpu::Texture,
42    /// Resource ID for the cube view registered in `internal_resources`
43    pub cube_view_id: u64,
44    /// Resource ID for the PMREM view registered in `internal_resources`
45    pub pmrem_view_id: u64,
46    /// Maximum mip level (`mip_levels - 1`) for roughness LOD
47    pub env_map_max_mip_level: f32,
48}
49
50impl ResourceManager {
51    /// Resolve (or create) the `GpuEnvironment` for the current scene environment.
52    ///
53    /// This must be called before `prepare_global` so that the uniform buffer
54    /// can be populated with the correct `env_map_max_mip_level`, and so that
55    /// real resource IDs are available for `BindGroup` creation.
56    ///
57    /// All GPU textures (cube, PMREM) are created here; `IBLComputePass` only
58    /// writes into them — it never creates or removes cache entries.
59    ///
60    /// Returns the resolved `env_map_max_mip_level` (0.0 if no env map).
61    #[allow(clippy::too_many_lines)]
62    pub fn resolve_gpu_environment(
63        &mut self,
64        assets: &AssetServer,
65        environment: &myth_scene::environment::Environment,
66    ) -> f32 {
67        let Some(source) = environment.source_env_map else {
68            return 0.0;
69        };
70
71        let mut current_version: u32 = 0;
72        let mut source_pending = false;
73        if let TextureSource::Asset(handle) = &source {
74            let state = self.prepare_texture(assets, *handle);
75            if matches!(state, ResourceState::Pending) {
76                source_pending = true;
77            }
78            if let Some(tex) = assets.textures.get(*handle) {
79                current_version = assets.images.get_version(tex.image).unwrap_or(0);
80            }
81        }
82
83        // If the source texture is still loading and we already have a cached
84        // entry, return the cached value without modification.
85        if source_pending && let Some(gpu_env) = self.environment_map_cache.get(&source) {
86            return gpu_env.env_map_max_mip_level;
87
88            // No cache entry yet — fall through to create one with defaults.
89            // The compute pass will regenerate once the texture arrives.
90        }
91
92        // --- Check existing cache entry ---
93        if let Some(gpu_env) = self.environment_map_cache.get_mut(&source) {
94            if gpu_env.source_version == current_version && !gpu_env.needs_compute {
95                return gpu_env.env_map_max_mip_level;
96            }
97            if gpu_env.source_version != current_version {
98                // Source texture content changed — need to regenerate
99                gpu_env.source_version = current_version;
100                gpu_env.needs_compute = true;
101                self.pending_ibl_source = Some(source);
102            }
103            return gpu_env.env_map_max_mip_level;
104        }
105
106        // --- No cached entry — determine source type ---
107        let (is_2d_source, source_cube_size, source_mip_count) = match &source {
108            TextureSource::Asset(handle) => {
109                if let Some(binding) = self.texture_bindings.get(*handle) {
110                    if let Some(img) = self.gpu_images.get(binding.image_handle) {
111                        let is_2d = img.default_view_dimension == TextureViewDimension::D2;
112                        (is_2d, img.size.width, img.mip_level_count)
113                    } else {
114                        (true, EQUIRECT_CUBE_SIZE, 1)
115                    }
116                } else {
117                    (true, EQUIRECT_CUBE_SIZE, 1)
118                }
119            }
120            TextureSource::Attachment(_, dim) => {
121                let is_2d = *dim == TextureViewDimension::D2;
122                // Cannot determine mip count for attachments; assume cube has mipmaps
123                (is_2d, EQUIRECT_CUBE_SIZE, if is_2d { 1 } else { 2 })
124            }
125        };
126
127        let source_type = if is_2d_source {
128            CubeSourceType::Equirectangular
129        } else if source_mip_count <= 1 {
130            CubeSourceType::CubeNoMipmaps
131        } else {
132            CubeSourceType::CubeWithMipmaps
133        };
134
135        // --- Create owned cube texture (Equirectangular & CubeNoMipmaps) ---
136        let needs_owned_cube = matches!(
137            source_type,
138            CubeSourceType::Equirectangular | CubeSourceType::CubeNoMipmaps
139        );
140
141        let owned_cube_size = match source_type {
142            CubeSourceType::Equirectangular => EQUIRECT_CUBE_SIZE,
143            CubeSourceType::CubeNoMipmaps => source_cube_size,
144            CubeSourceType::CubeWithMipmaps => 0, // unused
145        };
146
147        let cube_texture = if needs_owned_cube {
148            let mip_levels = (owned_cube_size as f32).log2().floor() as u32 + 1;
149            Some(self.device.create_texture(&wgpu::TextureDescriptor {
150                label: Some("Env Cube (Owned)"),
151                size: wgpu::Extent3d {
152                    width: owned_cube_size,
153                    height: owned_cube_size,
154                    depth_or_array_layers: 6,
155                },
156                dimension: wgpu::TextureDimension::D2,
157                format: wgpu::TextureFormat::Rgba16Float,
158                usage: wgpu::TextureUsages::STORAGE_BINDING
159                    | wgpu::TextureUsages::TEXTURE_BINDING
160                    | wgpu::TextureUsages::RENDER_ATTACHMENT,
161                mip_level_count: mip_levels,
162                sample_count: 1,
163                view_formats: &[],
164            }))
165        } else {
166            None
167        };
168
169        // --- Register cube view ---
170        let cube_view_id = if let Some(ref cube_tex) = cube_texture {
171            let view = cube_tex.create_view(&wgpu::TextureViewDescriptor {
172                dimension: Some(TextureViewDimension::Cube),
173                ..Default::default()
174            });
175            let id = generate_gpu_resource_id();
176            self.internal_resources.insert(id, view);
177            id
178        } else {
179            // CubeWithMipmaps — resolve from the asset
180            match &source {
181                TextureSource::Asset(handle) => {
182                    if let Some(binding) = self.texture_bindings.get(*handle) {
183                        if let Some(img) = self.gpu_images.get(binding.image_handle) {
184                            let view = img.texture.create_view(&wgpu::TextureViewDescriptor {
185                                dimension: Some(TextureViewDimension::Cube),
186                                ..Default::default()
187                            });
188                            let id = generate_gpu_resource_id();
189                            self.internal_resources.insert(id, view);
190                            id
191                        } else {
192                            self.system_textures.black_cube.id()
193                        }
194                    } else {
195                        self.system_textures.black_cube.id()
196                    }
197                }
198                TextureSource::Attachment(id, _) => *id,
199            }
200        };
201
202        // --- Create PMREM texture ---
203        let pmrem_mip_levels = (PMREM_SIZE as f32).log2().floor() as u32 + 1;
204        let pmrem_texture = self.device.create_texture(&wgpu::TextureDescriptor {
205            label: Some("PMREM Cubemap"),
206            size: wgpu::Extent3d {
207                width: PMREM_SIZE,
208                height: PMREM_SIZE,
209                depth_or_array_layers: 6,
210            },
211            mip_level_count: pmrem_mip_levels,
212            sample_count: 1,
213            dimension: wgpu::TextureDimension::D2,
214            format: wgpu::TextureFormat::Rgba16Float,
215            usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
216            view_formats: &[],
217        });
218
219        // Register PMREM view
220        let pmrem_view = pmrem_texture.create_view(&wgpu::TextureViewDescriptor {
221            label: Some("PMREM Cube View"),
222            dimension: Some(TextureViewDimension::Cube),
223            ..Default::default()
224        });
225        let pmrem_view_id = generate_gpu_resource_id();
226        self.internal_resources.insert(pmrem_view_id, pmrem_view);
227
228        let env_map_max_mip_level = (pmrem_mip_levels - 1) as f32;
229
230        let gpu_env = GpuEnvironment {
231            source_version: current_version,
232            needs_compute: true,
233            source_type,
234            cube_texture,
235            pmrem_texture,
236            cube_view_id,
237            pmrem_view_id,
238            env_map_max_mip_level,
239        };
240
241        self.pending_ibl_source = Some(source);
242        self.environment_map_cache.insert(source, gpu_env);
243
244        env_map_max_mip_level
245    }
246
247    /// Ensure the global BRDF LUT texture exists.
248    ///
249    /// Creates the texture on first call and sets `needs_brdf_compute`.
250    /// Returns the resource ID of the BRDF LUT view.
251    pub fn ensure_brdf_lut(&mut self) -> u64 {
252        if let Some(id) = self.brdf_lut_view_id {
253            return id;
254        }
255
256        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
257            label: Some("BRDF LUT"),
258            size: wgpu::Extent3d {
259                width: BRDF_LUT_SIZE,
260                height: BRDF_LUT_SIZE,
261                depth_or_array_layers: 1,
262            },
263            mip_level_count: 1,
264            sample_count: 1,
265            dimension: wgpu::TextureDimension::D2,
266            format: wgpu::TextureFormat::Rgba16Float,
267            usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
268            view_formats: &[],
269        });
270
271        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
272        let id = self.register_internal_texture_by_name("BRDF_LUT", view);
273
274        self.brdf_lut_texture = Some(texture);
275        self.brdf_lut_view_id = Some(id);
276        self.needs_brdf_compute = true;
277
278        id
279    }
280
281    /// Get the `env_map_max_mip_level` for a given environment source.
282    pub fn get_env_map_max_mip_level(&self, source: Option<TextureSource>) -> f32 {
283        if let Some(src) = source
284            && let Some(gpu_env) = self.environment_map_cache.get(&src)
285        {
286            return gpu_env.env_map_max_mip_level;
287        }
288        0.0
289    }
290}