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