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    pub(crate) skinning_compute_pipeline: wgpu::ComputePipeline,
294    pub(crate) skinning_bgl0: wgpu::BindGroupLayout,
295    pub(crate) skinning_bgl1: wgpu::BindGroupLayout,
296    pub(crate) skinning_bgl2: wgpu::BindGroupLayout,
297    /// Per-mesh skinning buffer pairs (src, dst) accumulated during frame submission.
298    /// Cleared at the start of each frame. SkinningNode dispatches compute for each pair.
299    pub(crate) skinning_buffer_pairs: Vec<(wgpu::Buffer, wgpu::Buffer)>,
300    pub(crate) skinning_joint_matrices: wgpu::Buffer,
301    pub(crate) skinning_morph_positions: wgpu::Buffer,
302    pub(crate) skinning_morph_weights: wgpu::Buffer,
303
304    // Telemetry
305    pub telemetry: cvkg_core::TelemetryData,
306
307    /// Pipeline cache for disk-persisted compiled shaders when the adapter exposes PIPELINE_CACHE.
308    /// None means pipelines compile normally without a disk cache.
309    pub(crate) pipeline_cache: Option<wgpu::PipelineCache>,
310
311    /// Configuration for render-loop frame timing and degradation strategies.
312    pub frame_budget: cvkg_core::FrameBudget,
313    /// Staging buffer for windowed frame capture.
314    pub(crate) capture_staging_buffer: Option<wgpu::Buffer>,
315    /// Instant at the start of the last redraw, used for measuring frame timings.
316    pub last_redraw_start: std::time::Instant,
317    /// Instant at the start of the last frame, used for frame_time_ms calculation.
318    pub last_frame_start: std::time::Instant,
319
320    // VRAM Tracking (Bytes)
321    pub(crate) vram_buffers_bytes: u64,
322    pub(crate) vram_textures_bytes: u64,
323
324    // Debugging
325    pub(crate) _debug_layout: bool,
326
327    // Transform Stack -- stores full affine matrices for correct SVG transform composition.
328    pub(crate) transform_stack: Vec<glam::Mat3>,
329    /// 3D Transform Stack -- stores model matrices for 3D object hierarchy.
330    pub(crate) transform_stack_3d: Vec<glam::Mat4>,
331    /// Whether a redraw has been requested for the next frame.
332    pub redraw_requested: bool,
333    /// Cursor for compositor draw call submission tracking.
334    pub(crate) compositor_index_cursor: u32,
335
336    /// Bloom post-processing enabled flag.
337    pub bloom_enabled: bool,
338    /// Dynamic toggle to enable or disable the volumetric raymarching pass, which handles fog and light shaft simulations.
339    pub volumetric_enabled: bool,
340
341    // Path Geometry Cache — avoids re-tessellating static paths every frame.
342    pub(crate) path_geometry_cache: lru::LruCache<u64, (Vec<Vertex>, Vec<u32>)>,
343    /// Color blindness bind group layout (texture + sampler + uniform).
344    pub(crate) color_blind_bind_group_layout: wgpu::BindGroupLayout,
345    /// Color blindness uniform buffer (updated each frame when mode changes).
346    pub(crate) color_blind_uniform_buffer: wgpu::Buffer,
347    /// Color blindness simulation mode (Normal = disabled).
348    pub color_blind_mode: crate::color_blindness::ColorBlindMode,
349    /// Color blindness effect intensity (0.0–1.0).
350    pub color_blind_intensity: f32,
351    /// Sampler for the color blindness pass (reused from main pipeline).
352    pub(crate) sampler: wgpu::Sampler,
353
354    // Timestamp Queries (Norse: Skuld = future/time/debt)
355    pub(crate) skuld_queries: Option<wgpu::QuerySet>,
356    pub(crate) skuld_buffer: Option<wgpu::Buffer>,
357    pub(crate) skuld_read_buffer: Option<wgpu::Buffer>,
358    pub(crate) skuld_period: f32,
359    pub last_gpu_time_ns: u64,
360
361    // Particle Compute Pipeline (Muspelheim Compute)
362    pub(crate) particle_compute_pipeline: wgpu::ComputePipeline,
363    pub(crate) particle_compute_bgl: wgpu::BindGroupLayout,
364    pub(crate) particle_buffer: wgpu::Buffer,
365    pub(crate) particle_uniform_buffer: wgpu::Buffer,
366    pub(crate) particles: crate::types::ParticleSubsystem,
367    pub(crate) particle_render_pipeline: wgpu::RenderPipeline,
368    pub(crate) particle_render_bgl: wgpu::BindGroupLayout,
369    pub(crate) particle_render_bind_group: Option<wgpu::BindGroup>,
370    pub(crate) particle_compute_bind_group: Option<wgpu::BindGroup>,
371
372    // VDOM node stack for hierarchy tracking
373    pub(crate) vnode_stack: Vec<(Rect, &'static str)>,
374
375    /// Event handlers registered during render passes.
376    pub(crate) event_handlers: std::collections::HashMap<
377        String,
378        Vec<std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>>,
379    >,
380
381    // Error tracking (set via RendererErrorHandler trait)
382    pub(crate) render_error_count: u64,
383    pub(crate) has_fatal_error: bool,
384
385    /// Bind group layout for reading blur output in glass composite pass.
386    pub(crate) glass_output_bind_group_layout: wgpu::BindGroupLayout,
387    /// Current material state -- draw calls are tagged with this material.
388    pub(crate) current_draw_material: cvkg_core::DrawMaterial,
389
390    /// Portal backdrop blur regions -- collected during portal enter/exit
391    pub(crate) portal_regions: std::collections::VecDeque<cvkg_core::Rect>,
392
393    /// Gradient stop texture (32 x 1, RGBA) for multi-stop gradient rendering.
394    /// RGB = stop color, A = stop position (0-1). Cached per unique stop set.
395    pub(crate) gradient_stop_texture: wgpu::Texture,
396    pub(crate) gradient_stop_texture_view: wgpu::TextureView,
397    pub(crate) gradient_bind_group: wgpu::BindGroup,
398    /// Gradient texture cache: maps stop-hash to (texture, bind_group) to avoid re-uploading.
399    pub(crate) gradient_texture_cache:
400        std::collections::HashMap<u64, (wgpu::Texture, wgpu::TextureView, wgpu::BindGroup)>,
401    /// Last uploaded gradient stops hash, to detect when we need to re-upload.
402    pub(crate) gradient_stops_hash: u64,
403    /// Layout for the gradient bind group (texture + sampler).
404    pub(crate) gradient_bind_group_layout: wgpu::BindGroupLayout,
405
406    /// Cache of the compiled Kvasir render graph execution plan.
407    pub(crate) cached_graph_plan: Option<crate::kvasir::graph_cache::CachedGraphPlan>,
408    /// Hash of the active material set, used to invalidate the graph plan
409    pub(crate) material_compilation_hash: u64,
410    /// Memoization cache for frame-level render skipping.
411    pub(crate) memo_cache: std::collections::HashMap<u64, crate::types::MemoEntry>,
412    /// Current frame generation counter.
413    pub(crate) frame_generation: u64,
414    /// P1-1: GpuRenderer configuration.
415    pub(crate) config: crate::subsystems::RendererConfig,
416    /// P1-10: Quality level controlling MSAA sample count.
417    pub(crate) quality_level: QualityLevel,
418    /// Thread-safe bind group cache to avoid per-frame allocations during render passes.
419    pub(crate) bind_group_cache: std::sync::Mutex<
420        std::collections::HashMap<
421            (
422                Option<winit::window::WindowId>,
423                crate::kvasir::resource::ResourceId,
424                u32,
425                bool,
426            ),
427            wgpu::BindGroup,
428        >,
429    >,
430    /// Thread-safe texture view cache to avoid per-frame allocations of TextureViews.
431    pub(crate) texture_view_cache: std::sync::Mutex<
432        std::collections::HashMap<
433            (
434                Option<winit::window::WindowId>,
435                crate::kvasir::resource::ResourceId,
436                u32,
437            ),
438            wgpu::TextureView,
439        >,
440    >,
441}
442
443#[cfg(target_arch = "wasm32")]
444unsafe impl Send for GpuRenderer {}
445#[cfg(target_arch = "wasm32")]
446unsafe impl Sync for GpuRenderer {}
447
448/// Per-hologram instance data submitted during the frame.
449#[derive(Debug, Clone)]
450pub struct HologramInstance {
451    /// Bounding rectangle in logical coordinates (x, y, width, height).
452    pub rect: cvkg_core::Rect,
453    /// Hash of the hologram_id string -- used for per-hologram visual variation.
454    pub id_hash: u32,
455    /// Application-provided time for this hologram instance.
456    pub time: f32,
457}
458
459/// Trait for types that can be cleared in place. Implemented for the
460/// collection types used as cache values (HashMap, Vec).
461pub trait ClearInto {
462    fn clear_into(&mut self);
463}
464
465impl<K, V, S> ClearInto for std::collections::HashMap<K, V, S>
466where
467    S: std::hash::BuildHasher,
468{
469    fn clear_into(&mut self) {
470        self.clear();
471    }
472}
473
474impl<T> ClearInto for Vec<T> {
475    fn clear_into(&mut self) {
476        self.clear();
477    }
478}
479
480// =========================================================================
481// P1-11: Pipeline cache integrity check
482// =========================================================================
483
484/// P1-11 fix: load a pipeline cache file from disk with SHA256 integrity check.
485fn load_pipeline_cache_with_integrity_check(
486    cache_path: &std::path::Path,
487) -> Result<Option<Vec<u8>>, String> {
488    let cache_data = match std::fs::read(cache_path) {
489        Ok(d) => d,
490        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
491        Err(e) => return Err(format!("read failed: {e}")),
492    };
493
494    let hash_path = cache_path.with_extension("bin.sha256");
495    let expected_hash = match std::fs::read_to_string(&hash_path) {
496        Ok(s) => s.trim().to_lowercase(),
497        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
498            return Err(format!(
499                "sidecar hash file missing at {}",
500                hash_path.display()
501            ));
502        }
503        Err(e) => return Err(format!("sidecar read failed: {e}")),
504    };
505
506    let actual = compute_sha256(&cache_data);
507    let actual_hex: String = actual.iter().map(|b| format!("{:02x}", b)).collect();
508    if actual_hex != expected_hash {
509        return Err(format!(
510            "hash mismatch: expected {expected_hash}, got {actual_hex}"
511        ));
512    }
513
514    Ok(Some(cache_data))
515}
516
517/// Compute SHA256 of a byte slice. Inline FIPS 180-4 implementation
518fn compute_sha256(data: &[u8]) -> [u8; 32] {
519    let mut hasher = Sha256::new();
520    hasher.update(data);
521    hasher.finalize()
522}
523
524/// Minimal SHA256 implementation (FIPS 180-4). Used only for the
525/// pipeline cache integrity check so we don't add a sha2 dependency.
526#[derive(Clone)]
527struct Sha256 {
528    state: [u32; 8],
529    buffer: [u8; 64],
530    buffer_len: usize,
531    total_len: u64,
532}
533
534impl Sha256 {
535    const K: [u32; 64] = [
536        0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
537        0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
538        0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
539        0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
540        0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
541        0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
542        0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
543        0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
544        0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
545        0xc67178f2,
546    ];
547
548    fn new() -> Self {
549        Self {
550            state: [
551                0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
552                0x5be0cd19,
553            ],
554            buffer: [0; 64],
555            buffer_len: 0,
556            total_len: 0,
557        }
558    }
559
560    fn update(&mut self, data: &[u8]) {
561        self.total_len = self.total_len.wrapping_add(data.len() as u64);
562        for &b in data {
563            self.buffer[self.buffer_len] = b;
564            self.buffer_len += 1;
565            if self.buffer_len == 64 {
566                let block = self.buffer;
567                self.compress(&block);
568                self.buffer_len = 0;
569            }
570        }
571    }
572
573    fn finalize(mut self) -> [u8; 32] {
574        self.buffer[self.buffer_len] = 0x80;
575        self.buffer_len += 1;
576        if self.buffer_len > 56 {
577            for b in &mut self.buffer[self.buffer_len..] {
578                *b = 0;
579            }
580            let block = self.buffer;
581            self.compress(&block);
582            self.buffer_len = 0;
583        }
584        for b in &mut self.buffer[self.buffer_len..56] {
585            *b = 0;
586        }
587        let bit_len = self.total_len.wrapping_mul(8);
588        self.buffer[56..64].copy_from_slice(&bit_len.to_be_bytes());
589        let block = self.buffer;
590        self.compress(&block);
591
592        let mut out = [0u8; 32];
593        for (i, &s) in self.state.iter().enumerate() {
594            out[i * 4..(i + 1) * 4].copy_from_slice(&s.to_be_bytes());
595        }
596        out
597    }
598
599    fn compress(&mut self, block: &[u8]) {
600        let mut w = [0u32; 64];
601        for i in 0..16 {
602            w[i] = u32::from_be_bytes([
603                block[i * 4],
604                block[i * 4 + 1],
605                block[i * 4 + 2],
606                block[i * 4 + 3],
607            ]);
608        }
609        for i in 16..64 {
610            let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
611            let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
612            w[i] = w[i - 16]
613                .wrapping_add(s0)
614                .wrapping_add(w[i - 7])
615                .wrapping_add(s1);
616        }
617        let mut a = self.state[0];
618        let mut b = self.state[1];
619        let mut c = self.state[2];
620        let mut d = self.state[3];
621        let mut e = self.state[4];
622        let mut f = self.state[5];
623        let mut g = self.state[6];
624        let mut h = self.state[7];
625        for (i, wi) in w.iter().enumerate() {
626            let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
627            let ch = (e & f) ^ ((!e) & g);
628            let t1 = h
629                .wrapping_add(s1)
630                .wrapping_add(ch)
631                .wrapping_add(Self::K[i])
632                .wrapping_add(*wi);
633            let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
634            let mj = (a & b) ^ (a & c) ^ (b & c);
635            let t2 = s0.wrapping_add(mj);
636            h = g;
637            g = f;
638            f = e;
639            e = d.wrapping_add(t1);
640            d = c;
641            c = b;
642            b = a;
643            a = t1.wrapping_add(t2);
644        }
645        self.state[0] = self.state[0].wrapping_add(a);
646        self.state[1] = self.state[1].wrapping_add(b);
647        self.state[2] = self.state[2].wrapping_add(c);
648        self.state[3] = self.state[3].wrapping_add(d);
649        self.state[4] = self.state[4].wrapping_add(e);
650        self.state[5] = self.state[5].wrapping_add(f);
651        self.state[6] = self.state[6].wrapping_add(g);
652        self.state[7] = self.state[7].wrapping_add(h);
653    }
654}
655
656fn compute_mip_levels(width: u32, height: u32) -> u32 {
657    let max_dim = width.max(height);
658    if max_dim <= 1 {
659        return 1;
660    }
661    (32 - max_dim.leading_zeros()).clamp(2, 8)
662}
663
664impl GpuRenderer {
665    /// Access the hologram instances submitted this frame.
666    pub fn hologram_instances(&self) -> &[HologramInstance] {
667        &self.hologram_instances
668    }
669
670    pub fn set_quality_level(&mut self, level: QualityLevel) {
671        self.quality_level = level;
672    }
673
674    pub fn set_config(&mut self, config: crate::subsystems::RendererConfig) {
675        self.config = config;
676    }
677
678    pub fn config(&self) -> &crate::subsystems::RendererConfig {
679        &self.config
680    }
681
682    pub fn quality_level(&self) -> QualityLevel {
683        self.quality_level
684    }
685
686    /// Access the PBR rendering pipeline for 3D meshes.
687    pub fn pbr_pipeline(&self) -> &wgpu::RenderPipeline {
688        &self.pbr_pipeline
689    }
690
691    /// Access the shadow map rendering pipeline.
692    pub fn shadow_pipeline(&self) -> &wgpu::RenderPipeline {
693        &self.shadow_pipeline
694    }
695
696    /// Access the scene uniform buffer.
697    pub fn scene_buffer(&self) -> &wgpu::Buffer {
698        &self.scene_buffer
699    }
700
701    pub(crate) fn lock_or_clear_cache<'a, T: ClearInto>(
702        lock: &'a std::sync::Mutex<T>,
703    ) -> std::sync::MutexGuard<'a, T> {
704        match lock.lock() {
705            Ok(guard) => guard,
706            Err(poisoned) => {
707                tracing::warn!("[GPU] lock_or_clear_cache: mutex poisoned, clearing cache...");
708                let mut guard = poisoned.into_inner();
709                guard.clear_into();
710                guard
711            }
712        }
713    }
714
715    pub fn update_mouse(&mut self, mouse: [f32; 2], velocity: [f32; 2]) {
716        self.current_scene.mouse = mouse;
717        self.current_scene.mouse_velocity = velocity;
718        self.queue.write_buffer(
719            &self.scene_buffer,
720            0,
721            bytemuck::bytes_of(&self.current_scene),
722        );
723    }
724
725    pub fn invalidate_material_cache(&mut self) {
726        self.cached_graph_plan = None;
727    }
728
729    pub fn invalidate_all_caches(&mut self) -> usize {
730        let mut cleared = 0;
731        {
732            let mut bg_cache = Self::lock_or_clear_cache(&self.bind_group_cache);
733            cleared += bg_cache.len();
734            bg_cache.clear();
735        }
736        {
737            let mut view_cache = Self::lock_or_clear_cache(&self.texture_view_cache);
738            cleared += view_cache.len();
739            view_cache.clear();
740        }
741        cleared += self.text.shaped_cache.len();
742        self.text.shaped_cache.clear();
743        cleared += self.svg.model_cache.len();
744        self.svg.model_cache.clear();
745        cleared += self.svg.tree_cache.len();
746        self.svg.tree_cache.clear();
747        self.svg.clear_filter_batches();
748        cleared
749    }
750
751    pub fn prewarm_text_cache(&mut self, labels: &[(&str, f32)]) {
752        let mut count = 0;
753        for (text, size) in labels {
754            let cache_key = (text.to_string(), (size * 100.0) as u32);
755            if self.text.shaped_cache.contains(&cache_key) {
756                continue;
757            }
758            let style = cvkg_runic_text::TextStyle::new("Inter", *size);
759            let spans = [cvkg_runic_text::TextSpan::new(text, style)];
760            if let Ok(shaped) = self.text.engine.shape_layout(
761                &spans,
762                None,
763                cvkg_runic_text::TextAlign::Start,
764                cvkg_runic_text::TextOverflow::Visible,
765            ) {
766                self.text
767                    .shaped_cache
768                    .put(cache_key, std::sync::Arc::new(shaped));
769                count += 1;
770            }
771        }
772        if count > 0 {
773            tracing::info!("[Surtr] prewarm_text_cache: pre-shaped {} labels", count);
774        }
775    }
776
777    pub(crate) fn select_best_surface_format(
778        formats: &[wgpu::TextureFormat],
779    ) -> wgpu::TextureFormat {
780        if formats.is_empty() {
781            return wgpu::TextureFormat::Rgba8Unorm;
782        }
783        let preferred_formats = [
784            wgpu::TextureFormat::Rgba16Float,
785            wgpu::TextureFormat::Rgba8Unorm,
786            wgpu::TextureFormat::Bgra8UnormSrgb,
787            wgpu::TextureFormat::Rgba8UnormSrgb,
788            wgpu::TextureFormat::Bgra8Unorm,
789            wgpu::TextureFormat::Rgba8Unorm,
790            wgpu::TextureFormat::Rgba8Unorm,
791        ];
792        for preferred in &preferred_formats {
793            if formats.contains(preferred) {
794                return *preferred;
795            }
796        }
797        if formats.contains(&wgpu::TextureFormat::Rgba8Unorm) {
798            return wgpu::TextureFormat::Rgba8Unorm;
799        }
800        formats[0]
801    }
802
803    pub(crate) fn rebuild_texture_array_bind_group(&mut self) {
804        let views: Vec<&wgpu::TextureView> = self.texture_views.iter().collect();
805        self.mega_heim_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
806            layout: &self.texture_bind_group_layout,
807            entries: &[
808                wgpu::BindGroupEntry {
809                    binding: 0,
810                    resource: wgpu::BindingResource::TextureViewArray(&views),
811                },
812                wgpu::BindGroupEntry {
813                    binding: 1,
814                    resource: wgpu::BindingResource::Sampler(&self.dummy_sampler),
815                },
816            ],
817            label: Some("Mega-Heim Rebuilt Bind Group"),
818        });
819    }
820
821    pub(crate) fn update_vram_telemetry(&mut self) {
822        let buffers = self.geometry_buffers.vertex_buffer.size()
823            + self.geometry_buffers.index_buffer.size()
824            + self.geometry_buffers.instance_buffer.size()
825            + self.scene_buffer.size()
826            + self.theme_buffer.size()
827            + self.particle_buffer.size()
828            + self.particle_uniform_buffer.size();
829
830        let mut textures = self.config.mega_heim_vram_bytes();
831        textures += 4; // Dummy texture
832
833        for surface in self.surfaces.values() {
834            let width = surface.config.width;
835            let height = surface.config.height;
836            let format_bytes = 8; // Rgba16Float
837            textures += (width * height * format_bytes) as u64; // Scene texture
838            textures +=
839                (width * height * format_bytes * self.quality_level.msaa_sample_count()) as u64; // MSAA texture
840            textures += (width * height * 4) as u64; // Depth texture (Depth32Float)
841
842            let blur_width = (width / 2).max(1);
843            let blur_height = (height / 2).max(1);
844            let blur_bytes = (blur_width * blur_height * 4) as u64;
845            textures += blur_bytes * 4; // 2x blur + 2x bloom textures
846        }
847
848        if let Some(ref ctx) = self.headless_context {
849            let format_bytes = 8; // Rgba16Float
850            textures += (ctx.width * ctx.height * format_bytes) as u64; // Scene texture
851            textures +=
852                (ctx.width * ctx.height * format_bytes * self.quality_level.msaa_sample_count())
853                    as u64; // MSAA texture
854            textures += (ctx.width * ctx.height * 4) as u64; // Depth texture
855            textures += (ctx.width * ctx.height * 4) as u64; // Output texture
856        }
857
858        self.vram_buffers_bytes = buffers;
859        self.vram_textures_bytes = textures;
860        self.telemetry.vram_usage_mb = (buffers + textures) as f32 / (1024.0 * 1024.0);
861    }
862
863    pub fn get_telemetry(&self) -> cvkg_core::TelemetryData {
864        self.telemetry.clone()
865    }
866
867    pub fn resize(
868        &mut self,
869        window_id: winit::window::WindowId,
870        width: u32,
871        height: u32,
872        scale_factor: f32,
873    ) {
874        if width > 0
875            && height > 0
876            && let Some(ctx) = self.surfaces.get_mut(&window_id)
877        {
878            if ctx.config.width == width && ctx.config.height == height {
879                return;
880            }
881
882            tracing::info!("[GPU] Reconfiguring surface: {}x{}", width, height);
883            GpuRenderer::lock_or_clear_cache(&self.bind_group_cache).clear();
884            GpuRenderer::lock_or_clear_cache(&self.texture_view_cache).clear();
885            self.text.shaped_cache.clear();
886            ctx.config.width = width;
887            ctx.config.height = height;
888            ctx.scale_factor = scale_factor;
889            ctx.surface.configure(&self.device, &ctx.config);
890
891            let texture_desc = wgpu::TextureDescriptor {
892                label: Some("Surtr Scene Texture"),
893                size: wgpu::Extent3d {
894                    width,
895                    height,
896                    depth_or_array_layers: 1,
897                },
898                mip_level_count: 1,
899                sample_count: 1,
900                dimension: wgpu::TextureDimension::D2,
901                format: wgpu::TextureFormat::Rgba16Float,
902                usage: wgpu::TextureUsages::RENDER_ATTACHMENT
903                    | wgpu::TextureUsages::TEXTURE_BINDING,
904                view_formats: &[],
905            };
906
907            let scene_tex = self.device.create_texture(&texture_desc);
908
909            let msaa_desc = wgpu::TextureDescriptor {
910                label: Some("Scene MSAA"),
911                size: texture_desc.size,
912                mip_level_count: 1,
913                sample_count: self.quality_level.msaa_sample_count(),
914                dimension: wgpu::TextureDimension::D2,
915                format: wgpu::TextureFormat::Rgba16Float,
916                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
917                view_formats: &[],
918            };
919            let scene_msaa_tex = self.device.create_texture(&msaa_desc);
920            ctx.scene_texture = scene_tex.create_view(&wgpu::TextureViewDescriptor::default());
921            ctx.scene_msaa_texture =
922                scene_msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
923
924            self.registry.remove_image(ctx.blur_tex_a);
925            self.registry.remove_image(ctx.blur_tex_b);
926            self.registry.remove_image(ctx.bloom_tex_a);
927            self.registry.remove_image(ctx.bloom_tex_b);
928
929            let blur_width = (width / 2).max(1);
930            let blur_height = (height / 2).max(1);
931
932            let blur_desc_a = crate::kvasir::resource::ResourceDescriptor {
933                label: Some("Surtr Blur Texture A".into()),
934                kind: crate::kvasir::resource::ResourceKind::Image {
935                    format: ctx.config.format,
936                    width: blur_width,
937                    height: blur_height,
938                    mip_level_count: compute_mip_levels(blur_width, blur_height),
939                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT
940                        | wgpu::TextureUsages::TEXTURE_BINDING
941                        | wgpu::TextureUsages::COPY_SRC,
942                },
943                lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
944            };
945            ctx.blur_tex_a = self.registry.allocate_image(&self.device, &blur_desc_a);
946
947            let blur_desc_b = crate::kvasir::resource::ResourceDescriptor {
948                label: Some("Surtr Blur Texture B".into()),
949                kind: crate::kvasir::resource::ResourceKind::Image {
950                    format: ctx.config.format,
951                    width: blur_width,
952                    height: blur_height,
953                    mip_level_count: compute_mip_levels(blur_width, blur_height),
954                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT
955                        | wgpu::TextureUsages::TEXTURE_BINDING
956                        | wgpu::TextureUsages::COPY_SRC,
957                },
958                lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
959            };
960            ctx.blur_tex_b = self.registry.allocate_image(&self.device, &blur_desc_b);
961
962            let bloom_desc_a = crate::kvasir::resource::ResourceDescriptor {
963                label: Some("Surtr Bloom Texture A".into()),
964                kind: crate::kvasir::resource::ResourceKind::Image {
965                    format: ctx.config.format,
966                    width: blur_width,
967                    height: blur_height,
968                    mip_level_count: compute_mip_levels(blur_width, blur_height),
969                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT
970                        | wgpu::TextureUsages::TEXTURE_BINDING
971                        | wgpu::TextureUsages::COPY_SRC,
972                },
973                lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
974            };
975            ctx.bloom_tex_a = self.registry.allocate_image(&self.device, &bloom_desc_a);
976
977            let bloom_desc_b = crate::kvasir::resource::ResourceDescriptor {
978                label: Some("Surtr Bloom Texture B".into()),
979                kind: crate::kvasir::resource::ResourceKind::Image {
980                    format: ctx.config.format,
981                    width: blur_width,
982                    height: blur_height,
983                    mip_level_count: compute_mip_levels(blur_width, blur_height),
984                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT
985                        | wgpu::TextureUsages::TEXTURE_BINDING
986                        | wgpu::TextureUsages::COPY_SRC,
987                },
988                lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
989            };
990            ctx.bloom_tex_b = self.registry.allocate_image(&self.device, &bloom_desc_b);
991
992            ctx.scene_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
993                layout: &self.env_bind_group_layout,
994                entries: &[
995                    wgpu::BindGroupEntry {
996                        binding: 0,
997                        resource: wgpu::BindingResource::TextureView(&ctx.scene_texture),
998                    },
999                    wgpu::BindGroupEntry {
1000                        binding: 1,
1001                        resource: wgpu::BindingResource::Sampler(&ctx.sampler),
1002                    },
1003                ],
1004                label: Some("Scene Bind Group Resize"),
1005            });
1006
1007            let scene_views: Vec<&wgpu::TextureView> =
1008                (0..32).map(|_| &ctx.scene_texture).collect();
1009            ctx.scene_texture_bind_group =
1010                self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1011                    layout: &self.texture_bind_group_layout,
1012                    entries: &[
1013                        wgpu::BindGroupEntry {
1014                            binding: 0,
1015                            resource: wgpu::BindingResource::TextureViewArray(&scene_views),
1016                        },
1017                        wgpu::BindGroupEntry {
1018                            binding: 1,
1019                            resource: wgpu::BindingResource::Sampler(&ctx.sampler),
1020                        },
1021                    ],
1022                    label: Some("Scene Texture Bind Group Resize"),
1023                });
1024
1025            let depth_texture = self.device.create_texture(&wgpu::TextureDescriptor {
1026                label: Some("Surtr Depth Texture"),
1027                size: wgpu::Extent3d {
1028                    width,
1029                    height,
1030                    depth_or_array_layers: 1,
1031                },
1032                mip_level_count: 1,
1033                sample_count: self.quality_level.msaa_sample_count(),
1034                dimension: wgpu::TextureDimension::D2,
1035                format: wgpu::TextureFormat::Depth32Float,
1036                usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1037                    | wgpu::TextureUsages::TEXTURE_BINDING,
1038                view_formats: &[],
1039            });
1040            ctx.depth_texture_view =
1041                depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
1042        }
1043    }
1044
1045    pub fn reset_time(&mut self) {
1046        self.start_time = std::time::Instant::now();
1047    }
1048
1049    pub fn reclaim_vram(&mut self) {
1050        tracing::warn!("[GPU] Sundr Compaction: Compacting Mega-Heim...");
1051
1052        let new_mega_heim_tex = self.device.create_texture(&wgpu::TextureDescriptor {
1053            label: Some("Sundr Mega-Heim (Compacted)"),
1054            size: wgpu::Extent3d {
1055                width: 4096,
1056                height: 4096,
1057                depth_or_array_layers: 1,
1058            },
1059            mip_level_count: 1,
1060            sample_count: 1,
1061            dimension: wgpu::TextureDimension::D2,
1062            format: wgpu::TextureFormat::Rgba8UnormSrgb,
1063            usage: wgpu::TextureUsages::TEXTURE_BINDING
1064                | wgpu::TextureUsages::COPY_DST
1065                | wgpu::TextureUsages::COPY_SRC,
1066            view_formats: &[],
1067        });
1068
1069        let mut new_packer = SkylinePacker::new(4096, 4096);
1070        let mut encoder = self
1071            .device
1072            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1073                label: Some("Heim Compaction Encoder"),
1074            });
1075
1076        let image_entries: Vec<(String, Rect)> = self
1077            .image_uv_registry
1078            .iter()
1079            .map(|(k, v)| (k.clone(), *v))
1080            .collect();
1081        for (name, old_uv) in image_entries {
1082            if let Some(&tex_idx) = self.texture_registry.get(&name)
1083                && tex_idx == 0
1084            {
1085                let w_px = (old_uv.width * 4096.0).round() as u32;
1086                let h_px = (old_uv.height * 4096.0).round() as u32;
1087                let old_x_px = (old_uv.x * 4096.0).round() as u32;
1088                let old_y_px = (old_uv.y * 4096.0).round() as u32;
1089
1090                if let Some((new_x, new_y)) = new_packer.pack(w_px, h_px) {
1091                    encoder.copy_texture_to_texture(
1092                        wgpu::TexelCopyTextureInfo {
1093                            texture: &self.mega_heim_tex,
1094                            mip_level: 0,
1095                            origin: wgpu::Origin3d {
1096                                x: old_x_px,
1097                                y: old_y_px,
1098                                z: 0,
1099                            },
1100                            aspect: wgpu::TextureAspect::All,
1101                        },
1102                        wgpu::TexelCopyTextureInfo {
1103                            texture: &new_mega_heim_tex,
1104                            mip_level: 0,
1105                            origin: wgpu::Origin3d {
1106                                x: new_x,
1107                                y: new_y,
1108                                z: 0,
1109                            },
1110                            aspect: wgpu::TextureAspect::All,
1111                        },
1112                        wgpu::Extent3d {
1113                            width: w_px,
1114                            height: h_px,
1115                            depth_or_array_layers: 1,
1116                        },
1117                    );
1118
1119                    let new_uv = Rect {
1120                        x: new_x as f32 / 4096.0,
1121                        y: new_y as f32 / 4096.0,
1122                        width: old_uv.width,
1123                        height: old_uv.height,
1124                    };
1125                    self.image_uv_registry.put(name.clone(), new_uv);
1126                }
1127            }
1128        }
1129
1130        let text_entries: Vec<(u64, (Rect, f32, f32, f32, f32))> = self
1131            .text
1132            .glyph_cache
1133            .iter()
1134            .map(|(k, v)| (*k, *v))
1135            .collect();
1136        for (hash, (old_uv, w_f, h_f, x_off, y_off)) in text_entries {
1137            let w_px = (old_uv.width * 4096.0).round() as u32;
1138            let h_px = (old_uv.height * 4096.0).round() as u32;
1139            let old_x_px = (old_uv.x * 4096.0).round() as u32;
1140            let old_y_px = (old_uv.y * 4096.0).round() as u32;
1141
1142            if let Some((new_x, new_y)) = new_packer.pack(w_px, h_px) {
1143                encoder.copy_texture_to_texture(
1144                    wgpu::TexelCopyTextureInfo {
1145                        texture: &self.mega_heim_tex,
1146                        mip_level: 0,
1147                        origin: wgpu::Origin3d {
1148                            x: old_x_px,
1149                            y: old_y_px,
1150                            z: 0,
1151                        },
1152                        aspect: wgpu::TextureAspect::All,
1153                    },
1154                    wgpu::TexelCopyTextureInfo {
1155                        texture: &new_mega_heim_tex,
1156                        mip_level: 0,
1157                        origin: wgpu::Origin3d {
1158                            x: new_x,
1159                            y: new_y,
1160                            z: 0,
1161                        },
1162                        aspect: wgpu::TextureAspect::All,
1163                    },
1164                    wgpu::Extent3d {
1165                        width: w_px,
1166                        height: h_px,
1167                        depth_or_array_layers: 1,
1168                    },
1169                );
1170
1171                let new_uv = Rect {
1172                    x: new_x as f32 / 4096.0,
1173                    y: new_y as f32 / 4096.0,
1174                    width: old_uv.width,
1175                    height: old_uv.height,
1176                };
1177                self.text
1178                    .glyph_cache
1179                    .put(hash, (new_uv, w_f, h_f, x_off, y_off));
1180            }
1181        }
1182
1183        self.queue.submit(std::iter::once(encoder.finish()));
1184
1185        self.mega_heim_tex = new_mega_heim_tex;
1186        let mega_heim_view_obj = self
1187            .mega_heim_tex
1188            .create_view(&wgpu::TextureViewDescriptor::default());
1189        self.texture_views[0] = mega_heim_view_obj.clone();
1190
1191        self.rebuild_texture_array_bind_group();
1192
1193        if !self.texture_bind_groups.is_empty() {
1194            self.texture_bind_groups[0] = self.mega_heim_bind_group.clone();
1195        }
1196
1197        self.heim_packer = new_packer;
1198        self.telemetry.vram_exhausted = false;
1199    }
1200}
1201
1202impl Drop for GpuRenderer {
1203    fn drop(&mut self) {
1204        let cache_dir = std::env::current_exe()
1205            .ok()
1206            .and_then(|p| p.parent().map(|d| d.join("pipeline_cache")))
1207            .unwrap_or_else(|| std::env::temp_dir().join("cvkg_pipeline_cache"));
1208        let _ = std::fs::create_dir_all(&cache_dir);
1209        let cache_path = cache_dir.join("cvkg_render_gpu.bin");
1210        if let Some(cache) = &self.pipeline_cache
1211            && let Some(data) = cache.get_data()
1212            && let Err(e) = std::fs::write(&cache_path, data)
1213        {
1214            tracing::warn!("Failed to persist pipeline cache: {}", e);
1215        }
1216
1217        let _ = self.device.poll(wgpu::PollType::Wait {
1218            submission_index: None,
1219            timeout: None,
1220        });
1221    }
1222}
1223
1224impl GpuRenderer {
1225    pub(crate) fn current_width(&self) -> u32 {
1226        if let Some(id) = self.current_window {
1227            self.surfaces.get(&id).map(|s| s.config.width).unwrap_or(1)
1228        } else {
1229            self.headless_context.as_ref().map(|h| h.width).unwrap_or(1)
1230        }
1231    }
1232
1233    pub(crate) fn current_height(&self) -> u32 {
1234        if let Some(id) = self.current_window {
1235            self.surfaces.get(&id).map(|s| s.config.height).unwrap_or(1)
1236        } else {
1237            self.headless_context
1238                .as_ref()
1239                .map(|h| h.height)
1240                .unwrap_or(1)
1241        }
1242    }
1243
1244    pub(crate) fn current_scale_factor(&self) -> f32 {
1245        if let Some(id) = self.current_window {
1246            self.surfaces
1247                .get(&id)
1248                .map(|s| s.scale_factor)
1249                .unwrap_or(1.0)
1250        } else {
1251            self.headless_context
1252                .as_ref()
1253                .map(|h| h.scale_factor)
1254                .unwrap_or(1.0)
1255        }
1256    }
1257
1258    pub(crate) fn current_time(&self) -> f32 {
1259        self.start_time.elapsed().as_secs_f32()
1260    }
1261
1262    /// forge_headless -- Initializes Surtr without a window for visual regression testing.
1263    pub async fn forge_headless(width: u32, height: u32) -> Self {
1264        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
1265            backends: wgpu::Backends::all(),
1266            flags: wgpu::InstanceFlags::default(),
1267            backend_options: wgpu::BackendOptions::default(),
1268            display: None,
1269            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
1270        });
1271
1272        // Request adapter with robust multi-stage fallback for Bumblebee/Optimus compatibility
1273        tracing::info!("[GPU] Requesting HighPerformance adapter (headless)...");
1274        let mut adapter = instance
1275            .request_adapter(&wgpu::RequestAdapterOptions {
1276                power_preference: wgpu::PowerPreference::HighPerformance,
1277                compatible_surface: None,
1278                force_fallback_adapter: false,
1279            })
1280            .await
1281            .ok();
1282
1283        if adapter.is_none() {
1284            tracing::warn!(
1285                "[GPU] HighPerformance adapter failed (possible Bumblebee/Optimus), trying LowPower..."
1286            );
1287            adapter = instance
1288                .request_adapter(&wgpu::RequestAdapterOptions {
1289                    power_preference: wgpu::PowerPreference::LowPower,
1290                    compatible_surface: None,
1291                    force_fallback_adapter: false,
1292                })
1293                .await
1294                .ok();
1295        }
1296
1297        if adapter.is_none() {
1298            tracing::warn!("[GPU] Hardware adapters failed, trying Software fallback...");
1299            adapter = instance
1300                .request_adapter(&wgpu::RequestAdapterOptions {
1301                    power_preference: wgpu::PowerPreference::LowPower,
1302                    compatible_surface: None,
1303                    force_fallback_adapter: true,
1304                })
1305                .await
1306                .ok();
1307        }
1308
1309        let adapter = adapter.expect("Failed to find a suitable GPU for Surtr");
1310        let info = adapter.get_info();
1311        let caps =
1312            crate::subsystems::GpuCapabilities::detect(&info.name, format!("{:?}", info.backend));
1313        tracing::info!(
1314            "[GPU] Selected adapter: {} ({:?}) on backend: {:?} -- detected as {}",
1315            info.name,
1316            info.device_type,
1317            info.backend,
1318            caps.vendor
1319        );
1320        tracing::info!("[GPU] Driver info: {} - {}", info.driver, info.driver_info);
1321        let required_features = adapter.features()
1322            & (wgpu::Features::TIMESTAMP_QUERY
1323                | wgpu::Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING
1324                | wgpu::Features::TEXTURE_BINDING_ARRAY);
1325
1326        let (device, queue) = adapter
1327            .request_device(&wgpu::DeviceDescriptor {
1328                label: Some("Surtr Headless Forge"),
1329                required_features,
1330                required_limits: wgpu::Limits {
1331                    max_bindings_per_bind_group: adapter
1332                        .limits()
1333                        .max_bindings_per_bind_group
1334                        .min(256),
1335                    max_binding_array_elements_per_shader_stage: adapter
1336                        .limits()
1337                        .max_binding_array_elements_per_shader_stage
1338                        .min(256),
1339                    ..wgpu::Limits::default()
1340                },
1341                memory_hints: wgpu::MemoryHints::default(),
1342                experimental_features: wgpu::ExperimentalFeatures::disabled(),
1343                trace: wgpu::Trace::Off,
1344            })
1345            .await
1346            .expect("Failed to create Surtr device");
1347
1348        let instance = Arc::new(instance);
1349        let adapter = Arc::new(adapter);
1350
1351        device.on_uncaptured_error(Arc::new(|error| {
1352            tracing::error!(
1353                "[GPU] Uncaptured device error (Device Lost or Panic): {:?}",
1354                error
1355            );
1356        }));
1357
1358        let device = Arc::new(device);
1359        let queue = Arc::new(queue);
1360
1361        Self::forge_internal(
1362            instance,
1363            adapter,
1364            device,
1365            queue,
1366            None,
1367            Some((width, height, wgpu::TextureFormat::Rgba8UnormSrgb)),
1368        )
1369        .await
1370    }
1371
1372    /// Read back the headless output texture as RGBA8 pixels.
1373    /// Must be called after `end_frame` on a headless renderer.
1374    pub fn readback_headless_rgba8(&self) -> Vec<u8> {
1375        let ctx = self
1376            .headless_context
1377            .as_ref()
1378            .expect("readback_headless_rgba8 requires a headless renderer");
1379
1380        let width = ctx.width;
1381        let height = ctx.height;
1382        let row_bytes = width * 4;
1383        let padded_row_bytes = row_bytes.div_ceil(256) * 256;
1384        let buffer_size = (padded_row_bytes * height) as u64;
1385
1386        let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
1387            label: Some("headless-readback-buffer"),
1388            size: buffer_size,
1389            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1390            mapped_at_creation: false,
1391        });
1392
1393        let mut encoder = self
1394            .device
1395            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1396                label: Some("headless-readback-encoder"),
1397            });
1398
1399        encoder.copy_texture_to_buffer(
1400            wgpu::TexelCopyTextureInfo {
1401                texture: &ctx.output_texture,
1402                mip_level: 0,
1403                origin: wgpu::Origin3d::ZERO,
1404                aspect: wgpu::TextureAspect::All,
1405            },
1406            wgpu::TexelCopyBufferInfo {
1407                buffer: &output_buffer,
1408                layout: wgpu::TexelCopyBufferLayout {
1409                    offset: 0,
1410                    bytes_per_row: Some(padded_row_bytes),
1411                    rows_per_image: Some(height),
1412                },
1413            },
1414            wgpu::Extent3d {
1415                width,
1416                height,
1417                depth_or_array_layers: 1,
1418            },
1419        );
1420
1421        self.queue.submit(std::iter::once(encoder.finish()));
1422        let buffer_slice = output_buffer.slice(..);
1423        buffer_slice.map_async(wgpu::MapMode::Read, |_| {});
1424        let _ = self.device.poll(wgpu::PollType::Wait {
1425            submission_index: None,
1426            timeout: None,
1427        });
1428
1429        let data = buffer_slice.get_mapped_range();
1430        let mut result = Vec::with_capacity((width * height * 4) as usize);
1431        for row in 0..height {
1432            let start = (row * padded_row_bytes) as usize;
1433            let end = start + row_bytes as usize;
1434            result.extend_from_slice(&data[start..end]);
1435        }
1436        drop(data);
1437        output_buffer.unmap();
1438        output_buffer.destroy();
1439        result
1440    }
1441
1442    /// Render a headless frame with a draw callback and read back pixels.
1443    pub fn render_headless_frame<F>(&mut self, draw: F) -> Vec<u8>
1444    where
1445        F: FnOnce(&mut Self),
1446    {
1447        let encoder = self.begin_frame_headless();
1448        draw(self);
1449        self.end_frame(encoder);
1450        self.readback_headless_rgba8()
1451    }
1452
1453    /// Create a headless GpuRenderer from an existing device and surface.
1454    ///
1455    /// This constructor does not require an event loop and is suitable for
1456    /// headless rendering (e.g., server-side rendering, tests).
1457    /// It delegates to the existing `forge_internal` which handles all
1458    /// pipeline, buffer, and bind group initialization.
1459    pub async fn from_external(
1460        device: Arc<wgpu::Device>,
1461        queue: Arc<wgpu::Queue>,
1462        surface: wgpu::Surface<'static>,
1463        width: u32,
1464        height: u32,
1465    ) -> Self {
1466        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
1467            backends: wgpu::Backends::all(),
1468            flags: wgpu::InstanceFlags::default(),
1469            backend_options: wgpu::BackendOptions::default(),
1470            display: None,
1471            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
1472        });
1473
1474        let adapter = instance
1475            .request_adapter(&wgpu::RequestAdapterOptions {
1476                power_preference: wgpu::PowerPreference::default(),
1477                compatible_surface: Some(&surface),
1478                force_fallback_adapter: false,
1479            })
1480            .await
1481            .expect("No compatible adapter found");
1482
1483        Self::forge_internal(
1484            Arc::new(instance),
1485            Arc::new(adapter),
1486            device,
1487            queue,
1488            None,
1489            Some((width, height, wgpu::TextureFormat::Rgba8UnormSrgb)),
1490        )
1491        .await
1492    }
1493
1494    pub fn set_world_space_panels(&mut self, panels: Vec<(u64, cvkg_vdom::WorldSpacePanel)>) {
1495        self.world_space_panels = panels;
1496    }
1497
1498    pub fn begin_world_space_panel(
1499        &mut self,
1500        node_id: u64,
1501        transform: &cvkg_core::Transform3D,
1502        glass: Option<cvkg_materials::GlassMaterial>,
1503        pixels_per_unit: f32,
1504        world_size: (f32, f32),
1505    ) {
1506        if let Some(prev) = self.current_panel_id {
1507            self.panel_stack.push(prev);
1508        }
1509        self.current_panel_id = Some(node_id);
1510
1511        let panel = cvkg_vdom::WorldSpacePanel {
1512            transform: *transform,
1513            glass,
1514            pixels_per_unit,
1515            world_size,
1516            ..Default::default()
1517        };
1518        // Record it so the rendering graph knows about it
1519        if !self.world_space_panels.iter().any(|(id, _)| *id == node_id) {
1520            self.world_space_panels.push((node_id, panel));
1521        }
1522    }
1523
1524    pub fn end_world_space_panel(&mut self, _node_id: u64) {
1525        self.current_panel_id = self.panel_stack.pop();
1526    }
1527}