use alpha2_model::ProjectSettings;
use alpha2_render::RenderSize;
use crate::EngineError;
const _: () = assert!(
alpha2_model::MAX_CANVAS_DIMENSION == RenderSize::MAX_DIMENSION
&& alpha2_model::MAX_CANVAS_PIXELS == RenderSize::MAX_PIXELS
);
#[derive(Copy, Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct QualityProfile {
pub resolution_scale: f32,
pub max_filter_samples: u32,
pub motion_blur_subframes: u32,
pub texture_budget_bytes: u64,
}
impl Default for QualityProfile {
fn default() -> Self {
Self::export()
}
}
impl QualityProfile {
pub const MAX_RESOLUTION_SCALE: f32 = 8.0;
pub const MAX_FILTER_SAMPLES: u32 = alpha2_render::RenderQualityControls::MAX_FILTER_SAMPLES;
pub const MAX_MOTION_BLUR_SUBFRAMES: u32 = 256;
pub const MIN_TEXTURE_BUDGET_BYTES: u64 = 1024 * 1024;
pub const MAX_TEXTURE_BUDGET_BYTES: u64 = 64 * 1024 * 1024 * 1024;
#[must_use]
pub const fn export() -> Self {
Self {
resolution_scale: 1.0,
max_filter_samples: 64,
motion_blur_subframes: 1,
texture_budget_bytes: 512 * 1024 * 1024,
}
}
#[must_use]
pub const fn preview() -> Self {
Self {
resolution_scale: 0.5,
max_filter_samples: 32,
motion_blur_subframes: 1,
texture_budget_bytes: 256 * 1024 * 1024,
}
}
#[must_use]
pub const fn with_resolution_scale(mut self, scale: f32) -> Self {
self.resolution_scale = scale;
self
}
pub fn validate(self) -> Result<(), EngineError> {
if self.resolution_scale.is_finite()
&& self.resolution_scale > 0.0
&& self.resolution_scale <= Self::MAX_RESOLUTION_SCALE
&& (1..=Self::MAX_FILTER_SAMPLES).contains(&self.max_filter_samples)
&& (1..=Self::MAX_MOTION_BLUR_SUBFRAMES).contains(&self.motion_blur_subframes)
&& (Self::MIN_TEXTURE_BUDGET_BYTES..=Self::MAX_TEXTURE_BUDGET_BYTES)
.contains(&self.texture_budget_bytes)
{
Ok(())
} else {
Err(EngineError::InvalidQualityProfile)
}
}
pub fn render_size(self, settings: &ProjectSettings) -> Result<RenderSize, EngineError> {
self.validate()?;
let width = scaled_dimension(settings.width, self.resolution_scale)?;
let height = scaled_dimension(settings.height, self.resolution_scale)?;
RenderSize::new(width, height).map_err(EngineError::from)
}
pub fn export_size(self, settings: &ProjectSettings) -> Result<RenderSize, EngineError> {
let size = self.render_size(settings)?;
if size.width % 2 != 0 || size.height % 2 != 0 {
let even = |value: u32| match value {
1 => 2,
odd if odd % 2 != 0 => odd - 1,
even => even,
};
return Err(EngineError::OddExportDimensions {
width: size.width,
height: size.height,
suggested: (even(size.width), even(size.height)),
});
}
Ok(size)
}
pub fn benchmark_metadata(
self,
project: impl Into<String>,
settings: &ProjectSettings,
renderer: &alpha2_render::Renderer,
) -> Result<alpha2_render::BenchmarkMetadata, EngineError> {
let size = self.render_size(settings)?;
Ok(alpha2_render::BenchmarkMetadata {
project: project.into(),
alpha2_version: env!("CARGO_PKG_VERSION"),
adapter: renderer.diagnostics().adapter,
dimensions: (size.width, size.height),
resolution_scale: self.resolution_scale,
max_filter_samples: self.max_filter_samples,
motion_blur_subframes: self.motion_blur_subframes,
texture_budget_bytes: self.texture_budget_bytes,
})
}
}
fn scaled_dimension(value: u32, scale: f32) -> Result<u32, EngineError> {
let scaled = f64::from(value) * f64::from(scale);
if scaled.is_finite() && scaled <= f64::from(u32::MAX) {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
Ok((scaled.round() as u32).max(1))
} else {
Err(EngineError::OutOfRange)
}
}
#[cfg(test)]
mod tests {
use alpha2_time::FrameRate;
use super::*;
#[test]
fn quality_scales_dimensions_and_rejects_zero() {
let settings = ProjectSettings::new(
1920,
1080,
FrameRate::new(30, 1).expect("valid rate"),
48_000,
)
.expect("valid settings");
assert_eq!(
QualityProfile::preview()
.render_size(&settings)
.expect("valid size"),
RenderSize::new(960, 540).expect("valid size")
);
assert_eq!(
QualityProfile {
resolution_scale: 0.0,
..QualityProfile::default()
}
.validate(),
Err(EngineError::InvalidQualityProfile)
);
assert_eq!(
QualityProfile {
motion_blur_subframes: u32::MAX,
..QualityProfile::default()
}
.validate(),
Err(EngineError::InvalidQualityProfile)
);
assert_eq!(
QualityProfile {
texture_budget_bytes: 1,
..QualityProfile::default()
}
.validate(),
Err(EngineError::InvalidQualityProfile)
);
}
#[test]
fn a_preview_scale_sizes_the_render_and_is_validated() {
let settings = |width, height| {
ProjectSettings::new(
width,
height,
FrameRate::new(30, 1).expect("valid rate"),
48_000,
)
.expect("valid settings")
};
let size = |scale: f32, width, height| {
QualityProfile::preview()
.with_resolution_scale(scale)
.render_size(&settings(width, height))
.map(|size| (size.width, size.height))
};
assert_eq!(size(0.5, 1280, 720), Ok((640, 360)));
assert_eq!(size(1.0, 1280, 720), Ok((1280, 720)));
assert_eq!(size(0.5, 1281, 721), Ok((641, 361)));
assert_eq!(size(0.25, 1282, 722), Ok((321, 181)));
for scale in [0.0, -1.0, 8.5, f32::NAN, f32::INFINITY] {
assert_eq!(
QualityProfile::preview()
.with_resolution_scale(scale)
.validate(),
Err(EngineError::InvalidQualityProfile),
"{scale}"
);
}
assert_eq!(
QualityProfile::preview()
.with_resolution_scale(QualityProfile::MAX_RESOLUTION_SCALE)
.validate(),
Ok(())
);
}
}