1
2use glam::Vec2;
6use std::collections::{HashMap, VecDeque};
7
8#[derive(Debug, Clone, Default)]
13pub struct GpuCapabilities {
14 pub vendor: String,
15 pub device_name: String,
16 pub driver_version: String,
17 pub api_version: String,
18 pub dedicated_vram_bytes: u64,
19 pub shared_memory_bytes: u64,
20 pub max_texture_size: u32,
21 pub max_compute_invocations: u32,
22 pub max_uniform_buffer_range: u32,
23 pub max_storage_buffer_range: u64,
24 pub supports_timestamp_queries: bool,
25 pub supports_pipeline_statistics: bool,
26 pub supports_mesh_shaders: bool,
27 pub supports_raytracing: bool,
28 pub supports_variable_rate_shading: bool,
29 pub supports_conservative_rasterization: bool,
30 pub supports_sparse_resources: bool,
31 pub supports_descriptor_indexing: bool,
32 pub max_draw_indirect_count: u32,
33 pub subgroup_size: u32,
34 pub max_compute_shared_memory: u32,
35}
36
37impl GpuCapabilities {
38 pub fn mock_discrete() -> Self {
39 Self {
40 vendor: "NVIDIA".to_string(),
41 device_name: "GeForce RTX 4080".to_string(),
42 driver_version: "546.01".to_string(),
43 api_version: "Vulkan 1.3".to_string(),
44 dedicated_vram_bytes: 16 * 1024 * 1024 * 1024,
45 shared_memory_bytes: 16 * 1024 * 1024 * 1024,
46 max_texture_size: 32768,
47 max_compute_invocations: 1024,
48 max_uniform_buffer_range: 65536,
49 max_storage_buffer_range: u64::MAX,
50 supports_timestamp_queries: true,
51 supports_pipeline_statistics: true,
52 supports_mesh_shaders: true,
53 supports_raytracing: true,
54 supports_variable_rate_shading: true,
55 supports_conservative_rasterization: true,
56 supports_sparse_resources: true,
57 supports_descriptor_indexing: true,
58 max_draw_indirect_count: u32::MAX,
59 subgroup_size: 32,
60 max_compute_shared_memory: 49152,
61 }
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
70pub enum GpuMemoryHeap {
71 DeviceLocal,
72 HostVisible,
73 HostCoherent,
74 HostCached,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
78pub enum GpuResourceKind {
79 VertexBuffer,
80 IndexBuffer,
81 UniformBuffer,
82 StorageBuffer,
83 Texture2D,
84 Texture3D,
85 TextureCube,
86 TextureArray,
87 RenderTarget,
88 DepthStencil,
89 AccelerationStructure,
90 ScratchBuffer,
91 UploadHeap,
92 ReadbackHeap,
93}
94
95impl GpuResourceKind {
96 pub fn label(&self) -> &'static str {
97 match self {
98 Self::VertexBuffer => "Vertex Buffer",
99 Self::IndexBuffer => "Index Buffer",
100 Self::UniformBuffer => "Uniform Buffer",
101 Self::StorageBuffer => "Storage Buffer",
102 Self::Texture2D => "Texture 2D",
103 Self::Texture3D => "Texture 3D",
104 Self::TextureCube => "Texture Cube",
105 Self::TextureArray => "Texture Array",
106 Self::RenderTarget => "Render Target",
107 Self::DepthStencil => "Depth Stencil",
108 Self::AccelerationStructure => "Acceleration Structure",
109 Self::ScratchBuffer => "Scratch Buffer",
110 Self::UploadHeap => "Upload Heap",
111 Self::ReadbackHeap => "Readback Heap",
112 }
113 }
114}
115
116#[derive(Debug, Clone)]
117pub struct GpuAllocation {
118 pub id: u64,
119 pub name: String,
120 pub kind: GpuResourceKind,
121 pub heap: GpuMemoryHeap,
122 pub size_bytes: u64,
123 pub alignment: u64,
124 pub offset: u64,
125 pub alive: bool,
126 pub frame_created: u64,
127 pub frame_destroyed: Option<u64>,
128}
129
130#[derive(Debug, Clone, Default)]
131pub struct GpuMemoryStats {
132 pub device_local_used: u64,
133 pub device_local_total: u64,
134 pub host_visible_used: u64,
135 pub host_visible_total: u64,
136 pub by_kind: HashMap<GpuResourceKind, u64>,
137 pub peak_device_local: u64,
138 pub allocation_count: u32,
139 pub free_count: u32,
140}
141
142impl GpuMemoryStats {
143 pub fn device_local_pct(&self) -> f32 {
144 if self.device_local_total == 0 { return 0.0; }
145 self.device_local_used as f32 / self.device_local_total as f32
146 }
147
148 pub fn host_visible_pct(&self) -> f32 {
149 if self.host_visible_total == 0 { return 0.0; }
150 self.host_visible_used as f32 / self.host_visible_total as f32
151 }
152
153 pub fn total_used_mb(&self) -> f32 {
154 (self.device_local_used + self.host_visible_used) as f32 / (1024.0 * 1024.0)
155 }
156}
157
158#[derive(Debug, Clone, Default)]
163pub struct PipelineStats {
164 pub input_assembly_vertices: u64,
165 pub input_assembly_primitives: u64,
166 pub vertex_shader_invocations: u64,
167 pub geometry_shader_invocations: u64,
168 pub geometry_shader_primitives: u64,
169 pub clipping_invocations: u64,
170 pub clipping_primitives: u64,
171 pub fragment_shader_invocations: u64,
172 pub tessellation_control_shader_patches: u64,
173 pub tessellation_eval_shader_invocations: u64,
174 pub compute_shader_invocations: u64,
175 pub mesh_shader_invocations: u64,
176 pub task_shader_invocations: u64,
177}
178
179impl PipelineStats {
180 pub fn overdraw_factor(&self) -> f32 {
181 if self.clipping_primitives == 0 { return 0.0; }
182 self.fragment_shader_invocations as f32 / (self.clipping_primitives as f32 * 4.0).max(1.0)
183 }
184
185 pub fn vertex_reuse_factor(&self) -> f32 {
186 if self.vertex_shader_invocations == 0 { return 0.0; }
187 self.input_assembly_vertices as f32 / self.vertex_shader_invocations as f32
188 }
189}
190
191#[derive(Debug, Clone)]
196pub struct TimestampQuery {
197 pub name: String,
198 pub begin_ns: u64,
199 pub end_ns: u64,
200 pub pipeline_stage: PipelineStage,
201 pub color: u32,
202}
203
204impl TimestampQuery {
205 pub fn duration_ms(&self) -> f32 {
206 (self.end_ns - self.begin_ns) as f32 / 1_000_000.0
207 }
208
209 pub fn duration_us(&self) -> f32 {
210 (self.end_ns - self.begin_ns) as f32 / 1_000.0
211 }
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
215pub enum PipelineStage {
216 TopOfPipe,
217 DrawIndirect,
218 VertexInput,
219 VertexShader,
220 TessellationControl,
221 TessellationEval,
222 GeometryShader,
223 EarlyFragTest,
224 FragmentShader,
225 LateFragTest,
226 ColorAttachmentOutput,
227 ComputeShader,
228 Transfer,
229 BottomOfPipe,
230 AllGraphics,
231 AllCommands,
232 AccelerationStructureBuild,
233 RayTracing,
234 MeshShader,
235 TaskShader,
236}
237
238impl PipelineStage {
239 pub fn label(&self) -> &'static str {
240 match self {
241 Self::TopOfPipe => "Top of Pipe",
242 Self::DrawIndirect => "Draw Indirect",
243 Self::VertexInput => "Vertex Input",
244 Self::VertexShader => "Vertex Shader",
245 Self::TessellationControl => "Tessellation Control",
246 Self::TessellationEval => "Tessellation Eval",
247 Self::GeometryShader => "Geometry Shader",
248 Self::EarlyFragTest => "Early Fragment Test",
249 Self::FragmentShader => "Fragment Shader",
250 Self::LateFragTest => "Late Fragment Test",
251 Self::ColorAttachmentOutput => "Color Attachment Output",
252 Self::ComputeShader => "Compute Shader",
253 Self::Transfer => "Transfer",
254 Self::BottomOfPipe => "Bottom of Pipe",
255 Self::AllGraphics => "All Graphics",
256 Self::AllCommands => "All Commands",
257 Self::AccelerationStructureBuild => "AS Build",
258 Self::RayTracing => "Ray Tracing",
259 Self::MeshShader => "Mesh Shader",
260 Self::TaskShader => "Task Shader",
261 }
262 }
263
264 pub fn color_rgba(&self) -> u32 {
265 match self {
266 Self::VertexShader => 0xFF4080FF,
267 Self::FragmentShader => 0xFF40FF80,
268 Self::ComputeShader => 0xFFFF8040,
269 Self::Transfer => 0xFFFFFF40,
270 Self::RayTracing => 0xFFFF40FF,
271 Self::AccelerationStructureBuild => 0xFF40FFFF,
272 _ => 0xFF808080,
273 }
274 }
275}
276
277#[derive(Debug, Clone)]
282pub struct RenderPassCapture {
283 pub name: String,
284 pub begin_ns: u64,
285 pub end_ns: u64,
286 pub draw_calls: u32,
287 pub dispatch_calls: u32,
288 pub index_count: u64,
289 pub vertex_count: u64,
290 pub triangle_count: u64,
291 pub render_targets: Vec<String>,
292 pub depth_target: Option<String>,
293 pub pipeline_stats: PipelineStats,
294 pub sub_queries: Vec<TimestampQuery>,
295 pub color: u32,
296}
297
298impl RenderPassCapture {
299 pub fn duration_ms(&self) -> f32 {
300 (self.end_ns - self.begin_ns) as f32 / 1_000_000.0
301 }
302}
303
304#[derive(Debug, Clone, Default)]
305pub struct FrameCapture {
306 pub frame_index: u64,
307 pub begin_ns: u64,
308 pub end_ns: u64,
309 pub cpu_build_ms: f32,
310 pub gpu_present_ms: f32,
311 pub render_passes: Vec<RenderPassCapture>,
312 pub memory_stats: GpuMemoryStats,
313 pub draw_call_count: u32,
314 pub dispatch_count: u32,
315 pub triangle_count: u64,
316 pub byte_uploaded: u64,
317 pub byte_downloaded: u64,
318 pub pipeline_cache_hits: u32,
319 pub pipeline_cache_misses: u32,
320 pub barrier_count: u32,
321 pub layout_transition_count: u32,
322}
323
324impl FrameCapture {
325 pub fn gpu_total_ms(&self) -> f32 {
326 (self.end_ns - self.begin_ns) as f32 / 1_000_000.0
327 }
328
329 pub fn frame_time_ms(&self) -> f32 {
330 self.gpu_total_ms() + self.cpu_build_ms
331 }
332
333 pub fn fps(&self) -> f32 {
334 if self.frame_time_ms() <= 0.0 { return 0.0; }
335 1000.0 / self.frame_time_ms()
336 }
337
338 pub fn bottleneck(&self) -> &'static str {
339 let gpu_pct = self.gpu_total_ms() / self.frame_time_ms().max(0.001);
340 if gpu_pct > 0.7 { "GPU-Bound" }
341 else if gpu_pct < 0.3 { "CPU-Bound" }
342 else { "Balanced" }
343 }
344
345 pub fn pass_by_name(&self, name: &str) -> Option<&RenderPassCapture> {
346 self.render_passes.iter().find(|p| p.name == name)
347 }
348
349 pub fn top_passes_by_duration(&self, n: usize) -> Vec<&RenderPassCapture> {
350 let mut sorted: Vec<&RenderPassCapture> = self.render_passes.iter().collect();
351 sorted.sort_by(|a, b| b.duration_ms().partial_cmp(&a.duration_ms()).unwrap_or(std::cmp::Ordering::Equal));
352 sorted.into_iter().take(n).collect()
353 }
354
355 pub fn synthetic_frame(frame_index: u64) -> Self {
356 let base_ns = frame_index * 16_666_666;
357 let passes = vec![
358 RenderPassCapture {
359 name: "ShadowMap".to_string(),
360 begin_ns: base_ns,
361 end_ns: base_ns + 800_000,
362 draw_calls: 42,
363 dispatch_calls: 0,
364 index_count: 120000,
365 vertex_count: 80000,
366 triangle_count: 40000,
367 render_targets: vec![],
368 depth_target: Some("shadow_depth".to_string()),
369 pipeline_stats: PipelineStats {
370 vertex_shader_invocations: 80000,
371 fragment_shader_invocations: 320000,
372 ..Default::default()
373 },
374 sub_queries: vec![],
375 color: 0xFF204080,
376 },
377 RenderPassCapture {
378 name: "GBuffer".to_string(),
379 begin_ns: base_ns + 900_000,
380 end_ns: base_ns + 4_200_000,
381 draw_calls: 186,
382 dispatch_calls: 0,
383 index_count: 1_200_000,
384 vertex_count: 800_000,
385 triangle_count: 400_000,
386 render_targets: vec!["gbuffer_albedo".to_string(), "gbuffer_normal".to_string(), "gbuffer_material".to_string()],
387 depth_target: Some("scene_depth".to_string()),
388 pipeline_stats: PipelineStats {
389 vertex_shader_invocations: 800_000,
390 fragment_shader_invocations: 2_000_000,
391 ..Default::default()
392 },
393 sub_queries: vec![],
394 color: 0xFF804020,
395 },
396 RenderPassCapture {
397 name: "SSAO".to_string(),
398 begin_ns: base_ns + 4_300_000,
399 end_ns: base_ns + 5_100_000,
400 draw_calls: 1,
401 dispatch_calls: 1,
402 index_count: 6,
403 vertex_count: 4,
404 triangle_count: 2,
405 render_targets: vec!["ssao_result".to_string()],
406 depth_target: None,
407 pipeline_stats: PipelineStats {
408 compute_shader_invocations: 1_920 * 1_080,
409 ..Default::default()
410 },
411 sub_queries: vec![],
412 color: 0xFF408040,
413 },
414 RenderPassCapture {
415 name: "DeferredLighting".to_string(),
416 begin_ns: base_ns + 5_200_000,
417 end_ns: base_ns + 7_500_000,
418 draw_calls: 1,
419 dispatch_calls: 0,
420 index_count: 6,
421 vertex_count: 4,
422 triangle_count: 2,
423 render_targets: vec!["hdr_buffer".to_string()],
424 depth_target: None,
425 pipeline_stats: PipelineStats {
426 fragment_shader_invocations: 1_920 * 1_080,
427 ..Default::default()
428 },
429 sub_queries: vec![],
430 color: 0xFFFF8040,
431 },
432 RenderPassCapture {
433 name: "Bloom".to_string(),
434 begin_ns: base_ns + 7_600_000,
435 end_ns: base_ns + 8_900_000,
436 draw_calls: 12,
437 dispatch_calls: 0,
438 index_count: 72,
439 vertex_count: 48,
440 triangle_count: 24,
441 render_targets: vec!["bloom_buffer".to_string()],
442 depth_target: None,
443 pipeline_stats: PipelineStats {
444 fragment_shader_invocations: 1_920 * 1_080 * 4,
445 ..Default::default()
446 },
447 sub_queries: vec![],
448 color: 0xFFFF4080,
449 },
450 RenderPassCapture {
451 name: "TAA".to_string(),
452 begin_ns: base_ns + 9_000_000,
453 end_ns: base_ns + 10_200_000,
454 draw_calls: 1,
455 dispatch_calls: 0,
456 index_count: 6,
457 vertex_count: 4,
458 triangle_count: 2,
459 render_targets: vec!["taa_output".to_string()],
460 depth_target: None,
461 pipeline_stats: PipelineStats {
462 fragment_shader_invocations: 1_920 * 1_080,
463 ..Default::default()
464 },
465 sub_queries: vec![],
466 color: 0xFF4080FF,
467 },
468 RenderPassCapture {
469 name: "PostProcess".to_string(),
470 begin_ns: base_ns + 10_300_000,
471 end_ns: base_ns + 12_500_000,
472 draw_calls: 8,
473 dispatch_calls: 0,
474 index_count: 48,
475 vertex_count: 32,
476 triangle_count: 16,
477 render_targets: vec!["backbuffer".to_string()],
478 depth_target: None,
479 pipeline_stats: PipelineStats {
480 fragment_shader_invocations: 1_920 * 1_080 * 5,
481 ..Default::default()
482 },
483 sub_queries: vec![],
484 color: 0xFF8040FF,
485 },
486 ];
487
488 let total_triangles: u64 = passes.iter().map(|p| p.triangle_count).sum();
489 let total_dc: u32 = passes.iter().map(|p| p.draw_calls).sum();
490
491 Self {
492 frame_index,
493 begin_ns: base_ns,
494 end_ns: base_ns + 12_666_666,
495 cpu_build_ms: 3.2,
496 gpu_present_ms: 0.4,
497 render_passes: passes,
498 memory_stats: GpuMemoryStats {
499 device_local_used: 4_200 * 1024 * 1024,
500 device_local_total: 16 * 1024 * 1024 * 1024,
501 host_visible_used: 256 * 1024 * 1024,
502 host_visible_total: 16 * 1024 * 1024 * 1024,
503 by_kind: HashMap::new(),
504 peak_device_local: 4_500 * 1024 * 1024,
505 allocation_count: 1240,
506 free_count: 48,
507 },
508 draw_call_count: total_dc,
509 dispatch_count: 2,
510 triangle_count: total_triangles,
511 byte_uploaded: 2 * 1024 * 1024,
512 byte_downloaded: 0,
513 pipeline_cache_hits: 820,
514 pipeline_cache_misses: 3,
515 barrier_count: 24,
516 layout_transition_count: 18,
517 }
518 }
519}
520
521#[derive(Debug, Clone)]
526pub struct ShaderCompileStat {
527 pub shader_path: String,
528 pub stage: ShaderStage,
529 pub permutation_key: u64,
530 pub instruction_count: u32,
531 pub sgpr_count: u32,
532 pub vgpr_count: u32,
533 pub scratch_bytes: u32,
534 pub spill_bytes: u32,
535 pub branch_count: u32,
536 pub texture_fetch_count: u32,
537 pub lds_usage_bytes: u32,
538 pub wavefront_occupancy: f32,
539 pub compile_ms: f32,
540}
541
542#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
543pub enum ShaderStage {
544 Vertex,
545 Fragment,
546 Compute,
547 Geometry,
548 TessellationControl,
549 TessellationEval,
550 Task,
551 Mesh,
552 RayGeneration,
553 ClosestHit,
554 AnyHit,
555 Miss,
556 Intersection,
557}
558
559impl ShaderStage {
560 pub fn label(&self) -> &'static str {
561 match self {
562 Self::Vertex => "Vertex",
563 Self::Fragment => "Fragment",
564 Self::Compute => "Compute",
565 Self::Geometry => "Geometry",
566 Self::TessellationControl => "Tess. Control",
567 Self::TessellationEval => "Tess. Eval",
568 Self::Task => "Task",
569 Self::Mesh => "Mesh",
570 Self::RayGeneration => "RayGen",
571 Self::ClosestHit => "ClosestHit",
572 Self::AnyHit => "AnyHit",
573 Self::Miss => "Miss",
574 Self::Intersection => "Intersection",
575 }
576 }
577
578 pub fn file_extension(&self) -> &'static str {
579 match self {
580 Self::Vertex => "vert",
581 Self::Fragment => "frag",
582 Self::Compute => "comp",
583 Self::Geometry => "geom",
584 Self::TessellationControl => "tesc",
585 Self::TessellationEval => "tese",
586 Self::Task => "task",
587 Self::Mesh => "mesh",
588 _ => "rgen",
589 }
590 }
591}
592
593#[derive(Debug, Clone)]
598pub struct PerfCounter {
599 pub name: String,
600 pub category: String,
601 pub value: f64,
602 pub unit: String,
603 pub description: String,
604 pub higher_is_better: bool,
605 pub warning_threshold: f64,
606 pub critical_threshold: f64,
607}
608
609impl PerfCounter {
610 pub fn status(&self) -> CounterStatus {
611 if (!self.higher_is_better && self.value >= self.critical_threshold)
612 || (self.higher_is_better && self.value <= self.critical_threshold)
613 {
614 CounterStatus::Critical
615 } else if (!self.higher_is_better && self.value >= self.warning_threshold)
616 || (self.higher_is_better && self.value <= self.warning_threshold)
617 {
618 CounterStatus::Warning
619 } else {
620 CounterStatus::Ok
621 }
622 }
623}
624
625#[derive(Debug, Clone, Copy, PartialEq)]
626pub enum CounterStatus { Ok, Warning, Critical }
627
628#[derive(Debug, Clone, Copy, PartialEq)]
633pub enum HeatmapMode {
634 Overdraw,
635 FragmentCost,
636 DepthComplexity,
637 LightingCost,
638 ShadowCost,
639 VertexDensity,
640 TextureCacheMiss,
641 MipVisualize,
642}
643
644impl HeatmapMode {
645 pub fn label(&self) -> &'static str {
646 match self {
647 Self::Overdraw => "Overdraw",
648 Self::FragmentCost => "Fragment Cost",
649 Self::DepthComplexity => "Depth Complexity",
650 Self::LightingCost => "Lighting Cost",
651 Self::ShadowCost => "Shadow Cost",
652 Self::VertexDensity => "Vertex Density",
653 Self::TextureCacheMiss => "Texture Cache Miss",
654 Self::MipVisualize => "Mip Level",
655 }
656 }
657
658 pub fn shader_define(&self) -> &'static str {
659 match self {
660 Self::Overdraw => "DEBUG_OVERDRAW",
661 Self::FragmentCost => "DEBUG_FRAG_COST",
662 Self::DepthComplexity => "DEBUG_DEPTH_COMPLEXITY",
663 Self::LightingCost => "DEBUG_LIGHTING_COST",
664 Self::ShadowCost => "DEBUG_SHADOW_COST",
665 Self::VertexDensity => "DEBUG_VERTEX_DENSITY",
666 Self::TextureCacheMiss => "DEBUG_TEXTURE_MISS",
667 Self::MipVisualize => "DEBUG_MIP_LEVEL",
668 }
669 }
670}
671
672#[derive(Debug, Clone)]
677pub struct FrameTimeGraph {
678 pub samples: VecDeque<f32>,
679 pub capacity: usize,
680 pub target_ms: f32,
681 pub warning_ms: f32,
682}
683
684impl FrameTimeGraph {
685 pub fn new(capacity: usize, target_ms: f32) -> Self {
686 Self {
687 samples: VecDeque::with_capacity(capacity),
688 capacity,
689 target_ms,
690 warning_ms: target_ms * 1.5,
691 }
692 }
693
694 pub fn push(&mut self, ms: f32) {
695 if self.samples.len() >= self.capacity {
696 self.samples.pop_front();
697 }
698 self.samples.push_back(ms);
699 }
700
701 pub fn avg(&self) -> f32 {
702 if self.samples.is_empty() { return 0.0; }
703 self.samples.iter().sum::<f32>() / self.samples.len() as f32
704 }
705
706 pub fn min(&self) -> f32 { self.samples.iter().cloned().fold(f32::MAX, f32::min) }
707 pub fn max(&self) -> f32 { self.samples.iter().cloned().fold(f32::MIN, f32::max) }
708
709 pub fn percentile_99(&self) -> f32 {
710 if self.samples.is_empty() { return 0.0; }
711 let mut sorted: Vec<f32> = self.samples.iter().cloned().collect();
712 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
713 let idx = ((sorted.len() as f32 * 0.99) as usize).min(sorted.len() - 1);
714 sorted[idx]
715 }
716
717 pub fn fps(&self) -> f32 {
718 let avg = self.avg();
719 if avg <= 0.0 { 0.0 } else { 1000.0 / avg }
720 }
721
722 pub fn normalized(&self, max_ms: f32) -> Vec<f32> {
724 self.samples.iter().map(|&s| (s / max_ms).min(1.0)).collect()
725 }
726}
727
728#[derive(Debug, Clone)]
733pub struct BarrierRecord {
734 pub frame_index: u64,
735 pub src_stage: PipelineStage,
736 pub dst_stage: PipelineStage,
737 pub src_access: u32,
738 pub dst_access: u32,
739 pub layout_old: Option<String>,
740 pub layout_new: Option<String>,
741 pub resource_name: String,
742 pub redundant: bool,
743 pub stall_ns: u64,
744}
745
746impl BarrierRecord {
747 pub fn stall_us(&self) -> f32 { self.stall_ns as f32 / 1_000.0 }
748}
749
750#[derive(Debug, Clone, PartialEq)]
755pub enum ResourceState {
756 Undefined,
757 General,
758 ColorAttachment,
759 DepthWrite,
760 DepthRead,
761 ShaderReadOnly,
762 TransferSrc,
763 TransferDst,
764 Present,
765 ComputeReadWrite,
766 AccelerationStructure,
767}
768
769impl ResourceState {
770 pub fn label(&self) -> &'static str {
771 match self {
772 Self::Undefined => "Undefined",
773 Self::General => "General",
774 Self::ColorAttachment => "Color Attachment",
775 Self::DepthWrite => "Depth Write",
776 Self::DepthRead => "Depth Read",
777 Self::ShaderReadOnly => "Shader Read-Only",
778 Self::TransferSrc => "Transfer Src",
779 Self::TransferDst => "Transfer Dst",
780 Self::Present => "Present",
781 Self::ComputeReadWrite => "Compute R/W",
782 Self::AccelerationStructure => "Acceleration Structure",
783 }
784 }
785}
786
787#[derive(Debug, Clone)]
788pub struct ResourceStateTracker {
789 pub states: HashMap<String, ResourceState>,
790 pub history: Vec<(String, ResourceState, ResourceState)>,
791}
792
793impl Default for ResourceStateTracker {
794 fn default() -> Self {
795 Self { states: HashMap::new(), history: Vec::new() }
796 }
797}
798
799impl ResourceStateTracker {
800 pub fn transition(&mut self, resource: &str, new_state: ResourceState) {
801 let old = self.states.get(resource).cloned().unwrap_or(ResourceState::Undefined);
802 if old != new_state {
803 self.history.push((resource.to_string(), old.clone(), new_state.clone()));
804 self.states.insert(resource.to_string(), new_state);
805 }
806 }
807
808 pub fn current_state(&self, resource: &str) -> ResourceState {
809 self.states.get(resource).cloned().unwrap_or(ResourceState::Undefined)
810 }
811
812 pub fn transitions_for(&self, resource: &str) -> Vec<(&ResourceState, &ResourceState)> {
813 self.history.iter()
814 .filter(|(r, _, _)| r == resource)
815 .map(|(_, old, new)| (old, new))
816 .collect()
817 }
818}
819
820#[derive(Debug, Clone, Copy, PartialEq)]
825pub enum ProfilerPanel {
826 FrameTimeline,
827 MemoryBudget,
828 PipelineStats,
829 ShaderStats,
830 BarrierAnalysis,
831 Counters,
832 Heatmap,
833 Captures,
834}
835
836impl ProfilerPanel {
837 pub fn label(&self) -> &'static str {
838 match self {
839 Self::FrameTimeline => "Frame Timeline",
840 Self::MemoryBudget => "Memory",
841 Self::PipelineStats => "Pipeline Stats",
842 Self::ShaderStats => "Shader Stats",
843 Self::BarrierAnalysis => "Barriers",
844 Self::Counters => "Counters",
845 Self::Heatmap => "Heatmap",
846 Self::Captures => "Captures",
847 }
848 }
849}
850
851#[derive(Debug)]
852pub struct GpuProfilerEditor {
853 pub gpu_caps: GpuCapabilities,
854 pub frame_captures: Vec<FrameCapture>,
855 pub selected_frame: Option<usize>,
856 pub selected_pass: Option<usize>,
857 pub frame_time_graph: FrameTimeGraph,
858 pub gpu_time_graph: FrameTimeGraph,
859 pub allocations: Vec<GpuAllocation>,
860 pub memory_stats: GpuMemoryStats,
861 pub shader_stats: Vec<ShaderCompileStat>,
862 pub perf_counters: Vec<PerfCounter>,
863 pub barrier_records: Vec<BarrierRecord>,
864 pub resource_tracker: ResourceStateTracker,
865 pub active_panel: ProfilerPanel,
866 pub capturing: bool,
867 pub capture_frame_count: u32,
868 pub pause_on_capture: bool,
869 pub heatmap_mode: HeatmapMode,
870 pub show_heatmap: bool,
871 pub frame_index: u64,
872 pub paused: bool,
873 pub zoom_range: (f32, f32),
874 pub timeline_scroll: f32,
875 pub search_filter: String,
876 pub expanded_passes: std::collections::HashSet<String>,
877 pub show_redundant_barriers: bool,
878 pub sort_shaders_by_instructions: bool,
879}
880
881impl Default for GpuProfilerEditor {
882 fn default() -> Self {
883 let mut captures: Vec<FrameCapture> = (0..32)
884 .map(|i| FrameCapture::synthetic_frame(i))
885 .collect();
886 let frame_times: Vec<f32> = captures.iter().map(|f| f.gpu_total_ms()).collect();
887
888 let mut ft_graph = FrameTimeGraph::new(256, 16.666);
889 let mut gpu_graph = FrameTimeGraph::new(256, 16.666);
890 for cap in &captures {
891 ft_graph.push(cap.frame_time_ms());
892 gpu_graph.push(cap.gpu_total_ms());
893 }
894
895 let mut memory_stats = GpuMemoryStats {
896 device_local_used: 4_200 * 1024 * 1024,
897 device_local_total: 16 * 1024 * 1024 * 1024,
898 host_visible_used: 256 * 1024 * 1024,
899 host_visible_total: 16 * 1024 * 1024 * 1024,
900 peak_device_local: 4_500 * 1024 * 1024,
901 allocation_count: 1240,
902 free_count: 48,
903 by_kind: HashMap::new(),
904 };
905 memory_stats.by_kind.insert(GpuResourceKind::Texture2D, 2_800 * 1024 * 1024);
906 memory_stats.by_kind.insert(GpuResourceKind::RenderTarget, 512 * 1024 * 1024);
907 memory_stats.by_kind.insert(GpuResourceKind::DepthStencil, 128 * 1024 * 1024);
908 memory_stats.by_kind.insert(GpuResourceKind::VertexBuffer, 400 * 1024 * 1024);
909 memory_stats.by_kind.insert(GpuResourceKind::IndexBuffer, 200 * 1024 * 1024);
910 memory_stats.by_kind.insert(GpuResourceKind::StorageBuffer, 160 * 1024 * 1024);
911
912 let shader_stats = vec![
913 ShaderCompileStat {
914 shader_path: "shaders/deferred_lighting.frag".to_string(),
915 stage: ShaderStage::Fragment,
916 permutation_key: 0xABCD1234,
917 instruction_count: 1420,
918 sgpr_count: 84,
919 vgpr_count: 96,
920 scratch_bytes: 0,
921 spill_bytes: 0,
922 branch_count: 42,
923 texture_fetch_count: 12,
924 lds_usage_bytes: 0,
925 wavefront_occupancy: 0.5,
926 compile_ms: 48.2,
927 },
928 ShaderCompileStat {
929 shader_path: "shaders/gbuffer.vert".to_string(),
930 stage: ShaderStage::Vertex,
931 permutation_key: 0x00000001,
932 instruction_count: 280,
933 sgpr_count: 32,
934 vgpr_count: 48,
935 scratch_bytes: 0,
936 spill_bytes: 0,
937 branch_count: 6,
938 texture_fetch_count: 0,
939 lds_usage_bytes: 0,
940 wavefront_occupancy: 0.75,
941 compile_ms: 8.4,
942 },
943 ShaderCompileStat {
944 shader_path: "shaders/ssao.comp".to_string(),
945 stage: ShaderStage::Compute,
946 permutation_key: 0x00000001,
947 instruction_count: 640,
948 sgpr_count: 48,
949 vgpr_count: 64,
950 scratch_bytes: 256,
951 spill_bytes: 0,
952 branch_count: 18,
953 texture_fetch_count: 8,
954 lds_usage_bytes: 4096,
955 wavefront_occupancy: 0.625,
956 compile_ms: 22.1,
957 },
958 ];
959
960 let perf_counters = vec![
961 PerfCounter {
962 name: "GPU Utilization".to_string(),
963 category: "General".to_string(),
964 value: 78.4,
965 unit: "%".to_string(),
966 description: "Percentage of time the GPU is doing useful work".to_string(),
967 higher_is_better: true,
968 warning_threshold: 50.0,
969 critical_threshold: 30.0,
970 },
971 PerfCounter {
972 name: "Memory Controller Usage".to_string(),
973 category: "Memory".to_string(),
974 value: 62.1,
975 unit: "%".to_string(),
976 description: "Memory controller bus utilization".to_string(),
977 higher_is_better: false,
978 warning_threshold: 75.0,
979 critical_threshold: 90.0,
980 },
981 PerfCounter {
982 name: "L2 Cache Hit Rate".to_string(),
983 category: "Cache".to_string(),
984 value: 84.5,
985 unit: "%".to_string(),
986 description: "L2 cache hit percentage".to_string(),
987 higher_is_better: true,
988 warning_threshold: 60.0,
989 critical_threshold: 40.0,
990 },
991 PerfCounter {
992 name: "Texture Cache Miss Rate".to_string(),
993 category: "Cache".to_string(),
994 value: 5.2,
995 unit: "%".to_string(),
996 description: "Percentage of texture fetches that miss the cache".to_string(),
997 higher_is_better: false,
998 warning_threshold: 15.0,
999 critical_threshold: 30.0,
1000 },
1001 PerfCounter {
1002 name: "Pipeline Stalls".to_string(),
1003 category: "Pipeline".to_string(),
1004 value: 2.8,
1005 unit: "%".to_string(),
1006 description: "Percentage of cycles wasted in pipeline stalls".to_string(),
1007 higher_is_better: false,
1008 warning_threshold: 10.0,
1009 critical_threshold: 20.0,
1010 },
1011 PerfCounter {
1012 name: "Shader Occupancy".to_string(),
1013 category: "Shader".to_string(),
1014 value: 62.5,
1015 unit: "%".to_string(),
1016 description: "Average shader occupancy across all dispatches".to_string(),
1017 higher_is_better: true,
1018 warning_threshold: 40.0,
1019 critical_threshold: 25.0,
1020 },
1021 ];
1022
1023 Self {
1024 gpu_caps: GpuCapabilities::mock_discrete(),
1025 selected_frame: captures.last().map(|_| captures.len() - 1),
1026 frame_time_graph: ft_graph,
1027 gpu_time_graph: gpu_graph,
1028 frame_captures: captures,
1029 allocations: Vec::new(),
1030 memory_stats,
1031 shader_stats,
1032 perf_counters,
1033 barrier_records: Vec::new(),
1034 resource_tracker: ResourceStateTracker::default(),
1035 active_panel: ProfilerPanel::FrameTimeline,
1036 capturing: false,
1037 capture_frame_count: 1,
1038 pause_on_capture: true,
1039 heatmap_mode: HeatmapMode::Overdraw,
1040 show_heatmap: false,
1041 frame_index: 32,
1042 paused: false,
1043 zoom_range: (0.0, 16.666),
1044 timeline_scroll: 0.0,
1045 search_filter: String::new(),
1046 expanded_passes: std::collections::HashSet::new(),
1047 show_redundant_barriers: false,
1048 sort_shaders_by_instructions: true,
1049 selected_pass: None,
1050 }
1051 }
1052}
1053
1054impl GpuProfilerEditor {
1055 pub fn selected_frame_capture(&self) -> Option<&FrameCapture> {
1056 self.selected_frame.and_then(|i| self.frame_captures.get(i))
1057 }
1058
1059 pub fn push_frame(&mut self, cap: FrameCapture) {
1060 if self.paused { return; }
1061 self.frame_time_graph.push(cap.frame_time_ms());
1062 self.gpu_time_graph.push(cap.gpu_total_ms());
1063 self.frame_captures.push(cap);
1064 if self.frame_captures.len() > 256 {
1065 self.frame_captures.remove(0);
1066 if let Some(ref mut sel) = self.selected_frame {
1067 if *sel > 0 { *sel -= 1; }
1068 }
1069 }
1070 self.frame_index += 1;
1071 }
1072
1073 pub fn begin_capture(&mut self) {
1074 self.capturing = true;
1075 }
1076
1077 pub fn end_capture(&mut self) {
1078 self.capturing = false;
1079 let cap = FrameCapture::synthetic_frame(self.frame_index);
1080 let last = self.frame_captures.len().saturating_sub(1);
1081 self.selected_frame = Some(last);
1082 if self.pause_on_capture {
1083 self.paused = true;
1084 }
1085 self.push_frame(cap);
1086 }
1087
1088 pub fn frame_avg_ms(&self) -> f32 {
1089 self.frame_time_graph.avg()
1090 }
1091
1092 pub fn gpu_avg_ms(&self) -> f32 {
1093 self.gpu_time_graph.avg()
1094 }
1095
1096 pub fn avg_fps(&self) -> f32 {
1097 self.frame_time_graph.fps()
1098 }
1099
1100 pub fn memory_device_local_mb(&self) -> f32 {
1101 self.memory_stats.device_local_used as f32 / (1024.0 * 1024.0)
1102 }
1103
1104 pub fn memory_device_local_total_mb(&self) -> f32 {
1105 self.memory_stats.device_local_total as f32 / (1024.0 * 1024.0)
1106 }
1107
1108 pub fn vram_usage_pct(&self) -> f32 {
1109 self.memory_stats.device_local_pct()
1110 }
1111
1112 pub fn top_shaders_by_instructions(&self, n: usize) -> Vec<&ShaderCompileStat> {
1113 let mut shaders: Vec<&ShaderCompileStat> = self.shader_stats.iter().collect();
1114 shaders.sort_by(|a, b| b.instruction_count.cmp(&a.instruction_count));
1115 shaders.into_iter().take(n).collect()
1116 }
1117
1118 pub fn counters_by_status(&self, status: CounterStatus) -> Vec<&PerfCounter> {
1119 self.perf_counters.iter().filter(|c| c.status() == status).collect()
1120 }
1121
1122 pub fn has_critical_counters(&self) -> bool {
1123 self.perf_counters.iter().any(|c| c.status() == CounterStatus::Critical)
1124 }
1125
1126 pub fn has_warning_counters(&self) -> bool {
1127 self.perf_counters.iter().any(|c| c.status() == CounterStatus::Warning)
1128 }
1129
1130 pub fn simulate_tick(&mut self, dt: f32) {
1131 if self.paused { return; }
1132 let cap = FrameCapture::synthetic_frame(self.frame_index);
1133 self.push_frame(cap);
1134 }
1135
1136 pub fn pass_timeline_rects(&self, timeline_width: f32) -> Vec<(String, f32, f32, u32)> {
1137 let cap = match self.selected_frame_capture() {
1138 Some(c) => c,
1139 None => return Vec::new(),
1140 };
1141 let total_ns = (cap.end_ns - cap.begin_ns).max(1) as f32;
1142 cap.render_passes.iter().map(|p| {
1143 let x = (p.begin_ns - cap.begin_ns) as f32 / total_ns * timeline_width;
1144 let w = (p.end_ns - p.begin_ns) as f32 / total_ns * timeline_width;
1145 (p.name.clone(), x, w, p.color)
1146 }).collect()
1147 }
1148
1149 pub fn timeline_zoom_in(&mut self) {
1150 let center = (self.zoom_range.0 + self.zoom_range.1) * 0.5;
1151 let half = (self.zoom_range.1 - self.zoom_range.0) * 0.4;
1152 self.zoom_range = ((center - half).max(0.0), (center + half).min(33.333));
1153 }
1154
1155 pub fn timeline_zoom_out(&mut self) {
1156 let center = (self.zoom_range.0 + self.zoom_range.1) * 0.5;
1157 let half = (self.zoom_range.1 - self.zoom_range.0) * 0.625;
1158 self.zoom_range = ((center - half).max(0.0), (center + half).min(33.333));
1159 }
1160
1161 pub fn toggle_pass_expand(&mut self, name: &str) {
1162 if self.expanded_passes.contains(name) {
1163 self.expanded_passes.remove(name);
1164 } else {
1165 self.expanded_passes.insert(name.to_string());
1166 }
1167 }
1168
1169 pub fn filtered_passes(&self) -> Vec<&RenderPassCapture> {
1170 let cap = match self.selected_frame_capture() {
1171 Some(c) => c,
1172 None => return Vec::new(),
1173 };
1174 let q = self.search_filter.to_lowercase();
1175 if q.is_empty() {
1176 cap.render_passes.iter().collect()
1177 } else {
1178 cap.render_passes.iter().filter(|p| p.name.to_lowercase().contains(&q)).collect()
1179 }
1180 }
1181
1182 pub fn generate_summary_report(&self) -> String {
1183 let mut lines = Vec::new();
1184 lines.push("=== GPU Profiler Summary ===".to_string());
1185 lines.push(format!("Device: {}", self.gpu_caps.device_name));
1186 lines.push(format!("Average Frame Time: {:.2} ms ({:.1} fps)", self.frame_avg_ms(), self.avg_fps()));
1187 lines.push(format!("Average GPU Time: {:.2} ms", self.gpu_avg_ms()));
1188 lines.push(format!("VRAM Usage: {:.0} MB / {:.0} MB ({:.1}%)",
1189 self.memory_device_local_mb(),
1190 self.memory_device_local_total_mb(),
1191 self.vram_usage_pct() * 100.0
1192 ));
1193 if let Some(cap) = self.selected_frame_capture() {
1194 lines.push(format!("Selected Frame #{}", cap.frame_index));
1195 lines.push(format!(" Bottleneck: {}", cap.bottleneck()));
1196 lines.push(format!(" Draw Calls: {}", cap.draw_call_count));
1197 lines.push(format!(" Triangles: {}", cap.triangle_count));
1198 lines.push(format!(" Render Passes: {}", cap.render_passes.len()));
1199 for pass in cap.top_passes_by_duration(5) {
1200 lines.push(format!(" - {}: {:.2} ms", pass.name, pass.duration_ms()));
1201 }
1202 }
1203 if self.has_critical_counters() {
1204 lines.push("CRITICAL: Some performance counters are in critical state!".to_string());
1205 }
1206 lines.join("\n")
1207 }
1208}