1use super::backend::BackendCapabilities;
5use super::renderer::MultiBackendRenderer;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub enum QualityLevel {
14 Potato = 0,
15 Low = 1,
16 Medium = 2,
17 High = 3,
18 Ultra = 4,
19}
20
21impl QualityLevel {
22 pub const ALL: [QualityLevel; 5] = [
24 Self::Potato, Self::Low, Self::Medium, Self::High, Self::Ultra,
25 ];
26
27 pub fn upgrade(self) -> Option<QualityLevel> {
29 match self {
30 Self::Potato => Some(Self::Low),
31 Self::Low => Some(Self::Medium),
32 Self::Medium => Some(Self::High),
33 Self::High => Some(Self::Ultra),
34 Self::Ultra => None,
35 }
36 }
37
38 pub fn downgrade(self) -> Option<QualityLevel> {
40 match self {
41 Self::Potato => None,
42 Self::Low => Some(Self::Potato),
43 Self::Medium => Some(Self::Low),
44 Self::High => Some(Self::Medium),
45 Self::Ultra => Some(Self::High),
46 }
47 }
48
49 pub fn index(self) -> usize { self as usize }
51}
52
53impl std::fmt::Display for QualityLevel {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 match self {
56 Self::Potato => write!(f, "Potato"),
57 Self::Low => write!(f, "Low"),
58 Self::Medium => write!(f, "Medium"),
59 Self::High => write!(f, "High"),
60 Self::Ultra => write!(f, "Ultra"),
61 }
62 }
63}
64
65#[derive(Debug, Clone)]
71pub struct QualityProfile {
72 pub particle_count: u32,
73 pub bloom_passes: u32,
74 pub shadow_resolution: u32,
75 pub postfx_enabled: bool,
76 pub compute_enabled: bool,
77 pub msaa: u32,
78}
79
80impl QualityProfile {
81 pub fn for_level(level: QualityLevel) -> Self {
83 match level {
84 QualityLevel::Potato => Self {
85 particle_count: 1_000,
86 bloom_passes: 0,
87 shadow_resolution: 256,
88 postfx_enabled: false,
89 compute_enabled: false,
90 msaa: 1,
91 },
92 QualityLevel::Low => Self {
93 particle_count: 5_000,
94 bloom_passes: 1,
95 shadow_resolution: 512,
96 postfx_enabled: false,
97 compute_enabled: false,
98 msaa: 1,
99 },
100 QualityLevel::Medium => Self {
101 particle_count: 20_000,
102 bloom_passes: 2,
103 shadow_resolution: 1024,
104 postfx_enabled: true,
105 compute_enabled: true,
106 msaa: 2,
107 },
108 QualityLevel::High => Self {
109 particle_count: 100_000,
110 bloom_passes: 3,
111 shadow_resolution: 2048,
112 postfx_enabled: true,
113 compute_enabled: true,
114 msaa: 4,
115 },
116 QualityLevel::Ultra => Self {
117 particle_count: 500_000,
118 bloom_passes: 4,
119 shadow_resolution: 4096,
120 postfx_enabled: true,
121 compute_enabled: true,
122 msaa: 8,
123 },
124 }
125 }
126
127 pub fn estimated_vram(&self) -> u64 {
129 let shadow = (self.shadow_resolution as u64) * (self.shadow_resolution as u64) * 4;
130 let particles = (self.particle_count as u64) * 64; let msaa_factor = self.msaa as u64;
132 shadow * msaa_factor + particles
133 }
134}
135
136pub fn auto_detect_quality(capabilities: &BackendCapabilities) -> QualityLevel {
142 let mut score = 0u32;
143
144 if capabilities.max_texture_size >= 16384 { score += 3; }
146 else if capabilities.max_texture_size >= 8192 { score += 2; }
147 else if capabilities.max_texture_size >= 4096 { score += 1; }
148
149 if capabilities.compute_shaders { score += 2; }
151
152 if capabilities.max_ssbo_size >= 1024 * 1024 * 1024 { score += 2; }
154 else if capabilities.max_ssbo_size >= 256 * 1024 * 1024 { score += 1; }
155
156 if capabilities.indirect_draw { score += 1; }
158 if capabilities.multi_draw_indirect { score += 1; }
159
160 if capabilities.max_workgroup_size[0] >= 1024 { score += 1; }
162
163 match score {
164 0..=2 => QualityLevel::Potato,
165 3..=4 => QualityLevel::Low,
166 5..=7 => QualityLevel::Medium,
167 8..=9 => QualityLevel::High,
168 _ => QualityLevel::Ultra,
169 }
170}
171
172pub struct QualityManager {
178 pub current: QualityLevel,
179 pub target_fps: f32,
180 fps_history: Vec<f32>,
181 max_history: usize,
182 upgrade_cooldown: f32,
183 downgrade_cooldown: f32,
184 time_since_change: f32,
185 pub cooldown_seconds: f32,
187 pub upgrade_headroom: f32,
189 pub downgrade_threshold: f32,
191}
192
193impl QualityManager {
194 pub fn new(initial: QualityLevel, target_fps: f32) -> Self {
195 Self {
196 current: initial,
197 target_fps,
198 fps_history: Vec::with_capacity(60),
199 max_history: 60,
200 upgrade_cooldown: 0.0,
201 downgrade_cooldown: 0.0,
202 time_since_change: 0.0,
203 cooldown_seconds: 3.0,
204 upgrade_headroom: 1.15, downgrade_threshold: 0.85, }
207 }
208
209 pub fn tick(&mut self, current_fps: f32, dt: f32) {
211 if self.fps_history.len() >= self.max_history {
212 self.fps_history.remove(0);
213 }
214 self.fps_history.push(current_fps);
215 self.time_since_change += dt;
216
217 if self.time_since_change < self.cooldown_seconds {
218 return;
219 }
220
221 let avg = self.average_fps();
222
223 if self.should_downgrade_at(avg) {
224 if let Some(lower) = self.current.downgrade() {
225 self.current = lower;
226 self.time_since_change = 0.0;
227 self.fps_history.clear();
228 }
229 } else if self.should_upgrade_at(avg) {
230 if let Some(higher) = self.current.upgrade() {
231 self.current = higher;
232 self.time_since_change = 0.0;
233 self.fps_history.clear();
234 }
235 }
236 }
237
238 pub fn average_fps(&self) -> f32 {
240 if self.fps_history.is_empty() { return 0.0; }
241 let sum: f32 = self.fps_history.iter().sum();
242 sum / self.fps_history.len() as f32
243 }
244
245 pub fn should_upgrade(&self) -> bool {
247 self.should_upgrade_at(self.average_fps())
248 }
249
250 pub fn should_downgrade(&self) -> bool {
252 self.should_downgrade_at(self.average_fps())
253 }
254
255 fn should_upgrade_at(&self, avg_fps: f32) -> bool {
256 avg_fps > self.target_fps * self.upgrade_headroom
257 && self.current.upgrade().is_some()
258 }
259
260 fn should_downgrade_at(&self, avg_fps: f32) -> bool {
261 avg_fps < self.target_fps * self.downgrade_threshold
262 && self.current.downgrade().is_some()
263 }
264
265 pub fn profile(&self) -> QualityProfile {
267 QualityProfile::for_level(self.current)
268 }
269
270 pub fn set_quality(&mut self, level: QualityLevel) {
272 self.current = level;
273 self.time_since_change = 0.0;
274 self.fps_history.clear();
275 }
276}
277
278#[derive(Debug, Clone)]
284pub struct BenchmarkResult {
285 pub fps: f32,
286 pub gpu_ms: f32,
287 pub cpu_ms: f32,
288 pub vram_used: u64,
289}
290
291impl BenchmarkResult {
292 pub fn score(&self) -> f32 {
293 if self.gpu_ms > 0.0 {
295 self.fps * (16.67 / self.gpu_ms)
296 } else {
297 self.fps
298 }
299 }
300}
301
302pub fn run_benchmark(renderer: &mut MultiBackendRenderer, duration_secs: f32) -> BenchmarkResult {
306 use std::time::Instant;
307 use super::backend::{BufferUsage, PipelineLayout, ShaderStage};
308 use super::renderer::{DrawCall, RenderPass};
309
310 let start = Instant::now();
311 let mut frames = 0u32;
312 let target_duration = std::time::Duration::from_secs_f32(duration_secs);
313
314 let vbuf = renderer.create_vertex_buffer(&[0u8; 48]);
316 let vs = renderer.backend.create_shader("v", ShaderStage::Vertex);
317 let fs = renderer.backend.create_shader("f", ShaderStage::Fragment);
318 let pipe = renderer.backend.create_pipeline(vs, fs, &PipelineLayout::default());
319 let pass = RenderPass::new();
320 let call = DrawCall::new(pipe, vbuf, 3);
321
322 while start.elapsed() < target_duration {
323 renderer.begin_frame();
324 renderer.draw(&pass, &[call.clone()]);
325 renderer.end_frame();
326 frames += 1;
327 }
328
329 let elapsed = start.elapsed().as_secs_f32();
330 let fps = frames as f32 / elapsed;
331 let frame_ms = elapsed * 1000.0 / frames.max(1) as f32;
332
333 renderer.destroy_buffer(vbuf);
334
335 BenchmarkResult {
336 fps,
337 gpu_ms: frame_ms, cpu_ms: frame_ms,
339 vram_used: 0,
340 }
341}
342
343#[cfg(test)]
348mod tests {
349 use super::*;
350 use crate::wgpu_backend::backend::{BackendCapabilities, GpuBackend};
351
352 #[test]
353 fn quality_level_ordering() {
354 assert!(QualityLevel::Potato < QualityLevel::Low);
355 assert!(QualityLevel::Low < QualityLevel::Medium);
356 assert!(QualityLevel::Medium < QualityLevel::High);
357 assert!(QualityLevel::High < QualityLevel::Ultra);
358 }
359
360 #[test]
361 fn quality_level_upgrade_downgrade() {
362 assert_eq!(QualityLevel::Potato.upgrade(), Some(QualityLevel::Low));
363 assert_eq!(QualityLevel::Ultra.upgrade(), None);
364 assert_eq!(QualityLevel::Ultra.downgrade(), Some(QualityLevel::High));
365 assert_eq!(QualityLevel::Potato.downgrade(), None);
366 }
367
368 #[test]
369 fn quality_level_display() {
370 assert_eq!(format!("{}", QualityLevel::Medium), "Medium");
371 assert_eq!(format!("{}", QualityLevel::Potato), "Potato");
372 }
373
374 #[test]
375 fn quality_level_index() {
376 assert_eq!(QualityLevel::Potato.index(), 0);
377 assert_eq!(QualityLevel::Ultra.index(), 4);
378 }
379
380 #[test]
381 fn quality_profile_for_each_level() {
382 for level in QualityLevel::ALL {
383 let profile = QualityProfile::for_level(level);
384 assert!(profile.particle_count > 0);
385 assert!(profile.msaa >= 1);
386 assert!(profile.shadow_resolution >= 256);
387 }
388 }
389
390 #[test]
391 fn quality_profiles_scale_up() {
392 let potato = QualityProfile::for_level(QualityLevel::Potato);
393 let ultra = QualityProfile::for_level(QualityLevel::Ultra);
394 assert!(ultra.particle_count > potato.particle_count);
395 assert!(ultra.shadow_resolution > potato.shadow_resolution);
396 assert!(ultra.msaa > potato.msaa);
397 }
398
399 #[test]
400 fn estimated_vram_increases_with_quality() {
401 let low = QualityProfile::for_level(QualityLevel::Low);
402 let high = QualityProfile::for_level(QualityLevel::High);
403 assert!(high.estimated_vram() > low.estimated_vram());
404 }
405
406 #[test]
407 fn auto_detect_vulkan() {
408 let caps = BackendCapabilities::for_backend(GpuBackend::Vulkan);
409 let level = auto_detect_quality(&caps);
410 assert!(level >= QualityLevel::High);
411 }
412
413 #[test]
414 fn auto_detect_software() {
415 let caps = BackendCapabilities::for_backend(GpuBackend::Software);
416 let level = auto_detect_quality(&caps);
417 assert!(level <= QualityLevel::Medium);
419 }
420
421 #[test]
422 fn auto_detect_minimal_caps() {
423 let caps = BackendCapabilities {
424 compute_shaders: false,
425 max_texture_size: 1024,
426 max_ssbo_size: 0,
427 max_workgroup_size: [64, 64, 1],
428 indirect_draw: false,
429 multi_draw_indirect: false,
430 };
431 assert_eq!(auto_detect_quality(&caps), QualityLevel::Potato);
432 }
433
434 #[test]
435 fn quality_manager_downgrade_on_low_fps() {
436 let mut mgr = QualityManager::new(QualityLevel::High, 60.0);
437 mgr.cooldown_seconds = 0.0; for _ in 0..10 {
441 mgr.tick(30.0, 0.016);
442 }
443 assert!(mgr.current < QualityLevel::High);
445 }
446
447 #[test]
448 fn quality_manager_upgrade_on_high_fps() {
449 let mut mgr = QualityManager::new(QualityLevel::Low, 60.0);
450 mgr.cooldown_seconds = 0.0;
451
452 for _ in 0..10 {
454 mgr.tick(120.0, 0.008);
455 }
456 assert!(mgr.current > QualityLevel::Low);
457 }
458
459 #[test]
460 fn quality_manager_stays_stable() {
461 let mut mgr = QualityManager::new(QualityLevel::Medium, 60.0);
462 mgr.cooldown_seconds = 0.0;
463
464 for _ in 0..20 {
466 mgr.tick(60.0, 0.016);
467 }
468 assert_eq!(mgr.current, QualityLevel::Medium);
469 }
470
471 #[test]
472 fn quality_manager_cooldown() {
473 let mut mgr = QualityManager::new(QualityLevel::High, 60.0);
474 mgr.cooldown_seconds = 5.0;
475
476 mgr.tick(10.0, 1.0);
478 assert_eq!(mgr.current, QualityLevel::High);
479 }
480
481 #[test]
482 fn quality_manager_should_upgrade_downgrade() {
483 let mut mgr = QualityManager::new(QualityLevel::Medium, 60.0);
484 mgr.cooldown_seconds = 0.0;
485 for _ in 0..5 { mgr.tick(120.0, 0.008); }
486 assert!(mgr.should_upgrade());
487 assert!(!mgr.should_downgrade());
488 }
489
490 #[test]
491 fn quality_manager_average_fps() {
492 let mut mgr = QualityManager::new(QualityLevel::Medium, 60.0);
493 mgr.tick(50.0, 0.016);
494 mgr.tick(70.0, 0.016);
495 let avg = mgr.average_fps();
496 assert!((avg - 60.0).abs() < 0.01);
497 }
498
499 #[test]
500 fn quality_manager_set_quality() {
501 let mut mgr = QualityManager::new(QualityLevel::Low, 60.0);
502 mgr.set_quality(QualityLevel::Ultra);
503 assert_eq!(mgr.current, QualityLevel::Ultra);
504 }
505
506 #[test]
507 fn quality_manager_profile() {
508 let mgr = QualityManager::new(QualityLevel::High, 60.0);
509 let profile = mgr.profile();
510 assert_eq!(profile.msaa, 4);
511 }
512
513 #[test]
514 fn benchmark_result_score() {
515 let result = BenchmarkResult {
516 fps: 60.0,
517 gpu_ms: 16.67,
518 cpu_ms: 16.67,
519 vram_used: 0,
520 };
521 let score = result.score();
522 assert!((score - 60.0).abs() < 0.1);
523 }
524
525 #[test]
526 fn run_benchmark_returns_result() {
527 let mut renderer = MultiBackendRenderer::software();
528 let result = run_benchmark(&mut renderer, 0.05);
529 assert!(result.fps > 0.0);
530 assert!(result.gpu_ms > 0.0);
531 }
532
533 #[test]
534 fn quality_manager_cannot_go_below_potato() {
535 let mut mgr = QualityManager::new(QualityLevel::Potato, 60.0);
536 mgr.cooldown_seconds = 0.0;
537 for _ in 0..20 {
538 mgr.tick(5.0, 0.2);
539 }
540 assert_eq!(mgr.current, QualityLevel::Potato);
541 }
542
543 #[test]
544 fn quality_manager_cannot_go_above_ultra() {
545 let mut mgr = QualityManager::new(QualityLevel::Ultra, 60.0);
546 mgr.cooldown_seconds = 0.0;
547 for _ in 0..20 {
548 mgr.tick(300.0, 0.003);
549 }
550 assert_eq!(mgr.current, QualityLevel::Ultra);
551 }
552}