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::{HashMap, 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 context_helpers;
16pub(crate) mod draw;
17pub(crate) mod init;
18pub(crate) mod pipelines;
19pub(crate) mod svg;
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, Default, PartialEq, Eq)]
55pub enum QualityLevel {
56    #[default]
57    High,
58    Medium,
59    Low,
60}
61
62impl QualityLevel {
63    /// Returns the MSAA sample count for this quality level.
64    pub fn msaa_sample_count(self) -> u32 {
65        match self {
66            QualityLevel::High => 4,
67            QualityLevel::Medium => 2,
68            QualityLevel::Low => 1,
69        }
70    }
71}
72
73/// GpuRenderer implements the high-performance GPU backend.
74pub struct GpuRenderer {
75    pub(crate) instance: Arc<wgpu::Instance>,
76    pub(crate) adapter: Arc<wgpu::Adapter>,
77    pub(crate) device: Arc<wgpu::Device>,
78    pub(crate) queue: Arc<wgpu::Queue>,
79
80    // Kvasir resource registry -- tracks GPU resource lifetimes
81    pub(crate) registry: crate::kvasir::registry::ResourceRegistry,
82
83    pub(crate) active_offscreens: Vec<crate::types::OffscreenEffectConfig>,
84    pub(crate) world_space_panels: Vec<(u64, cvkg_vdom::WorldSpacePanel)>,
85    /// VDOM subtrees for WorldSpacePanel offscreen rendering.
86    /// Key is the panel ID (matches world_space_panels).
87    pub(crate) panel_vdoms: HashMap<u64, cvkg_vdom::VDom>,
88    pub(crate) effect_pipelines: std::collections::HashMap<String, wgpu::RenderPipeline>,
89    pub(crate) effect_params_buffer: wgpu::Buffer,
90    pub(crate) effect_params_bind_group: wgpu::BindGroup,
91    pub(crate) linear_sampler: wgpu::Sampler,
92    // AI Generator Channel
93    pub ai_material_rx: Option<
94        std::sync::mpsc::Receiver<
95            Result<crate::material::CompiledMaterial, crate::ai::GeneratorError>,
96        >,
97    >,
98
99    // Multi-Window Surface Management
100    pub(crate) surfaces: std::collections::HashMap<winit::window::WindowId, SurfaceContext>,
101    pub(crate) current_window: Option<winit::window::WindowId>,
102    pub headless_context: Option<HeadlessContext>,
103
104    // Mega-Heim (Shared across all windows)
105    pub(crate) text: crate::types::TextSubsystem,
106    pub(crate) mega_heim_tex: wgpu::Texture,
107    pub(crate) mega_heim_bind_group: wgpu::BindGroup,
108    pub(crate) heim_packer: SkylinePacker,
109    pub(crate) image_uv_registry: LruCache<String, Rect>,
110    pub(crate) texture_registry: LruCache<String, u32>,
111    pub(crate) texture_views: Vec<wgpu::TextureView>,
112    pub(crate) dummy_sampler: wgpu::Sampler,
113    /// Dummy 1x1 white texture view.
114    pub(crate) dummy_view: wgpu::TextureView,
115    /// Dummy single-sampled depth texture view.
116    ///
117    /// WHY: Used in the volumetric shader to bind a valid single-sampled depth view
118    /// when MSAA is enabled (since the actual scene depth view is multisampled).
119    ///
120    /// CONTRACT: Always sample_count = 1, format = Depth32Float.
121    pub(crate) dummy_depth_view: wgpu::TextureView,
122    /// Dummy multisampled depth texture view.
123    ///
124    /// WHY: Used in the volumetric shader to bind a valid multisampled depth view
125    /// when MSAA is disabled (since the actual scene depth view is single-sampled).
126    ///
127    /// CONTRACT: Always sample_count = 4, format = Depth32Float.
128    pub(crate) dummy_depth_view_msaa: wgpu::TextureView,
129    /// Shadow map resources for 3D shadow mapping.
130    /// Depth-only texture for rendering shadow map from light's perspective.
131    pub(crate) shadow_map_texture: Option<wgpu::Texture>,
132    /// View of the shadow map texture for depth comparison sampling.
133    pub(crate) shadow_map_view: Option<wgpu::TextureView>,
134    /// Comparison sampler for PCF shadow filtering.
135    pub(crate) shadow_sampler: Option<wgpu::Sampler>,
136    /// Light view-projection matrix for shadow map rendering (from light's perspective).
137    pub(crate) shadow_light_vp: glam::Mat4,
138    /// Shadow map resolution (width = height).
139    pub(crate) shadow_map_size: u32,
140    /// Shadow bias to prevent shadow acne.
141    pub(crate) shadow_bias: f32,
142    pub(crate) svg: crate::types::SvgSubsystem,
143
144    // Niflheim Resources (Shared)
145    pub(crate) dummy_texture_bind_group: wgpu::BindGroup,
146    pub(crate) dummy_env_bind_group: wgpu::BindGroup,
147    pub(crate) texture_bind_group_layout: wgpu::BindGroupLayout,
148    pub(crate) texture_bind_groups: Vec<wgpu::BindGroup>,
149    pub(crate) shared_elements: LruCache<String, cvkg_core::Rect>,
150
151    // The Forge's Anvil (GPU Buffers)
152    pub(crate) geometry_buffers: crate::types::GeometryBuffers,
153    pub(crate) vertices: Vec<Vertex>,
154    pub(crate) indices: Vec<u32>,
155    pub(crate) instance_data: Vec<InstanceData>,
156    /// Per-instance 3D model matrices (used by vs_main_3d once instanced 3D
157    /// rendering is wired up). draw_mesh_3d records into both this and
158    /// instance_data so the data is ready when the GPU path lands.
159    pub(crate) instance_data_3d: Vec<InstanceData3D>,
160    /// GPU buffer for 3D instance data (model matrices, material overrides, UV params).
161    /// Created during forge() and used by instanced 3D rendering.
162    pub(crate) instance_buffer_3d: Option<wgpu::Buffer>,
163    pub(crate) staging_belt: wgpu::util::StagingBelt,
164    pub(crate) staging_command_buffers: Vec<wgpu::CommandBuffer>,
165    pub(crate) draw_calls: Vec<DrawCall>,
166    pub(crate) current_texture_id: Option<u32>,
167    /// Current WorldSpacePanel ID being rendered. Some(id) = inside panel VDOM subtree.
168    /// None = rendering to main surface (2D UI).
169    pub(crate) current_panel_id: Option<u64>,
170    pub(crate) panel_stack: Vec<u64>,
171
172    // Opacity & Clip Stacks
173    pub(crate) opacity_stack: Vec<f32>,
174    pub(crate) clip_stack: Vec<Rect>,
175    pub(crate) slice_stack: Vec<(f32, f32)>,
176    pub(crate) shadow_stack: Vec<ShadowState>,
177
178    // SVG Filter Engine Resources
179    /// Render pipeline for Gaussian blur (two-pass separable kernel).
180    /// Initialized lazily on first use.
181    pub blur_pipeline: Option<wgpu::RenderPipeline>,
182    /// Uniform buffer for blur parameters (std_deviation, kernel_size, direction).
183    /// Initialized lazily on first use.
184    pub blur_uniform: Option<wgpu::Buffer>,
185    /// Bind group layout for blur shader.
186    /// Initialized lazily on first use.
187    pub blur_bind_group_layout: Option<wgpu::BindGroupLayout>,
188    /// Render pipeline for blend operations (feBlend, feComposite).
189    /// Initialized lazily on first use.
190    pub blend_pipeline: Option<wgpu::RenderPipeline>,
191    /// Bind group layout for blend shader.
192    /// Initialized lazily on first use.
193    pub blend_bind_group_layout: Option<wgpu::BindGroupLayout>,
194    /// Render pipeline for flood fill (feFlood).
195    /// Initialized lazily on first use.
196    pub flood_pipeline: Option<wgpu::RenderPipeline>,
197    /// Bind group layout for copy/offset operations.
198    /// Initialized lazily on first use.
199    pub copy_bind_group_layout: Option<wgpu::BindGroupLayout>,
200
201    // The Forge's Heart (Shared Berserker State)
202    pub(crate) theme_buffer: wgpu::Buffer,
203    pub(crate) scene_buffer: wgpu::Buffer,
204    /// Theme stack used when entering portals — push on `enter_portal`, pop (and apply) in `exit_portal`.
205    pub(crate) theme_stack: Vec<ColorTheme>,
206    /// Portal theme save stack for enter_portal/exit_portal theme inheritance.
207    pub(crate) portal_theme_stack: Vec<ColorTheme>,
208    pub(crate) berserker_bind_group: wgpu::BindGroup,
209    pub(crate) berserker_bind_group_layout: wgpu::BindGroupLayout,
210    pub(crate) start_time: std::time::Instant,
211    pub(crate) current_theme: ColorTheme,
212    pub(crate) current_scene: SceneUniforms,
213    pub(crate) current_z: f32,
214
215    /// Default background color for the canvas (RGBA).
216    /// Used when the app does not draw its own background.
217    /// Defaults to Deep Void [0.02, 0.02, 0.05, 1.0].
218    pub(crate) default_background_color: [f32; 4],
219
220    /// Whether the app drew any background geometry this frame.
221    /// If false, the renderer clears to default_background_color.
222    pub(crate) app_drew_background: bool,
223
224    /// Whether render_frame() was called this frame.
225    /// Used by end_frame() to auto-flush staging if render_frame() was skipped.
226    pub(crate) frame_rendered: bool,
227
228    /// Current draw order for SVG and other direct draw calls.
229    /// Set by draw_svg_with_order(), used by emit_draw_call().
230    pub(crate) current_draw_order: i32,
231
232    // Muspelheim Pipelines (Shared)
233    pub(crate) pipeline: wgpu::RenderPipeline,
234    /// Specialized opaque/2D material pipeline (modes 0-20 excluding 7,13-15,18,21).
235    pub(crate) opaque_pipeline: wgpu::RenderPipeline,
236    /// Non-multisampled pipeline used specifically to draw UI overlays.
237    /// Drawn with sample count 1 and no depth testing/depth stencil attachment.
238    pub(crate) ui_pipeline: wgpu::RenderPipeline,
239    /// Specialized glass material pipeline (mode 7 only, ~150 lines of complex math).
240    pub(crate) glass_pipeline: wgpu::RenderPipeline,
241    /// PBR rendering pipeline for 3D surfaces (modes 13+ with shadow mapping).
242    pub(crate) pbr_pipeline: wgpu::RenderPipeline,
243    /// Transparent 3D rendering pipeline.
244    pub(crate) transparent_pipeline: wgpu::RenderPipeline,
245    /// Shadow map rendering pipeline (depth-only for cascaded shadows).
246    pub(crate) shadow_pipeline: wgpu::RenderPipeline,
247    pub(crate) background_pipeline: wgpu::RenderPipeline,
248    pub(crate) bloom_extract_pipeline: wgpu::RenderPipeline,
249    /// Identity copy pipeline for Pass 2 backdrop blur (all pixels, no luminance gate).
250    pub(crate) copy_pipeline: wgpu::RenderPipeline,
251    pub(crate) composite_pipeline: wgpu::RenderPipeline,
252    /// Color blindness simulation pipeline (fullscreen triangle).
253    pub(crate) color_blind_pipeline: wgpu::RenderPipeline,
254    /// Volumetric raymarching pipeline (fullscreen triangle with SDF raymarch).
255    pub(crate) volumetric_pipeline: wgpu::RenderPipeline,
256    /// Volumetric bind group layout for scene uniforms (time/resolution/light).
257    pub(crate) volumetric_bind_group_layout: wgpu::BindGroupLayout,
258    /// Persistent uniform buffer for volumetric data (updated each frame).
259    pub(crate) volumetric_uniform_buffer: wgpu::Buffer,
260    /// Persistent uniform buffer for CSM (Cascaded Shadow Map) data.
261    pub(crate) csm_buffer: wgpu::Buffer,
262    /// Bind group layout for 3D material-specific resources (shadows, IBL, normal maps).
263    pub(crate) pbr_material_bind_group_layout: wgpu::BindGroupLayout,
264    /// Comparison sampler for volumetric depth comparison.
265    pub(crate) volumetric_depth_sampler: wgpu::Sampler,
266    /// CPU-side list of hologram instances submitted this frame.
267    /// Cleared each frame in reset_frame_state; consumed by VolumetricNode::execute.
268    pub(crate) hologram_instances: Vec<HologramInstance>,
269    /// Pending directional light for 3D shadow pass.
270    /// Populated by submit_mesh_3d when meshes are submitted.
271    pub(crate) pending_directional_light: Option<crate::passes::shadow::DirectionalLight>,
272    /// Pending 3D mesh instances for shadow + opaque passes.
273    /// Populated by submit_mesh_3d; consumed and cleared in frame graph construction.
274    pub(crate) pending_mesh_instances_3d: Vec<crate::passes::shadow::GpuMesh3d>,
275    /// Pending transparent 3D mesh instances for transparent pass.
276    pub(crate) pending_transparent_instances_3d: Vec<crate::passes::shadow::GpuMesh3d>,
277    /// Pending scene radius for light VP frustum computation.
278    /// Derived from mesh bounds or default 100.0.
279    pub(crate) pending_scene_radius: f32,
280    /// Kawase blur pyramid downsample pipeline (separate shader module).
281    pub(crate) kawase_down_pipeline: wgpu::RenderPipeline,
282    /// Kawase blur pyramid upsample pipeline (separate shader module).
283    pub(crate) kawase_up_pipeline: wgpu::RenderPipeline,
284    /// Kawase blur bind group layout (uniform + texture + sampler).
285    pub(crate) kawase_bind_group_layout: wgpu::BindGroupLayout,
286    /// Persistent uniform buffer for Kawase blur operations (avoids per-frame allocation).
287    pub(crate) kawase_uniform: wgpu::Buffer,
288    /// Pool of persistent uniform buffers for Kawase blur operations.
289    pub(crate) kawase_uniform_buffers: Vec<wgpu::Buffer>,
290    /// Environment bind group layout (texture + sampler).
291    pub(crate) env_bind_group_layout: wgpu::BindGroupLayout,
292
293    // Telemetry
294    pub telemetry: cvkg_core::TelemetryData,
295
296    /// Pipeline cache for disk-persisted compiled shaders when the adapter exposes PIPELINE_CACHE.
297    /// None means pipelines compile normally without a disk cache.
298    pub(crate) pipeline_cache: Option<wgpu::PipelineCache>,
299
300    /// Configuration for render-loop frame timing and degradation strategies.
301    pub frame_budget: cvkg_core::FrameBudget,
302    /// Staging buffer for windowed frame capture.
303    pub(crate) capture_staging_buffer: Option<wgpu::Buffer>,
304    /// Instant at the start of the last redraw, used for measuring frame timings.
305    pub last_redraw_start: std::time::Instant,
306    /// Instant at the start of the last frame, used for frame_time_ms calculation.
307    pub last_frame_start: std::time::Instant,
308
309    // VRAM Tracking (Bytes)
310    pub(crate) vram_buffers_bytes: u64,
311    pub(crate) vram_textures_bytes: u64,
312
313    // Debugging
314    pub(crate) _debug_layout: bool,
315
316    // Transform Stack -- stores full affine matrices for correct SVG transform composition.
317    pub(crate) transform_stack: Vec<glam::Mat3>,
318    /// 3D Transform Stack -- stores model matrices for 3D object hierarchy.
319    pub(crate) transform_stack_3d: Vec<glam::Mat4>,
320    /// Whether a redraw has been requested for the next frame.
321    pub redraw_requested: bool,
322    /// Cursor for compositor draw call submission tracking.
323    pub(crate) compositor_index_cursor: u32,
324
325    /// Bloom post-processing enabled flag.
326    pub bloom_enabled: bool,
327    /// Dynamic toggle to enable or disable the volumetric raymarching pass, which handles fog and light shaft simulations.
328    pub volumetric_enabled: bool,
329
330    // Path Geometry Cache — avoids re-tessellating static paths every frame.
331    pub(crate) path_geometry_cache: lru::LruCache<u64, (Vec<Vertex>, Vec<u32>)>,
332    /// Color blindness bind group layout (texture + sampler + uniform).
333    pub(crate) color_blind_bind_group_layout: wgpu::BindGroupLayout,
334    /// Color blindness uniform buffer (updated each frame when mode changes).
335    pub(crate) color_blind_uniform_buffer: wgpu::Buffer,
336    /// Color blindness simulation mode (Normal = disabled).
337    pub color_blind_mode: crate::color_blindness::ColorBlindMode,
338    /// Color blindness effect intensity (0.0–1.0).
339    pub color_blind_intensity: f32,
340    /// Sampler for the color blindness pass (reused from main pipeline).
341    pub(crate) sampler: wgpu::Sampler,
342
343    // Timestamp Queries (Norse: Skuld = future/time/debt)
344    pub(crate) skuld_queries: Option<wgpu::QuerySet>,
345    pub(crate) skuld_buffer: Option<wgpu::Buffer>,
346    pub(crate) skuld_read_buffer: Option<wgpu::Buffer>,
347    pub(crate) skuld_period: f32,
348    pub last_gpu_time_ns: u64,
349
350    // Particle Compute Pipeline (Muspelheim Compute)
351    pub(crate) particle_compute_pipeline: wgpu::ComputePipeline,
352    pub(crate) particle_compute_bgl: wgpu::BindGroupLayout,
353    pub(crate) particle_buffer: wgpu::Buffer,
354    pub(crate) particle_uniform_buffer: wgpu::Buffer,
355    pub(crate) particles: crate::types::ParticleSubsystem,
356    pub(crate) particle_render_pipeline: wgpu::RenderPipeline,
357    pub(crate) particle_render_bgl: wgpu::BindGroupLayout,
358    pub(crate) particle_render_bind_group: Option<wgpu::BindGroup>,
359    pub(crate) particle_compute_bind_group: Option<wgpu::BindGroup>,
360
361    // VDOM node stack for hierarchy tracking
362    pub(crate) vnode_stack: Vec<(Rect, &'static str)>,
363
364    /// Event handlers registered during render passes.
365    pub(crate) event_handlers: std::collections::HashMap<
366        String,
367        Vec<std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>>,
368    >,
369
370    // Error tracking (set via RendererErrorHandler trait)
371    pub(crate) render_error_count: u64,
372    pub(crate) has_fatal_error: bool,
373
374    /// Bind group layout for reading blur output in glass composite pass.
375    pub(crate) glass_output_bind_group_layout: wgpu::BindGroupLayout,
376    /// Current material state -- draw calls are tagged with this material.
377    pub(crate) current_draw_material: cvkg_core::DrawMaterial,
378
379    /// Portal backdrop blur regions -- collected during portal enter/exit
380    pub(crate) portal_regions: std::collections::VecDeque<cvkg_core::Rect>,
381
382    /// Gradient stop texture (32 x 1, RGBA) for multi-stop gradient rendering.
383    /// RGB = stop color, A = stop position (0-1). Cached per unique stop set.
384    pub(crate) gradient_stop_texture: wgpu::Texture,
385    pub(crate) gradient_stop_texture_view: wgpu::TextureView,
386    pub(crate) gradient_bind_group: wgpu::BindGroup,
387    /// Gradient texture cache: maps stop-hash to (texture, bind_group) to avoid re-uploading.
388    pub(crate) gradient_texture_cache:
389        std::collections::HashMap<u64, (wgpu::Texture, wgpu::TextureView, wgpu::BindGroup)>,
390    /// Last uploaded gradient stops hash, to detect when we need to re-upload.
391    pub(crate) gradient_stops_hash: u64,
392    /// Layout for the gradient bind group (texture + sampler).
393    pub(crate) gradient_bind_group_layout: wgpu::BindGroupLayout,
394
395    /// Cache of the compiled Kvasir render graph execution plan.
396    pub(crate) cached_graph_plan: Option<crate::kvasir::graph_cache::CachedGraphPlan>,
397    /// Hash of the active material set, used to invalidate the graph plan
398    pub(crate) material_compilation_hash: u64,
399    /// Memoization cache for frame-level render skipping.
400    pub(crate) memo_cache: std::collections::HashMap<u64, crate::types::MemoEntry>,
401    /// Current frame generation counter.
402    pub(crate) frame_generation: u64,
403    /// P1-1: GpuRenderer configuration.
404    pub(crate) config: crate::subsystems::RendererConfig,
405    /// P1-10: Quality level controlling MSAA sample count.
406    pub(crate) quality_level: QualityLevel,
407    /// Thread-safe bind group cache to avoid per-frame allocations during render passes.
408    pub(crate) bind_group_cache: std::sync::Mutex<
409        std::collections::HashMap<
410            (
411                Option<winit::window::WindowId>,
412                crate::kvasir::resource::ResourceId,
413                u32,
414                bool,
415            ),
416            wgpu::BindGroup,
417        >,
418    >,
419    /// Thread-safe texture view cache to avoid per-frame allocations of TextureViews.
420    pub(crate) texture_view_cache: std::sync::Mutex<
421        std::collections::HashMap<
422            (
423                Option<winit::window::WindowId>,
424                crate::kvasir::resource::ResourceId,
425                u32,
426            ),
427            wgpu::TextureView,
428        >,
429    >,
430}
431
432#[cfg(target_arch = "wasm32")]
433unsafe impl Send for GpuRenderer {}
434#[cfg(target_arch = "wasm32")]
435unsafe impl Sync for GpuRenderer {}
436
437/// Per-hologram instance data submitted during the frame.
438#[derive(Debug, Clone)]
439pub struct HologramInstance {
440    /// Bounding rectangle in logical coordinates (x, y, width, height).
441    pub rect: cvkg_core::Rect,
442    /// Hash of the hologram_id string -- used for per-hologram visual variation.
443    pub id_hash: u32,
444    /// Application-provided time for this hologram instance.
445    pub time: f32,
446}
447
448/// Trait for types that can be cleared in place. Implemented for the
449/// collection types used as cache values (HashMap, Vec).
450pub trait ClearInto {
451    fn clear_into(&mut self);
452}
453
454impl<K, V, S> ClearInto for std::collections::HashMap<K, V, S>
455where
456    S: std::hash::BuildHasher,
457{
458    fn clear_into(&mut self) {
459        self.clear();
460    }
461}
462
463impl<T> ClearInto for Vec<T> {
464    fn clear_into(&mut self) {
465        self.clear();
466    }
467}
468
469// =========================================================================
470// P1-11: Pipeline cache integrity check
471// =========================================================================
472
473/// P1-11 fix: load a pipeline cache file from disk with SHA256 integrity check.
474fn load_pipeline_cache_with_integrity_check(
475    cache_path: &std::path::Path,
476) -> Result<Option<Vec<u8>>, String> {
477    let cache_data = match std::fs::read(cache_path) {
478        Ok(d) => d,
479        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
480        Err(e) => return Err(format!("read failed: {e}")),
481    };
482
483    let hash_path = cache_path.with_extension("bin.sha256");
484    let expected_hash = match std::fs::read_to_string(&hash_path) {
485        Ok(s) => s.trim().to_lowercase(),
486        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
487            return Err(format!(
488                "sidecar hash file missing at {}",
489                hash_path.display()
490            ));
491        }
492        Err(e) => return Err(format!("sidecar read failed: {e}")),
493    };
494
495    let actual = compute_sha256(&cache_data);
496    let actual_hex: String = actual.iter().map(|b| format!("{:02x}", b)).collect();
497    if actual_hex != expected_hash {
498        return Err(format!(
499            "hash mismatch: expected {expected_hash}, got {actual_hex}"
500        ));
501    }
502
503    Ok(Some(cache_data))
504}
505
506/// Compute SHA256 of a byte slice. Inline FIPS 180-4 implementation
507fn compute_sha256(data: &[u8]) -> [u8; 32] {
508    let mut hasher = Sha256::new();
509    hasher.update(data);
510    hasher.finalize()
511}
512
513/// Minimal SHA256 implementation (FIPS 180-4). Used only for the
514/// pipeline cache integrity check so we don't add a sha2 dependency.
515#[derive(Clone)]
516struct Sha256 {
517    state: [u32; 8],
518    buffer: [u8; 64],
519    buffer_len: usize,
520    total_len: u64,
521}
522
523impl Sha256 {
524    const K: [u32; 64] = [
525        0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
526        0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
527        0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
528        0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
529        0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
530        0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
531        0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
532        0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
533        0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
534        0xc67178f2,
535    ];
536
537    fn new() -> Self {
538        Self {
539            state: [
540                0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
541                0x5be0cd19,
542            ],
543            buffer: [0; 64],
544            buffer_len: 0,
545            total_len: 0,
546        }
547    }
548
549    fn update(&mut self, data: &[u8]) {
550        self.total_len = self.total_len.wrapping_add(data.len() as u64);
551        for &b in data {
552            self.buffer[self.buffer_len] = b;
553            self.buffer_len += 1;
554            if self.buffer_len == 64 {
555                let block = self.buffer;
556                self.compress(&block);
557                self.buffer_len = 0;
558            }
559        }
560    }
561
562    fn finalize(mut self) -> [u8; 32] {
563        self.buffer[self.buffer_len] = 0x80;
564        self.buffer_len += 1;
565        if self.buffer_len > 56 {
566            for b in &mut self.buffer[self.buffer_len..] {
567                *b = 0;
568            }
569            let block = self.buffer;
570            self.compress(&block);
571            self.buffer_len = 0;
572        }
573        for b in &mut self.buffer[self.buffer_len..56] {
574            *b = 0;
575        }
576        let bit_len = self.total_len.wrapping_mul(8);
577        self.buffer[56..64].copy_from_slice(&bit_len.to_be_bytes());
578        let block = self.buffer;
579        self.compress(&block);
580
581        let mut out = [0u8; 32];
582        for (i, &s) in self.state.iter().enumerate() {
583            out[i * 4..(i + 1) * 4].copy_from_slice(&s.to_be_bytes());
584        }
585        out
586    }
587
588    fn compress(&mut self, block: &[u8]) {
589        let mut w = [0u32; 64];
590        for i in 0..16 {
591            w[i] = u32::from_be_bytes([
592                block[i * 4],
593                block[i * 4 + 1],
594                block[i * 4 + 2],
595                block[i * 4 + 3],
596            ]);
597        }
598        for i in 16..64 {
599            let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
600            let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
601            w[i] = w[i - 16]
602                .wrapping_add(s0)
603                .wrapping_add(w[i - 7])
604                .wrapping_add(s1);
605        }
606        let mut a = self.state[0];
607        let mut b = self.state[1];
608        let mut c = self.state[2];
609        let mut d = self.state[3];
610        let mut e = self.state[4];
611        let mut f = self.state[5];
612        let mut g = self.state[6];
613        let mut h = self.state[7];
614        for (i, wi) in w.iter().enumerate() {
615            let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
616            let ch = (e & f) ^ ((!e) & g);
617            let t1 = h
618                .wrapping_add(s1)
619                .wrapping_add(ch)
620                .wrapping_add(Self::K[i])
621                .wrapping_add(*wi);
622            let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
623            let mj = (a & b) ^ (a & c) ^ (b & c);
624            let t2 = s0.wrapping_add(mj);
625            h = g;
626            g = f;
627            f = e;
628            e = d.wrapping_add(t1);
629            d = c;
630            c = b;
631            b = a;
632            a = t1.wrapping_add(t2);
633        }
634        self.state[0] = self.state[0].wrapping_add(a);
635        self.state[1] = self.state[1].wrapping_add(b);
636        self.state[2] = self.state[2].wrapping_add(c);
637        self.state[3] = self.state[3].wrapping_add(d);
638        self.state[4] = self.state[4].wrapping_add(e);
639        self.state[5] = self.state[5].wrapping_add(f);
640        self.state[6] = self.state[6].wrapping_add(g);
641        self.state[7] = self.state[7].wrapping_add(h);
642    }
643}
644
645fn compute_mip_levels(width: u32, height: u32) -> u32 {
646    let max_dim = width.max(height);
647    if max_dim <= 1 {
648        return 1;
649    }
650    (32 - max_dim.leading_zeros()).clamp(2, 8)
651}
652
653impl GpuRenderer {
654    /// Access the hologram instances submitted this frame.
655    pub fn hologram_instances(&self) -> &[HologramInstance] {
656        &self.hologram_instances
657    }
658
659    pub fn set_quality_level(&mut self, level: QualityLevel) {
660        self.quality_level = level;
661    }
662
663    pub fn set_config(&mut self, config: crate::subsystems::RendererConfig) {
664        self.config = config;
665    }
666
667    pub fn config(&self) -> &crate::subsystems::RendererConfig {
668        &self.config
669    }
670
671    pub fn quality_level(&self) -> QualityLevel {
672        self.quality_level
673    }
674
675    pub(crate) fn lock_or_clear_cache<'a, T: ClearInto>(
676        lock: &'a std::sync::Mutex<T>,
677    ) -> std::sync::MutexGuard<'a, T> {
678        match lock.lock() {
679            Ok(guard) => guard,
680            Err(poisoned) => {
681                tracing::warn!("[GPU] lock_or_clear_cache: mutex poisoned, clearing cache...");
682                let mut guard = poisoned.into_inner();
683                guard.clear_into();
684                guard
685            }
686        }
687    }
688
689    pub fn update_mouse(&mut self, mouse: [f32; 2], velocity: [f32; 2]) {
690        self.current_scene.mouse = mouse;
691        self.current_scene.mouse_velocity = velocity;
692        self.queue.write_buffer(
693            &self.scene_buffer,
694            0,
695            bytemuck::bytes_of(&self.current_scene),
696        );
697    }
698
699    pub fn invalidate_material_cache(&mut self) {
700        self.cached_graph_plan = None;
701    }
702
703    pub fn invalidate_all_caches(&mut self) -> usize {
704        let mut cleared = 0;
705        {
706            let mut bg_cache = Self::lock_or_clear_cache(&self.bind_group_cache);
707            cleared += bg_cache.len();
708            bg_cache.clear();
709        }
710        {
711            let mut view_cache = Self::lock_or_clear_cache(&self.texture_view_cache);
712            cleared += view_cache.len();
713            view_cache.clear();
714        }
715        cleared += self.text.shaped_cache.len();
716        self.text.shaped_cache.clear();
717        cleared += self.svg.model_cache.len();
718        self.svg.model_cache.clear();
719        cleared += self.svg.tree_cache.len();
720        self.svg.tree_cache.clear();
721        self.svg.clear_filter_batches();
722        cleared
723    }
724
725    pub fn prewarm_text_cache(&mut self, labels: &[(&str, f32)]) {
726        let mut count = 0;
727        for (text, size) in labels {
728            let cache_key = (text.to_string(), (size * 100.0) as u32);
729            if self.text.shaped_cache.contains(&cache_key) {
730                continue;
731            }
732            let style = cvkg_runic_text::TextStyle::new("Inter", *size);
733            let spans = [cvkg_runic_text::TextSpan::new(text, style)];
734            if let Ok(shaped) = self.text.engine.shape_layout(
735                &spans,
736                None,
737                cvkg_runic_text::TextAlign::Start,
738                cvkg_runic_text::TextOverflow::Visible,
739            ) {
740                self.text
741                    .shaped_cache
742                    .put(cache_key, std::sync::Arc::new(shaped));
743                count += 1;
744            }
745        }
746        if count > 0 {
747            tracing::info!("[Surtr] prewarm_text_cache: pre-shaped {} labels", count);
748        }
749    }
750
751    pub(crate) fn select_best_surface_format(
752        formats: &[wgpu::TextureFormat],
753    ) -> wgpu::TextureFormat {
754        if formats.is_empty() {
755            return wgpu::TextureFormat::Rgba8Unorm;
756        }
757        let preferred_formats = [
758            wgpu::TextureFormat::Rgba16Float,
759            wgpu::TextureFormat::Rgba8Unorm,
760            wgpu::TextureFormat::Bgra8UnormSrgb,
761            wgpu::TextureFormat::Rgba8UnormSrgb,
762            wgpu::TextureFormat::Bgra8Unorm,
763            wgpu::TextureFormat::Rgba8Unorm,
764            wgpu::TextureFormat::Rgba8Unorm,
765        ];
766        for preferred in &preferred_formats {
767            if formats.contains(preferred) {
768                return *preferred;
769            }
770        }
771        if formats.contains(&wgpu::TextureFormat::Rgba8Unorm) {
772            return wgpu::TextureFormat::Rgba8Unorm;
773        }
774        formats[0]
775    }
776
777    pub(crate) fn rebuild_texture_array_bind_group(&mut self) {
778        let views: Vec<&wgpu::TextureView> = self.texture_views.iter().collect();
779        self.mega_heim_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
780            layout: &self.texture_bind_group_layout,
781            entries: &[
782                wgpu::BindGroupEntry {
783                    binding: 0,
784                    resource: wgpu::BindingResource::TextureViewArray(&views),
785                },
786                wgpu::BindGroupEntry {
787                    binding: 1,
788                    resource: wgpu::BindingResource::Sampler(&self.dummy_sampler),
789                },
790            ],
791            label: Some("Mega-Heim Rebuilt Bind Group"),
792        });
793    }
794
795    pub(crate) fn update_vram_telemetry(&mut self) {
796        let buffers = self.geometry_buffers.vertex_buffer.size()
797            + self.geometry_buffers.index_buffer.size()
798            + self.geometry_buffers.instance_buffer.size()
799            + self.scene_buffer.size()
800            + self.theme_buffer.size()
801            + self.particle_buffer.size()
802            + self.particle_uniform_buffer.size();
803
804        let mut textures = self.config.mega_heim_vram_bytes();
805        textures += 4; // Dummy texture
806
807        for surface in self.surfaces.values() {
808            let width = surface.config.width;
809            let height = surface.config.height;
810            let format_bytes = 8; // Rgba16Float
811            textures += (width * height * format_bytes) as u64; // Scene texture
812            textures +=
813                (width * height * format_bytes * self.quality_level.msaa_sample_count()) as u64; // MSAA texture
814            textures += (width * height * 4) as u64; // Depth texture (Depth32Float)
815
816            let blur_width = (width / 2).max(1);
817            let blur_height = (height / 2).max(1);
818            let blur_bytes = (blur_width * blur_height * 4) as u64;
819            textures += blur_bytes * 4; // 2x blur + 2x bloom textures
820        }
821
822        if let Some(ref ctx) = self.headless_context {
823            let format_bytes = 8; // Rgba16Float
824            textures += (ctx.width * ctx.height * format_bytes) as u64; // Scene texture
825            textures +=
826                (ctx.width * ctx.height * format_bytes * self.quality_level.msaa_sample_count())
827                    as u64; // MSAA texture
828            textures += (ctx.width * ctx.height * 4) as u64; // Depth texture
829            textures += (ctx.width * ctx.height * 4) as u64; // Output texture
830        }
831
832        self.vram_buffers_bytes = buffers;
833        self.vram_textures_bytes = textures;
834        self.telemetry.vram_usage_mb = (buffers + textures) as f32 / (1024.0 * 1024.0);
835    }
836
837    pub fn get_telemetry(&self) -> cvkg_core::TelemetryData {
838        self.telemetry.clone()
839    }
840
841    pub fn resize(
842        &mut self,
843        window_id: winit::window::WindowId,
844        width: u32,
845        height: u32,
846        scale_factor: f32,
847    ) {
848        if width > 0
849            && height > 0
850            && let Some(ctx) = self.surfaces.get_mut(&window_id)
851        {
852            if ctx.config.width == width && ctx.config.height == height {
853                return;
854            }
855
856            tracing::info!("[GPU] Reconfiguring surface: {}x{}", width, height);
857            GpuRenderer::lock_or_clear_cache(&self.bind_group_cache).clear();
858            GpuRenderer::lock_or_clear_cache(&self.texture_view_cache).clear();
859            self.text.shaped_cache.clear();
860            ctx.config.width = width;
861            ctx.config.height = height;
862            ctx.scale_factor = scale_factor;
863            ctx.surface.configure(&self.device, &ctx.config);
864
865            let texture_desc = wgpu::TextureDescriptor {
866                label: Some("Surtr Scene Texture"),
867                size: wgpu::Extent3d {
868                    width,
869                    height,
870                    depth_or_array_layers: 1,
871                },
872                mip_level_count: 1,
873                sample_count: 1,
874                dimension: wgpu::TextureDimension::D2,
875                format: wgpu::TextureFormat::Rgba16Float,
876                usage: wgpu::TextureUsages::RENDER_ATTACHMENT
877                    | wgpu::TextureUsages::TEXTURE_BINDING,
878                view_formats: &[],
879            };
880
881            let scene_tex = self.device.create_texture(&texture_desc);
882
883            let msaa_desc = wgpu::TextureDescriptor {
884                label: Some("Scene MSAA"),
885                size: texture_desc.size,
886                mip_level_count: 1,
887                sample_count: self.quality_level.msaa_sample_count(),
888                dimension: wgpu::TextureDimension::D2,
889                format: wgpu::TextureFormat::Rgba16Float,
890                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
891                view_formats: &[],
892            };
893            let scene_msaa_tex = self.device.create_texture(&msaa_desc);
894            ctx.scene_texture = scene_tex.create_view(&wgpu::TextureViewDescriptor::default());
895            ctx.scene_msaa_texture =
896                scene_msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
897
898            self.registry.remove_image(ctx.blur_tex_a);
899            self.registry.remove_image(ctx.blur_tex_b);
900            self.registry.remove_image(ctx.bloom_tex_a);
901            self.registry.remove_image(ctx.bloom_tex_b);
902
903            let blur_width = (width / 2).max(1);
904            let blur_height = (height / 2).max(1);
905
906            let blur_desc_a = crate::kvasir::resource::ResourceDescriptor {
907                label: Some("Surtr Blur Texture A".into()),
908                kind: crate::kvasir::resource::ResourceKind::Image {
909                    format: ctx.config.format,
910                    width: blur_width,
911                    height: blur_height,
912                    mip_level_count: compute_mip_levels(blur_width, blur_height),
913                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT
914                        | wgpu::TextureUsages::TEXTURE_BINDING
915                        | wgpu::TextureUsages::COPY_SRC,
916                },
917                lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
918            };
919            ctx.blur_tex_a = self.registry.allocate_image(&self.device, &blur_desc_a);
920
921            let blur_desc_b = crate::kvasir::resource::ResourceDescriptor {
922                label: Some("Surtr Blur Texture B".into()),
923                kind: crate::kvasir::resource::ResourceKind::Image {
924                    format: ctx.config.format,
925                    width: blur_width,
926                    height: blur_height,
927                    mip_level_count: compute_mip_levels(blur_width, blur_height),
928                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT
929                        | wgpu::TextureUsages::TEXTURE_BINDING
930                        | wgpu::TextureUsages::COPY_SRC,
931                },
932                lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
933            };
934            ctx.blur_tex_b = self.registry.allocate_image(&self.device, &blur_desc_b);
935
936            let bloom_desc_a = crate::kvasir::resource::ResourceDescriptor {
937                label: Some("Surtr Bloom Texture A".into()),
938                kind: crate::kvasir::resource::ResourceKind::Image {
939                    format: ctx.config.format,
940                    width: blur_width,
941                    height: blur_height,
942                    mip_level_count: compute_mip_levels(blur_width, blur_height),
943                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT
944                        | wgpu::TextureUsages::TEXTURE_BINDING
945                        | wgpu::TextureUsages::COPY_SRC,
946                },
947                lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
948            };
949            ctx.bloom_tex_a = self.registry.allocate_image(&self.device, &bloom_desc_a);
950
951            let bloom_desc_b = crate::kvasir::resource::ResourceDescriptor {
952                label: Some("Surtr Bloom Texture B".into()),
953                kind: crate::kvasir::resource::ResourceKind::Image {
954                    format: ctx.config.format,
955                    width: blur_width,
956                    height: blur_height,
957                    mip_level_count: compute_mip_levels(blur_width, blur_height),
958                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT
959                        | wgpu::TextureUsages::TEXTURE_BINDING
960                        | wgpu::TextureUsages::COPY_SRC,
961                },
962                lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
963            };
964            ctx.bloom_tex_b = self.registry.allocate_image(&self.device, &bloom_desc_b);
965
966            ctx.scene_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
967                layout: &self.env_bind_group_layout,
968                entries: &[
969                    wgpu::BindGroupEntry {
970                        binding: 0,
971                        resource: wgpu::BindingResource::TextureView(&ctx.scene_texture),
972                    },
973                    wgpu::BindGroupEntry {
974                        binding: 1,
975                        resource: wgpu::BindingResource::Sampler(&ctx.sampler),
976                    },
977                ],
978                label: Some("Scene Bind Group Resize"),
979            });
980
981            let scene_views: Vec<&wgpu::TextureView> =
982                (0..32).map(|_| &ctx.scene_texture).collect();
983            ctx.scene_texture_bind_group =
984                self.device.create_bind_group(&wgpu::BindGroupDescriptor {
985                    layout: &self.texture_bind_group_layout,
986                    entries: &[
987                        wgpu::BindGroupEntry {
988                            binding: 0,
989                            resource: wgpu::BindingResource::TextureViewArray(&scene_views),
990                        },
991                        wgpu::BindGroupEntry {
992                            binding: 1,
993                            resource: wgpu::BindingResource::Sampler(&ctx.sampler),
994                        },
995                    ],
996                    label: Some("Scene Texture Bind Group Resize"),
997                });
998
999            let depth_texture = self.device.create_texture(&wgpu::TextureDescriptor {
1000                label: Some("Surtr Depth Texture"),
1001                size: wgpu::Extent3d {
1002                    width,
1003                    height,
1004                    depth_or_array_layers: 1,
1005                },
1006                mip_level_count: 1,
1007                sample_count: self.quality_level.msaa_sample_count(),
1008                dimension: wgpu::TextureDimension::D2,
1009                format: wgpu::TextureFormat::Depth32Float,
1010                usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1011                    | wgpu::TextureUsages::TEXTURE_BINDING,
1012                view_formats: &[],
1013            });
1014            ctx.depth_texture_view =
1015                depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
1016        }
1017    }
1018
1019    pub fn reset_time(&mut self) {
1020        self.start_time = std::time::Instant::now();
1021    }
1022
1023    pub fn reclaim_vram(&mut self) {
1024        tracing::warn!("[GPU] Sundr Compaction: Compacting Mega-Heim...");
1025
1026        let new_mega_heim_tex = self.device.create_texture(&wgpu::TextureDescriptor {
1027            label: Some("Sundr Mega-Heim (Compacted)"),
1028            size: wgpu::Extent3d {
1029                width: 4096,
1030                height: 4096,
1031                depth_or_array_layers: 1,
1032            },
1033            mip_level_count: 1,
1034            sample_count: 1,
1035            dimension: wgpu::TextureDimension::D2,
1036            format: wgpu::TextureFormat::Rgba8UnormSrgb,
1037            usage: wgpu::TextureUsages::TEXTURE_BINDING
1038                | wgpu::TextureUsages::COPY_DST
1039                | wgpu::TextureUsages::COPY_SRC,
1040            view_formats: &[],
1041        });
1042
1043        let mut new_packer = SkylinePacker::new(4096, 4096);
1044        let mut encoder = self
1045            .device
1046            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1047                label: Some("Heim Compaction Encoder"),
1048            });
1049
1050        let image_entries: Vec<(String, Rect)> = self
1051            .image_uv_registry
1052            .iter()
1053            .map(|(k, v)| (k.clone(), *v))
1054            .collect();
1055        for (name, old_uv) in image_entries {
1056            if let Some(&tex_idx) = self.texture_registry.get(&name)
1057                && tex_idx == 0
1058            {
1059                let w_px = (old_uv.width * 4096.0).round() as u32;
1060                let h_px = (old_uv.height * 4096.0).round() as u32;
1061                let old_x_px = (old_uv.x * 4096.0).round() as u32;
1062                let old_y_px = (old_uv.y * 4096.0).round() as u32;
1063
1064                if let Some((new_x, new_y)) = new_packer.pack(w_px, h_px) {
1065                    encoder.copy_texture_to_texture(
1066                        wgpu::TexelCopyTextureInfo {
1067                            texture: &self.mega_heim_tex,
1068                            mip_level: 0,
1069                            origin: wgpu::Origin3d {
1070                                x: old_x_px,
1071                                y: old_y_px,
1072                                z: 0,
1073                            },
1074                            aspect: wgpu::TextureAspect::All,
1075                        },
1076                        wgpu::TexelCopyTextureInfo {
1077                            texture: &new_mega_heim_tex,
1078                            mip_level: 0,
1079                            origin: wgpu::Origin3d {
1080                                x: new_x,
1081                                y: new_y,
1082                                z: 0,
1083                            },
1084                            aspect: wgpu::TextureAspect::All,
1085                        },
1086                        wgpu::Extent3d {
1087                            width: w_px,
1088                            height: h_px,
1089                            depth_or_array_layers: 1,
1090                        },
1091                    );
1092
1093                    let new_uv = Rect {
1094                        x: new_x as f32 / 4096.0,
1095                        y: new_y as f32 / 4096.0,
1096                        width: old_uv.width,
1097                        height: old_uv.height,
1098                    };
1099                    self.image_uv_registry.put(name.clone(), new_uv);
1100                }
1101            }
1102        }
1103
1104        let text_entries: Vec<(u64, (Rect, f32, f32, f32, f32))> = self
1105            .text
1106            .glyph_cache
1107            .iter()
1108            .map(|(k, v)| (*k, *v))
1109            .collect();
1110        for (hash, (old_uv, w_f, h_f, x_off, y_off)) in text_entries {
1111            let w_px = (old_uv.width * 4096.0).round() as u32;
1112            let h_px = (old_uv.height * 4096.0).round() as u32;
1113            let old_x_px = (old_uv.x * 4096.0).round() as u32;
1114            let old_y_px = (old_uv.y * 4096.0).round() as u32;
1115
1116            if let Some((new_x, new_y)) = new_packer.pack(w_px, h_px) {
1117                encoder.copy_texture_to_texture(
1118                    wgpu::TexelCopyTextureInfo {
1119                        texture: &self.mega_heim_tex,
1120                        mip_level: 0,
1121                        origin: wgpu::Origin3d {
1122                            x: old_x_px,
1123                            y: old_y_px,
1124                            z: 0,
1125                        },
1126                        aspect: wgpu::TextureAspect::All,
1127                    },
1128                    wgpu::TexelCopyTextureInfo {
1129                        texture: &new_mega_heim_tex,
1130                        mip_level: 0,
1131                        origin: wgpu::Origin3d {
1132                            x: new_x,
1133                            y: new_y,
1134                            z: 0,
1135                        },
1136                        aspect: wgpu::TextureAspect::All,
1137                    },
1138                    wgpu::Extent3d {
1139                        width: w_px,
1140                        height: h_px,
1141                        depth_or_array_layers: 1,
1142                    },
1143                );
1144
1145                let new_uv = Rect {
1146                    x: new_x as f32 / 4096.0,
1147                    y: new_y as f32 / 4096.0,
1148                    width: old_uv.width,
1149                    height: old_uv.height,
1150                };
1151                self.text
1152                    .glyph_cache
1153                    .put(hash, (new_uv, w_f, h_f, x_off, y_off));
1154            }
1155        }
1156
1157        self.queue.submit(std::iter::once(encoder.finish()));
1158
1159        self.mega_heim_tex = new_mega_heim_tex;
1160        let mega_heim_view_obj = self
1161            .mega_heim_tex
1162            .create_view(&wgpu::TextureViewDescriptor::default());
1163        self.texture_views[0] = mega_heim_view_obj.clone();
1164
1165        self.rebuild_texture_array_bind_group();
1166
1167        if !self.texture_bind_groups.is_empty() {
1168            self.texture_bind_groups[0] = self.mega_heim_bind_group.clone();
1169        }
1170
1171        self.heim_packer = new_packer;
1172        self.telemetry.vram_exhausted = false;
1173    }
1174}
1175
1176impl Drop for GpuRenderer {
1177    fn drop(&mut self) {
1178        let cache_dir = std::env::current_exe()
1179            .ok()
1180            .and_then(|p| p.parent().map(|d| d.join("pipeline_cache")))
1181            .unwrap_or_else(|| std::env::temp_dir().join("cvkg_pipeline_cache"));
1182        let _ = std::fs::create_dir_all(&cache_dir);
1183        let cache_path = cache_dir.join("cvkg_render_gpu.bin");
1184        if let Some(cache) = &self.pipeline_cache
1185            && let Some(data) = cache.get_data()
1186            && let Err(e) = std::fs::write(&cache_path, data)
1187        {
1188            tracing::warn!("Failed to persist pipeline cache: {}", e);
1189        }
1190
1191        let _ = self.device.poll(wgpu::PollType::Wait {
1192            submission_index: None,
1193            timeout: None,
1194        });
1195    }
1196}
1197
1198impl GpuRenderer {
1199    pub(crate) fn current_width(&self) -> u32 {
1200        if let Some(id) = self.current_window {
1201            self.surfaces.get(&id).map(|s| s.config.width).unwrap_or(1)
1202        } else {
1203            self.headless_context.as_ref().map(|h| h.width).unwrap_or(1)
1204        }
1205    }
1206
1207    pub(crate) fn current_height(&self) -> u32 {
1208        if let Some(id) = self.current_window {
1209            self.surfaces.get(&id).map(|s| s.config.height).unwrap_or(1)
1210        } else {
1211            self.headless_context
1212                .as_ref()
1213                .map(|h| h.height)
1214                .unwrap_or(1)
1215        }
1216    }
1217
1218    pub(crate) fn current_scale_factor(&self) -> f32 {
1219        if let Some(id) = self.current_window {
1220            self.surfaces
1221                .get(&id)
1222                .map(|s| s.scale_factor)
1223                .unwrap_or(1.0)
1224        } else {
1225            self.headless_context
1226                .as_ref()
1227                .map(|h| h.scale_factor)
1228                .unwrap_or(1.0)
1229        }
1230    }
1231
1232    pub(crate) fn current_time(&self) -> f32 {
1233        self.start_time.elapsed().as_secs_f32()
1234    }
1235
1236    /// forge_headless -- Initializes Surtr without a window for visual regression testing.
1237    pub async fn forge_headless(width: u32, height: u32) -> Self {
1238        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
1239            backends: wgpu::Backends::all(),
1240            flags: wgpu::InstanceFlags::default(),
1241            backend_options: wgpu::BackendOptions::default(),
1242            display: None,
1243            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
1244        });
1245
1246        // Request adapter with robust multi-stage fallback for Bumblebee/Optimus compatibility
1247        tracing::info!("[GPU] Requesting HighPerformance adapter (headless)...");
1248        let mut adapter = instance
1249            .request_adapter(&wgpu::RequestAdapterOptions {
1250                power_preference: wgpu::PowerPreference::HighPerformance,
1251                compatible_surface: None,
1252                force_fallback_adapter: false,
1253            })
1254            .await
1255            .ok();
1256
1257        if adapter.is_none() {
1258            tracing::warn!(
1259                "[GPU] HighPerformance adapter failed (possible Bumblebee/Optimus), trying LowPower..."
1260            );
1261            adapter = instance
1262                .request_adapter(&wgpu::RequestAdapterOptions {
1263                    power_preference: wgpu::PowerPreference::LowPower,
1264                    compatible_surface: None,
1265                    force_fallback_adapter: false,
1266                })
1267                .await
1268                .ok();
1269        }
1270
1271        if adapter.is_none() {
1272            tracing::warn!("[GPU] Hardware adapters failed, trying Software fallback...");
1273            adapter = instance
1274                .request_adapter(&wgpu::RequestAdapterOptions {
1275                    power_preference: wgpu::PowerPreference::LowPower,
1276                    compatible_surface: None,
1277                    force_fallback_adapter: true,
1278                })
1279                .await
1280                .ok();
1281        }
1282
1283        let adapter = adapter.expect("Failed to find a suitable GPU for Surtr");
1284        let info = adapter.get_info();
1285        let caps =
1286            crate::subsystems::GpuCapabilities::detect(&info.name, format!("{:?}", info.backend));
1287        tracing::info!(
1288            "[GPU] Selected adapter: {} ({:?}) on backend: {:?} -- detected as {}",
1289            info.name,
1290            info.device_type,
1291            info.backend,
1292            caps.vendor
1293        );
1294        tracing::info!("[GPU] Driver info: {} - {}", info.driver, info.driver_info);
1295        let required_features = adapter.features()
1296            & (wgpu::Features::TIMESTAMP_QUERY
1297                | wgpu::Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING
1298                | wgpu::Features::TEXTURE_BINDING_ARRAY);
1299
1300        let (device, queue) = adapter
1301            .request_device(&wgpu::DeviceDescriptor {
1302                label: Some("Surtr Headless Forge"),
1303                required_features,
1304                required_limits: wgpu::Limits {
1305                    max_bindings_per_bind_group: adapter
1306                        .limits()
1307                        .max_bindings_per_bind_group
1308                        .min(256),
1309                    max_binding_array_elements_per_shader_stage: adapter
1310                        .limits()
1311                        .max_binding_array_elements_per_shader_stage
1312                        .min(256),
1313                    ..wgpu::Limits::default()
1314                },
1315                memory_hints: wgpu::MemoryHints::default(),
1316                experimental_features: wgpu::ExperimentalFeatures::disabled(),
1317                trace: wgpu::Trace::Off,
1318            })
1319            .await
1320            .expect("Failed to create Surtr device");
1321
1322        let instance = Arc::new(instance);
1323        let adapter = Arc::new(adapter);
1324
1325        device.on_uncaptured_error(Arc::new(|error| {
1326            tracing::error!(
1327                "[GPU] Uncaptured device error (Device Lost or Panic): {:?}",
1328                error
1329            );
1330        }));
1331
1332        let device = Arc::new(device);
1333        let queue = Arc::new(queue);
1334
1335        Self::forge_internal(
1336            instance,
1337            adapter,
1338            device,
1339            queue,
1340            None,
1341            Some((width, height, wgpu::TextureFormat::Rgba8UnormSrgb)),
1342        )
1343        .await
1344    }
1345
1346    /// Read back the headless output texture as RGBA8 pixels.
1347    /// Must be called after `end_frame` on a headless renderer.
1348    pub fn readback_headless_rgba8(&self) -> Vec<u8> {
1349        let ctx = self
1350            .headless_context
1351            .as_ref()
1352            .expect("readback_headless_rgba8 requires a headless renderer");
1353
1354        let width = ctx.width;
1355        let height = ctx.height;
1356        let row_bytes = width * 4;
1357        let padded_row_bytes = row_bytes.div_ceil(256) * 256;
1358        let buffer_size = (padded_row_bytes * height) as u64;
1359
1360        let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
1361            label: Some("headless-readback-buffer"),
1362            size: buffer_size,
1363            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1364            mapped_at_creation: false,
1365        });
1366
1367        let mut encoder = self
1368            .device
1369            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1370                label: Some("headless-readback-encoder"),
1371            });
1372
1373        encoder.copy_texture_to_buffer(
1374            wgpu::TexelCopyTextureInfo {
1375                texture: &ctx.output_texture,
1376                mip_level: 0,
1377                origin: wgpu::Origin3d::ZERO,
1378                aspect: wgpu::TextureAspect::All,
1379            },
1380            wgpu::TexelCopyBufferInfo {
1381                buffer: &output_buffer,
1382                layout: wgpu::TexelCopyBufferLayout {
1383                    offset: 0,
1384                    bytes_per_row: Some(padded_row_bytes),
1385                    rows_per_image: Some(height),
1386                },
1387            },
1388            wgpu::Extent3d {
1389                width,
1390                height,
1391                depth_or_array_layers: 1,
1392            },
1393        );
1394
1395        self.queue.submit(std::iter::once(encoder.finish()));
1396        let buffer_slice = output_buffer.slice(..);
1397        buffer_slice.map_async(wgpu::MapMode::Read, |_| {});
1398        let _ = self.device.poll(wgpu::PollType::Wait {
1399            submission_index: None,
1400            timeout: None,
1401        });
1402
1403        let data = buffer_slice.get_mapped_range();
1404        let mut result = Vec::with_capacity((width * height * 4) as usize);
1405        for row in 0..height {
1406            let start = (row * padded_row_bytes) as usize;
1407            let end = start + row_bytes as usize;
1408            result.extend_from_slice(&data[start..end]);
1409        }
1410        drop(data);
1411        output_buffer.unmap();
1412        output_buffer.destroy();
1413        result
1414    }
1415
1416    /// Render a headless frame with a draw callback and read back pixels.
1417    pub fn render_headless_frame<F>(&mut self, draw: F) -> Vec<u8>
1418    where
1419        F: FnOnce(&mut Self),
1420    {
1421        let encoder = self.begin_frame_headless();
1422        draw(self);
1423        self.end_frame(encoder);
1424        self.readback_headless_rgba8()
1425    }
1426
1427    /// Create a headless GpuRenderer from an existing device and surface.
1428    ///
1429    /// This constructor does not require an event loop and is suitable for
1430    /// headless rendering (e.g., server-side rendering, tests).
1431    /// It delegates to the existing `forge_internal` which handles all
1432    /// pipeline, buffer, and bind group initialization.
1433    pub async fn from_external(
1434        device: Arc<wgpu::Device>,
1435        queue: Arc<wgpu::Queue>,
1436        surface: wgpu::Surface<'static>,
1437        width: u32,
1438        height: u32,
1439    ) -> Self {
1440        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
1441            backends: wgpu::Backends::all(),
1442            flags: wgpu::InstanceFlags::default(),
1443            backend_options: wgpu::BackendOptions::default(),
1444            display: None,
1445            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
1446        });
1447
1448        let adapter = instance
1449            .request_adapter(&wgpu::RequestAdapterOptions {
1450                power_preference: wgpu::PowerPreference::default(),
1451                compatible_surface: Some(&surface),
1452                force_fallback_adapter: false,
1453            })
1454            .await
1455            .expect("No compatible adapter found");
1456
1457        Self::forge_internal(
1458            Arc::new(instance),
1459            Arc::new(adapter),
1460            device,
1461            queue,
1462            None,
1463            Some((width, height, wgpu::TextureFormat::Rgba8UnormSrgb)),
1464        )
1465        .await
1466    }
1467
1468    pub fn set_world_space_panels(&mut self, panels: Vec<(u64, cvkg_vdom::WorldSpacePanel)>) {
1469        self.world_space_panels = panels;
1470    }
1471
1472    pub fn begin_world_space_panel(
1473        &mut self,
1474        node_id: u64,
1475        transform: &cvkg_core::Transform3D,
1476        glass: Option<cvkg_materials::GlassMaterial>,
1477        pixels_per_unit: f32,
1478        world_size: (f32, f32),
1479    ) {
1480        if let Some(prev) = self.current_panel_id {
1481            self.panel_stack.push(prev);
1482        }
1483        self.current_panel_id = Some(node_id);
1484
1485        let panel = cvkg_vdom::WorldSpacePanel {
1486            transform: *transform,
1487            glass,
1488            pixels_per_unit,
1489            world_size,
1490            ..Default::default()
1491        };
1492        // Record it so the rendering graph knows about it
1493        if !self.world_space_panels.iter().any(|(id, _)| *id == node_id) {
1494            self.world_space_panels.push((node_id, panel));
1495        }
1496    }
1497
1498    pub fn end_world_space_panel(&mut self, _node_id: u64) {
1499        self.current_panel_id = self.panel_stack.pop();
1500    }
1501}