Skip to main content

proof_engine/editor/
gpu_profiler.rs

1
2//! GPU profiler — timestamp queries, pipeline statistics, memory tracking,
3//! frame graph visualization, shader hot-spot analysis, and heat-map overlays.
4
5use glam::Vec2;
6use std::collections::{HashMap, VecDeque};
7
8// ---------------------------------------------------------------------------
9// GPU capability flags
10// ---------------------------------------------------------------------------
11
12#[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// ---------------------------------------------------------------------------
66// GPU memory categories
67// ---------------------------------------------------------------------------
68
69#[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// ---------------------------------------------------------------------------
159// Pipeline statistics
160// ---------------------------------------------------------------------------
161
162#[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// ---------------------------------------------------------------------------
192// Timestamp queries
193// ---------------------------------------------------------------------------
194
195#[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// ---------------------------------------------------------------------------
278// Frame capture
279// ---------------------------------------------------------------------------
280
281#[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// ---------------------------------------------------------------------------
522// Shader compilation stats
523// ---------------------------------------------------------------------------
524
525#[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// ---------------------------------------------------------------------------
594// Performance counter
595// ---------------------------------------------------------------------------
596
597#[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// ---------------------------------------------------------------------------
629// Heatmap overlay data
630// ---------------------------------------------------------------------------
631
632#[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// ---------------------------------------------------------------------------
673// Frame time graph
674// ---------------------------------------------------------------------------
675
676#[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    /// Normalize samples to [0, 1] range for graph display.
723    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// ---------------------------------------------------------------------------
729// Barrier and synchronization analysis
730// ---------------------------------------------------------------------------
731
732#[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// ---------------------------------------------------------------------------
751// Resource state tracker
752// ---------------------------------------------------------------------------
753
754#[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// ---------------------------------------------------------------------------
821// GPU profiler state
822// ---------------------------------------------------------------------------
823
824#[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}