use std::collections::HashSet;
use crate::backend::RenderBackend;
use crate::detect::default_perf;
#[cfg(not(target_arch = "wasm32"))]
use crate::detect::detect_backend;
use crate::metrics::RenderMetrics;
#[derive(Debug, thiserror::Error)]
pub enum HubError {
#[error("backend {0:?} not in pool")]
NotAvailable(RenderBackend),
#[error("no wgpu adapter found")]
NoAdapter,
}
#[derive(Debug, Clone)]
pub struct BackendPool {
pub has_gpu: bool,
pub initialized: HashSet<RenderBackend>,
pub recommended: RenderBackend,
}
impl BackendPool {
#[cfg(not(target_arch = "wasm32"))]
fn from_gpu(recommended: RenderBackend) -> Self {
let mut initialized = HashSet::new();
initialized.insert(RenderBackend::VelloGpu);
initialized.insert(RenderBackend::VelloHybrid);
initialized.insert(RenderBackend::InstancedWgpu);
initialized.insert(RenderBackend::VelloCpu);
initialized.insert(RenderBackend::TinySkia);
Self { has_gpu: true, initialized, recommended }
}
#[cfg(not(target_arch = "wasm32"))]
fn software_only() -> Self {
let mut initialized = HashSet::new();
initialized.insert(RenderBackend::VelloCpu);
initialized.insert(RenderBackend::TinySkia);
Self {
has_gpu: false,
initialized,
recommended: RenderBackend::TinySkia,
}
}
fn single(backend: RenderBackend) -> Self {
let mut initialized = HashSet::new();
initialized.insert(backend);
Self {
has_gpu: backend.is_gpu_swapchain(),
initialized,
recommended: backend,
}
}
}
#[derive(Debug, Clone)]
pub struct PerfSettings {
pub fps_limit: u32,
pub msaa_samples: u8,
pub vsync: bool,
pub perf_log: bool,
pub recalc_mode: String,
}
impl Default for PerfSettings {
fn default() -> Self {
Self {
fps_limit: 60,
msaa_samples: 8,
vsync: true,
perf_log: false,
recalc_mode: "on_change".into(),
}
}
}
pub struct RenderHub {
pool: BackendPool,
active: RenderBackend,
settings: PerfSettings,
metrics: RenderMetrics,
}
impl RenderHub {
pub fn autodetect() -> Self {
#[cfg(target_arch = "wasm32")]
{
return Self::fixed(RenderBackend::Canvas2d);
}
#[cfg(not(target_arch = "wasm32"))]
{
let (pool, recommended) = match probe_adapter() {
Some(info) => {
let rec = detect_backend(&info);
(BackendPool::from_gpu(rec), rec)
}
None => {
let rec = RenderBackend::TinySkia;
(BackendPool::software_only(), rec)
}
};
let perf = default_perf(recommended);
let settings = PerfSettings {
fps_limit: perf.fps_limit,
msaa_samples: perf.msaa_samples,
..PerfSettings::default()
};
Self {
active: pool.recommended,
pool,
settings,
metrics: RenderMetrics::default(),
}
}
}
pub fn fixed(backend: RenderBackend) -> Self {
let pool = BackendPool::single(backend);
let perf = default_perf(backend);
let settings = PerfSettings {
fps_limit: perf.fps_limit,
msaa_samples: perf.msaa_samples,
..PerfSettings::default()
};
Self {
active: backend,
pool,
settings,
metrics: RenderMetrics::default(),
}
}
pub fn pool(&self) -> &BackendPool {
&self.pool
}
pub fn active(&self) -> RenderBackend {
self.active
}
pub fn is_available(&self, backend: RenderBackend) -> bool {
self.pool.initialized.contains(&backend)
}
pub fn available_backends(&self) -> Vec<RenderBackend> {
let mut v: Vec<RenderBackend> = self.pool.initialized.iter().copied().collect();
v.sort_by_key(|b| b.as_str());
v
}
pub fn set_active(&mut self, backend: RenderBackend) -> Result<(), HubError> {
if !self.pool.initialized.contains(&backend) {
return Err(HubError::NotAvailable(backend));
}
self.active = backend;
Ok(())
}
pub fn settings(&self) -> &PerfSettings {
&self.settings
}
pub fn settings_mut(&mut self) -> &mut PerfSettings {
&mut self.settings
}
pub fn set_fps_limit(&mut self, fps: u32) {
self.settings.fps_limit = fps;
}
pub fn set_msaa(&mut self, samples: u8) {
self.settings.msaa_samples = samples;
}
pub fn set_vsync(&mut self, on: bool) {
self.settings.vsync = on;
}
pub fn metrics(&self) -> &RenderMetrics {
&self.metrics
}
pub fn update_metrics(&mut self, m: RenderMetrics) {
self.metrics = m;
}
#[cfg(not(target_arch = "wasm32"))]
pub fn factory_for(&self, backend: RenderBackend) -> Option<Box<dyn crate::surface::RenderSurfaceFactory>> {
use crate::factories::{
VelloGpuSurfaceFactory, VelloHybridSurfaceFactory,
WgpuInstancedSurfaceFactory, TinySkiaSurfaceFactory, VelloCpuSurfaceFactory,
};
if !self.pool.initialized.contains(&backend) {
return None;
}
let factory: Box<dyn crate::surface::RenderSurfaceFactory> = match backend {
RenderBackend::VelloGpu => Box::new(VelloGpuSurfaceFactory::new()),
RenderBackend::VelloHybrid => Box::new(VelloHybridSurfaceFactory::new(1.0)),
RenderBackend::InstancedWgpu => Box::new(WgpuInstancedSurfaceFactory::new()),
RenderBackend::TinySkia => Box::new(TinySkiaSurfaceFactory::new()),
RenderBackend::VelloCpu => Box::new(VelloCpuSurfaceFactory::new(1.0)),
_ => return None,
};
Some(factory)
}
#[cfg(target_arch = "wasm32")]
pub fn factory_for(&self, _backend: RenderBackend) -> Option<Box<dyn crate::surface::RenderSurfaceFactory>> {
None
}
}
#[cfg(not(target_arch = "wasm32"))]
fn probe_adapter() -> Option<wgpu::AdapterInfo> {
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: None,
force_fallback_adapter: false,
})).ok()?;
Some(adapter.get_info())
}
#[cfg(test)]
#[cfg(not(target_arch = "wasm32"))]
mod tests {
use super::*;
#[test]
fn autodetect_returns_nonempty_pool() {
let hub = RenderHub::autodetect();
assert!(!hub.pool().initialized.is_empty(), "pool must have at least one backend");
}
#[test]
fn fixed_pool_has_exactly_one_backend() {
let hub = RenderHub::fixed(RenderBackend::VelloGpu);
assert_eq!(hub.pool().initialized.len(), 1);
assert!(hub.is_available(RenderBackend::VelloGpu));
assert!(!hub.is_available(RenderBackend::TinySkia));
}
#[test]
fn set_active_rejects_non_pooled() {
let mut hub = RenderHub::fixed(RenderBackend::VelloGpu);
let result = hub.set_active(RenderBackend::TinySkia);
assert!(matches!(result, Err(HubError::NotAvailable(RenderBackend::TinySkia))));
}
#[test]
fn set_active_accepts_pooled() {
let mut hub = RenderHub::autodetect();
assert!(hub.set_active(RenderBackend::TinySkia).is_ok());
assert_eq!(hub.active(), RenderBackend::TinySkia);
}
#[test]
fn perf_settings_round_trip() {
let mut hub = RenderHub::autodetect();
hub.set_fps_limit(144);
hub.set_msaa(16);
hub.set_vsync(false);
assert_eq!(hub.settings().fps_limit, 144);
assert_eq!(hub.settings().msaa_samples, 16);
assert!(!hub.settings().vsync);
}
#[test]
fn perf_settings_mut() {
let mut hub = RenderHub::autodetect();
hub.settings_mut().recalc_mode = "always".into();
assert_eq!(hub.settings().recalc_mode, "always");
}
}