Skip to main content

cvkg_render_gpu/renderer/
init.rs

1use crate::heim::SkylinePacker;
2use crate::renderer::context_helpers::{
3    compute_mip_levels, create_headless_context, create_surface_context,
4    load_pipeline_cache_with_integrity_check,
5};
6use crate::renderer::pipelines::compile_render_pipelines;
7use crate::renderer::{GpuRenderer, QualityLevel};
8use crate::types::{
9    GpuParticle, HeadlessContext, MAX_INDICES, MAX_PARTICLES, MAX_VERTICES, ParticleUniforms,
10    SurfaceContext,
11};
12use crate::{
13    WGSL_BIFROST, WGSL_BLOOM, WGSL_COLOR_BLIND, WGSL_COMMON, WGSL_MATERIAL_GLASS,
14    WGSL_MATERIAL_OPAQUE, WGSL_MATERIAL_PBR, WGSL_MATERIAL_SHADOW, WGSL_SHAPES,
15};
16use cvkg_core::{ColorTheme, Rect, SceneUniforms};
17use lru::LruCache;
18use std::collections::HashMap;
19use std::num::NonZeroUsize;
20use std::sync::Arc;
21
22impl GpuRenderer {
23    /// forge -- Initializes the Surtr GPU renderer from a winit window.
24    ///
25    /// This method performs the following:
26    /// 1. Negotiates a wgpu surface and adapter.
27    /// 2. Forges the Muspelheim multi-pass pipeline layouts.
28    /// 3. Initializes the Berserker state buffers and texture registries.
29    pub async fn forge(window: Arc<winit::window::Window>) -> Self {
30        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
31            backends: wgpu::Backends::all(),
32            flags: wgpu::InstanceFlags::default(),
33            backend_options: wgpu::BackendOptions::default(),
34            display: None,
35            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
36        });
37
38        let surface = instance
39            .create_surface(window.clone())
40            .expect("Failed to create surface");
41
42        tracing::info!("[Surtr] Renderer backend: GpuRenderer (wgpu)");
43
44        // Request adapter with robust multi-stage fallback for Bumblebee/Optimus compatibility
45        tracing::info!("[GPU] Requesting HighPerformance adapter...");
46
47        let mut adapter = None;
48
49        #[cfg(not(target_arch = "wasm32"))]
50        if let Ok(filter) = std::env::var("WGPU_ADAPTER_NAME") {
51            let adapters = instance.enumerate_adapters(wgpu::Backends::all()).await;
52            tracing::info!("[GPU] Available adapters:");
53            for a in &adapters {
54                let info = a.get_info();
55                tracing::info!(
56                    "  - Name: '{}' | Driver: '{}' | Backend: {:?}",
57                    info.name,
58                    info.driver,
59                    info.backend
60                );
61            }
62
63            adapter = adapters.into_iter().find(|a| {
64                let info = a.get_info();
65                let match_found = info.name.to_lowercase().contains(&filter.to_lowercase())
66                    || info.driver.to_lowercase().contains(&filter.to_lowercase());
67                if match_found {
68                    tracing::info!(
69                        "[GPU] Manual selection match: {} | Driver: {}",
70                        info.name,
71                        info.driver
72                    );
73                }
74                match_found
75            });
76
77            if adapter.is_some() {
78                tracing::info!(
79                    "[GPU] Forced adapter selection via WGPU_ADAPTER_NAME='{}'",
80                    filter
81                );
82            } else {
83                tracing::warn!(
84                    "[GPU] WGPU_ADAPTER_NAME='{}' provided but no matching adapter found. Falling back...",
85                    filter
86                );
87            }
88        }
89
90        if adapter.is_none() {
91            adapter = instance
92                .request_adapter(&wgpu::RequestAdapterOptions {
93                    power_preference: wgpu::PowerPreference::HighPerformance,
94                    compatible_surface: Some(&surface),
95                    force_fallback_adapter: false,
96                })
97                .await
98                .ok();
99        }
100
101        if adapter.is_none() {
102            tracing::warn!(
103                "[GPU] HighPerformance adapter failed (possible Bumblebee/Optimus), trying LowPower..."
104            );
105            adapter = instance
106                .request_adapter(&wgpu::RequestAdapterOptions {
107                    power_preference: wgpu::PowerPreference::LowPower,
108                    compatible_surface: Some(&surface),
109                    force_fallback_adapter: false,
110                })
111                .await
112                .ok();
113        }
114
115        if adapter.is_none() {
116            tracing::warn!("[GPU] Hardware adapters failed, trying Software fallback...");
117            adapter = instance
118                .request_adapter(&wgpu::RequestAdapterOptions {
119                    power_preference: wgpu::PowerPreference::LowPower,
120                    compatible_surface: Some(&surface),
121                    force_fallback_adapter: true,
122                })
123                .await
124                .ok();
125        }
126
127        let adapter = adapter.expect("Failed to find a suitable GPU for Surtr");
128        let info = adapter.get_info();
129        // P1-26: detect GPU vendor for logging and future
130        // capability-based shader selection.
131        let caps =
132            crate::subsystems::GpuCapabilities::detect(&info.name, format!("{:?}", info.backend));
133        tracing::info!(
134            "[GPU] Selected adapter: {} ({:?}) on backend: {:?} -- detected as {}",
135            info.name,
136            info.device_type,
137            info.backend,
138            caps.vendor
139        );
140        tracing::info!("[GPU] Driver info: {} - {}", info.driver, info.driver_info);
141        let supports_timestamps = adapter.features().contains(wgpu::Features::TIMESTAMP_QUERY);
142        let supports_pipeline_cache = adapter.features().contains(wgpu::Features::PIPELINE_CACHE);
143        #[cfg(not(target_arch = "wasm32"))]
144        let mut required_features =
145            wgpu::Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING
146                | wgpu::Features::TEXTURE_BINDING_ARRAY;
147
148        #[cfg(target_arch = "wasm32")]
149        let mut required_features = wgpu::Features::empty(); // Fallbacks for WebGL
150        if supports_timestamps {
151            required_features |= wgpu::Features::TIMESTAMP_QUERY;
152        }
153        if supports_pipeline_cache {
154            required_features |= wgpu::Features::PIPELINE_CACHE;
155        }
156        // Enable validation layer in debug builds for better error reporting
157        #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
158        {
159            tracing::info!("[GPU] Validation layer enabled (debug build)");
160        }
161
162        let (device, queue) = adapter
163            .request_device(&wgpu::DeviceDescriptor {
164                label: Some("Surtr Forge"),
165                required_features,
166                required_limits: wgpu::Limits {
167                    max_bindings_per_bind_group: 256,
168                    max_binding_array_elements_per_shader_stage: 256,
169                    ..wgpu::Limits::default()
170                },
171                memory_hints: wgpu::MemoryHints::default(),
172                experimental_features: wgpu::ExperimentalFeatures::disabled(),
173                trace: wgpu::Trace::Off,
174            })
175            .await
176            .expect("Failed to create Surtr device");
177
178        let instance = Arc::new(instance);
179        let adapter = Arc::new(adapter);
180
181        device.on_uncaptured_error(Arc::new(|error| {
182            tracing::error!(
183                "[GPU] Uncaptured device error (Device Lost or Panic): {:?}",
184                error
185            );
186        }));
187
188        let device = Arc::new(device);
189        let queue = Arc::new(queue);
190
191        let size = window.inner_size();
192        // Ensure we have valid dimensions - Wayland may return 0 for not-yet-committed surfaces
193        let width = if size.width > 0 { size.width } else { 1280 };
194        let height = if size.height > 0 { size.height } else { 720 };
195        let surface_caps = surface.get_capabilities(&adapter);
196        // HDR/Display P3 surface format selection:
197        // WHY: Tahoe requires wide-gamut Display P3 or HDR (Rgba16Float) color spaces when available.
198        // CONTRACT: Uses select_best_surface_format to safely fall back on mobile/legacy GPUs.
199        let surface_format = Self::select_best_surface_format(&surface_caps.formats);
200
201        tracing::info!(
202            "[GPU] Available present modes: {:?}",
203            surface_caps.present_modes
204        );
205        tracing::info!(
206            "[GPU] Adapter: {} ({:?})",
207            adapter.get_info().name,
208            adapter.get_info().backend
209        );
210        let present_mode = if surface_caps
211            .present_modes
212            .contains(&wgpu::PresentMode::Immediate)
213        {
214            tracing::info!("[GPU] Selected: Immediate (no vsync, uncapped)");
215            wgpu::PresentMode::Immediate
216        } else if surface_caps
217            .present_modes
218            .contains(&wgpu::PresentMode::Mailbox)
219        {
220            tracing::info!("[GPU] Selected: Mailbox (no vsync)");
221            wgpu::PresentMode::Mailbox
222        } else {
223            tracing::info!("[GPU] Selected: Fifo (V-Sync capped at compositor rate)");
224            wgpu::PresentMode::Fifo
225        };
226
227        let alpha_mode = if surface_caps
228            .alpha_modes
229            .contains(&wgpu::CompositeAlphaMode::PostMultiplied)
230        {
231            wgpu::CompositeAlphaMode::PostMultiplied
232        } else if surface_caps
233            .alpha_modes
234            .contains(&wgpu::CompositeAlphaMode::PreMultiplied)
235        {
236            wgpu::CompositeAlphaMode::PreMultiplied
237        } else {
238            surface_caps.alpha_modes[0]
239        };
240
241        tracing::info!(
242            "[GPU] Configuring surface: {}x{} | {:?} | {:?}",
243            width,
244            height,
245            present_mode,
246            alpha_mode
247        );
248
249        let config = wgpu::SurfaceConfiguration {
250            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
251            format: surface_format,
252            width,
253            height,
254            present_mode,
255            alpha_mode,
256            view_formats: vec![],
257            desired_maximum_frame_latency: 1,
258        };
259        surface.configure(&device, &config);
260        tracing::info!("[GPU] Surface configuration successful.");
261
262        let renderer = Self::forge_internal(
263            instance,
264            adapter,
265            device,
266            queue,
267            Some((window, surface, config)),
268            None,
269        )
270        .await;
271        tracing::info!("[GPU] Forge internal complete.");
272        renderer
273    }
274
275    /// Internal rendering pipeline constructor.
276    pub(crate) async fn forge_internal(
277        instance: Arc<wgpu::Instance>,
278        adapter: Arc<wgpu::Adapter>,
279        device: Arc<wgpu::Device>,
280        queue: Arc<wgpu::Queue>,
281        surface_info: Option<(
282            Arc<winit::window::Window>,
283            wgpu::Surface<'static>,
284            wgpu::SurfaceConfiguration,
285        )>,
286        headless_info: Option<(u32, u32, wgpu::TextureFormat)>,
287    ) -> Self {
288        let format = if let Some((_, _, ref config)) = surface_info {
289            config.format
290        } else if let Some((_, _, f)) = headless_info {
291            f
292        } else {
293            wgpu::TextureFormat::Rgba8UnormSrgb
294        };
295
296        let supports_timestamps = adapter.features().contains(wgpu::Features::TIMESTAMP_QUERY);
297        let skuld_period = queue.get_timestamp_period();
298        let (skuld_queries, skuld_buffer, skuld_read_buffer) = if supports_timestamps {
299            let q = device.create_query_set(&wgpu::QuerySetDescriptor {
300                label: Some("Skuld Timestamp Queries"),
301                count: 2,
302                ty: wgpu::QueryType::Timestamp,
303            });
304            let b = device.create_buffer(&wgpu::BufferDescriptor {
305                label: Some("Skuld Query Buffer"),
306                size: 16,
307                usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
308                mapped_at_creation: false,
309            });
310            let rb = device.create_buffer(&wgpu::BufferDescriptor {
311                label: Some("Skuld Read Buffer"),
312                size: 16,
313                usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
314                mapped_at_creation: false,
315            });
316            (Some(q), Some(b), Some(rb))
317        } else {
318            (None, None, None)
319        };
320
321        let pipeline_cache = if device.features().contains(wgpu::Features::PIPELINE_CACHE) {
322            let cache_dir = std::env::current_exe()
323                .ok()
324                .and_then(|p| p.parent().map(|d| d.join("pipeline_cache")))
325                .unwrap_or_else(|| std::env::temp_dir().join("cvkg_pipeline_cache"));
326            let _ = std::fs::create_dir_all(&cache_dir);
327            let cache_path = cache_dir.join("cvkg_render_gpu.bin");
328            let cache_data = match load_pipeline_cache_with_integrity_check(&cache_path) {
329                Ok(data) => data,
330                Err(reason) => {
331                    tracing::warn!(
332                        "[GPU] pipeline cache integrity check failed: {reason}; using empty cache"
333                    );
334                    None
335                }
336            };
337            // SAFETY: create_pipeline_cache is marked unsafe in wgpu but only reads the
338            // data slice and does not require lifetime guarantees beyond the descriptor.
339            // The cache_data is a Vec<u8> that lives for the duration of this call.
340            Some(unsafe {
341                device.create_pipeline_cache(&wgpu::PipelineCacheDescriptor {
342                    label: Some("CVKG Pipeline Cache"),
343                    data: cache_data.as_deref(),
344                    fallback: true,
345                })
346            })
347        } else {
348            tracing::debug!(
349                "[GPU] device does not expose PIPELINE_CACHE; compiling pipelines without cache"
350            );
351            None
352        };
353        let materials_generated = crate::material::generate_builtins_wgsl();
354
355        let wgsl_src = format!(
356            "{}{}{}{}{}{}",
357            WGSL_COMMON,
358            WGSL_SHAPES,
359            WGSL_BIFROST,
360            WGSL_BLOOM,
361            WGSL_COLOR_BLIND,
362            materials_generated
363        );
364        let wgsl_opaque = format!(
365            "{}{}{}{}{}{}",
366            WGSL_COMMON,
367            WGSL_MATERIAL_OPAQUE,
368            WGSL_BIFROST,
369            WGSL_BLOOM,
370            WGSL_COLOR_BLIND,
371            materials_generated
372        );
373        let wgsl_glass = format!(
374            "{}{}{}{}{}{}",
375            WGSL_COMMON,
376            WGSL_MATERIAL_GLASS,
377            WGSL_BIFROST,
378            WGSL_BLOOM,
379            WGSL_COLOR_BLIND,
380            materials_generated
381        );
382        let wgsl_pbr = format!(
383            "{}{}{}{}{}{}",
384            WGSL_COMMON,
385            WGSL_MATERIAL_PBR,
386            WGSL_BIFROST,
387            WGSL_BLOOM,
388            WGSL_COLOR_BLIND,
389            materials_generated
390        );
391        let wgsl_shadow = format!(
392            "{}{}{}",
393            WGSL_COMMON, WGSL_MATERIAL_SHADOW, materials_generated
394        );
395
396        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
397            label: Some("Surtr Main Shader"),
398            source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Owned(wgsl_src)),
399        });
400
401        #[cfg(target_arch = "wasm32")]
402        let texture_array_count: Option<std::num::NonZeroU32> = None;
403        #[cfg(not(target_arch = "wasm32"))]
404        let texture_array_count: Option<std::num::NonZeroU32> = std::num::NonZeroU32::new(32);
405
406        let texture_bind_group_layout =
407            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
408                entries: &[
409                    wgpu::BindGroupLayoutEntry {
410                        binding: 0,
411                        visibility: wgpu::ShaderStages::FRAGMENT,
412                        ty: wgpu::BindingType::Texture {
413                            multisampled: false,
414                            view_dimension: wgpu::TextureViewDimension::D2,
415                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
416                        },
417                        count: texture_array_count,
418                    },
419                    wgpu::BindGroupLayoutEntry {
420                        binding: 1,
421                        visibility: wgpu::ShaderStages::FRAGMENT,
422                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
423                        count: None,
424                    },
425                ],
426                label: Some("Niflheim Texture Bind Group Layout"),
427            });
428
429        let env_bind_group_layout =
430            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
431                entries: &[
432                    wgpu::BindGroupLayoutEntry {
433                        binding: 0,
434                        visibility: wgpu::ShaderStages::FRAGMENT,
435                        ty: wgpu::BindingType::Texture {
436                            multisampled: false,
437                            view_dimension: wgpu::TextureViewDimension::D2,
438                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
439                        },
440                        count: None,
441                    },
442                    wgpu::BindGroupLayoutEntry {
443                        binding: 1,
444                        visibility: wgpu::ShaderStages::FRAGMENT,
445                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
446                        count: None,
447                    },
448                ],
449                label: Some("Surtr Environment Bind Group Layout"),
450            });
451
452        let berserker_bind_group_layout =
453            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
454                entries: &[
455                    wgpu::BindGroupLayoutEntry {
456                        binding: 0,
457                        visibility: wgpu::ShaderStages::FRAGMENT,
458                        ty: wgpu::BindingType::Buffer {
459                            ty: wgpu::BufferBindingType::Uniform,
460                            has_dynamic_offset: false,
461                            min_binding_size: None,
462                        },
463                        count: None,
464                    },
465                    wgpu::BindGroupLayoutEntry {
466                        binding: 1,
467                        visibility: wgpu::ShaderStages::FRAGMENT | wgpu::ShaderStages::VERTEX,
468                        ty: wgpu::BindingType::Buffer {
469                            ty: wgpu::BufferBindingType::Uniform,
470                            has_dynamic_offset: false,
471                            min_binding_size: None,
472                        },
473                        count: None,
474                    },
475                    wgpu::BindGroupLayoutEntry {
476                        binding: 2,
477                        visibility: wgpu::ShaderStages::FRAGMENT | wgpu::ShaderStages::VERTEX,
478                        ty: wgpu::BindingType::Buffer {
479                            ty: wgpu::BufferBindingType::Uniform,
480                            has_dynamic_offset: false,
481                            min_binding_size: None,
482                        },
483                        count: None,
484                    },
485                ],
486                label: Some("Surtr Berserker Bind Group Layout"),
487            });
488
489        let gradient_bind_group_layout =
490            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
491                entries: &[
492                    wgpu::BindGroupLayoutEntry {
493                        binding: 0,
494                        visibility: wgpu::ShaderStages::FRAGMENT,
495                        ty: wgpu::BindingType::Texture {
496                            multisampled: false,
497                            view_dimension: wgpu::TextureViewDimension::D2,
498                            sample_type: wgpu::TextureSampleType::Float { filterable: false },
499                        },
500                        count: None,
501                    },
502                    wgpu::BindGroupLayoutEntry {
503                        binding: 1,
504                        visibility: wgpu::ShaderStages::FRAGMENT,
505                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
506                        count: None,
507                    },
508                ],
509                label: Some("Surtr Gradient Bind Group Layout"),
510            });
511
512        let pbr_material_bind_group_layout =
513            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
514                entries: &[
515                    // Binding 0: Shadow Map Texture Atlas (single 2D depth)
516                    wgpu::BindGroupLayoutEntry {
517                        binding: 0,
518                        visibility: wgpu::ShaderStages::FRAGMENT,
519                        ty: wgpu::BindingType::Texture {
520                            multisampled: false,
521                            view_dimension: wgpu::TextureViewDimension::D2,
522                            sample_type: wgpu::TextureSampleType::Depth,
523                        },
524                        count: None,
525                    },
526                    // Binding 1: Shadow Sampler (comparison)
527                    wgpu::BindGroupLayoutEntry {
528                        binding: 1,
529                        visibility: wgpu::ShaderStages::FRAGMENT,
530                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
531                        count: None,
532                    },
533                    // Binding 8: IBL Texture (standard 2D)
534                    wgpu::BindGroupLayoutEntry {
535                        binding: 8,
536                        visibility: wgpu::ShaderStages::FRAGMENT,
537                        ty: wgpu::BindingType::Texture {
538                            multisampled: false,
539                            view_dimension: wgpu::TextureViewDimension::D2,
540                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
541                        },
542                        count: None,
543                    },
544                    // Binding 9: IBL Sampler (filtering)
545                    wgpu::BindGroupLayoutEntry {
546                        binding: 9,
547                        visibility: wgpu::ShaderStages::FRAGMENT,
548                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
549                        count: None,
550                    },
551                    // Binding 6: Normal Map Texture (standard 2D)
552                    wgpu::BindGroupLayoutEntry {
553                        binding: 6,
554                        visibility: wgpu::ShaderStages::FRAGMENT,
555                        ty: wgpu::BindingType::Texture {
556                            multisampled: false,
557                            view_dimension: wgpu::TextureViewDimension::D2,
558                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
559                        },
560                        count: None,
561                    },
562                    // Binding 7: Normal Map Sampler (filtering)
563                    wgpu::BindGroupLayoutEntry {
564                        binding: 7,
565                        visibility: wgpu::ShaderStages::FRAGMENT,
566                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
567                        count: None,
568                    },
569                ],
570                label: Some("Surtr PBR Material Bind Group Layout"),
571            });
572
573        let pipes = compile_render_pipelines(
574            &device,
575            format,
576            pipeline_cache.as_ref(),
577            &texture_bind_group_layout,
578            &env_bind_group_layout,
579            &berserker_bind_group_layout,
580            &gradient_bind_group_layout,
581            &pbr_material_bind_group_layout,
582            &shader,
583            wgsl_opaque.as_str(),
584            wgsl_glass.as_str(),
585            wgsl_pbr.as_str(),
586            wgsl_shadow.as_str(),
587            &queue,
588        );
589
590        // Forge the Mega-Heim (4096x4096 RGBA for production batching)
591        let mega_heim_tex = device.create_texture(&wgpu::TextureDescriptor {
592            label: Some("Surtr Mega-Heim"),
593            size: wgpu::Extent3d {
594                width: 4096,
595                height: 4096,
596                depth_or_array_layers: 1,
597            },
598            mip_level_count: 1,
599            sample_count: 1,
600            dimension: wgpu::TextureDimension::D2,
601            format: wgpu::TextureFormat::Rgba8UnormSrgb,
602            usage: wgpu::TextureUsages::TEXTURE_BINDING
603                | wgpu::TextureUsages::COPY_DST
604                | wgpu::TextureUsages::COPY_SRC,
605            view_formats: &[],
606        });
607        let mega_heim_view_obj = mega_heim_tex.create_view(&wgpu::TextureViewDescriptor::default());
608        let text_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
609            address_mode_u: wgpu::AddressMode::ClampToEdge,
610            address_mode_v: wgpu::AddressMode::ClampToEdge,
611            mag_filter: wgpu::FilterMode::Linear,
612            min_filter: wgpu::FilterMode::Linear,
613            ..Default::default()
614        });
615
616        // Forge the Niflheim Dummy Texture (1x1 White)
617        let dummy_size = wgpu::Extent3d {
618            width: 1,
619            height: 1,
620            depth_or_array_layers: 1,
621        };
622        let dummy_texture = device.create_texture(&wgpu::TextureDescriptor {
623            label: Some("Niflheim Dummy Texture"),
624            size: dummy_size,
625            mip_level_count: 1,
626            sample_count: 1,
627            dimension: wgpu::TextureDimension::D2,
628            format: wgpu::TextureFormat::Rgba8UnormSrgb,
629            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
630            view_formats: &[],
631        });
632        queue.write_texture(
633            wgpu::TexelCopyTextureInfo {
634                texture: &dummy_texture,
635                mip_level: 0,
636                origin: wgpu::Origin3d::ZERO,
637                aspect: wgpu::TextureAspect::All,
638            },
639            &[255, 255, 255, 255],
640            wgpu::TexelCopyBufferLayout {
641                offset: 0,
642                bytes_per_row: Some(4),
643                rows_per_image: Some(1),
644            },
645            dummy_size,
646        );
647
648        let dummy_view = dummy_texture.create_view(&wgpu::TextureViewDescriptor::default());
649
650        // Non-filtering sampler required by the gradient bind group layout.
651        let gradient_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
652            address_mode_u: wgpu::AddressMode::ClampToEdge,
653            address_mode_v: wgpu::AddressMode::ClampToEdge,
654            address_mode_w: wgpu::AddressMode::ClampToEdge,
655            mag_filter: wgpu::FilterMode::Nearest,
656            min_filter: wgpu::FilterMode::Nearest,
657            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
658            ..Default::default()
659        });
660
661        // Gradient bind group: requires non-filterable texture + non-filtering sampler.
662        // The gradient layout expects Float { filterable: false } texture.
663        let gradient_dummy_texture = device.create_texture(&wgpu::TextureDescriptor {
664            label: Some("Gradient Dummy Texture"),
665            size: wgpu::Extent3d {
666                width: 1,
667                height: 1,
668                depth_or_array_layers: 1,
669            },
670            mip_level_count: 1,
671            sample_count: 1,
672            dimension: wgpu::TextureDimension::D2,
673            format: wgpu::TextureFormat::Rgba16Float,
674            usage: wgpu::TextureUsages::TEXTURE_BINDING,
675            view_formats: &[],
676        });
677        let gradient_dummy_view =
678            gradient_dummy_texture.create_view(&wgpu::TextureViewDescriptor::default());
679        let gradient_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
680            layout: &gradient_bind_group_layout,
681            entries: &[
682                wgpu::BindGroupEntry {
683                    binding: 0,
684                    resource: wgpu::BindingResource::TextureView(&gradient_dummy_view),
685                },
686                wgpu::BindGroupEntry {
687                    binding: 1,
688                    resource: wgpu::BindingResource::Sampler(&gradient_sampler),
689                },
690            ],
691            label: Some("Gradient Dummy Bind Group"),
692        });
693        let dummy_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
694            address_mode_u: wgpu::AddressMode::ClampToEdge,
695            address_mode_v: wgpu::AddressMode::ClampToEdge,
696            address_mode_w: wgpu::AddressMode::ClampToEdge,
697            mag_filter: wgpu::FilterMode::Linear,
698            min_filter: wgpu::FilterMode::Nearest,
699            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
700            ..Default::default()
701        });
702
703        // Non-filtering sampler required by the gradient bind group layout.
704        let gradient_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
705            address_mode_u: wgpu::AddressMode::ClampToEdge,
706            address_mode_v: wgpu::AddressMode::ClampToEdge,
707            address_mode_w: wgpu::AddressMode::ClampToEdge,
708            mag_filter: wgpu::FilterMode::Nearest,
709            min_filter: wgpu::FilterMode::Nearest,
710            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
711            ..Default::default()
712        });
713
714        let mut texture_views_list: Vec<wgpu::TextureView> =
715            (0..32).map(|_| dummy_view.clone()).collect();
716        texture_views_list[0] = mega_heim_view_obj.clone();
717
718        let views_refs: Vec<&wgpu::TextureView> = texture_views_list.iter().collect();
719        let mega_heim_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
720            layout: &texture_bind_group_layout,
721            entries: &[
722                wgpu::BindGroupEntry {
723                    binding: 0,
724                    resource: wgpu::BindingResource::TextureViewArray(&views_refs),
725                },
726                wgpu::BindGroupEntry {
727                    binding: 1,
728                    resource: wgpu::BindingResource::Sampler(&text_sampler),
729                },
730            ],
731            label: Some("Mega-Heim Bind Group"),
732        });
733
734        let dummy_views_refs: Vec<&wgpu::TextureView> = (0..32).map(|_| &dummy_view).collect();
735        let dummy_texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
736            layout: &texture_bind_group_layout,
737            entries: &[
738                wgpu::BindGroupEntry {
739                    binding: 0,
740                    resource: wgpu::BindingResource::TextureViewArray(&dummy_views_refs),
741                },
742                wgpu::BindGroupEntry {
743                    binding: 1,
744                    resource: wgpu::BindingResource::Sampler(&dummy_sampler),
745                },
746            ],
747            label: Some("Dummy Texture Bind Group"),
748        });
749
750        let dummy_env_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
751            layout: &env_bind_group_layout,
752            entries: &[
753                wgpu::BindGroupEntry {
754                    binding: 0,
755                    resource: wgpu::BindingResource::TextureView(&dummy_view),
756                },
757                wgpu::BindGroupEntry {
758                    binding: 1,
759                    resource: wgpu::BindingResource::Sampler(&dummy_sampler),
760                },
761            ],
762            label: Some("Dummy Env Bind Group"),
763        });
764        let dummy_depth_tex = device.create_texture(&wgpu::TextureDescriptor {
765            label: Some("Surtr Dummy Depth Texture"),
766            size: wgpu::Extent3d {
767                width: 1,
768                height: 1,
769                depth_or_array_layers: 1,
770            },
771            mip_level_count: 1,
772            sample_count: 1,
773            dimension: wgpu::TextureDimension::D2,
774            format: wgpu::TextureFormat::Depth32Float,
775            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
776            view_formats: &[],
777        });
778        let dummy_depth_view = dummy_depth_tex.create_view(&wgpu::TextureViewDescriptor::default());
779
780        let dummy_depth_tex_msaa = device.create_texture(&wgpu::TextureDescriptor {
781            label: Some("Surtr Dummy Depth Texture MSAA"),
782            size: wgpu::Extent3d {
783                width: 1,
784                height: 1,
785                depth_or_array_layers: 1,
786            },
787            mip_level_count: 1,
788            sample_count: 4,
789            dimension: wgpu::TextureDimension::D2,
790            format: wgpu::TextureFormat::Depth32Float,
791            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
792            view_formats: &[],
793        });
794        let dummy_depth_view_msaa =
795            dummy_depth_tex_msaa.create_view(&wgpu::TextureViewDescriptor::default());
796
797        let shadow_map_size = 1024;
798        let shadow_map_texture = device.create_texture(&wgpu::TextureDescriptor {
799            label: Some("Surtr CSM Shadow Map Texture"),
800            size: wgpu::Extent3d {
801                width: shadow_map_size * 2, // 2x2 grid double resolution
802                height: shadow_map_size * 2,
803                depth_or_array_layers: 1, // Single 2D texture atlas
804            },
805            mip_level_count: 1,
806            sample_count: 1,
807            dimension: wgpu::TextureDimension::D2,
808            format: wgpu::TextureFormat::Depth32Float,
809            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
810            view_formats: &[],
811        });
812
813        let shadow_map_view = shadow_map_texture.create_view(&wgpu::TextureViewDescriptor {
814            label: Some("Surtr CSM Shadow Map View"),
815            dimension: Some(wgpu::TextureViewDimension::D2),
816            ..wgpu::TextureViewDescriptor::default()
817        });
818
819        let shadow_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
820            label: Some("Surtr CSM Shadow Sampler"),
821            address_mode_u: wgpu::AddressMode::ClampToEdge,
822            address_mode_v: wgpu::AddressMode::ClampToEdge,
823            address_mode_w: wgpu::AddressMode::ClampToEdge,
824            mag_filter: wgpu::FilterMode::Linear,
825            min_filter: wgpu::FilterMode::Linear,
826            compare: Some(wgpu::CompareFunction::LessEqual),
827            ..wgpu::SamplerDescriptor::default()
828        });
829
830        let mut texture_registry = LruCache::new(NonZeroUsize::new(31).unwrap());
831        let mut texture_bind_groups = Vec::new();
832
833        // Index 0 is permanently reserved for the Mega-Heim atlas. Loaded images start at 1.
834        texture_registry.put("__mega_heim".to_string(), 0);
835        texture_bind_groups.push(mega_heim_bind_group.clone());
836
837        let geometry_buffers =
838            crate::types::GeometryBuffers::forge(&device, MAX_VERTICES, MAX_INDICES);
839
840        let instance_buffer_3d = device.create_buffer(&wgpu::BufferDescriptor {
841            label: Some("Surtr 3D Instance Buffer"),
842            size: (MAX_VERTICES / 4 * std::mem::size_of::<crate::vertex::InstanceData3D>()) as u64,
843            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
844            mapped_at_creation: false,
845        });
846
847        // Forge the Heart (Berserker Uniforms)
848        let current_theme = ColorTheme::default();
849        use wgpu::util::DeviceExt;
850        let theme_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
851            label: Some("Surtr Theme Buffer"),
852            contents: bytemuck::bytes_of(&current_theme),
853            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
854        });
855
856        let (width, height, scale_factor) = if let Some((ref window, _, ref config)) = surface_info
857        {
858            (config.width, config.height, window.scale_factor() as f32)
859        } else if let Some((w, h, _)) = headless_info {
860            (w, h, 1.0)
861        } else {
862            (1280, 720, 1.0)
863        };
864
865        let mut current_scene =
866            SceneUniforms::new(width as f32 / scale_factor, height as f32 / scale_factor);
867        current_scene.scale_factor = scale_factor;
868        let msaa_sample_count = QualityLevel::default().msaa_sample_count();
869        let scene_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
870            label: Some("Surtr Scene Buffer"),
871            contents: bytemuck::bytes_of(&current_scene),
872            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
873        });
874
875        let csm_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
876            label: Some("Surtr CSM Buffer"),
877            contents: bytemuck::bytes_of(&cvkg_core::render_tier::CsmUniforms::default()),
878            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
879        });
880
881        let berserker_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
882            layout: &berserker_bind_group_layout,
883            entries: &[
884                wgpu::BindGroupEntry {
885                    binding: 0,
886                    resource: theme_buffer.as_entire_binding(),
887                },
888                wgpu::BindGroupEntry {
889                    binding: 1,
890                    resource: scene_buffer.as_entire_binding(),
891                },
892                wgpu::BindGroupEntry {
893                    binding: 2,
894                    resource: csm_buffer.as_entire_binding(),
895                },
896            ],
897            label: Some("Surtr Berserker Bind Group"),
898        });
899
900        let mut registry = crate::kvasir::registry::ResourceRegistry::new();
901        let mut surfaces = std::collections::HashMap::new();
902        let mut current_window = None;
903        let mut headless_context = None;
904
905        if let Some((window, surface, config)) = surface_info {
906            let window_id = window.id();
907            let ctx = create_surface_context(
908                &device,
909                surface,
910                config,
911                &env_bind_group_layout,
912                &texture_bind_group_layout,
913                scale_factor,
914                msaa_sample_count,
915                &mut registry,
916            );
917            surfaces.insert(window_id, ctx);
918            current_window = Some(window_id);
919        } else if let Some((w, h, f)) = headless_info {
920            headless_context = Some(create_headless_context(
921                &device,
922                w,
923                h,
924                f,
925                &env_bind_group_layout,
926                &texture_bind_group_layout,
927                &mut registry,
928                msaa_sample_count,
929            ));
930        }
931
932        let staging_belt = wgpu::util::StagingBelt::new((*device).clone(), 1024 * 1024);
933
934        let glass_output_bind_group_layout = env_bind_group_layout.clone();
935
936        // Skinning buffers are now managed per-mesh in skinning_buffer_pairs (Vec<(Buffer, Buffer)>)
937        // No persistent src/dst buffers needed — each submit_mesh_3d creates its own pair.
938
939        let skinning_joint_matrices = device.create_buffer(&wgpu::BufferDescriptor {
940            label: Some("Skinning Joint Matrices Buffer"),
941            size: (256 * std::mem::size_of::<glam::Mat4>()) as u64,
942            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
943            mapped_at_creation: false,
944        });
945
946        let skinning_morph_positions = device.create_buffer(&wgpu::BufferDescriptor {
947            label: Some("Skinning Morph Positions Buffer"),
948            size: (65536 * 2 * 16) as u64,
949            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
950            mapped_at_creation: false,
951        });
952
953        let skinning_morph_weights = device.create_buffer(&wgpu::BufferDescriptor {
954            label: Some("Skinning Morph Weights Buffer"),
955            size: 16,
956            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
957            mapped_at_creation: false,
958        });
959
960        Self {
961            skinning_compute_pipeline: pipes.skinning_compute_pipeline.clone(),
962            skinning_bgl0: pipes.skinning_bgl0.clone(),
963            skinning_bgl1: pipes.skinning_bgl1.clone(),
964            skinning_bgl2: pipes.skinning_bgl2.clone(),
965            skinning_buffer_pairs: Vec::new(),
966            skinning_joint_matrices,
967            skinning_morph_positions,
968            skinning_morph_weights,
969            registry,
970            ai_material_rx: None,
971            active_offscreens: Vec::new(),
972            effect_pipelines: std::collections::HashMap::new(),
973            effect_params_buffer: device.create_buffer(&wgpu::BufferDescriptor {
974                label: Some("Dummy Effect Buffer"),
975                size: 256,
976                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
977                mapped_at_creation: false,
978            }),
979            effect_params_bind_group: device.create_bind_group(&wgpu::BindGroupDescriptor {
980                label: Some("Dummy Effect Bind Group"),
981                layout: &device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
982                    label: None,
983                    entries: &[],
984                }),
985                entries: &[],
986            }),
987            linear_sampler: device.create_sampler(&wgpu::SamplerDescriptor {
988                label: Some("Linear Sampler"),
989                address_mode_u: wgpu::AddressMode::ClampToEdge,
990                address_mode_v: wgpu::AddressMode::ClampToEdge,
991                address_mode_w: wgpu::AddressMode::ClampToEdge,
992                mag_filter: wgpu::FilterMode::Linear,
993                min_filter: wgpu::FilterMode::Linear,
994                mipmap_filter: wgpu::MipmapFilterMode::Linear,
995                ..Default::default()
996            }),
997            instance,
998            adapter,
999            device: device.clone(),
1000            queue: queue.clone(),
1001
1002            surfaces,
1003            current_window,
1004            headless_context,
1005            pipeline: pipes.pipeline,
1006            opaque_pipeline: pipes.opaque_pipeline,
1007            ui_pipeline: pipes.ui_pipeline,
1008            glass_pipeline: pipes.glass_pipeline,
1009            pbr_pipeline: pipes.pbr_pipeline,
1010            transparent_pipeline: pipes.transparent_pipeline,
1011            shadow_pipeline: pipes.shadow_pipeline,
1012            bloom_extract_pipeline: pipes.bloom_extract_pipeline,
1013            copy_pipeline: pipes.copy_pipeline,
1014            composite_pipeline: pipes.composite_pipeline,
1015            env_bind_group_layout,
1016            mega_heim_tex,
1017            mega_heim_bind_group,
1018            config: crate::subsystems::RendererConfig::default(),
1019            text: crate::types::TextSubsystem::forge(NonZeroUsize::new(8192).unwrap()),
1020            heim_packer: SkylinePacker::new(4096, 4096),
1021            image_uv_registry: {
1022                let mut cache = LruCache::new(NonZeroUsize::new(256).unwrap());
1023                cache.put(
1024                    "__mega_heim".to_string(),
1025                    cvkg_core::Rect {
1026                        x: 0.0,
1027                        y: 0.0,
1028                        width: 1.0,
1029                        height: 1.0,
1030                    },
1031                );
1032                cache
1033            },
1034            texture_registry,
1035            texture_views: texture_views_list,
1036            dummy_sampler,
1037            dummy_view: dummy_view.clone(),
1038            dummy_depth_view,
1039            dummy_depth_view_msaa,
1040            svg: crate::types::SvgSubsystem::forge(
1041                &device,
1042                &queue,
1043                NonZeroUsize::new(512).unwrap(),
1044                NonZeroUsize::new(512).unwrap(),
1045            ),
1046            dummy_texture_bind_group,
1047            gradient_stop_texture: dummy_texture.clone(),
1048            gradient_stop_texture_view: dummy_view.clone(),
1049            gradient_bind_group,
1050            gradient_texture_cache: std::collections::HashMap::new(),
1051            gradient_stops_hash: 0,
1052            gradient_bind_group_layout,
1053            dummy_env_bind_group,
1054            texture_bind_group_layout,
1055            texture_bind_groups,
1056            shared_elements: LruCache::new(NonZeroUsize::new(1024).unwrap()),
1057            geometry_buffers,
1058            vertices: Vec::with_capacity(MAX_VERTICES),
1059            indices: Vec::with_capacity(MAX_INDICES),
1060            instance_data: Vec::with_capacity(MAX_VERTICES / 4),
1061            instance_data_3d: Vec::with_capacity(MAX_VERTICES / 4),
1062            instance_buffer_3d: Some(instance_buffer_3d),
1063            draw_calls: Vec::new(),
1064            current_texture_id: None,
1065            current_panel_id: None,
1066            panel_stack: Vec::new(),
1067            panel_vdoms: HashMap::new(),
1068            world_space_panels: Vec::new(),
1069            opacity_stack: vec![1.0],
1070            clip_stack: Vec::new(),
1071            slice_stack: Vec::new(),
1072            shadow_stack: Vec::new(),
1073            theme_buffer,
1074            scene_buffer,
1075            berserker_bind_group,
1076            berserker_bind_group_layout,
1077            start_time: std::time::Instant::now(),
1078            current_theme,
1079            current_scene,
1080            background_pipeline: pipes.background_pipeline,
1081            current_z: 0.0,
1082            default_background_color: [0.02, 0.02, 0.05, 1.0],
1083            app_drew_background: false,
1084            frame_rendered: false,
1085            current_draw_order: 0,
1086            telemetry: cvkg_core::TelemetryData::default(),
1087            last_frame_start: std::time::Instant::now(),
1088            last_redraw_start: std::time::Instant::now(),
1089            frame_budget: cvkg_core::FrameBudget::default(),
1090            capture_staging_buffer: None,
1091            compositor_index_cursor: 0,
1092            vram_buffers_bytes: 0,
1093            vram_textures_bytes: 0,
1094            _debug_layout: false,
1095            transform_stack: Vec::new(),
1096            transform_stack_3d: Vec::new(),
1097            redraw_requested: false,
1098            skuld_queries,
1099            skuld_buffer,
1100            skuld_read_buffer,
1101            skuld_period,
1102            last_gpu_time_ns: 0,
1103            particle_compute_pipeline: pipes.particle_compute_pipeline,
1104            particle_compute_bgl: pipes.particle_compute_bgl,
1105            particle_buffer: pipes.particle_buffer,
1106            particle_uniform_buffer: pipes.particle_uniform_buffer,
1107            particles: crate::types::ParticleSubsystem::forge(),
1108            particle_render_pipeline: pipes.particle_render_pipeline,
1109            particle_render_bgl: pipes.particle_render_bgl,
1110            particle_render_bind_group: None,
1111            particle_compute_bind_group: None,
1112            vnode_stack: Vec::new(),
1113            event_handlers: std::collections::HashMap::new(),
1114            staging_belt,
1115            staging_command_buffers: Vec::new(),
1116            glass_output_bind_group_layout,
1117            current_draw_material: cvkg_core::DrawMaterial::Opaque,
1118            portal_regions: std::collections::VecDeque::new(),
1119            cached_graph_plan: None,
1120            material_compilation_hash: 0,
1121            memo_cache: std::collections::HashMap::new(),
1122            frame_generation: 0,
1123            quality_level: QualityLevel::default(),
1124            pipeline_cache,
1125            bloom_enabled: true,
1126            volumetric_enabled: false,
1127            path_geometry_cache: lru::LruCache::new(NonZeroUsize::new(64).unwrap()),
1128            color_blind_mode: crate::color_blindness::ColorBlindMode::Normal,
1129            color_blind_intensity: 1.0,
1130            color_blind_pipeline: pipes.color_blind_pipeline,
1131            volumetric_pipeline: pipes.volumetric_pipeline,
1132            volumetric_bind_group_layout: pipes.volumetric_bind_group_layout,
1133            volumetric_uniform_buffer: pipes.volumetric_uniform_buffer,
1134            csm_buffer,
1135            pbr_material_bind_group_layout,
1136            volumetric_depth_sampler: pipes.volumetric_depth_sampler,
1137            hologram_instances: Vec::new(),
1138            color_blind_bind_group_layout: pipes.color_blind_bind_group_layout,
1139            color_blind_uniform_buffer: pipes.color_blind_uniform_buffer,
1140            sampler: pipes.sampler,
1141            kawase_down_pipeline: pipes.kawase_down_pipeline,
1142            kawase_up_pipeline: pipes.kawase_up_pipeline,
1143            kawase_bind_group_layout: pipes.kawase_bind_group_layout,
1144            kawase_uniform: pipes.kawase_uniform,
1145            kawase_uniform_buffers: pipes.kawase_uniform_buffers,
1146            bind_group_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
1147            texture_view_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
1148
1149            // SVG Filter Engine Resources (initialized lazily on first use)
1150            blur_pipeline: None,
1151            blur_uniform: None,
1152            blur_bind_group_layout: None,
1153            blend_pipeline: None,
1154            blend_bind_group_layout: None,
1155            flood_pipeline: None,
1156            copy_bind_group_layout: None,
1157
1158            // Error tracking
1159            render_error_count: 0,
1160            has_fatal_error: false,
1161
1162            // Shadow map resources
1163            shadow_map_texture: Some(shadow_map_texture),
1164            shadow_map_view: Some(shadow_map_view),
1165            shadow_sampler: Some(shadow_sampler),
1166            shadow_light_vp: glam::Mat4::IDENTITY,
1167            shadow_map_size: 1024,
1168            shadow_bias: 0.005,
1169
1170            // 3D mesh staging
1171            pending_directional_light: None,
1172            pending_mesh_instances_3d: Vec::new(),
1173            pending_transparent_instances_3d: Vec::new(),
1174            pending_scene_radius: 100.0,
1175
1176            theme_stack: Vec::new(),
1177            portal_theme_stack: Vec::new(),
1178        }
1179    }
1180}