1use std::collections::HashMap;
9use std::fmt;
10
11use super::{Viewport, clampf};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum GBufferAttachmentFormat {
20 Rgba32F,
22 Rg16F,
24 Rgba8,
26 Rgba16F,
28 R8,
30 D32F,
32 D24S8,
34 R16F,
36 Rg8,
38 R32F,
40}
41
42impl GBufferAttachmentFormat {
43 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 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 pub fn is_depth(&self) -> bool {
70 matches!(self, Self::D32F | Self::D24S8)
71 }
72
73 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 pub fn gl_internal_format(&self) -> u32 {
83 match self {
84 Self::Rgba32F => 0x8814, Self::Rg16F => 0x822F, Self::Rgba8 => 0x8058, Self::Rgba16F => 0x881A, Self::R8 => 0x8229, Self::D32F => 0x8CAC, Self::D24S8 => 0x88F0, Self::R16F => 0x822D, Self::Rg8 => 0x822B, Self::R32F => 0x822E, }
95 }
96
97 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#[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#[derive(Debug, Clone)]
173pub struct GBufferAttachment {
174 pub semantic: GBufferSemantic,
176 pub format: GBufferAttachmentFormat,
178 pub color_index: u32,
180 pub texture_unit: u32,
182 pub clear_value: ClearValue,
184 pub linear_filter: bool,
186 pub mip_levels: u32,
188 pub texture_handle: u64,
190 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 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 (base as f64 * 1.334).ceil() as u64
231 }
232 }
233
234 pub fn with_clear_value(mut self, cv: ClearValue) -> Self {
236 self.clear_value = cv;
237 self
238 }
239
240 pub fn with_linear_filter(mut self, enabled: bool) -> Self {
242 self.linear_filter = enabled;
243 self
244 }
245
246 pub fn with_mip_levels(mut self, levels: u32) -> Self {
248 self.mip_levels = levels.max(1);
249 self
250 }
251
252 pub fn with_label(mut self, label: impl Into<String>) -> Self {
254 self.label = label.into();
255 self
256 }
257}
258
259#[derive(Debug, Clone, Copy)]
265pub enum ClearValue {
266 Color([f32; 4]),
268 Depth(f32),
270 DepthStencil(f32, u8),
272 IntColor([i32; 4]),
274 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#[derive(Debug, Clone)]
322pub struct GBufferLayout {
323 pub color_attachments: Vec<GBufferAttachment>,
325 pub depth_attachment: GBufferAttachment,
327 pub max_color_attachments: u32,
329 pub use_octahedral_normals: bool,
331 pub thin_gbuffer: bool,
333}
334
335impl GBufferLayout {
336 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, ),
346 max_color_attachments: 8,
347 use_octahedral_normals: true,
348 thin_gbuffer: false,
349 }
350 }
351
352 pub fn default_layout() -> Self {
354 let mut layout = Self::new();
355 layout.use_octahedral_normals = true;
356
357 layout.add_color_attachment(GBufferAttachment::new(
359 GBufferSemantic::Position,
360 GBufferAttachmentFormat::Rgba32F,
361 0,
362 0,
363 ).with_label("GBuffer_Position"));
364
365 layout.add_color_attachment(GBufferAttachment::new(
367 GBufferSemantic::Normal,
368 GBufferAttachmentFormat::Rg16F,
369 1,
370 1,
371 ).with_label("GBuffer_Normal"));
372
373 layout.add_color_attachment(GBufferAttachment::new(
375 GBufferSemantic::Albedo,
376 GBufferAttachmentFormat::Rgba8,
377 2,
378 2,
379 ).with_label("GBuffer_Albedo"));
380
381 layout.add_color_attachment(GBufferAttachment::new(
383 GBufferSemantic::Emission,
384 GBufferAttachmentFormat::Rgba16F,
385 3,
386 3,
387 ).with_label("GBuffer_Emission"));
388
389 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 layout.add_color_attachment(GBufferAttachment::new(
401 GBufferSemantic::Roughness,
402 GBufferAttachmentFormat::R8,
403 5,
404 5,
405 ).with_label("GBuffer_Roughness"));
406
407 layout.add_color_attachment(GBufferAttachment::new(
409 GBufferSemantic::Metallic,
410 GBufferAttachmentFormat::R8,
411 6,
412 6,
413 ).with_label("GBuffer_Metallic"));
414
415 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 pub fn thin_layout() -> Self {
430 let mut layout = Self::new();
431 layout.thin_gbuffer = true;
432 layout.use_octahedral_normals = true;
433
434 layout.add_color_attachment(GBufferAttachment::new(
438 GBufferSemantic::Normal,
439 GBufferAttachmentFormat::Rgba16F,
440 0,
441 0,
442 ).with_label("Thin_NormalRoughnessMetallic"));
443
444 layout.add_color_attachment(GBufferAttachment::new(
446 GBufferSemantic::Albedo,
447 GBufferAttachmentFormat::Rgba8,
448 1,
449 1,
450 ).with_label("Thin_AlbedoMatID"));
451
452 layout.add_color_attachment(GBufferAttachment::new(
454 GBufferSemantic::Emission,
455 GBufferAttachmentFormat::Rgba16F,
456 2,
457 2,
458 ).with_label("Thin_Emission"));
459
460 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 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 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 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 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 pub fn color_attachment_count(&self) -> u32 {
506 self.color_attachments.len() as u32
507 }
508
509 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 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 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 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#[derive(Debug, Clone)]
576pub struct MrtConfig {
577 pub draw_buffers: Vec<u32>,
579 pub use_explicit_locations: bool,
581 pub max_draw_buffers: u32,
583 pub blend_enabled: Vec<bool>,
585 pub write_masks: Vec<u8>,
587}
588
589impl MrtConfig {
590 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], }
603 }
604
605 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 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 pub fn disable_attachment(&mut self, index: usize) {
621 self.set_write_mask(index, 0x00);
622 }
623
624 pub fn enable_attachment(&mut self, index: usize) {
626 self.set_write_mask(index, 0x0F);
627 }
628
629 pub fn active_count(&self) -> usize {
631 self.write_masks.iter().filter(|&&m| m != 0).count()
632 }
633
634 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 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
677pub 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
697pub 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
718pub 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
726pub 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#[derive(Debug, Clone)]
740pub struct TextureState {
741 pub handle: u64,
743 pub width: u32,
745 pub height: u32,
747 pub allocated: bool,
749 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#[derive(Debug, Clone)]
796pub struct GBufferStats {
797 pub total_memory_bytes: u64,
799 pub per_attachment_memory: HashMap<String, u64>,
801 pub width: u32,
803 pub height: u32,
804 pub color_attachment_count: u32,
806 pub fill_rate_mpix_per_sec: f64,
808 pub bandwidth_gb_per_write: f64,
810 pub resize_count: u32,
812 pub clears_this_frame: u32,
814 pub geometry_draw_calls: u32,
816 pub avg_triangles_per_draw: u32,
818 pub overdraw_ratio: f32,
820 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 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 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 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; 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
940pub enum GBufferDebugChannel {
941 Position,
943 Normal,
945 Albedo,
947 Emission,
949 MaterialId,
951 Roughness,
953 Metallic,
955 Depth,
957 All,
959 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 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 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#[derive(Debug, Clone)]
1020pub struct GBufferDebugView {
1021 pub active_channel: GBufferDebugChannel,
1023 pub enabled: bool,
1025 pub exposure: f32,
1027 pub depth_near: f32,
1029 pub depth_far: f32,
1031 pub grid_cols: u32,
1033 pub grid_rows: u32,
1034 pub overlay_opacity: f32,
1036 pub material_id_palette: Vec<[f32; 3]>,
1038 pub zoom: f32,
1040 pub pan: [f32; 2],
1042 pub show_pixel_values: bool,
1044 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 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 pub fn toggle(&mut self) {
1080 self.enabled = !self.enabled;
1081 }
1082
1083 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 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 pub fn set_channel(&mut self, channel: GBufferDebugChannel) {
1101 self.active_channel = channel;
1102 self.enabled = channel != GBufferDebugChannel::None;
1103 }
1104
1105 pub fn adjust_exposure(&mut self, delta: f32) {
1107 self.exposure = (self.exposure + delta).max(0.01).min(100.0);
1108 }
1109
1110 pub fn reset_view(&mut self) {
1112 self.zoom = 1.0;
1113 self.pan = [0.0, 0.0];
1114 }
1115
1116 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 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 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 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] }
1162 }
1163
1164 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
1221fn 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#[derive(Debug)]
1255pub struct GBuffer {
1256 pub layout: GBufferLayout,
1258 pub viewport: Viewport,
1260 pub fbo_handle: u64,
1262 pub texture_states: Vec<TextureState>,
1264 pub depth_texture_state: TextureState,
1266 pub mrt_config: MrtConfig,
1268 pub debug_view: GBufferDebugView,
1270 pub stats: GBufferStats,
1272 pub is_bound: bool,
1274 pub generation: u32,
1276 pub is_created: bool,
1278 next_handle: u64,
1280}
1281
1282impl GBuffer {
1283 pub fn new(viewport: Viewport) -> Self {
1285 Self::with_layout(GBufferLayout::default_layout(), viewport)
1286 }
1287
1288 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 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 self.fbo_handle = self.alloc_handle();
1324
1325 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 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 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 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 pub fn unbind(&mut self) {
1369 self.is_bound = false;
1370 }
1371
1372 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 bindings.push((
1384 self.layout.depth_attachment.texture_unit,
1385 self.depth_texture_state.handle,
1386 ));
1387
1388 Ok(bindings)
1389 }
1390
1391 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; return Ok(self.texture_states[i].handle);
1409 }
1410 }
1411
1412 Err(GBufferError::AttachmentNotFound(semantic))
1413 }
1414
1415 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 for ts in &mut self.texture_states {
1431 let handle = ts.handle; 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 pub fn clear_all(&mut self) {
1450 self.stats.clears_this_frame += 1;
1451 }
1454
1455 pub fn clear_attachment(&self, semantic: GBufferSemantic) -> Result<(), GBufferError> {
1457 if semantic == GBufferSemantic::Depth {
1458 return Ok(());
1460 }
1461 if self.layout.find_attachment(semantic).is_none() {
1462 return Err(GBufferError::AttachmentNotFound(semantic));
1463 }
1464 Ok(())
1465 }
1466
1467 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 pub fn viewport(&self) -> Viewport {
1482 self.viewport
1483 }
1484
1485 pub fn aspect_ratio(&self) -> f32 {
1487 self.viewport.aspect_ratio()
1488 }
1489
1490 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 fn alloc_handle(&mut self) -> u64 {
1501 let h = self.next_handle;
1502 self.next_handle += 1;
1503 h
1504 }
1505
1506 pub fn stats(&self) -> &GBufferStats {
1508 &self.stats
1509 }
1510
1511 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 pub fn needs_recreate(&self) -> bool {
1547 if !self.is_created {
1548 return true;
1549 }
1550 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 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 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 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" } 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#[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
1667pub 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 pub fn with_default_layout(mut self) -> Self {
1689 self.layout = GBufferLayout::default_layout();
1690 self
1691 }
1692
1693 pub fn with_thin_layout(mut self) -> Self {
1695 self.layout = GBufferLayout::thin_layout();
1696 self
1697 }
1698
1699 pub fn add_attachment(mut self, attachment: GBufferAttachment) -> Self {
1701 self.layout.add_color_attachment(attachment);
1702 self
1703 }
1704
1705 pub fn with_depth_format(mut self, format: GBufferAttachmentFormat) -> Self {
1707 self.layout.depth_attachment.format = format;
1708 self
1709 }
1710
1711 pub fn with_octahedral_normals(mut self, enabled: bool) -> Self {
1713 self.layout.use_octahedral_normals = enabled;
1714 self
1715 }
1716
1717 pub fn with_max_attachments(mut self, max: u32) -> Self {
1719 self.layout.max_color_attachments = max;
1720 self
1721 }
1722
1723 pub fn auto_create(mut self, enabled: bool) -> Self {
1725 self.auto_create = enabled;
1726 self
1727 }
1728
1729 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#[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]).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}