use super::config::QualityConfig;
use super::level::QualityLevel;
use super::monitor::FrameTimeMonitor;
use crate::compat::Duration;
#[derive(Debug, Clone, Copy)]
pub struct GpuCapability {
pub supports_high_quality: bool,
pub is_integrated: bool,
pub performance_tier: u8,
}
impl Default for GpuCapability {
fn default() -> Self {
Self { supports_high_quality: true, is_integrated: false, performance_tier: 3 }
}
}
impl GpuCapability {
#[cfg(feature = "gpu-wgpu")]
pub fn from_adapter_info(adapter_info: &wgpu::AdapterInfo) -> Self {
let supports_high_quality = matches!(
adapter_info.device_type,
wgpu::DeviceType::DiscreteGpu | wgpu::DeviceType::IntegratedGpu
);
let is_integrated = matches!(adapter_info.device_type, wgpu::DeviceType::IntegratedGpu);
let performance_tier = match adapter_info.device_type {
wgpu::DeviceType::DiscreteGpu => 5,
wgpu::DeviceType::IntegratedGpu => 3,
wgpu::DeviceType::Other => 2,
wgpu::DeviceType::VirtualGpu => 2,
wgpu::DeviceType::Cpu => 1,
};
Self { supports_high_quality, is_integrated, performance_tier }
}
pub fn default_capability() -> Self {
Self::default()
}
pub fn recommended_initial_quality(&self) -> QualityLevel {
if self.supports_high_quality && self.performance_tier >= 4 {
QualityLevel::High
} else if self.supports_high_quality && self.performance_tier >= 2 {
QualityLevel::Medium
} else {
QualityLevel::Low
}
}
}
#[derive(Debug, Clone)]
pub struct QualityManager {
current_level: QualityLevel,
config: QualityConfig,
frame_monitor: FrameTimeMonitor,
gpu_capability: GpuCapability,
}
impl QualityManager {
pub fn new() -> Self {
Self::with_config_and_capability(QualityConfig::default(), GpuCapability::default())
}
pub fn with_config(config: QualityConfig) -> Self {
Self::with_config_and_capability(config, GpuCapability::default())
}
pub fn with_config_and_capability(
config: QualityConfig,
gpu_capability: GpuCapability,
) -> Self {
let config = config.normalized();
let initial_quality = gpu_capability
.recommended_initial_quality()
.clamp(config.min_quality, config.max_quality);
let frame_monitor = FrameTimeMonitor::with_capacity_for_counts(
config.target_frame_rate,
config.degrade_frame_count,
config.upgrade_frame_count,
);
Self { current_level: initial_quality, config, frame_monitor, gpu_capability }
}
pub fn finish_frame(&mut self, frame_duration: Duration) {
let frame_duration_secs = frame_duration.as_secs_f32();
self.finish_frame_secs(frame_duration_secs);
}
pub fn finish_frame_secs(&mut self, frame_duration: f32) {
self.frame_monitor.record_frame(frame_duration);
self.update_quality_level();
}
fn update_quality_level(&mut self) {
match self.current_level {
QualityLevel::High => {
if self.frame_monitor.should_degrade(
self.config.degrade_frame_duration(),
self.config.degrade_frame_count,
) {
if let Some(lower) = self.current_level.lower() {
if lower >= self.config.min_quality {
self.current_level = lower;
}
}
}
}
QualityLevel::Medium => {
if self.frame_monitor.should_degrade(
self.config.degrade_frame_duration(),
self.config.degrade_frame_count,
) {
if let Some(lower) = self.current_level.lower() {
if lower >= self.config.min_quality {
self.current_level = lower;
}
}
} else if self.frame_monitor.should_upgrade(
self.config.upgrade_frame_duration(),
self.config.upgrade_frame_count,
) {
if let Some(higher) = self.current_level.higher() {
if higher <= self.config.max_quality {
self.current_level = higher;
}
}
}
}
QualityLevel::Low => {
if self.frame_monitor.should_upgrade(
self.config.upgrade_frame_duration(),
self.config.upgrade_frame_count,
) {
if let Some(higher) = self.current_level.higher() {
if higher <= self.config.max_quality {
self.current_level = higher;
}
}
}
}
}
}
pub fn quality_level(&self) -> QualityLevel {
self.current_level
}
pub fn set_quality_level(&mut self, level: QualityLevel) {
self.current_level = level.clamp(self.config.min_quality, self.config.max_quality);
}
pub fn config(&self) -> &QualityConfig {
&self.config
}
pub fn set_config(&mut self, config: QualityConfig) {
self.config = config.normalized();
self.frame_monitor.set_target_frame_rate(self.config.target_frame_rate);
self.current_level =
self.current_level.clamp(self.config.min_quality, self.config.max_quality);
}
pub fn gpu_capability(&self) -> &GpuCapability {
&self.gpu_capability
}
pub fn frame_monitor(&self) -> &FrameTimeMonitor {
&self.frame_monitor
}
pub fn current_fps(&self) -> f32 {
self.frame_monitor.current_fps()
}
pub fn average_frame_time(&self) -> f32 {
self.frame_monitor.average_frame_time()
}
pub fn reset(&mut self) {
self.frame_monitor.reset();
self.current_level = self
.gpu_capability
.recommended_initial_quality()
.clamp(self.config.min_quality, self.config.max_quality);
}
}
crate::impl_default_via_new!(QualityManager);
#[cfg(test)]
mod tests {
use super::*;
fn config_with_range(min: QualityLevel, max: QualityLevel) -> QualityConfig {
QualityConfig {
min_quality: min,
max_quality: max,
degrade_frame_count: 5,
upgrade_frame_count: 10,
..QualityConfig::default()
}
}
#[test]
fn shrinking_the_range_makes_the_level_immediately_compliant() {
let mut manager = QualityManager::new();
manager.set_quality_level(QualityLevel::High);
assert_eq!(manager.quality_level(), QualityLevel::High);
manager.set_config(config_with_range(QualityLevel::Low, QualityLevel::Low));
assert_eq!(
manager.quality_level(),
QualityLevel::Low,
"the level must be reclamped the moment the range shrinks"
);
for _ in 0..100 {
manager.finish_frame_secs(0.016);
}
assert_eq!(manager.quality_level(), QualityLevel::Low);
}
#[test]
fn widening_the_range_preserves_the_current_choice() {
let mut manager = QualityManager::with_config(config_with_range(
QualityLevel::Medium,
QualityLevel::Medium,
));
assert_eq!(manager.quality_level(), QualityLevel::Medium);
manager.set_config(config_with_range(QualityLevel::Low, QualityLevel::High));
assert_eq!(
manager.quality_level(),
QualityLevel::Medium,
"a widened range must not reset the user's choice"
);
}
#[test]
fn a_config_with_a_long_window_can_actually_degrade() {
let config = QualityConfig {
degrade_frame_count: 90,
upgrade_frame_count: 90,
..QualityConfig::default()
};
let mut manager = QualityManager::with_config(config);
manager.set_quality_level(QualityLevel::High);
for _ in 0..89 {
manager.finish_frame_secs(0.05);
}
assert_eq!(
manager.quality_level(),
QualityLevel::High,
"89 of the required 90 slow frames is not yet enough"
);
manager.finish_frame_secs(0.05);
assert!(
manager.quality_level() < QualityLevel::High,
"the 90th slow frame must complete the window and degrade"
);
}
}