Skip to main content

proof_engine/deferred/
gbuffer.rs

1//! G-Buffer management for deferred rendering.
2//!
3//! The G-Buffer stores per-pixel geometric and material data that the lighting
4//! pass reads to compute final shading. This module provides configurable
5//! attachment layouts, bind/unbind semantics, debug visualization, MRT
6//! (Multiple Render Target) configuration, and memory usage statistics.
7
8use std::collections::HashMap;
9use std::fmt;
10
11use super::{Viewport, clampf};
12
13// ---------------------------------------------------------------------------
14// Attachment format definitions
15// ---------------------------------------------------------------------------
16
17/// Pixel format for a single G-Buffer attachment.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum GBufferAttachmentFormat {
20    /// 4x 32-bit float (position, high-precision data).
21    Rgba32F,
22    /// 2x 16-bit float (octahedral-encoded normals).
23    Rg16F,
24    /// 4x 8-bit unsigned normalized (albedo, base color).
25    Rgba8,
26    /// 4x 16-bit float (emission / HDR color).
27    Rgba16F,
28    /// 1x 8-bit unsigned integer (material ID).
29    R8,
30    /// 32-bit float depth.
31    D32F,
32    /// 24-bit depth + 8-bit stencil.
33    D24S8,
34    /// 1x 16-bit float.
35    R16F,
36    /// 2x 8-bit unsigned normalized.
37    Rg8,
38    /// 1x 32-bit float (single channel high precision).
39    R32F,
40}
41
42impl GBufferAttachmentFormat {
43    /// Returns the number of bytes per pixel for this format.
44    pub fn bytes_per_pixel(&self) -> u32 {
45        match self {
46            Self::Rgba32F => 16,
47            Self::Rg16F => 4,
48            Self::Rgba8 => 4,
49            Self::Rgba16F => 8,
50            Self::R8 => 1,
51            Self::D32F => 4,
52            Self::D24S8 => 4,
53            Self::R16F => 2,
54            Self::Rg8 => 2,
55            Self::R32F => 4,
56        }
57    }
58
59    /// Number of components in this format.
60    pub fn component_count(&self) -> u32 {
61        match self {
62            Self::Rgba32F | Self::Rgba8 | Self::Rgba16F => 4,
63            Self::Rg16F | Self::Rg8 => 2,
64            Self::R8 | Self::D32F | Self::D24S8 | Self::R16F | Self::R32F => 1,
65        }
66    }
67
68    /// Whether this is a depth or depth-stencil format.
69    pub fn is_depth(&self) -> bool {
70        matches!(self, Self::D32F | Self::D24S8)
71    }
72
73    /// Whether this format contains floating point data.
74    pub fn is_float(&self) -> bool {
75        matches!(
76            self,
77            Self::Rgba32F | Self::Rg16F | Self::Rgba16F | Self::R16F | Self::R32F | Self::D32F
78        )
79    }
80
81    /// Returns an OpenGL-style internal format constant (symbolic).
82    pub fn gl_internal_format(&self) -> u32 {
83        match self {
84            Self::Rgba32F => 0x8814,  // GL_RGBA32F
85            Self::Rg16F => 0x822F,    // GL_RG16F
86            Self::Rgba8 => 0x8058,    // GL_RGBA8
87            Self::Rgba16F => 0x881A,  // GL_RGBA16F
88            Self::R8 => 0x8229,       // GL_R8
89            Self::D32F => 0x8CAC,     // GL_DEPTH_COMPONENT32F
90            Self::D24S8 => 0x88F0,    // GL_DEPTH24_STENCIL8
91            Self::R16F => 0x822D,     // GL_R16F
92            Self::Rg8 => 0x822B,      // GL_RG8
93            Self::R32F => 0x822E,     // GL_R32F
94        }
95    }
96
97    /// Returns a human-readable name for this format.
98    pub fn name(&self) -> &'static str {
99        match self {
100            Self::Rgba32F => "RGBA32F",
101            Self::Rg16F => "RG16F",
102            Self::Rgba8 => "RGBA8",
103            Self::Rgba16F => "RGBA16F",
104            Self::R8 => "R8",
105            Self::D32F => "D32F",
106            Self::D24S8 => "D24S8",
107            Self::R16F => "R16F",
108            Self::Rg8 => "RG8",
109            Self::R32F => "R32F",
110        }
111    }
112}
113
114impl fmt::Display for GBufferAttachmentFormat {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        write!(f, "{}", self.name())
117    }
118}
119
120// ---------------------------------------------------------------------------
121// Attachment semantic names
122// ---------------------------------------------------------------------------
123
124/// Named semantic for a G-Buffer attachment.
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
126pub enum GBufferSemantic {
127    Position,
128    Normal,
129    Albedo,
130    Emission,
131    MaterialId,
132    Roughness,
133    Metallic,
134    Depth,
135    Velocity,
136    AmbientOcclusion,
137    Custom(u32),
138}
139
140impl GBufferSemantic {
141    pub fn name(&self) -> &'static str {
142        match self {
143            Self::Position => "Position",
144            Self::Normal => "Normal",
145            Self::Albedo => "Albedo",
146            Self::Emission => "Emission",
147            Self::MaterialId => "MaterialID",
148            Self::Roughness => "Roughness",
149            Self::Metallic => "Metallic",
150            Self::Depth => "Depth",
151            Self::Velocity => "Velocity",
152            Self::AmbientOcclusion => "AO",
153            Self::Custom(_) => "Custom",
154        }
155    }
156}
157
158impl fmt::Display for GBufferSemantic {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        match self {
161            Self::Custom(id) => write!(f, "Custom({})", id),
162            _ => write!(f, "{}", self.name()),
163        }
164    }
165}
166
167// ---------------------------------------------------------------------------
168// Attachment descriptor
169// ---------------------------------------------------------------------------
170
171/// Describes a single attachment in the G-Buffer.
172#[derive(Debug, Clone)]
173pub struct GBufferAttachment {
174    /// Semantic role of this attachment.
175    pub semantic: GBufferSemantic,
176    /// Pixel format.
177    pub format: GBufferAttachmentFormat,
178    /// The color attachment index (for MRT). Depth attachments use index u32::MAX.
179    pub color_index: u32,
180    /// Texture unit to bind when sampling this attachment in the lighting pass.
181    pub texture_unit: u32,
182    /// Clear value for this attachment (RGBA or depth).
183    pub clear_value: ClearValue,
184    /// Whether this attachment should be sampled with linear filtering.
185    pub linear_filter: bool,
186    /// Mip levels (1 = no mipmaps).
187    pub mip_levels: u32,
188    /// Internal texture handle (opaque ID).
189    pub texture_handle: u64,
190    /// Optional label for debug tooling.
191    pub label: String,
192}
193
194impl GBufferAttachment {
195    pub fn new(
196        semantic: GBufferSemantic,
197        format: GBufferAttachmentFormat,
198        color_index: u32,
199        texture_unit: u32,
200    ) -> Self {
201        let clear_value = if format.is_depth() {
202            ClearValue::Depth(1.0)
203        } else {
204            ClearValue::Color([0.0, 0.0, 0.0, 0.0])
205        };
206
207        Self {
208            semantic,
209            format,
210            color_index,
211            texture_unit,
212            clear_value,
213            linear_filter: !matches!(
214                format,
215                GBufferAttachmentFormat::R8
216            ),
217            mip_levels: 1,
218            texture_handle: 0,
219            label: format!("GBuffer_{}", semantic),
220        }
221    }
222
223    /// Returns the memory size in bytes for a given resolution.
224    pub fn memory_bytes(&self, width: u32, height: u32) -> u64 {
225        let base = width as u64 * height as u64 * self.format.bytes_per_pixel() as u64;
226        if self.mip_levels <= 1 {
227            base
228        } else {
229            // Approximate mip chain: sum of 1 + 1/4 + 1/16 + ... converges to 4/3
230            (base as f64 * 1.334).ceil() as u64
231        }
232    }
233
234    /// Set a custom clear value.
235    pub fn with_clear_value(mut self, cv: ClearValue) -> Self {
236        self.clear_value = cv;
237        self
238    }
239
240    /// Enable or disable linear filtering.
241    pub fn with_linear_filter(mut self, enabled: bool) -> Self {
242        self.linear_filter = enabled;
243        self
244    }
245
246    /// Set the number of mip levels.
247    pub fn with_mip_levels(mut self, levels: u32) -> Self {
248        self.mip_levels = levels.max(1);
249        self
250    }
251
252    /// Set a debug label.
253    pub fn with_label(mut self, label: impl Into<String>) -> Self {
254        self.label = label.into();
255        self
256    }
257}
258
259// ---------------------------------------------------------------------------
260// Clear values
261// ---------------------------------------------------------------------------
262
263/// Clear value for a G-Buffer attachment.
264#[derive(Debug, Clone, Copy)]
265pub enum ClearValue {
266    /// RGBA color clear.
267    Color([f32; 4]),
268    /// Depth clear value (typically 1.0 for far plane).
269    Depth(f32),
270    /// Depth + stencil clear.
271    DepthStencil(f32, u8),
272    /// Integer clear value (for material IDs, etc.).
273    IntColor([i32; 4]),
274    /// Do not clear this attachment.
275    DontCare,
276}
277
278impl ClearValue {
279    pub fn black() -> Self {
280        Self::Color([0.0, 0.0, 0.0, 1.0])
281    }
282
283    pub fn transparent() -> Self {
284        Self::Color([0.0, 0.0, 0.0, 0.0])
285    }
286
287    pub fn white() -> Self {
288        Self::Color([1.0, 1.0, 1.0, 1.0])
289    }
290
291    pub fn far_depth() -> Self {
292        Self::Depth(1.0)
293    }
294
295    pub fn near_depth() -> Self {
296        Self::Depth(0.0)
297    }
298}
299
300impl Default for ClearValue {
301    fn default() -> Self {
302        Self::Color([0.0, 0.0, 0.0, 0.0])
303    }
304}
305
306// ---------------------------------------------------------------------------
307// G-Buffer layout
308// ---------------------------------------------------------------------------
309
310/// Describes the full set of attachments in a G-Buffer.
311///
312/// The default layout provides:
313/// - Position (RGBA32F) at color attachment 0
314/// - Normal (RG16F, octahedral encoding) at color attachment 1
315/// - Albedo (RGBA8) at color attachment 2
316/// - Emission (RGBA16F) at color attachment 3
317/// - MaterialID (R8) at color attachment 4
318/// - Roughness (R8) at color attachment 5
319/// - Metallic (R8) at color attachment 6
320/// - Depth (D32F) as the depth attachment
321#[derive(Debug, Clone)]
322pub struct GBufferLayout {
323    /// Ordered list of color attachments.
324    pub color_attachments: Vec<GBufferAttachment>,
325    /// The depth (or depth-stencil) attachment.
326    pub depth_attachment: GBufferAttachment,
327    /// Maximum number of simultaneous render targets supported.
328    pub max_color_attachments: u32,
329    /// Whether to use octahedral normal encoding (saves bandwidth vs RGBA16F).
330    pub use_octahedral_normals: bool,
331    /// Whether thin G-Buffer packing is enabled (combine roughness+metallic into one RG8).
332    pub thin_gbuffer: bool,
333}
334
335impl GBufferLayout {
336    /// Create a new empty layout.
337    pub fn new() -> Self {
338        Self {
339            color_attachments: Vec::new(),
340            depth_attachment: GBufferAttachment::new(
341                GBufferSemantic::Depth,
342                GBufferAttachmentFormat::D32F,
343                u32::MAX,
344                15, // typically last texture unit
345            ),
346            max_color_attachments: 8,
347            use_octahedral_normals: true,
348            thin_gbuffer: false,
349        }
350    }
351
352    /// Create the default G-Buffer layout used by the deferred pipeline.
353    pub fn default_layout() -> Self {
354        let mut layout = Self::new();
355        layout.use_octahedral_normals = true;
356
357        // Position: world-space XYZ + W (view distance or custom)
358        layout.add_color_attachment(GBufferAttachment::new(
359            GBufferSemantic::Position,
360            GBufferAttachmentFormat::Rgba32F,
361            0,
362            0,
363        ).with_label("GBuffer_Position"));
364
365        // Normal: octahedral-encoded in RG16F
366        layout.add_color_attachment(GBufferAttachment::new(
367            GBufferSemantic::Normal,
368            GBufferAttachmentFormat::Rg16F,
369            1,
370            1,
371        ).with_label("GBuffer_Normal"));
372
373        // Albedo: base color RGBA8
374        layout.add_color_attachment(GBufferAttachment::new(
375            GBufferSemantic::Albedo,
376            GBufferAttachmentFormat::Rgba8,
377            2,
378            2,
379        ).with_label("GBuffer_Albedo"));
380
381        // Emission: HDR emission color
382        layout.add_color_attachment(GBufferAttachment::new(
383            GBufferSemantic::Emission,
384            GBufferAttachmentFormat::Rgba16F,
385            3,
386            3,
387        ).with_label("GBuffer_Emission"));
388
389        // Material ID: single byte
390        layout.add_color_attachment(GBufferAttachment::new(
391            GBufferSemantic::MaterialId,
392            GBufferAttachmentFormat::R8,
393            4,
394            4,
395        ).with_clear_value(ClearValue::IntColor([0, 0, 0, 0]))
396         .with_linear_filter(false)
397         .with_label("GBuffer_MaterialID"));
398
399        // Roughness: single byte
400        layout.add_color_attachment(GBufferAttachment::new(
401            GBufferSemantic::Roughness,
402            GBufferAttachmentFormat::R8,
403            5,
404            5,
405        ).with_label("GBuffer_Roughness"));
406
407        // Metallic: single byte
408        layout.add_color_attachment(GBufferAttachment::new(
409            GBufferSemantic::Metallic,
410            GBufferAttachmentFormat::R8,
411            6,
412            6,
413        ).with_label("GBuffer_Metallic"));
414
415        // Depth: D32F
416        layout.depth_attachment = GBufferAttachment::new(
417            GBufferSemantic::Depth,
418            GBufferAttachmentFormat::D32F,
419            u32::MAX,
420            7,
421        ).with_clear_value(ClearValue::Depth(1.0))
422         .with_label("GBuffer_Depth");
423
424        layout
425    }
426
427    /// Create a thin/minimal G-Buffer layout that packs more data per attachment.
428    /// Uses fewer render targets at the cost of some precision.
429    pub fn thin_layout() -> Self {
430        let mut layout = Self::new();
431        layout.thin_gbuffer = true;
432        layout.use_octahedral_normals = true;
433
434        // Position: reconstruct from depth + view rays (no position buffer).
435        // Normal + roughness packed: RG = octahedral normal, BA not used here,
436        // but we store normal in RG16F and pack roughness+metallic separately.
437        layout.add_color_attachment(GBufferAttachment::new(
438            GBufferSemantic::Normal,
439            GBufferAttachmentFormat::Rgba16F,
440            0,
441            0,
442        ).with_label("Thin_NormalRoughnessMetallic"));
443
444        // Albedo + material ID packed: RGB = albedo, A = material ID / 255
445        layout.add_color_attachment(GBufferAttachment::new(
446            GBufferSemantic::Albedo,
447            GBufferAttachmentFormat::Rgba8,
448            1,
449            1,
450        ).with_label("Thin_AlbedoMatID"));
451
452        // Emission
453        layout.add_color_attachment(GBufferAttachment::new(
454            GBufferSemantic::Emission,
455            GBufferAttachmentFormat::Rgba16F,
456            2,
457            2,
458        ).with_label("Thin_Emission"));
459
460        // Depth
461        layout.depth_attachment = GBufferAttachment::new(
462            GBufferSemantic::Depth,
463            GBufferAttachmentFormat::D32F,
464            u32::MAX,
465            3,
466        ).with_label("Thin_Depth");
467
468        layout
469    }
470
471    /// Add a color attachment to the layout.
472    pub fn add_color_attachment(&mut self, attachment: GBufferAttachment) {
473        assert!(
474            (self.color_attachments.len() as u32) < self.max_color_attachments,
475            "Exceeded maximum color attachments ({})",
476            self.max_color_attachments
477        );
478        self.color_attachments.push(attachment);
479    }
480
481    /// Remove a color attachment by semantic.
482    pub fn remove_attachment(&mut self, semantic: GBufferSemantic) -> bool {
483        let before = self.color_attachments.len();
484        self.color_attachments.retain(|a| a.semantic != semantic);
485        self.color_attachments.len() < before
486    }
487
488    /// Find an attachment by semantic.
489    pub fn find_attachment(&self, semantic: GBufferSemantic) -> Option<&GBufferAttachment> {
490        if semantic == GBufferSemantic::Depth {
491            return Some(&self.depth_attachment);
492        }
493        self.color_attachments.iter().find(|a| a.semantic == semantic)
494    }
495
496    /// Find an attachment by semantic (mutable).
497    pub fn find_attachment_mut(&mut self, semantic: GBufferSemantic) -> Option<&mut GBufferAttachment> {
498        if semantic == GBufferSemantic::Depth {
499            return Some(&mut self.depth_attachment);
500        }
501        self.color_attachments.iter_mut().find(|a| a.semantic == semantic)
502    }
503
504    /// Total number of color attachments.
505    pub fn color_attachment_count(&self) -> u32 {
506        self.color_attachments.len() as u32
507    }
508
509    /// Calculate total memory usage for a given resolution.
510    pub fn total_memory_bytes(&self, width: u32, height: u32) -> u64 {
511        let color_mem: u64 = self.color_attachments.iter()
512            .map(|a| a.memory_bytes(width, height))
513            .sum();
514        let depth_mem = self.depth_attachment.memory_bytes(width, height);
515        color_mem + depth_mem
516    }
517
518    /// Validate the layout, returning any issues found.
519    pub fn validate(&self) -> Vec<String> {
520        let mut issues = Vec::new();
521
522        if self.color_attachments.is_empty() {
523            issues.push("No color attachments defined".to_string());
524        }
525
526        if self.color_attachments.len() as u32 > self.max_color_attachments {
527            issues.push(format!(
528                "Too many color attachments: {} > {}",
529                self.color_attachments.len(),
530                self.max_color_attachments
531            ));
532        }
533
534        // Check for duplicate color indices
535        let mut seen_indices = std::collections::HashSet::new();
536        for att in &self.color_attachments {
537            if !seen_indices.insert(att.color_index) {
538                issues.push(format!(
539                    "Duplicate color attachment index {} ({})",
540                    att.color_index, att.semantic
541                ));
542            }
543        }
544
545        // Check for duplicate texture units
546        let mut seen_units = std::collections::HashSet::new();
547        for att in &self.color_attachments {
548            if !seen_units.insert(att.texture_unit) {
549                issues.push(format!(
550                    "Duplicate texture unit {} ({})",
551                    att.texture_unit, att.semantic
552                ));
553            }
554        }
555
556        if !self.depth_attachment.format.is_depth() {
557            issues.push("Depth attachment does not have a depth format".to_string());
558        }
559
560        issues
561    }
562}
563
564impl Default for GBufferLayout {
565    fn default() -> Self {
566        Self::default_layout()
567    }
568}
569
570// ---------------------------------------------------------------------------
571// MRT (Multiple Render Target) configuration
572// ---------------------------------------------------------------------------
573
574/// Configuration for Multiple Render Targets output from the geometry pass.
575#[derive(Debug, Clone)]
576pub struct MrtConfig {
577    /// List of draw buffer indices that the fragment shader writes to.
578    pub draw_buffers: Vec<u32>,
579    /// Whether to use gl_FragData[n] or layout(location = n) out.
580    pub use_explicit_locations: bool,
581    /// Maximum number of draw buffers supported by the hardware.
582    pub max_draw_buffers: u32,
583    /// Whether blending is enabled per-attachment.
584    pub blend_enabled: Vec<bool>,
585    /// Write mask per attachment (bitmask: R=1, G=2, B=4, A=8).
586    pub write_masks: Vec<u8>,
587}
588
589impl MrtConfig {
590    /// Create an MRT configuration from a G-Buffer layout.
591    pub fn from_layout(layout: &GBufferLayout) -> Self {
592        let count = layout.color_attachment_count() as usize;
593        let draw_buffers: Vec<u32> = layout.color_attachments.iter()
594            .map(|a| a.color_index)
595            .collect();
596        Self {
597            draw_buffers,
598            use_explicit_locations: true,
599            max_draw_buffers: layout.max_color_attachments,
600            blend_enabled: vec![false; count],
601            write_masks: vec![0x0F; count], // RGBA write enabled
602        }
603    }
604
605    /// Set blend state for a specific attachment index.
606    pub fn set_blend(&mut self, index: usize, enabled: bool) {
607        if index < self.blend_enabled.len() {
608            self.blend_enabled[index] = enabled;
609        }
610    }
611
612    /// Set write mask for a specific attachment.
613    pub fn set_write_mask(&mut self, index: usize, mask: u8) {
614        if index < self.write_masks.len() {
615            self.write_masks[index] = mask;
616        }
617    }
618
619    /// Disable writing to a specific attachment (useful for conditional output).
620    pub fn disable_attachment(&mut self, index: usize) {
621        self.set_write_mask(index, 0x00);
622    }
623
624    /// Enable all channels for a specific attachment.
625    pub fn enable_attachment(&mut self, index: usize) {
626        self.set_write_mask(index, 0x0F);
627    }
628
629    /// Returns the number of active draw buffers.
630    pub fn active_count(&self) -> usize {
631        self.write_masks.iter().filter(|&&m| m != 0).count()
632    }
633
634    /// Check that this MRT config is valid.
635    pub fn validate(&self) -> bool {
636        if self.draw_buffers.len() > self.max_draw_buffers as usize {
637            return false;
638        }
639        if self.blend_enabled.len() != self.draw_buffers.len() {
640            return false;
641        }
642        if self.write_masks.len() != self.draw_buffers.len() {
643            return false;
644        }
645        true
646    }
647
648    /// Generate GLSL output declarations for the MRT layout.
649    pub fn generate_glsl_outputs(&self, layout: &GBufferLayout) -> String {
650        let mut glsl = String::new();
651        for (i, att) in layout.color_attachments.iter().enumerate() {
652            let type_name = match att.format {
653                GBufferAttachmentFormat::Rgba32F | GBufferAttachmentFormat::Rgba16F => "vec4",
654                GBufferAttachmentFormat::Rgba8 => "vec4",
655                GBufferAttachmentFormat::Rg16F | GBufferAttachmentFormat::Rg8 => "vec2",
656                GBufferAttachmentFormat::R8 | GBufferAttachmentFormat::R16F |
657                GBufferAttachmentFormat::R32F => "float",
658                _ => "vec4",
659            };
660            glsl.push_str(&format!(
661                "layout(location = {}) out {} out_{};\n",
662                i,
663                type_name,
664                att.semantic.name().to_lowercase()
665            ));
666        }
667        glsl
668    }
669}
670
671impl Default for MrtConfig {
672    fn default() -> Self {
673        Self::from_layout(&GBufferLayout::default_layout())
674    }
675}
676
677// ---------------------------------------------------------------------------
678// Octahedral normal encoding helpers
679// ---------------------------------------------------------------------------
680
681/// Octahedral normal encoding: packs a unit normal into 2 floats in [-1, 1].
682/// This is more bandwidth-efficient than storing XYZ in RGBA16F.
683pub fn octahedral_encode(n: [f32; 3]) -> [f32; 2] {
684    let abs_sum = n[0].abs() + n[1].abs() + n[2].abs();
685    let mut oct = [n[0] / abs_sum, n[1] / abs_sum];
686    if n[2] < 0.0 {
687        let sign_x = if oct[0] >= 0.0 { 1.0 } else { -1.0 };
688        let sign_y = if oct[1] >= 0.0 { 1.0 } else { -1.0 };
689        oct = [
690            (1.0 - oct[1].abs()) * sign_x,
691            (1.0 - oct[0].abs()) * sign_y,
692        ];
693    }
694    oct
695}
696
697/// Decode an octahedral-encoded normal back to a unit vector.
698pub fn octahedral_decode(oct: [f32; 2]) -> [f32; 3] {
699    let z = 1.0 - oct[0].abs() - oct[1].abs();
700    let (x, y) = if z >= 0.0 {
701        (oct[0], oct[1])
702    } else {
703        let sign_x = if oct[0] >= 0.0 { 1.0 } else { -1.0 };
704        let sign_y = if oct[1] >= 0.0 { 1.0 } else { -1.0 };
705        (
706            (1.0 - oct[1].abs()) * sign_x,
707            (1.0 - oct[0].abs()) * sign_y,
708        )
709    };
710    let len = (x * x + y * y + z * z).sqrt();
711    if len < 1e-10 {
712        [0.0, 0.0, 1.0]
713    } else {
714        [x / len, y / len, z / len]
715    }
716}
717
718/// Pack a normal into a u32 using 16-bit snorm for each component.
719pub fn pack_normal_snorm16(n: [f32; 3]) -> u32 {
720    let enc = octahedral_encode(n);
721    let x = ((clampf(enc[0], -1.0, 1.0) * 32767.0) as i16) as u16;
722    let y = ((clampf(enc[1], -1.0, 1.0) * 32767.0) as i16) as u16;
723    (x as u32) | ((y as u32) << 16)
724}
725
726/// Unpack a u32 snorm16 packed normal.
727pub fn unpack_normal_snorm16(packed: u32) -> [f32; 3] {
728    let x = (packed & 0xFFFF) as u16 as i16;
729    let y = ((packed >> 16) & 0xFFFF) as u16 as i16;
730    let oct = [x as f32 / 32767.0, y as f32 / 32767.0];
731    octahedral_decode(oct)
732}
733
734// ---------------------------------------------------------------------------
735// G-Buffer state
736// ---------------------------------------------------------------------------
737
738/// Represents the runtime state of an individual texture in the G-Buffer.
739#[derive(Debug, Clone)]
740pub struct TextureState {
741    /// GPU texture handle (opaque).
742    pub handle: u64,
743    /// Current width.
744    pub width: u32,
745    /// Current height.
746    pub height: u32,
747    /// Whether this texture has been allocated.
748    pub allocated: bool,
749    /// Generation counter (incremented on resize/recreate).
750    pub generation: u32,
751}
752
753impl TextureState {
754    pub fn new() -> Self {
755        Self {
756            handle: 0,
757            width: 0,
758            height: 0,
759            allocated: false,
760            generation: 0,
761        }
762    }
763
764    pub fn allocate(&mut self, handle: u64, width: u32, height: u32) {
765        self.handle = handle;
766        self.width = width;
767        self.height = height;
768        self.allocated = true;
769        self.generation += 1;
770    }
771
772    pub fn deallocate(&mut self) {
773        self.handle = 0;
774        self.width = 0;
775        self.height = 0;
776        self.allocated = false;
777    }
778
779    pub fn needs_resize(&self, width: u32, height: u32) -> bool {
780        self.width != width || self.height != height
781    }
782}
783
784impl Default for TextureState {
785    fn default() -> Self {
786        Self::new()
787    }
788}
789
790// ---------------------------------------------------------------------------
791// G-Buffer statistics
792// ---------------------------------------------------------------------------
793
794/// Statistics about G-Buffer memory usage and performance.
795#[derive(Debug, Clone)]
796pub struct GBufferStats {
797    /// Total GPU memory used by all attachments (bytes).
798    pub total_memory_bytes: u64,
799    /// Memory per attachment.
800    pub per_attachment_memory: HashMap<String, u64>,
801    /// Current resolution.
802    pub width: u32,
803    pub height: u32,
804    /// Number of color attachments.
805    pub color_attachment_count: u32,
806    /// Estimated fill rate in megapixels per second (set externally).
807    pub fill_rate_mpix_per_sec: f64,
808    /// Estimated bandwidth usage in GB/s for a single full G-Buffer write.
809    pub bandwidth_gb_per_write: f64,
810    /// Number of times the G-Buffer has been resized.
811    pub resize_count: u32,
812    /// Number of times the G-Buffer has been cleared this frame.
813    pub clears_this_frame: u32,
814    /// Number of draw calls that wrote to the G-Buffer this frame.
815    pub geometry_draw_calls: u32,
816    /// Average triangle count per draw call (estimated).
817    pub avg_triangles_per_draw: u32,
818    /// Overdraw ratio (pixels written / total pixels). 1.0 = no overdraw.
819    pub overdraw_ratio: f32,
820    /// Total bytes written per frame (approx).
821    pub bytes_per_frame: u64,
822}
823
824impl GBufferStats {
825    pub fn new() -> Self {
826        Self {
827            total_memory_bytes: 0,
828            per_attachment_memory: HashMap::new(),
829            width: 0,
830            height: 0,
831            color_attachment_count: 0,
832            fill_rate_mpix_per_sec: 0.0,
833            bandwidth_gb_per_write: 0.0,
834            resize_count: 0,
835            clears_this_frame: 0,
836            geometry_draw_calls: 0,
837            avg_triangles_per_draw: 0,
838            overdraw_ratio: 1.0,
839            bytes_per_frame: 0,
840        }
841    }
842
843    /// Calculate stats from a layout and resolution.
844    pub fn from_layout(layout: &GBufferLayout, width: u32, height: u32) -> Self {
845        let mut stats = Self::new();
846        stats.width = width;
847        stats.height = height;
848        stats.color_attachment_count = layout.color_attachment_count();
849
850        for att in &layout.color_attachments {
851            let mem = att.memory_bytes(width, height);
852            stats.per_attachment_memory.insert(att.label.clone(), mem);
853            stats.total_memory_bytes += mem;
854        }
855
856        let depth_mem = layout.depth_attachment.memory_bytes(width, height);
857        stats.per_attachment_memory.insert(
858            layout.depth_attachment.label.clone(),
859            depth_mem,
860        );
861        stats.total_memory_bytes += depth_mem;
862
863        // Estimate bandwidth: bytes per pixel across all attachments
864        let bytes_per_pixel: u32 = layout.color_attachments.iter()
865            .map(|a| a.format.bytes_per_pixel())
866            .sum::<u32>()
867            + layout.depth_attachment.format.bytes_per_pixel();
868
869        let pixels = width as u64 * height as u64;
870        stats.bandwidth_gb_per_write =
871            (pixels * bytes_per_pixel as u64) as f64 / 1_000_000_000.0;
872
873        stats
874    }
875
876    /// Update frame-specific stats.
877    pub fn update_frame(&mut self, draw_calls: u32, overdraw: f32) {
878        self.geometry_draw_calls = draw_calls;
879        self.overdraw_ratio = overdraw;
880        self.clears_this_frame = 1; // typically once per frame
881
882        let pixels = self.width as u64 * self.height as u64;
883        let bpp: u64 = self.per_attachment_memory.values().sum::<u64>()
884            / pixels.max(1);
885        self.bytes_per_frame = (pixels as f64 * bpp as f64 * overdraw as f64) as u64;
886    }
887
888    /// Format stats as a human-readable summary.
889    pub fn summary(&self) -> String {
890        let mut s = String::new();
891        s.push_str(&format!(
892            "G-Buffer Stats ({}x{}):\n",
893            self.width, self.height
894        ));
895        s.push_str(&format!(
896            "  Total memory: {:.2} MB\n",
897            self.total_memory_bytes as f64 / (1024.0 * 1024.0)
898        ));
899        s.push_str(&format!(
900            "  Color attachments: {}\n",
901            self.color_attachment_count
902        ));
903        s.push_str(&format!(
904            "  Bandwidth/write: {:.3} GB\n",
905            self.bandwidth_gb_per_write
906        ));
907        s.push_str(&format!(
908            "  Overdraw ratio: {:.2}\n",
909            self.overdraw_ratio
910        ));
911        s.push_str(&format!(
912            "  Draw calls: {}\n",
913            self.geometry_draw_calls
914        ));
915
916        for (name, mem) in &self.per_attachment_memory {
917            s.push_str(&format!(
918                "  {} : {:.2} MB\n",
919                name,
920                *mem as f64 / (1024.0 * 1024.0)
921            ));
922        }
923
924        s
925    }
926}
927
928impl Default for GBufferStats {
929    fn default() -> Self {
930        Self::new()
931    }
932}
933
934// ---------------------------------------------------------------------------
935// G-Buffer debug visualization
936// ---------------------------------------------------------------------------
937
938/// Which channel of the G-Buffer to visualize.
939#[derive(Debug, Clone, Copy, PartialEq, Eq)]
940pub enum GBufferDebugChannel {
941    /// Show the position buffer (RGB = XYZ, normalized).
942    Position,
943    /// Show world-space normals (RGB mapped from [-1,1] to [0,1]).
944    Normal,
945    /// Show albedo (raw color).
946    Albedo,
947    /// Show emission (tone-mapped for display).
948    Emission,
949    /// Show material ID as a false-color map.
950    MaterialId,
951    /// Show roughness as grayscale.
952    Roughness,
953    /// Show metallic as grayscale.
954    Metallic,
955    /// Show linear depth (near = white, far = black).
956    Depth,
957    /// Show all channels in a grid layout.
958    All,
959    /// Show only the lighting result (no debug).
960    None,
961}
962
963impl GBufferDebugChannel {
964    pub fn label(&self) -> &'static str {
965        match self {
966            Self::Position => "Position",
967            Self::Normal => "Normal",
968            Self::Albedo => "Albedo",
969            Self::Emission => "Emission",
970            Self::MaterialId => "Material ID",
971            Self::Roughness => "Roughness",
972            Self::Metallic => "Metallic",
973            Self::Depth => "Depth",
974            Self::All => "All Channels",
975            Self::None => "Final",
976        }
977    }
978
979    /// Cycle to the next debug channel.
980    pub fn next(&self) -> Self {
981        match self {
982            Self::None => Self::Position,
983            Self::Position => Self::Normal,
984            Self::Normal => Self::Albedo,
985            Self::Albedo => Self::Emission,
986            Self::Emission => Self::MaterialId,
987            Self::MaterialId => Self::Roughness,
988            Self::Roughness => Self::Metallic,
989            Self::Metallic => Self::Depth,
990            Self::Depth => Self::All,
991            Self::All => Self::None,
992        }
993    }
994
995    /// Cycle to the previous debug channel.
996    pub fn prev(&self) -> Self {
997        match self {
998            Self::None => Self::All,
999            Self::Position => Self::None,
1000            Self::Normal => Self::Position,
1001            Self::Albedo => Self::Normal,
1002            Self::Emission => Self::Albedo,
1003            Self::MaterialId => Self::Emission,
1004            Self::Roughness => Self::MaterialId,
1005            Self::Metallic => Self::Roughness,
1006            Self::Depth => Self::Metallic,
1007            Self::All => Self::Depth,
1008        }
1009    }
1010}
1011
1012impl Default for GBufferDebugChannel {
1013    fn default() -> Self {
1014        Self::None
1015    }
1016}
1017
1018/// Debug view configuration and state for visualizing G-Buffer contents.
1019#[derive(Debug, Clone)]
1020pub struct GBufferDebugView {
1021    /// Which channel is currently being displayed.
1022    pub active_channel: GBufferDebugChannel,
1023    /// Whether the debug view is enabled.
1024    pub enabled: bool,
1025    /// Exposure multiplier for HDR channels (emission, position range).
1026    pub exposure: f32,
1027    /// Depth visualization near plane override (0 = auto).
1028    pub depth_near: f32,
1029    /// Depth visualization far plane override (0 = auto).
1030    pub depth_far: f32,
1031    /// Grid layout dimensions when showing all channels.
1032    pub grid_cols: u32,
1033    pub grid_rows: u32,
1034    /// Overlay opacity when compositing debug view over the scene (0..1).
1035    pub overlay_opacity: f32,
1036    /// False color palette for material ID visualization.
1037    pub material_id_palette: Vec<[f32; 3]>,
1038    /// Zoom level for the debug view (1.0 = fit to screen).
1039    pub zoom: f32,
1040    /// Pan offset in normalized coordinates.
1041    pub pan: [f32; 2],
1042    /// Whether to show numeric values at cursor position.
1043    pub show_pixel_values: bool,
1044    /// Cursor position for pixel value readback.
1045    pub cursor_pos: [u32; 2],
1046}
1047
1048impl GBufferDebugView {
1049    pub fn new() -> Self {
1050        Self {
1051            active_channel: GBufferDebugChannel::None,
1052            enabled: false,
1053            exposure: 1.0,
1054            depth_near: 0.1,
1055            depth_far: 100.0,
1056            grid_cols: 3,
1057            grid_rows: 3,
1058            overlay_opacity: 1.0,
1059            material_id_palette: Self::generate_default_palette(256),
1060            zoom: 1.0,
1061            pan: [0.0, 0.0],
1062            show_pixel_values: false,
1063            cursor_pos: [0, 0],
1064        }
1065    }
1066
1067    /// Generate a default false-color palette for material IDs.
1068    fn generate_default_palette(count: usize) -> Vec<[f32; 3]> {
1069        let mut palette = Vec::with_capacity(count);
1070        for i in 0..count {
1071            let hue = (i as f32 / count as f32) * 360.0;
1072            let (r, g, b) = hsv_to_rgb(hue, 0.8, 0.9);
1073            palette.push([r, g, b]);
1074        }
1075        palette
1076    }
1077
1078    /// Toggle the debug view on/off.
1079    pub fn toggle(&mut self) {
1080        self.enabled = !self.enabled;
1081    }
1082
1083    /// Cycle to the next channel.
1084    pub fn cycle_next(&mut self) {
1085        self.active_channel = self.active_channel.next();
1086        if self.active_channel != GBufferDebugChannel::None {
1087            self.enabled = true;
1088        }
1089    }
1090
1091    /// Cycle to the previous channel.
1092    pub fn cycle_prev(&mut self) {
1093        self.active_channel = self.active_channel.prev();
1094        if self.active_channel != GBufferDebugChannel::None {
1095            self.enabled = true;
1096        }
1097    }
1098
1099    /// Set the debug channel directly.
1100    pub fn set_channel(&mut self, channel: GBufferDebugChannel) {
1101        self.active_channel = channel;
1102        self.enabled = channel != GBufferDebugChannel::None;
1103    }
1104
1105    /// Adjust exposure for HDR debug visualization.
1106    pub fn adjust_exposure(&mut self, delta: f32) {
1107        self.exposure = (self.exposure + delta).max(0.01).min(100.0);
1108    }
1109
1110    /// Reset zoom and pan to defaults.
1111    pub fn reset_view(&mut self) {
1112        self.zoom = 1.0;
1113        self.pan = [0.0, 0.0];
1114    }
1115
1116    /// Compute the viewport region for a specific channel in the "All" grid view.
1117    pub fn grid_cell_viewport(
1118        &self,
1119        channel_index: u32,
1120        full_viewport: &Viewport,
1121    ) -> Viewport {
1122        let col = channel_index % self.grid_cols;
1123        let row = channel_index / self.grid_cols;
1124        let cell_w = full_viewport.width / self.grid_cols;
1125        let cell_h = full_viewport.height / self.grid_rows;
1126        Viewport {
1127            x: full_viewport.x + (col * cell_w) as i32,
1128            y: full_viewport.y + (row * cell_h) as i32,
1129            width: cell_w,
1130            height: cell_h,
1131        }
1132    }
1133
1134    /// Map a raw depth value to a visualizable [0, 1] range using the configured near/far.
1135    pub fn linearize_depth(&self, raw_depth: f32) -> f32 {
1136        if self.depth_far <= self.depth_near {
1137            return 0.0;
1138        }
1139        let near = self.depth_near;
1140        let far = self.depth_far;
1141        let ndc = 2.0 * raw_depth - 1.0;
1142        let linear = (2.0 * near * far) / (far + near - ndc * (far - near));
1143        clampf((linear - near) / (far - near), 0.0, 1.0)
1144    }
1145
1146    /// Convert a normal from [-1,1] to [0,1] for visualization.
1147    pub fn visualize_normal(n: [f32; 3]) -> [f32; 3] {
1148        [
1149            n[0] * 0.5 + 0.5,
1150            n[1] * 0.5 + 0.5,
1151            n[2] * 0.5 + 0.5,
1152        ]
1153    }
1154
1155    /// Get the false color for a material ID.
1156    pub fn material_id_color(&self, id: u8) -> [f32; 3] {
1157        if (id as usize) < self.material_id_palette.len() {
1158            self.material_id_palette[id as usize]
1159        } else {
1160            [1.0, 0.0, 1.0] // magenta fallback
1161        }
1162    }
1163
1164    /// Generate a GLSL snippet for the currently selected debug visualization.
1165    pub fn generate_debug_shader(&self) -> String {
1166        match self.active_channel {
1167            GBufferDebugChannel::Position => {
1168                format!(
1169                    "vec3 debug_color = abs(texture(g_position, uv).xyz) * {:.4};\n",
1170                    self.exposure
1171                )
1172            }
1173            GBufferDebugChannel::Normal => {
1174                "vec3 debug_color = texture(g_normal, uv).xyz * 0.5 + 0.5;\n".to_string()
1175            }
1176            GBufferDebugChannel::Albedo => {
1177                "vec3 debug_color = texture(g_albedo, uv).rgb;\n".to_string()
1178            }
1179            GBufferDebugChannel::Emission => {
1180                format!(
1181                    "vec3 debug_color = texture(g_emission, uv).rgb * {:.4};\n",
1182                    self.exposure
1183                )
1184            }
1185            GBufferDebugChannel::MaterialId => {
1186                "vec3 debug_color = material_id_palette[int(texture(g_matid, uv).r * 255.0)];\n"
1187                    .to_string()
1188            }
1189            GBufferDebugChannel::Roughness => {
1190                "float r = texture(g_roughness, uv).r;\nvec3 debug_color = vec3(r);\n".to_string()
1191            }
1192            GBufferDebugChannel::Metallic => {
1193                "float m = texture(g_metallic, uv).r;\nvec3 debug_color = vec3(m);\n".to_string()
1194            }
1195            GBufferDebugChannel::Depth => {
1196                format!(
1197                    concat!(
1198                        "float d = texture(g_depth, uv).r;\n",
1199                        "float linear_d = (2.0 * {near:.4} * {far:.4}) / ",
1200                        "({far:.4} + {near:.4} - (2.0 * d - 1.0) * ({far:.4} - {near:.4}));\n",
1201                        "float vis_d = clamp((linear_d - {near:.4}) / ({far:.4} - {near:.4}), 0.0, 1.0);\n",
1202                        "vec3 debug_color = vec3(1.0 - vis_d);\n",
1203                    ),
1204                    near = self.depth_near,
1205                    far = self.depth_far
1206                )
1207            }
1208            GBufferDebugChannel::All | GBufferDebugChannel::None => {
1209                "vec3 debug_color = vec3(0.0);\n".to_string()
1210            }
1211        }
1212    }
1213}
1214
1215impl Default for GBufferDebugView {
1216    fn default() -> Self {
1217        Self::new()
1218    }
1219}
1220
1221// ---------------------------------------------------------------------------
1222// HSV to RGB helper
1223// ---------------------------------------------------------------------------
1224
1225/// Convert HSV (hue 0..360, saturation 0..1, value 0..1) to RGB (each 0..1).
1226fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (f32, f32, f32) {
1227    let c = v * s;
1228    let h2 = h / 60.0;
1229    let x = c * (1.0 - ((h2 % 2.0) - 1.0).abs());
1230    let (r1, g1, b1) = if h2 < 1.0 {
1231        (c, x, 0.0)
1232    } else if h2 < 2.0 {
1233        (x, c, 0.0)
1234    } else if h2 < 3.0 {
1235        (0.0, c, x)
1236    } else if h2 < 4.0 {
1237        (0.0, x, c)
1238    } else if h2 < 5.0 {
1239        (x, 0.0, c)
1240    } else {
1241        (c, 0.0, x)
1242    };
1243    let m = v - c;
1244    (r1 + m, g1 + m, b1 + m)
1245}
1246
1247// ---------------------------------------------------------------------------
1248// G-Buffer struct
1249// ---------------------------------------------------------------------------
1250
1251/// The main G-Buffer object that manages all attachment textures and the
1252/// framebuffer. This struct owns the GPU resource handles (as opaque u64 IDs)
1253/// and provides bind/unbind/resize/clear operations.
1254#[derive(Debug)]
1255pub struct GBuffer {
1256    /// The layout describing all attachments.
1257    pub layout: GBufferLayout,
1258    /// Current viewport dimensions.
1259    pub viewport: Viewport,
1260    /// Framebuffer object handle.
1261    pub fbo_handle: u64,
1262    /// Per-attachment texture states.
1263    pub texture_states: Vec<TextureState>,
1264    /// Depth texture state.
1265    pub depth_texture_state: TextureState,
1266    /// MRT configuration derived from the layout.
1267    pub mrt_config: MrtConfig,
1268    /// Debug view state.
1269    pub debug_view: GBufferDebugView,
1270    /// Statistics.
1271    pub stats: GBufferStats,
1272    /// Whether the G-Buffer is currently bound as the render target.
1273    pub is_bound: bool,
1274    /// Generation counter (incremented on resize).
1275    pub generation: u32,
1276    /// Whether the G-Buffer resources have been created.
1277    pub is_created: bool,
1278    /// Handle counter for generating unique texture handles.
1279    next_handle: u64,
1280}
1281
1282impl GBuffer {
1283    /// Create a new G-Buffer with the default layout.
1284    pub fn new(viewport: Viewport) -> Self {
1285        Self::with_layout(GBufferLayout::default_layout(), viewport)
1286    }
1287
1288    /// Create a new G-Buffer with a custom layout.
1289    pub fn with_layout(layout: GBufferLayout, viewport: Viewport) -> Self {
1290        let mrt_config = MrtConfig::from_layout(&layout);
1291        let texture_states = layout
1292            .color_attachments
1293            .iter()
1294            .map(|_| TextureState::new())
1295            .collect();
1296        let stats = GBufferStats::from_layout(&layout, viewport.width, viewport.height);
1297
1298        Self {
1299            layout,
1300            viewport,
1301            fbo_handle: 0,
1302            texture_states,
1303            depth_texture_state: TextureState::new(),
1304            mrt_config,
1305            debug_view: GBufferDebugView::new(),
1306            stats,
1307            is_bound: false,
1308            generation: 0,
1309            is_created: false,
1310            next_handle: 1,
1311        }
1312    }
1313
1314    /// Allocate GPU resources for the G-Buffer. In a real engine this would
1315    /// call OpenGL/Vulkan; here we simulate handle allocation.
1316    pub fn create(&mut self) -> Result<(), GBufferError> {
1317        let issues = self.layout.validate();
1318        if !issues.is_empty() {
1319            return Err(GBufferError::ValidationFailed(issues));
1320        }
1321
1322        // Allocate framebuffer handle
1323        self.fbo_handle = self.alloc_handle();
1324
1325        // Allocate color attachment textures
1326        let num_attachments = self.layout.color_attachments.len();
1327        for i in 0..num_attachments {
1328            let handle = self.alloc_handle();
1329            self.texture_states[i].allocate(handle, self.viewport.width, self.viewport.height);
1330        }
1331
1332        // Allocate depth texture
1333        let depth_handle = self.alloc_handle();
1334        self.depth_texture_state.allocate(
1335            depth_handle,
1336            self.viewport.width,
1337            self.viewport.height,
1338        );
1339
1340        self.is_created = true;
1341        self.generation += 1;
1342        self.update_stats();
1343
1344        Ok(())
1345    }
1346
1347    /// Destroy GPU resources.
1348    pub fn destroy(&mut self) {
1349        for ts in &mut self.texture_states {
1350            ts.deallocate();
1351        }
1352        self.depth_texture_state.deallocate();
1353        self.fbo_handle = 0;
1354        self.is_created = false;
1355        self.is_bound = false;
1356    }
1357
1358    /// Bind the G-Buffer as the current render target.
1359    pub fn bind(&mut self) -> Result<(), GBufferError> {
1360        if !self.is_created {
1361            return Err(GBufferError::NotCreated);
1362        }
1363        self.is_bound = true;
1364        Ok(())
1365    }
1366
1367    /// Unbind the G-Buffer (restore default framebuffer).
1368    pub fn unbind(&mut self) {
1369        self.is_bound = false;
1370    }
1371
1372    /// Bind all G-Buffer textures for reading in the lighting pass.
1373    pub fn bind_for_reading(&self) -> Result<Vec<(u32, u64)>, GBufferError> {
1374        if !self.is_created {
1375            return Err(GBufferError::NotCreated);
1376        }
1377
1378        let mut bindings = Vec::with_capacity(self.layout.color_attachments.len() + 1);
1379        for (i, att) in self.layout.color_attachments.iter().enumerate() {
1380            bindings.push((att.texture_unit, self.texture_states[i].handle));
1381        }
1382        // Bind depth texture
1383        bindings.push((
1384            self.layout.depth_attachment.texture_unit,
1385            self.depth_texture_state.handle,
1386        ));
1387
1388        Ok(bindings)
1389    }
1390
1391    /// Bind a single attachment for sampling.
1392    pub fn bind_attachment(
1393        &self,
1394        semantic: GBufferSemantic,
1395        texture_unit: u32,
1396    ) -> Result<u64, GBufferError> {
1397        if !self.is_created {
1398            return Err(GBufferError::NotCreated);
1399        }
1400
1401        if semantic == GBufferSemantic::Depth {
1402            return Ok(self.depth_texture_state.handle);
1403        }
1404
1405        for (i, att) in self.layout.color_attachments.iter().enumerate() {
1406            if att.semantic == semantic {
1407                let _ = texture_unit; // would be used in real GL call
1408                return Ok(self.texture_states[i].handle);
1409            }
1410        }
1411
1412        Err(GBufferError::AttachmentNotFound(semantic))
1413    }
1414
1415    /// Resize the G-Buffer to a new resolution. Recreates all textures.
1416    pub fn resize(&mut self, width: u32, height: u32) -> Result<(), GBufferError> {
1417        if width == 0 || height == 0 {
1418            return Err(GBufferError::InvalidDimensions(width, height));
1419        }
1420
1421        if self.viewport.width == width && self.viewport.height == height {
1422            return Ok(());
1423        }
1424
1425        self.viewport.width = width;
1426        self.viewport.height = height;
1427
1428        if self.is_created {
1429            // Reallocate all textures at new size
1430            for ts in &mut self.texture_states {
1431                let handle = ts.handle; // keep same handle
1432                ts.allocate(handle, width, height);
1433            }
1434            self.depth_texture_state.allocate(
1435                self.depth_texture_state.handle,
1436                width,
1437                height,
1438            );
1439
1440            self.generation += 1;
1441            self.stats.resize_count += 1;
1442        }
1443
1444        self.update_stats();
1445        Ok(())
1446    }
1447
1448    /// Clear all G-Buffer attachments using their configured clear values.
1449    pub fn clear_all(&mut self) {
1450        self.stats.clears_this_frame += 1;
1451        // In a real engine, this would issue glClearBuffer calls per attachment.
1452        // Here we just track the operation.
1453    }
1454
1455    /// Clear a specific attachment.
1456    pub fn clear_attachment(&self, semantic: GBufferSemantic) -> Result<(), GBufferError> {
1457        if semantic == GBufferSemantic::Depth {
1458            // Clear depth
1459            return Ok(());
1460        }
1461        if self.layout.find_attachment(semantic).is_none() {
1462            return Err(GBufferError::AttachmentNotFound(semantic));
1463        }
1464        Ok(())
1465    }
1466
1467    /// Get the texture handle for a specific attachment.
1468    pub fn texture_handle(&self, semantic: GBufferSemantic) -> Option<u64> {
1469        if semantic == GBufferSemantic::Depth {
1470            return Some(self.depth_texture_state.handle);
1471        }
1472        for (i, att) in self.layout.color_attachments.iter().enumerate() {
1473            if att.semantic == semantic {
1474                return Some(self.texture_states[i].handle);
1475            }
1476        }
1477        None
1478    }
1479
1480    /// Get the current viewport.
1481    pub fn viewport(&self) -> Viewport {
1482        self.viewport
1483    }
1484
1485    /// Get the aspect ratio.
1486    pub fn aspect_ratio(&self) -> f32 {
1487        self.viewport.aspect_ratio()
1488    }
1489
1490    /// Recalculate statistics.
1491    fn update_stats(&mut self) {
1492        self.stats = GBufferStats::from_layout(
1493            &self.layout,
1494            self.viewport.width,
1495            self.viewport.height,
1496        );
1497    }
1498
1499    /// Generate a unique handle.
1500    fn alloc_handle(&mut self) -> u64 {
1501        let h = self.next_handle;
1502        self.next_handle += 1;
1503        h
1504    }
1505
1506    /// Get a reference to the stats.
1507    pub fn stats(&self) -> &GBufferStats {
1508        &self.stats
1509    }
1510
1511    /// Get a human-readable description of the G-Buffer configuration.
1512    pub fn describe(&self) -> String {
1513        let mut desc = String::new();
1514        desc.push_str(&format!(
1515            "G-Buffer [{}x{}, gen {}]\n",
1516            self.viewport.width, self.viewport.height, self.generation
1517        ));
1518        desc.push_str(&format!(
1519            "  FBO: {}, Created: {}, Bound: {}\n",
1520            self.fbo_handle, self.is_created, self.is_bound
1521        ));
1522        desc.push_str(&format!(
1523            "  Layout: {} color attachments + depth\n",
1524            self.layout.color_attachment_count()
1525        ));
1526        for (i, att) in self.layout.color_attachments.iter().enumerate() {
1527            desc.push_str(&format!(
1528                "    [{}] {} : {} (unit {}, idx {})\n",
1529                i, att.semantic, att.format, att.texture_unit, att.color_index
1530            ));
1531        }
1532        desc.push_str(&format!(
1533            "    [D] {} : {} (unit {})\n",
1534            self.layout.depth_attachment.semantic,
1535            self.layout.depth_attachment.format,
1536            self.layout.depth_attachment.texture_unit
1537        ));
1538        desc.push_str(&format!(
1539            "  Memory: {:.2} MB\n",
1540            self.stats.total_memory_bytes as f64 / (1024.0 * 1024.0)
1541        ));
1542        desc
1543    }
1544
1545    /// Check if the G-Buffer needs to be recreated (e.g., after layout change).
1546    pub fn needs_recreate(&self) -> bool {
1547        if !self.is_created {
1548            return true;
1549        }
1550        // Check if any texture state dimensions differ from viewport
1551        for ts in &self.texture_states {
1552            if ts.needs_resize(self.viewport.width, self.viewport.height) {
1553                return true;
1554            }
1555        }
1556        if self.depth_texture_state.needs_resize(self.viewport.width, self.viewport.height) {
1557            return true;
1558        }
1559        false
1560    }
1561
1562    /// Convenience: create a fullscreen quad vertex data for the lighting pass.
1563    /// Returns (positions, uvs) for two triangles covering NDC [-1, 1].
1564    pub fn fullscreen_quad_vertices() -> ([f32; 12], [f32; 8]) {
1565        let positions = [
1566            -1.0, -1.0,
1567             1.0, -1.0,
1568             1.0,  1.0,
1569            -1.0, -1.0,
1570             1.0,  1.0,
1571            -1.0,  1.0,
1572        ];
1573        let uvs = [
1574            0.0, 0.0,
1575            1.0, 0.0,
1576            1.0, 1.0,
1577            0.0, 1.0,
1578        ];
1579        (positions, uvs)
1580    }
1581
1582    /// Generate the complete GLSL vertex shader for the fullscreen lighting quad.
1583    pub fn lighting_vertex_shader() -> &'static str {
1584        r#"#version 330 core
1585layout(location = 0) in vec2 a_position;
1586layout(location = 1) in vec2 a_texcoord;
1587out vec2 v_texcoord;
1588void main() {
1589    v_texcoord = a_texcoord;
1590    gl_Position = vec4(a_position, 0.0, 1.0);
1591}
1592"#
1593    }
1594
1595    /// Generate the lighting pass fragment shader preamble (sampler uniforms).
1596    pub fn lighting_fragment_preamble(&self) -> String {
1597        let mut glsl = String::from("#version 330 core\n");
1598        glsl.push_str("in vec2 v_texcoord;\n");
1599        glsl.push_str("out vec4 frag_color;\n\n");
1600
1601        for att in &self.layout.color_attachments {
1602            let sampler_type = if att.format == GBufferAttachmentFormat::R8 {
1603                "sampler2D" // still float sampler for normalized formats
1604            } else {
1605                "sampler2D"
1606            };
1607            glsl.push_str(&format!(
1608                "uniform {} g_{};\n",
1609                sampler_type,
1610                att.semantic.name().to_lowercase()
1611            ));
1612        }
1613        glsl.push_str("uniform sampler2D g_depth;\n\n");
1614
1615        glsl
1616    }
1617}
1618
1619impl Drop for GBuffer {
1620    fn drop(&mut self) {
1621        if self.is_created {
1622            self.destroy();
1623        }
1624    }
1625}
1626
1627// ---------------------------------------------------------------------------
1628// G-Buffer errors
1629// ---------------------------------------------------------------------------
1630
1631/// Errors that can occur during G-Buffer operations.
1632#[derive(Debug, Clone)]
1633pub enum GBufferError {
1634    NotCreated,
1635    AlreadyCreated,
1636    AttachmentNotFound(GBufferSemantic),
1637    InvalidDimensions(u32, u32),
1638    ValidationFailed(Vec<String>),
1639    TextureAllocationFailed(String),
1640    FramebufferIncomplete(String),
1641}
1642
1643impl fmt::Display for GBufferError {
1644    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1645        match self {
1646            Self::NotCreated => write!(f, "G-Buffer has not been created"),
1647            Self::AlreadyCreated => write!(f, "G-Buffer is already created"),
1648            Self::AttachmentNotFound(s) => write!(f, "Attachment not found: {}", s),
1649            Self::InvalidDimensions(w, h) => {
1650                write!(f, "Invalid dimensions: {}x{}", w, h)
1651            }
1652            Self::ValidationFailed(issues) => {
1653                write!(f, "Validation failed: {}", issues.join("; "))
1654            }
1655            Self::TextureAllocationFailed(msg) => {
1656                write!(f, "Texture allocation failed: {}", msg)
1657            }
1658            Self::FramebufferIncomplete(msg) => {
1659                write!(f, "Framebuffer incomplete: {}", msg)
1660            }
1661        }
1662    }
1663}
1664
1665impl std::error::Error for GBufferError {}
1666
1667// ---------------------------------------------------------------------------
1668// Builder pattern for G-Buffer
1669// ---------------------------------------------------------------------------
1670
1671/// Fluent builder for constructing a G-Buffer with a custom layout.
1672pub struct GBufferBuilder {
1673    layout: GBufferLayout,
1674    viewport: Viewport,
1675    auto_create: bool,
1676}
1677
1678impl GBufferBuilder {
1679    pub fn new(width: u32, height: u32) -> Self {
1680        Self {
1681            layout: GBufferLayout::new(),
1682            viewport: Viewport::new(width, height),
1683            auto_create: true,
1684        }
1685    }
1686
1687    /// Use the default layout.
1688    pub fn with_default_layout(mut self) -> Self {
1689        self.layout = GBufferLayout::default_layout();
1690        self
1691    }
1692
1693    /// Use the thin/minimal layout.
1694    pub fn with_thin_layout(mut self) -> Self {
1695        self.layout = GBufferLayout::thin_layout();
1696        self
1697    }
1698
1699    /// Add a custom color attachment.
1700    pub fn add_attachment(mut self, attachment: GBufferAttachment) -> Self {
1701        self.layout.add_color_attachment(attachment);
1702        self
1703    }
1704
1705    /// Set the depth format.
1706    pub fn with_depth_format(mut self, format: GBufferAttachmentFormat) -> Self {
1707        self.layout.depth_attachment.format = format;
1708        self
1709    }
1710
1711    /// Enable or disable octahedral normal encoding.
1712    pub fn with_octahedral_normals(mut self, enabled: bool) -> Self {
1713        self.layout.use_octahedral_normals = enabled;
1714        self
1715    }
1716
1717    /// Set maximum color attachments.
1718    pub fn with_max_attachments(mut self, max: u32) -> Self {
1719        self.layout.max_color_attachments = max;
1720        self
1721    }
1722
1723    /// Whether to auto-create GPU resources on build.
1724    pub fn auto_create(mut self, enabled: bool) -> Self {
1725        self.auto_create = enabled;
1726        self
1727    }
1728
1729    /// Build the G-Buffer.
1730    pub fn build(self) -> Result<GBuffer, GBufferError> {
1731        let mut gbuffer = GBuffer::with_layout(self.layout, self.viewport);
1732        if self.auto_create {
1733            gbuffer.create()?;
1734        }
1735        Ok(gbuffer)
1736    }
1737}
1738
1739// ---------------------------------------------------------------------------
1740// Tests
1741// ---------------------------------------------------------------------------
1742
1743#[cfg(test)]
1744mod tests {
1745    use super::*;
1746
1747    #[test]
1748    fn test_attachment_format_sizes() {
1749        assert_eq!(GBufferAttachmentFormat::Rgba32F.bytes_per_pixel(), 16);
1750        assert_eq!(GBufferAttachmentFormat::Rg16F.bytes_per_pixel(), 4);
1751        assert_eq!(GBufferAttachmentFormat::Rgba8.bytes_per_pixel(), 4);
1752        assert_eq!(GBufferAttachmentFormat::Rgba16F.bytes_per_pixel(), 8);
1753        assert_eq!(GBufferAttachmentFormat::R8.bytes_per_pixel(), 1);
1754        assert_eq!(GBufferAttachmentFormat::D32F.bytes_per_pixel(), 4);
1755    }
1756
1757    #[test]
1758    fn test_octahedral_encoding_roundtrip() {
1759        let normals = [
1760            [0.0, 0.0, 1.0],
1761            [0.0, 0.0, -1.0],
1762            [1.0, 0.0, 0.0],
1763            [0.0, 1.0, 0.0],
1764            [0.577, 0.577, 0.577],
1765        ];
1766        for n in &normals {
1767            let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2] as f32).sqrt();
1768            let normalized = [n[0] / len, n[1] / len, n[2] / len];
1769            let encoded = octahedral_encode(normalized);
1770            let decoded = octahedral_decode(encoded);
1771            for i in 0..3 {
1772                assert!(
1773                    (decoded[i] - normalized[i]).abs() < 0.01,
1774                    "Component {} mismatch: {} vs {}",
1775                    i, decoded[i], normalized[i]
1776                );
1777            }
1778        }
1779    }
1780
1781    #[test]
1782    fn test_default_layout_validation() {
1783        let layout = GBufferLayout::default_layout();
1784        let issues = layout.validate();
1785        assert!(issues.is_empty(), "Default layout should be valid: {:?}", issues);
1786    }
1787
1788    #[test]
1789    fn test_gbuffer_create_and_bind() {
1790        let mut gb = GBuffer::new(Viewport::new(1920, 1080));
1791        assert!(!gb.is_created);
1792        gb.create().unwrap();
1793        assert!(gb.is_created);
1794        gb.bind().unwrap();
1795        assert!(gb.is_bound);
1796        gb.unbind();
1797        assert!(!gb.is_bound);
1798    }
1799
1800    #[test]
1801    fn test_gbuffer_resize() {
1802        let mut gb = GBuffer::new(Viewport::new(800, 600));
1803        gb.create().unwrap();
1804        gb.resize(1920, 1080).unwrap();
1805        assert_eq!(gb.viewport.width, 1920);
1806        assert_eq!(gb.viewport.height, 1080);
1807    }
1808
1809    #[test]
1810    fn test_gbuffer_stats() {
1811        let gb = GBuffer::new(Viewport::new(1920, 1080));
1812        let stats = &gb.stats;
1813        assert!(stats.total_memory_bytes > 0);
1814        assert_eq!(stats.width, 1920);
1815        assert_eq!(stats.height, 1080);
1816    }
1817
1818    #[test]
1819    fn test_debug_channel_cycling() {
1820        let mut ch = GBufferDebugChannel::None;
1821        ch = ch.next();
1822        assert_eq!(ch, GBufferDebugChannel::Position);
1823        ch = ch.next();
1824        assert_eq!(ch, GBufferDebugChannel::Normal);
1825        ch = ch.prev();
1826        assert_eq!(ch, GBufferDebugChannel::Position);
1827    }
1828
1829    #[test]
1830    fn test_builder() {
1831        let gb = GBufferBuilder::new(1280, 720)
1832            .with_default_layout()
1833            .build()
1834            .unwrap();
1835        assert!(gb.is_created);
1836        assert_eq!(gb.viewport.width, 1280);
1837        assert_eq!(gb.layout.color_attachment_count(), 7);
1838    }
1839
1840    #[test]
1841    fn test_mrt_config() {
1842        let layout = GBufferLayout::default_layout();
1843        let mrt = MrtConfig::from_layout(&layout);
1844        assert!(mrt.validate());
1845        assert_eq!(mrt.draw_buffers.len(), 7);
1846        assert_eq!(mrt.active_count(), 7);
1847    }
1848
1849    #[test]
1850    fn test_thin_layout() {
1851        let layout = GBufferLayout::thin_layout();
1852        let issues = layout.validate();
1853        assert!(issues.is_empty());
1854        assert_eq!(layout.color_attachment_count(), 3);
1855        let mem_thin = layout.total_memory_bytes(1920, 1080);
1856        let mem_full = GBufferLayout::default_layout().total_memory_bytes(1920, 1080);
1857        assert!(mem_thin < mem_full, "Thin layout should use less memory");
1858    }
1859}