Skip to main content

cvkg_render_gpu/renderer/
mod.rs

1//! The main GpuRenderer struct and core frame lifecycle.
2use crate::heim::SkylinePacker;
3use crate::types::*;
4use crate::vertex::*;
5use cvkg_core::Rect;
6use cvkg_core::{ColorTheme, SceneUniforms};
7use lru::LruCache;
8use std::collections::VecDeque;
9use std::num::NonZeroUsize;
10use std::sync::Arc;
11
12// Re-export for test access
13pub use crate::subsystems::RendererConfig;
14
15pub(crate) mod pipelines;
16pub(crate) mod init;
17pub(crate) mod draw;
18pub(crate) mod svg;
19pub(crate) mod context_helpers;
20#[cfg(test)]
21pub(crate) mod tests;
22
23/// Material ID constants used in vertex `material_id` and DrawMaterial routing.
24/// These map to shader material indices and control per-draw-call pipeline selection.
25pub(crate) mod material_id {
26    /// Opaque geometry (default, depth-tested, no blending).
27    pub const OPAQUE: u32 = 0;
28    /// Ellipse shape (SDF circle, no blending).
29    pub const ELLIPSE: u32 = 4;
30    /// Top UI layer (alpha blended, no blur).
31    pub const TOP_UI: u32 = 6;
32    /// Glass / frosted blur material.
33    pub const GLASS: u32 = 7;
34    /// Blend modes occupy IDs 8..=22 (mapping to blend mode 1..=15).
35    pub const BLEND_START: u32 = 8;
36    pub const BLEND_END: u32 = 22;
37    /// Radial gradient (blend mode 9).
38    pub const RADIAL_GRADIENT: u32 = 16;
39    /// Squircle stroke / circular progress (blend mode 10).
40    pub const SQUIRCLE_STROKE: u32 = 17;
41    /// Drop shadow / glow SDF (blend mode 11).
42    pub const DROP_SHADOW: u32 = 18;
43    /// Dashed stroke (blend mode 12).
44    pub const DASHED_STROKE: u32 = 19;
45    /// 3D cube mesh (blend mode 14).
46    pub const MESH_3D: u32 = 21;
47}
48
49/// P1-10: Quality level for adaptive rendering on different GPU tiers.
50///
51/// `High` matches the previous hardcoded behavior (MSAA 4x).
52/// `Medium` reduces MSAA to 2x for moderate savings on mobile.
53/// `Low` disables MSAA entirely for low-end GPUs (Adreno 3xx, etc.).
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum QualityLevel {
56    High,
57    Medium,
58    Low,
59}
60
61impl QualityLevel {
62    /// Returns the MSAA sample count for this quality level.
63    pub fn msaa_sample_count(self) -> u32 {
64        match self {
65            QualityLevel::High => 4,
66            QualityLevel::Medium => 2,
67            QualityLevel::Low => 1,
68        }
69    }
70}
71
72impl Default for QualityLevel {
73    fn default() -> Self {
74        QualityLevel::High
75    }
76}
77
78/// GpuRenderer implements the high-performance GPU backend.
79pub struct GpuRenderer {
80    pub(crate) instance: Arc<wgpu::Instance>,
81    pub(crate) adapter: Arc<wgpu::Adapter>,
82    pub(crate) device: Arc<wgpu::Device>,
83    pub(crate) queue: Arc<wgpu::Queue>,
84
85    // Kvasir resource registry -- tracks GPU resource lifetimes
86    pub(crate) registry: crate::kvasir::registry::ResourceRegistry,
87
88    pub(crate) active_offscreens: Vec<crate::types::OffscreenEffectConfig>,
89    pub(crate) effect_pipelines: std::collections::HashMap<String, wgpu::RenderPipeline>,
90    pub(crate) effect_params_buffer: wgpu::Buffer,
91    pub(crate) effect_params_bind_group: wgpu::BindGroup,
92    pub(crate) linear_sampler: wgpu::Sampler,
93    // AI Generator Channel
94    pub ai_material_rx: Option<
95        std::sync::mpsc::Receiver<
96            Result<crate::material::CompiledMaterial, crate::ai::GeneratorError>,
97        >,
98    >,
99
100    // Multi-Window Surface Management
101    pub(crate) surfaces: std::collections::HashMap<winit::window::WindowId, SurfaceContext>,
102    pub(crate) current_window: Option<winit::window::WindowId>,
103    pub headless_context: Option<HeadlessContext>,
104
105    // Mega-Heim (Shared across all windows)
106    pub(crate) text: crate::types::TextSubsystem,
107    pub(crate) mega_heim_tex: wgpu::Texture,
108    pub(crate) mega_heim_bind_group: wgpu::BindGroup,
109    pub(crate) heim_packer: SkylinePacker,
110    pub(crate) image_uv_registry: LruCache<String, Rect>,
111    pub(crate) texture_registry: LruCache<String, u32>,
112    pub(crate) texture_views: Vec<wgpu::TextureView>,
113    pub(crate) dummy_sampler: wgpu::Sampler,
114    pub(crate) svg: crate::types::SvgSubsystem,
115
116    // Niflheim Resources (Shared)
117    pub(crate) dummy_texture_bind_group: wgpu::BindGroup,
118    pub(crate) dummy_env_bind_group: wgpu::BindGroup,
119    pub(crate) texture_bind_group_layout: wgpu::BindGroupLayout,
120    pub(crate) texture_bind_groups: Vec<wgpu::BindGroup>,
121    pub(crate) shared_elements: LruCache<String, cvkg_core::Rect>,
122
123    // The Forge's Anvil (GPU Buffers)
124    pub(crate) geometry_buffers: crate::types::GeometryBuffers,
125    pub(crate) vertices: Vec<Vertex>,
126    pub(crate) indices: Vec<u32>,
127    pub(crate) instance_data: Vec<InstanceData>,
128    pub(crate) staging_belt: wgpu::util::StagingBelt,
129    pub(crate) staging_command_buffers: Vec<wgpu::CommandBuffer>,
130    pub(crate) draw_calls: Vec<DrawCall>,
131    pub(crate) current_texture_id: Option<u32>,
132
133    // Opacity & Clip Stacks
134    pub(crate) opacity_stack: Vec<f32>,
135    pub(crate) clip_stack: Vec<Rect>,
136    pub(crate) slice_stack: Vec<(f32, f32)>,
137    pub(crate) shadow_stack: Vec<ShadowState>,
138
139    // SVG Filter Engine Resources
140    /// Render pipeline for Gaussian blur (two-pass separable kernel).
141    /// Initialized lazily on first use.
142    pub blur_pipeline: Option<wgpu::RenderPipeline>,
143    /// Uniform buffer for blur parameters (std_deviation, kernel_size, direction).
144    /// Initialized lazily on first use.
145    pub blur_uniform: Option<wgpu::Buffer>,
146    /// Bind group layout for blur shader.
147    /// Initialized lazily on first use.
148    pub blur_bind_group_layout: Option<wgpu::BindGroupLayout>,
149    /// Render pipeline for blend operations (feBlend, feComposite).
150    /// Initialized lazily on first use.
151    pub blend_pipeline: Option<wgpu::RenderPipeline>,
152    /// Bind group layout for blend shader.
153    /// Initialized lazily on first use.
154    pub blend_bind_group_layout: Option<wgpu::BindGroupLayout>,
155    /// Render pipeline for flood fill (feFlood).
156    /// Initialized lazily on first use.
157    pub flood_pipeline: Option<wgpu::RenderPipeline>,
158    /// Bind group layout for copy/offset operations.
159    /// Initialized lazily on first use.
160    pub copy_bind_group_layout: Option<wgpu::BindGroupLayout>,
161
162    // The Forge's Heart (Shared Berserker State)
163    pub(crate) theme_buffer: wgpu::Buffer,
164    pub(crate) scene_buffer: wgpu::Buffer,
165    pub(crate) berserker_bind_group: wgpu::BindGroup,
166    pub(crate) berserker_bind_group_layout: wgpu::BindGroupLayout,
167    pub(crate) start_time: std::time::Instant,
168    pub(crate) current_theme: ColorTheme,
169    pub(crate) current_scene: SceneUniforms,
170    pub(crate) current_z: f32,
171
172    /// Default background color for the canvas (RGBA).
173    /// Used when the app does not draw its own background.
174    /// Defaults to Deep Void [0.02, 0.02, 0.05, 1.0].
175    pub(crate) default_background_color: [f32; 4],
176
177    /// Whether the app drew any background geometry this frame.
178    /// If false, the renderer clears to default_background_color.
179    pub(crate) app_drew_background: bool,
180
181    /// Whether render_frame() was called this frame.
182    /// Used by end_frame() to auto-flush staging if render_frame() was skipped.
183    pub(crate) frame_rendered: bool,
184
185    /// Current draw order for SVG and other direct draw calls.
186    /// Set by draw_svg_with_order(), used by emit_draw_call().
187    pub(crate) current_draw_order: i32,
188
189    // Muspelheim Pipelines (Shared)
190    pub(crate) pipeline: wgpu::RenderPipeline,
191    /// Specialized opaque/2D material pipeline (modes 0-20 excluding 7,13-15,18,21).
192    pub(crate) opaque_pipeline: wgpu::RenderPipeline,
193    /// Non-multisampled pipeline used specifically to draw UI overlays.
194    /// Drawn with sample count 1 and no depth testing/depth stencil attachment.
195    pub(crate) ui_pipeline: wgpu::RenderPipeline,
196    /// Specialized glass material pipeline (mode 7 only, ~150 lines of complex math).
197    pub(crate) glass_pipeline: wgpu::RenderPipeline,
198    pub(crate) background_pipeline: wgpu::RenderPipeline,
199    pub(crate) bloom_extract_pipeline: wgpu::RenderPipeline,
200    /// Identity copy pipeline for Pass 2 backdrop blur (all pixels, no luminance gate).
201    pub(crate) copy_pipeline: wgpu::RenderPipeline,
202    pub(crate) composite_pipeline: wgpu::RenderPipeline,
203    /// Color blindness simulation pipeline (fullscreen triangle).
204    pub(crate) color_blind_pipeline: wgpu::RenderPipeline,
205    /// Volumetric raymarching pipeline (fullscreen triangle with SDF raymarch).
206    pub(crate) volumetric_pipeline: wgpu::RenderPipeline,
207    /// Volumetric bind group layout for scene uniforms (time/resolution/light).
208    pub(crate) volumetric_bind_group_layout: wgpu::BindGroupLayout,
209    /// Persistent uniform buffer for volumetric data (updated each frame).
210    pub(crate) volumetric_uniform_buffer: wgpu::Buffer,
211    /// Comparison sampler for volumetric depth comparison.
212    pub(crate) volumetric_depth_sampler: wgpu::Sampler,
213    /// CPU-side list of hologram instances submitted this frame.
214    /// Cleared each frame in reset_frame_state; consumed by VolumetricNode::execute.
215    pub(crate) hologram_instances: Vec<HologramInstance>,
216    /// Kawase blur pyramid downsample pipeline (separate shader module).
217    pub(crate) kawase_down_pipeline: wgpu::RenderPipeline,
218    /// Kawase blur pyramid upsample pipeline (separate shader module).
219    pub(crate) kawase_up_pipeline: wgpu::RenderPipeline,
220    /// Kawase blur bind group layout (uniform + texture + sampler).
221    pub(crate) kawase_bind_group_layout: wgpu::BindGroupLayout,
222    /// Persistent uniform buffer for Kawase blur operations (avoids per-frame allocation).
223    pub(crate) kawase_uniform: wgpu::Buffer,
224    /// Pool of persistent uniform buffers for Kawase blur operations.
225    pub(crate) kawase_uniform_buffers: Vec<wgpu::Buffer>,
226    /// Environment bind group layout (texture + sampler).
227    pub(crate) env_bind_group_layout: wgpu::BindGroupLayout,
228
229    // Telemetry
230    pub telemetry: cvkg_core::TelemetryData,
231
232    /// Pipeline cache for disk-persisted compiled shaders when the adapter exposes PIPELINE_CACHE.
233    /// None means pipelines compile normally without a disk cache.
234    pub(crate) pipeline_cache: Option<wgpu::PipelineCache>,
235
236    /// Configuration for render-loop frame timing and degradation strategies.
237    pub frame_budget: cvkg_core::FrameBudget,
238    /// Staging buffer for windowed frame capture.
239    pub(crate) capture_staging_buffer: Option<wgpu::Buffer>,
240    /// Instant at the start of the last redraw, used for measuring frame timings.
241    pub last_redraw_start: std::time::Instant,
242    /// Instant at the start of the last frame, used for frame_time_ms calculation.
243    pub last_frame_start: std::time::Instant,
244
245    // VRAM Tracking (Bytes)
246    pub(crate) vram_buffers_bytes: u64,
247    pub(crate) vram_textures_bytes: u64,
248
249    // Debugging
250    pub(crate) _debug_layout: bool,
251
252    // Transform Stack -- stores full affine matrices for correct SVG transform composition.
253    pub(crate) transform_stack: Vec<glam::Mat3>,
254    /// Whether a redraw has been requested for the next frame.
255    pub redraw_requested: bool,
256    /// Cursor for compositor draw call submission tracking.
257    pub(crate) compositor_index_cursor: u32,
258
259    /// Bloom post-processing enabled flag.
260    pub bloom_enabled: bool,
261    /// Dynamic toggle to enable or disable the volumetric raymarching pass, which handles fog and light shaft simulations.
262    pub volumetric_enabled: bool,
263
264    // Path Geometry Cache — avoids re-tessellating static paths every frame.
265    pub(crate) path_geometry_cache: lru::LruCache<u64, (Vec<Vertex>, Vec<u32>)>,
266    /// Color blindness bind group layout (texture + sampler + uniform).
267    pub(crate) color_blind_bind_group_layout: wgpu::BindGroupLayout,
268    /// Color blindness uniform buffer (updated each frame when mode changes).
269    pub(crate) color_blind_uniform_buffer: wgpu::Buffer,
270    /// Color blindness simulation mode (Normal = disabled).
271    pub color_blind_mode: crate::color_blindness::ColorBlindMode,
272    /// Color blindness effect intensity (0.0–1.0).
273    pub color_blind_intensity: f32,
274    /// Sampler for the color blindness pass (reused from main pipeline).
275    pub(crate) sampler: wgpu::Sampler,
276
277    // Timestamp Queries (Norse: Skuld = future/time/debt)
278    pub(crate) skuld_queries: Option<wgpu::QuerySet>,
279    pub(crate) skuld_buffer: Option<wgpu::Buffer>,
280    pub(crate) skuld_read_buffer: Option<wgpu::Buffer>,
281    pub(crate) skuld_period: f32,
282    pub last_gpu_time_ns: u64,
283
284    // Particle Compute Pipeline (Muspelheim Compute)
285    pub(crate) particle_compute_pipeline: wgpu::ComputePipeline,
286    pub(crate) particle_compute_bgl: wgpu::BindGroupLayout,
287    pub(crate) particle_buffer: wgpu::Buffer,
288    pub(crate) particle_uniform_buffer: wgpu::Buffer,
289    pub(crate) particles: crate::types::ParticleSubsystem,
290    pub(crate) particle_render_pipeline: wgpu::RenderPipeline,
291    pub(crate) particle_render_bgl: wgpu::BindGroupLayout,
292    pub(crate) particle_render_bind_group: Option<wgpu::BindGroup>,
293    pub(crate) particle_compute_bind_group: Option<wgpu::BindGroup>,
294
295    // VDOM node stack for hierarchy tracking
296    pub(crate) vnode_stack: Vec<(Rect, &'static str)>,
297
298    /// Event handlers registered during render passes.
299    pub(crate) event_handlers: std::collections::HashMap<
300        String,
301        Vec<std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>>,
302    >,
303
304    /// Bind group layout for reading blur output in glass composite pass.
305    pub(crate) glass_output_bind_group_layout: wgpu::BindGroupLayout,
306    /// Current material state -- draw calls are tagged with this material.
307    pub(crate) current_draw_material: cvkg_core::DrawMaterial,
308
309    /// Portal backdrop blur regions -- collected during portal enter/exit
310    pub(crate) portal_regions: std::collections::VecDeque<cvkg_core::Rect>,
311
312    /// Cache of the compiled Kvasir render graph execution plan.
313    pub(crate) cached_graph_plan: Option<crate::kvasir::graph_cache::CachedGraphPlan>,
314    /// Hash of the active material set, used to invalidate the graph plan
315    pub(crate) material_compilation_hash: u64,
316    /// Memoization cache for frame-level render skipping.
317    pub(crate) memo_cache: std::collections::HashMap<u64, crate::types::MemoEntry>,
318    /// Current frame generation counter.
319    pub(crate) frame_generation: u64,
320    /// P1-1: GpuRenderer configuration.
321    pub(crate) config: crate::subsystems::RendererConfig,
322    /// P1-10: Quality level controlling MSAA sample count.
323    pub(crate) quality_level: QualityLevel,
324    /// Thread-safe bind group cache to avoid per-frame allocations during render passes.
325    pub(crate) bind_group_cache: std::sync::Mutex<
326        std::collections::HashMap<
327            (crate::kvasir::resource::ResourceId, u32, bool),
328            wgpu::BindGroup,
329        >,
330    >,
331    /// Thread-safe texture view cache to avoid per-frame allocations of TextureViews.
332    pub(crate) texture_view_cache: std::sync::Mutex<
333        std::collections::HashMap<(crate::kvasir::resource::ResourceId, u32), wgpu::TextureView>,
334    >,
335}
336
337#[cfg(target_arch = "wasm32")]
338unsafe impl Send for GpuRenderer {}
339#[cfg(target_arch = "wasm32")]
340unsafe impl Sync for GpuRenderer {}
341
342/// Per-hologram instance data submitted during the frame.
343#[derive(Debug, Clone)]
344pub struct HologramInstance {
345    /// Bounding rectangle in logical coordinates (x, y, width, height).
346    pub rect: cvkg_core::Rect,
347    /// Hash of the hologram_id string -- used for per-hologram visual variation.
348    pub id_hash: u32,
349    /// Application-provided time for this hologram instance.
350    pub time: f32,
351}
352
353/// Trait for types that can be cleared in place. Implemented for the
354/// collection types used as cache values (HashMap, Vec).
355pub trait ClearInto {
356    fn clear_into(&mut self);
357}
358
359impl<K, V, S> ClearInto for std::collections::HashMap<K, V, S>
360where
361    S: std::hash::BuildHasher,
362{
363    fn clear_into(&mut self) {
364        self.clear();
365    }
366}
367
368impl<T> ClearInto for Vec<T> {
369    fn clear_into(&mut self) {
370        self.clear();
371    }
372}
373
374// =========================================================================
375// P1-11: Pipeline cache integrity check
376// =========================================================================
377
378/// P1-11 fix: load a pipeline cache file from disk with SHA256 integrity check.
379fn load_pipeline_cache_with_integrity_check(
380    cache_path: &std::path::Path,
381) -> Result<Option<Vec<u8>>, String> {
382    let cache_data = match std::fs::read(cache_path) {
383        Ok(d) => d,
384        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
385        Err(e) => return Err(format!("read failed: {e}")),
386    };
387
388    let hash_path = cache_path.with_extension("bin.sha256");
389    let expected_hash = match std::fs::read_to_string(&hash_path) {
390        Ok(s) => s.trim().to_lowercase(),
391        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
392            return Err(format!(
393                "sidecar hash file missing at {}",
394                hash_path.display()
395            ))
396        }
397        Err(e) => return Err(format!("sidecar read failed: {e}")),
398    };
399
400    let actual = compute_sha256(&cache_data);
401    let actual_hex: String = actual.iter().map(|b| format!("{:02x}", b)).collect();
402    if actual_hex != expected_hash {
403        return Err(format!(
404            "hash mismatch: expected {expected_hash}, got {actual_hex}"
405        ));
406    }
407
408    Ok(Some(cache_data))
409}
410
411/// Compute SHA256 of a byte slice. Inline FIPS 180-4 implementation
412fn compute_sha256(data: &[u8]) -> [u8; 32] {
413    let mut hasher = Sha256::new();
414    hasher.update(data);
415    hasher.finalize()
416}
417
418/// Minimal SHA256 implementation (FIPS 180-4). Used only for the
419/// pipeline cache integrity check so we don't add a sha2 dependency.
420#[derive(Clone)]
421struct Sha256 {
422    state: [u32; 8],
423    buffer: [u8; 64],
424    buffer_len: usize,
425    total_len: u64,
426}
427
428impl Sha256 {
429    const K: [u32; 64] = [
430        0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
431        0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
432        0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
433        0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
434        0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
435        0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
436        0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
437        0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
438        0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
439        0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
440        0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
441        0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
442        0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
443        0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
444        0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
445        0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
446    ];
447
448    fn new() -> Self {
449        Self {
450            state: [
451                0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
452                0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
453            ],
454            buffer: [0; 64],
455            buffer_len: 0,
456            total_len: 0,
457        }
458    }
459
460    fn update(&mut self, data: &[u8]) {
461        self.total_len = self.total_len.wrapping_add(data.len() as u64);
462        for &b in data {
463            self.buffer[self.buffer_len] = b;
464            self.buffer_len += 1;
465            if self.buffer_len == 64 {
466                let block = self.buffer;
467                self.compress(&block);
468                self.buffer_len = 0;
469            }
470        }
471    }
472
473    fn finalize(mut self) -> [u8; 32] {
474        self.buffer[self.buffer_len] = 0x80;
475        self.buffer_len += 1;
476        if self.buffer_len > 56 {
477            for b in &mut self.buffer[self.buffer_len..] { *b = 0; }
478            let block = self.buffer;
479            self.compress(&block);
480            self.buffer_len = 0;
481        }
482        for b in &mut self.buffer[self.buffer_len..56] { *b = 0; }
483        let bit_len = self.total_len.wrapping_mul(8);
484        self.buffer[56..64].copy_from_slice(&bit_len.to_be_bytes());
485        let block = self.buffer;
486        self.compress(&block);
487
488        let mut out = [0u8; 32];
489        for (i, &s) in self.state.iter().enumerate() {
490            out[i*4..(i+1)*4].copy_from_slice(&s.to_be_bytes());
491        }
492        out
493    }
494
495    fn compress(&mut self, block: &[u8]) {
496        let mut w = [0u32; 64];
497        for i in 0..16 {
498            w[i] = u32::from_be_bytes([
499                block[i*4], block[i*4+1], block[i*4+2], block[i*4+3]
500            ]);
501        }
502        for i in 16..64 {
503            let s0 = w[i-15].rotate_right(7) ^ w[i-15].rotate_right(18) ^ (w[i-15] >> 3);
504            let s1 = w[i-2].rotate_right(17) ^ w[i-2].rotate_right(19) ^ (w[i-2] >> 10);
505            w[i] = w[i-16].wrapping_add(s0).wrapping_add(w[i-7]).wrapping_add(s1);
506        }
507        let mut a = self.state[0];
508        let mut b = self.state[1];
509        let mut c = self.state[2];
510        let mut d = self.state[3];
511        let mut e = self.state[4];
512        let mut f = self.state[5];
513        let mut g = self.state[6];
514        let mut h = self.state[7];
515        for i in 0..64 {
516            let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
517            let ch = (e & f) ^ ((!e) & g);
518            let t1 = h.wrapping_add(s1).wrapping_add(ch).wrapping_add(Self::K[i]).wrapping_add(w[i]);
519            let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
520            let mj = (a & b) ^ (a & c) ^ (b & c);
521            let t2 = s0.wrapping_add(mj);
522            h = g; g = f; f = e;
523            e = d.wrapping_add(t1);
524            d = c; c = b; b = a;
525            a = t1.wrapping_add(t2);
526        }
527        self.state[0] = self.state[0].wrapping_add(a);
528        self.state[1] = self.state[1].wrapping_add(b);
529        self.state[2] = self.state[2].wrapping_add(c);
530        self.state[3] = self.state[3].wrapping_add(d);
531        self.state[4] = self.state[4].wrapping_add(e);
532        self.state[5] = self.state[5].wrapping_add(f);
533        self.state[6] = self.state[6].wrapping_add(g);
534        self.state[7] = self.state[7].wrapping_add(h);
535    }
536}
537
538fn compute_mip_levels(width: u32, height: u32) -> u32 {
539    let max_dim = width.max(height);
540    if max_dim <= 1 {
541        return 1;
542    }
543    let mips = (32 - max_dim.leading_zeros()).clamp(2, 8);
544    mips
545}
546
547impl GpuRenderer {
548    /// Access the hologram instances submitted this frame.
549    pub fn hologram_instances(&self) -> &[HologramInstance] {
550        &self.hologram_instances
551    }
552
553    pub fn set_quality_level(&mut self, level: QualityLevel) {
554        self.quality_level = level;
555    }
556
557    pub fn set_config(&mut self, config: crate::subsystems::RendererConfig) {
558        self.config = config;
559    }
560
561    pub fn config(&self) -> &crate::subsystems::RendererConfig {
562        &self.config
563    }
564
565    pub fn quality_level(&self) -> QualityLevel {
566        self.quality_level
567    }
568
569    pub(crate) fn lock_or_clear_cache<'a, T: ClearInto>(
570        lock: &'a std::sync::Mutex<T>,
571    ) -> std::sync::MutexGuard<'a, T> {
572        match lock.lock() {
573            Ok(guard) => guard,
574            Err(poisoned) => {
575                log::warn!("[GPU] lock_or_clear_cache: mutex poisoned, clearing cache...");
576                let mut guard = poisoned.into_inner();
577                guard.clear_into();
578                guard
579            }
580        }
581    }
582
583    pub fn update_mouse(&mut self, mouse: [f32; 2], velocity: [f32; 2]) {
584        self.current_scene.mouse = mouse;
585        self.current_scene.mouse_velocity = velocity;
586        self.queue.write_buffer(
587            &self.scene_buffer,
588            0,
589            bytemuck::bytes_of(&self.current_scene),
590        );
591    }
592
593    pub fn invalidate_material_cache(&mut self) {
594        self.cached_graph_plan = None;
595    }
596
597    pub fn invalidate_all_caches(&mut self) -> usize {
598        let mut cleared = 0;
599        {
600            let mut bg_cache = Self::lock_or_clear_cache(&self.bind_group_cache);
601            cleared += bg_cache.len();
602            bg_cache.clear();
603        }
604        {
605            let mut view_cache = Self::lock_or_clear_cache(&self.texture_view_cache);
606            cleared += view_cache.len();
607            view_cache.clear();
608        }
609        cleared += self.text.shaped_cache.len();
610        self.text.shaped_cache.clear();
611        cleared += self.svg.model_cache.len();
612        self.svg.model_cache.clear();
613        cleared += self.svg.tree_cache.len();
614        self.svg.tree_cache.clear();
615        self.svg.clear_filter_batches();
616        cleared
617    }
618
619    pub fn prewarm_text_cache(&mut self, labels: &[(&str, f32)]) {
620        let mut count = 0;
621        for (text, size) in labels {
622            let cache_key = (text.to_string(), (size * 100.0) as u32);
623            if self.text.shaped_cache.contains(&cache_key) {
624                continue;
625            }
626            let style = cvkg_runic_text::TextStyle::new("Inter", *size);
627            let spans = [cvkg_runic_text::TextSpan::new(text, style)];
628            if let Some(shaped) = self.text.engine.shape_layout(
629                &spans,
630                None,
631                cvkg_runic_text::TextAlign::Start,
632                cvkg_runic_text::TextOverflow::Visible,
633            ).ok() {
634                self.text.shaped_cache.put(cache_key, std::sync::Arc::new(shaped));
635                count += 1;
636            }
637        }
638        if count > 0 {
639            log::info!("[Surtr] prewarm_text_cache: pre-shaped {} labels", count);
640        }
641    }
642
643    pub(crate) fn select_best_surface_format(
644        formats: &[wgpu::TextureFormat],
645    ) -> wgpu::TextureFormat {
646        if formats.is_empty() {
647            return wgpu::TextureFormat::Rgba8Unorm;
648        }
649        let preferred_formats = [
650            wgpu::TextureFormat::Rgba16Float,
651            wgpu::TextureFormat::Rgba8Unorm,
652            wgpu::TextureFormat::Bgra8UnormSrgb,
653            wgpu::TextureFormat::Rgba8UnormSrgb,
654            wgpu::TextureFormat::Bgra8Unorm,
655            wgpu::TextureFormat::Rgba8Unorm,
656            wgpu::TextureFormat::Rgba8Unorm,
657        ];
658        for preferred in &preferred_formats {
659            if formats.contains(preferred) {
660                return *preferred;
661            }
662        }
663        if formats.contains(&wgpu::TextureFormat::Rgba8Unorm) {
664            return wgpu::TextureFormat::Rgba8Unorm;
665        }
666        formats[0]
667    }
668
669    pub(crate) fn rebuild_texture_array_bind_group(&mut self) {
670        let views: Vec<&wgpu::TextureView> = self.texture_views.iter().collect();
671        self.mega_heim_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
672            layout: &self.texture_bind_group_layout,
673            entries: &[
674                wgpu::BindGroupEntry {
675                    binding: 0,
676                    resource: wgpu::BindingResource::TextureViewArray(&views),
677                },
678                wgpu::BindGroupEntry {
679                    binding: 1,
680                    resource: wgpu::BindingResource::Sampler(&self.dummy_sampler),
681                },
682            ],
683            label: Some("Mega-Heim Rebuilt Bind Group"),
684        });
685    }
686
687    pub(crate) fn update_vram_telemetry(&mut self) {
688        let buffers = self.geometry_buffers.vertex_buffer.size()
689            + self.geometry_buffers.index_buffer.size()
690            + self.geometry_buffers.instance_buffer.size()
691            + self.scene_buffer.size()
692            + self.theme_buffer.size()
693            + self.particle_buffer.size()
694            + self.particle_uniform_buffer.size();
695
696        let mut textures = self.config.mega_heim_vram_bytes();
697        textures += 1 * 1 * 4; // Dummy texture
698
699        for surface in self.surfaces.values() {
700            let width = surface.config.width;
701            let height = surface.config.height;
702            let format_bytes = 8; // Rgba16Float
703            textures += (width * height * format_bytes) as u64; // Scene texture
704            textures += (width * height * format_bytes * self.quality_level.msaa_sample_count() as u32) as u64; // MSAA texture
705            textures += (width * height * 4) as u64; // Depth texture (Depth32Float)
706
707            let blur_width = (width / 2).max(1);
708            let blur_height = (height / 2).max(1);
709            let blur_bytes = (blur_width * blur_height * 4) as u64;
710            textures += blur_bytes * 4; // 2x blur + 2x bloom textures
711        }
712
713        if let Some(ref ctx) = self.headless_context {
714            let format_bytes = 8; // Rgba16Float
715            textures += (ctx.width * ctx.height * format_bytes) as u64; // Scene texture
716            textures += (ctx.width * ctx.height * format_bytes * self.quality_level.msaa_sample_count() as u32) as u64; // MSAA texture
717            textures += (ctx.width * ctx.height * 4) as u64; // Depth texture
718            textures += (ctx.width * ctx.height * 4) as u64; // Output texture
719        }
720
721        self.vram_buffers_bytes = buffers;
722        self.vram_textures_bytes = textures;
723        self.telemetry.vram_usage_mb = (buffers + textures) as f32 / (1024.0 * 1024.0);
724    }
725
726    pub fn get_telemetry(&self) -> cvkg_core::TelemetryData {
727        self.telemetry.clone()
728    }
729
730    pub fn resize(
731        &mut self,
732        window_id: winit::window::WindowId,
733        width: u32,
734        height: u32,
735        scale_factor: f32,
736    ) {
737        if width > 0
738            && height > 0
739            && let Some(ctx) = self.surfaces.get_mut(&window_id)
740        {
741            if ctx.config.width == width && ctx.config.height == height {
742                return;
743            }
744
745            log::info!("[GPU] Reconfiguring surface: {}x{}", width, height);
746            GpuRenderer::lock_or_clear_cache(&self.bind_group_cache).clear();
747            GpuRenderer::lock_or_clear_cache(&self.texture_view_cache).clear();
748            self.text.shaped_cache.clear();
749            ctx.config.width = width;
750            ctx.config.height = height;
751            ctx.scale_factor = scale_factor;
752            ctx.surface.configure(&self.device, &ctx.config);
753
754            let texture_desc = wgpu::TextureDescriptor {
755                label: Some("Surtr Scene Texture"),
756                size: wgpu::Extent3d {
757                    width,
758                    height,
759                    depth_or_array_layers: 1,
760                },
761                mip_level_count: 1,
762                sample_count: 1,
763                dimension: wgpu::TextureDimension::D2,
764                format: wgpu::TextureFormat::Rgba16Float,
765                usage: wgpu::TextureUsages::RENDER_ATTACHMENT
766                    | wgpu::TextureUsages::TEXTURE_BINDING,
767                view_formats: &[],
768            };
769
770            let scene_tex = self.device.create_texture(&texture_desc);
771
772            let msaa_desc = wgpu::TextureDescriptor {
773                label: Some("Scene MSAA"),
774                size: texture_desc.size,
775                mip_level_count: 1,
776                sample_count: self.quality_level.msaa_sample_count(),
777                dimension: wgpu::TextureDimension::D2,
778                format: wgpu::TextureFormat::Rgba16Float,
779                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
780                view_formats: &[],
781            };
782            let scene_msaa_tex = self.device.create_texture(&msaa_desc);
783            ctx.scene_texture = scene_tex.create_view(&wgpu::TextureViewDescriptor::default());
784            ctx.scene_msaa_texture =
785                scene_msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
786
787            self.registry.remove_image(ctx.blur_tex_a);
788            self.registry.remove_image(ctx.blur_tex_b);
789            self.registry.remove_image(ctx.bloom_tex_a);
790            self.registry.remove_image(ctx.bloom_tex_b);
791
792            let blur_width = (width / 2).max(1);
793            let blur_height = (height / 2).max(1);
794
795            let blur_desc_a = crate::kvasir::resource::ResourceDescriptor {
796                label: Some("Surtr Blur Texture A".into()),
797                kind: crate::kvasir::resource::ResourceKind::Image {
798                    format: ctx.config.format,
799                    width: blur_width,
800                    height: blur_height,
801                    mip_level_count: compute_mip_levels(blur_width, blur_height),
802                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT
803                        | wgpu::TextureUsages::TEXTURE_BINDING
804                        | wgpu::TextureUsages::COPY_SRC,
805                },
806                lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
807            };
808            ctx.blur_tex_a = self.registry.allocate_image(&self.device, &blur_desc_a);
809
810            let blur_desc_b = crate::kvasir::resource::ResourceDescriptor {
811                label: Some("Surtr Blur Texture B".into()),
812                kind: crate::kvasir::resource::ResourceKind::Image {
813                    format: ctx.config.format,
814                    width: blur_width,
815                    height: blur_height,
816                    mip_level_count: compute_mip_levels(blur_width, blur_height),
817                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT
818                        | wgpu::TextureUsages::TEXTURE_BINDING
819                        | wgpu::TextureUsages::COPY_SRC,
820                },
821                lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
822            };
823            ctx.blur_tex_b = self.registry.allocate_image(&self.device, &blur_desc_b);
824
825            let bloom_desc_a = crate::kvasir::resource::ResourceDescriptor {
826                label: Some("Surtr Bloom Texture A".into()),
827                kind: crate::kvasir::resource::ResourceKind::Image {
828                    format: ctx.config.format,
829                    width: blur_width,
830                    height: blur_height,
831                    mip_level_count: compute_mip_levels(blur_width, blur_height),
832                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT
833                        | wgpu::TextureUsages::TEXTURE_BINDING
834                        | wgpu::TextureUsages::COPY_SRC,
835                },
836                lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
837            };
838            ctx.bloom_tex_a = self.registry.allocate_image(&self.device, &bloom_desc_a);
839
840            let bloom_desc_b = crate::kvasir::resource::ResourceDescriptor {
841                label: Some("Surtr Bloom Texture B".into()),
842                kind: crate::kvasir::resource::ResourceKind::Image {
843                    format: ctx.config.format,
844                    width: blur_width,
845                    height: blur_height,
846                    mip_level_count: compute_mip_levels(blur_width, blur_height),
847                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT
848                        | wgpu::TextureUsages::TEXTURE_BINDING
849                        | wgpu::TextureUsages::COPY_SRC,
850                },
851                lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
852            };
853            ctx.bloom_tex_b = self.registry.allocate_image(&self.device, &bloom_desc_b);
854
855            ctx.scene_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
856                layout: &self.env_bind_group_layout,
857                entries: &[
858                    wgpu::BindGroupEntry {
859                        binding: 0,
860                        resource: wgpu::BindingResource::TextureView(&ctx.scene_texture),
861                    },
862                    wgpu::BindGroupEntry {
863                        binding: 1,
864                        resource: wgpu::BindingResource::Sampler(&ctx.sampler),
865                    },
866                ],
867                label: Some("Scene Bind Group Resize"),
868            });
869
870            let scene_views: Vec<&wgpu::TextureView> =
871                (0..32).map(|_| &ctx.scene_texture).collect();
872            ctx.scene_texture_bind_group =
873                self.device.create_bind_group(&wgpu::BindGroupDescriptor {
874                    layout: &self.texture_bind_group_layout,
875                    entries: &[
876                        wgpu::BindGroupEntry {
877                            binding: 0,
878                            resource: wgpu::BindingResource::TextureViewArray(&scene_views),
879                        },
880                        wgpu::BindGroupEntry {
881                            binding: 1,
882                            resource: wgpu::BindingResource::Sampler(&ctx.sampler),
883                        },
884                    ],
885                    label: Some("Scene Texture Bind Group Resize"),
886                });
887
888            let depth_texture = self.device.create_texture(&wgpu::TextureDescriptor {
889                label: Some("Surtr Depth Texture"),
890                size: wgpu::Extent3d {
891                    width,
892                    height,
893                    depth_or_array_layers: 1,
894                },
895                mip_level_count: 1,
896                sample_count: self.quality_level.msaa_sample_count(),
897                dimension: wgpu::TextureDimension::D2,
898                format: wgpu::TextureFormat::Depth32Float,
899                usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
900                view_formats: &[],
901            });
902            ctx.depth_texture_view =
903                depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
904        }
905    }
906
907    pub fn reset_time(&mut self) {
908        self.start_time = std::time::Instant::now();
909    }
910
911    pub fn reclaim_vram(&mut self) {
912        log::warn!("[GPU] Sundr Compaction: Compacting Mega-Heim...");
913
914        let new_mega_heim_tex = self.device.create_texture(&wgpu::TextureDescriptor {
915            label: Some("Sundr Mega-Heim (Compacted)"),
916            size: wgpu::Extent3d {
917                width: 4096,
918                height: 4096,
919                depth_or_array_layers: 1,
920            },
921            mip_level_count: 1,
922            sample_count: 1,
923            dimension: wgpu::TextureDimension::D2,
924            format: wgpu::TextureFormat::Rgba8UnormSrgb,
925            usage: wgpu::TextureUsages::TEXTURE_BINDING
926                | wgpu::TextureUsages::COPY_DST
927                | wgpu::TextureUsages::COPY_SRC,
928            view_formats: &[],
929        });
930
931        let mut new_packer = SkylinePacker::new(4096, 4096);
932        let mut encoder = self
933            .device
934            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
935                label: Some("Heim Compaction Encoder"),
936            });
937
938        let image_entries: Vec<(String, Rect)> = self
939            .image_uv_registry
940            .iter()
941            .map(|(k, v)| (k.clone(), *v))
942            .collect();
943        for (name, old_uv) in image_entries {
944            if let Some(&tex_idx) = self.texture_registry.get(&name)
945                && tex_idx == 0
946            {
947                let w_px = (old_uv.width * 4096.0).round() as u32;
948                let h_px = (old_uv.height * 4096.0).round() as u32;
949                let old_x_px = (old_uv.x * 4096.0).round() as u32;
950                let old_y_px = (old_uv.y * 4096.0).round() as u32;
951
952                if let Some((new_x, new_y)) = new_packer.pack(w_px, h_px) {
953                    encoder.copy_texture_to_texture(
954                        wgpu::TexelCopyTextureInfo {
955                            texture: &self.mega_heim_tex,
956                            mip_level: 0,
957                            origin: wgpu::Origin3d {
958                                x: old_x_px,
959                                y: old_y_px,
960                                z: 0,
961                            },
962                            aspect: wgpu::TextureAspect::All,
963                        },
964                        wgpu::TexelCopyTextureInfo {
965                            texture: &new_mega_heim_tex,
966                            mip_level: 0,
967                            origin: wgpu::Origin3d {
968                                x: new_x,
969                                y: new_y,
970                                z: 0,
971                            },
972                            aspect: wgpu::TextureAspect::All,
973                        },
974                        wgpu::Extent3d {
975                            width: w_px,
976                            height: h_px,
977                            depth_or_array_layers: 1,
978                        },
979                    );
980
981                    let new_uv = Rect {
982                        x: new_x as f32 / 4096.0,
983                        y: new_y as f32 / 4096.0,
984                        width: old_uv.width,
985                        height: old_uv.height,
986                    };
987                    self.image_uv_registry.put(name.clone(), new_uv);
988                }
989            }
990        }
991
992        let text_entries: Vec<(u64, (Rect, f32, f32, f32, f32))> =
993            self.text.glyph_cache.iter().map(|(k, v)| (*k, *v)).collect();
994        for (hash, (old_uv, w_f, h_f, x_off, y_off)) in text_entries {
995            let w_px = (old_uv.width * 4096.0).round() as u32;
996            let h_px = (old_uv.height * 4096.0).round() as u32;
997            let old_x_px = (old_uv.x * 4096.0).round() as u32;
998            let old_y_px = (old_uv.y * 4096.0).round() as u32;
999
1000            if let Some((new_x, new_y)) = new_packer.pack(w_px, h_px) {
1001                encoder.copy_texture_to_texture(
1002                    wgpu::TexelCopyTextureInfo {
1003                        texture: &self.mega_heim_tex,
1004                        mip_level: 0,
1005                        origin: wgpu::Origin3d {
1006                            x: old_x_px,
1007                            y: old_y_px,
1008                            z: 0,
1009                        },
1010                        aspect: wgpu::TextureAspect::All,
1011                    },
1012                    wgpu::TexelCopyTextureInfo {
1013                        texture: &new_mega_heim_tex,
1014                        mip_level: 0,
1015                        origin: wgpu::Origin3d {
1016                            x: new_x,
1017                            y: new_y,
1018                            z: 0,
1019                        },
1020                        aspect: wgpu::TextureAspect::All,
1021                    },
1022                    wgpu::Extent3d {
1023                        width: w_px,
1024                        height: h_px,
1025                        depth_or_array_layers: 1,
1026                    },
1027                );
1028
1029                let new_uv = Rect {
1030                    x: new_x as f32 / 4096.0,
1031                    y: new_y as f32 / 4096.0,
1032                    width: old_uv.width,
1033                    height: old_uv.height,
1034                };
1035                self.text.glyph_cache.put(hash, (new_uv, w_f, h_f, x_off, y_off));
1036            }
1037        }
1038
1039        self.queue.submit(std::iter::once(encoder.finish()));
1040
1041        self.mega_heim_tex = new_mega_heim_tex;
1042        let mega_heim_view_obj = self
1043            .mega_heim_tex
1044            .create_view(&wgpu::TextureViewDescriptor::default());
1045        self.texture_views[0] = mega_heim_view_obj.clone();
1046
1047        self.rebuild_texture_array_bind_group();
1048
1049        if !self.texture_bind_groups.is_empty() {
1050            self.texture_bind_groups[0] = self.mega_heim_bind_group.clone();
1051        }
1052
1053        self.heim_packer = new_packer;
1054        self.telemetry.vram_exhausted = false;
1055    }
1056}
1057
1058impl Drop for GpuRenderer {
1059    fn drop(&mut self) {
1060        let cache_dir = std::env::current_exe()
1061            .ok()
1062            .and_then(|p| p.parent().map(|d| d.join("pipeline_cache")))
1063            .unwrap_or_else(|| std::env::temp_dir().join("cvkg_pipeline_cache"));
1064        let _ = std::fs::create_dir_all(&cache_dir);
1065        let cache_path = cache_dir.join("cvkg_render_gpu.bin");
1066        if let Some(cache) = &self.pipeline_cache {
1067            if let Some(data) = cache.get_data() {
1068                if let Err(e) = std::fs::write(&cache_path, data) {
1069                    log::warn!("Failed to persist pipeline cache: {}", e);
1070                }
1071            }
1072        }
1073
1074        let _ = self.device.poll(wgpu::PollType::Wait {
1075            submission_index: None,
1076            timeout: None,
1077        });
1078    }
1079}
1080
1081impl GpuRenderer {
1082    pub(crate) fn current_width(&self) -> u32 {
1083        if let Some(id) = self.current_window {
1084            self.surfaces.get(&id).map(|s| s.config.width).unwrap_or(1)
1085        } else {
1086            self.headless_context.as_ref().map(|h| h.width).unwrap_or(1)
1087        }
1088    }
1089
1090    pub(crate) fn current_height(&self) -> u32 {
1091        if let Some(id) = self.current_window {
1092            self.surfaces.get(&id).map(|s| s.config.height).unwrap_or(1)
1093        } else {
1094            self.headless_context
1095                .as_ref()
1096                .map(|h| h.height)
1097                .unwrap_or(1)
1098        }
1099    }
1100
1101    pub(crate) fn current_scale_factor(&self) -> f32 {
1102        if let Some(id) = self.current_window {
1103            self.surfaces
1104                .get(&id)
1105                .map(|s| s.scale_factor)
1106                .unwrap_or(1.0)
1107        } else {
1108            self.headless_context
1109                .as_ref()
1110                .map(|h| h.scale_factor)
1111                .unwrap_or(1.0)
1112        }
1113    }
1114
1115    pub(crate) fn current_time(&self) -> f32 {
1116        self.start_time.elapsed().as_secs_f32()
1117    }
1118
1119    /// forge_headless -- Initializes Surtr without a window for visual regression testing.
1120    pub async fn forge_headless(width: u32, height: u32) -> Self {
1121        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
1122            backends: wgpu::Backends::all(),
1123            flags: wgpu::InstanceFlags::default(),
1124            backend_options: wgpu::BackendOptions::default(),
1125            display: None,
1126            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
1127        });
1128
1129        // Request adapter with robust multi-stage fallback for Bumblebee/Optimus compatibility
1130        log::info!("[GPU] Requesting HighPerformance adapter (headless)...");
1131        let mut adapter = instance
1132            .request_adapter(&wgpu::RequestAdapterOptions {
1133                power_preference: wgpu::PowerPreference::HighPerformance,
1134                compatible_surface: None,
1135                force_fallback_adapter: false,
1136            })
1137            .await
1138            .ok();
1139
1140        if adapter.is_none() {
1141            log::warn!(
1142                "[GPU] HighPerformance adapter failed (possible Bumblebee/Optimus), trying LowPower..."
1143            );
1144            adapter = instance
1145                .request_adapter(&wgpu::RequestAdapterOptions {
1146                    power_preference: wgpu::PowerPreference::LowPower,
1147                    compatible_surface: None,
1148                    force_fallback_adapter: false,
1149                })
1150                .await
1151                .ok();
1152        }
1153
1154        if adapter.is_none() {
1155            log::warn!("[GPU] Hardware adapters failed, trying Software fallback...");
1156            adapter = instance
1157                .request_adapter(&wgpu::RequestAdapterOptions {
1158                    power_preference: wgpu::PowerPreference::LowPower,
1159                    compatible_surface: None,
1160                    force_fallback_adapter: true,
1161                })
1162                .await
1163                .ok();
1164        }
1165
1166        let adapter = adapter.expect("Failed to find a suitable GPU for Surtr");
1167        let info = adapter.get_info();
1168        let caps = crate::subsystems::GpuCapabilities::detect(
1169            &info.name,
1170            format!("{:?}", info.backend),
1171        );
1172        log::info!(
1173            "[GPU] Selected adapter: {} ({:?}) on backend: {:?} -- detected as {}",
1174            info.name,
1175            info.device_type,
1176            info.backend,
1177            caps.vendor
1178        );
1179        log::info!("[GPU] Driver info: {} - {}", info.driver, info.driver_info);
1180        let required_features = adapter.features()
1181            & (wgpu::Features::TIMESTAMP_QUERY
1182                | wgpu::Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING
1183                | wgpu::Features::TEXTURE_BINDING_ARRAY);
1184
1185        let (device, queue) = adapter
1186            .request_device(&wgpu::DeviceDescriptor {
1187                label: Some("Surtr Headless Forge"),
1188                required_features,
1189                required_limits: wgpu::Limits {
1190                    max_bindings_per_bind_group: adapter
1191                        .limits()
1192                        .max_bindings_per_bind_group
1193                        .min(256),
1194                    max_binding_array_elements_per_shader_stage: adapter
1195                        .limits()
1196                        .max_binding_array_elements_per_shader_stage
1197                        .min(256),
1198                    ..wgpu::Limits::default()
1199                },
1200                memory_hints: wgpu::MemoryHints::default(),
1201                experimental_features: wgpu::ExperimentalFeatures::disabled(),
1202                trace: wgpu::Trace::Off,
1203            })
1204            .await
1205            .expect("Failed to create Surtr device");
1206
1207        let instance = Arc::new(instance);
1208        let adapter = Arc::new(adapter);
1209
1210        device.on_uncaptured_error(Arc::new(|error| {
1211            log::error!(
1212                "[GPU] Uncaptured device error (Device Lost or Panic): {:?}",
1213                error
1214            );
1215        }));
1216
1217        let device = Arc::new(device);
1218        let queue = Arc::new(queue);
1219
1220        Self::forge_internal(
1221            instance,
1222            adapter,
1223            device,
1224            queue,
1225            None,
1226            Some((width, height, wgpu::TextureFormat::Rgba8UnormSrgb)),
1227        )
1228        .await
1229    }
1230}