1use alpha2_model::ProjectSettings;
2use alpha2_render::RenderSize;
3
4use crate::EngineError;
5
6const _: () = assert!(
9 alpha2_model::MAX_CANVAS_DIMENSION == RenderSize::MAX_DIMENSION
10 && alpha2_model::MAX_CANVAS_PIXELS == RenderSize::MAX_PIXELS
11);
12
13#[derive(Copy, Clone, Debug, PartialEq)]
15#[non_exhaustive]
16pub struct QualityProfile {
17 pub resolution_scale: f32,
19 pub max_filter_samples: u32,
21 pub motion_blur_subframes: u32,
23 pub texture_budget_bytes: u64,
25}
26
27impl Default for QualityProfile {
28 fn default() -> Self {
29 Self::export()
30 }
31}
32
33impl QualityProfile {
34 pub const MAX_RESOLUTION_SCALE: f32 = 8.0;
36 pub const MAX_FILTER_SAMPLES: u32 = alpha2_render::RenderQualityControls::MAX_FILTER_SAMPLES;
40 pub const MAX_MOTION_BLUR_SUBFRAMES: u32 = 256;
42 pub const MIN_TEXTURE_BUDGET_BYTES: u64 = 1024 * 1024;
44 pub const MAX_TEXTURE_BUDGET_BYTES: u64 = 64 * 1024 * 1024 * 1024;
46
47 #[must_use]
49 pub const fn export() -> Self {
50 Self {
51 resolution_scale: 1.0,
52 max_filter_samples: 64,
53 motion_blur_subframes: 1,
54 texture_budget_bytes: 512 * 1024 * 1024,
55 }
56 }
57
58 #[must_use]
60 pub const fn preview() -> Self {
61 Self {
62 resolution_scale: 0.5,
63 max_filter_samples: 32,
64 motion_blur_subframes: 1,
65 texture_budget_bytes: 256 * 1024 * 1024,
66 }
67 }
68
69 #[must_use]
72 pub const fn with_resolution_scale(mut self, scale: f32) -> Self {
73 self.resolution_scale = scale;
74 self
75 }
76
77 pub fn validate(self) -> Result<(), EngineError> {
82 if self.resolution_scale.is_finite()
83 && self.resolution_scale > 0.0
84 && self.resolution_scale <= Self::MAX_RESOLUTION_SCALE
85 && (1..=Self::MAX_FILTER_SAMPLES).contains(&self.max_filter_samples)
86 && (1..=Self::MAX_MOTION_BLUR_SUBFRAMES).contains(&self.motion_blur_subframes)
87 && (Self::MIN_TEXTURE_BUDGET_BYTES..=Self::MAX_TEXTURE_BUDGET_BYTES)
88 .contains(&self.texture_budget_bytes)
89 {
90 Ok(())
91 } else {
92 Err(EngineError::InvalidQualityProfile)
93 }
94 }
95
96 pub fn render_size(self, settings: &ProjectSettings) -> Result<RenderSize, EngineError> {
101 self.validate()?;
102 let width = scaled_dimension(settings.width, self.resolution_scale)?;
103 let height = scaled_dimension(settings.height, self.resolution_scale)?;
104 RenderSize::new(width, height).map_err(EngineError::from)
105 }
106
107 pub fn export_size(self, settings: &ProjectSettings) -> Result<RenderSize, EngineError> {
117 let size = self.render_size(settings)?;
118 if size.width % 2 != 0 || size.height % 2 != 0 {
119 let even = |value: u32| match value {
120 1 => 2,
121 odd if odd % 2 != 0 => odd - 1,
122 even => even,
123 };
124 return Err(EngineError::OddExportDimensions {
125 width: size.width,
126 height: size.height,
127 suggested: (even(size.width), even(size.height)),
128 });
129 }
130 Ok(size)
131 }
132
133 pub fn benchmark_metadata(
138 self,
139 project: impl Into<String>,
140 settings: &ProjectSettings,
141 renderer: &alpha2_render::Renderer,
142 ) -> Result<alpha2_render::BenchmarkMetadata, EngineError> {
143 let size = self.render_size(settings)?;
144 Ok(alpha2_render::BenchmarkMetadata {
145 project: project.into(),
146 alpha2_version: env!("CARGO_PKG_VERSION"),
147 adapter: renderer.diagnostics().adapter,
148 dimensions: (size.width, size.height),
149 resolution_scale: self.resolution_scale,
150 max_filter_samples: self.max_filter_samples,
151 motion_blur_subframes: self.motion_blur_subframes,
152 texture_budget_bytes: self.texture_budget_bytes,
153 })
154 }
155}
156
157fn scaled_dimension(value: u32, scale: f32) -> Result<u32, EngineError> {
158 let scaled = f64::from(value) * f64::from(scale);
159 if scaled.is_finite() && scaled <= f64::from(u32::MAX) {
160 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
161 Ok((scaled.round() as u32).max(1))
162 } else {
163 Err(EngineError::OutOfRange)
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use alpha2_time::FrameRate;
170
171 use super::*;
172
173 #[test]
174 fn quality_scales_dimensions_and_rejects_zero() {
175 let settings = ProjectSettings::new(
176 1920,
177 1080,
178 FrameRate::new(30, 1).expect("valid rate"),
179 48_000,
180 )
181 .expect("valid settings");
182 assert_eq!(
183 QualityProfile::preview()
184 .render_size(&settings)
185 .expect("valid size"),
186 RenderSize::new(960, 540).expect("valid size")
187 );
188 assert_eq!(
189 QualityProfile {
190 resolution_scale: 0.0,
191 ..QualityProfile::default()
192 }
193 .validate(),
194 Err(EngineError::InvalidQualityProfile)
195 );
196 assert_eq!(
197 QualityProfile {
198 motion_blur_subframes: u32::MAX,
199 ..QualityProfile::default()
200 }
201 .validate(),
202 Err(EngineError::InvalidQualityProfile)
203 );
204 assert_eq!(
205 QualityProfile {
206 texture_budget_bytes: 1,
207 ..QualityProfile::default()
208 }
209 .validate(),
210 Err(EngineError::InvalidQualityProfile)
211 );
212 }
213
214 #[test]
215 fn a_preview_scale_sizes_the_render_and_is_validated() {
216 let settings = |width, height| {
217 ProjectSettings::new(
218 width,
219 height,
220 FrameRate::new(30, 1).expect("valid rate"),
221 48_000,
222 )
223 .expect("valid settings")
224 };
225 let size = |scale: f32, width, height| {
226 QualityProfile::preview()
227 .with_resolution_scale(scale)
228 .render_size(&settings(width, height))
229 .map(|size| (size.width, size.height))
230 };
231 assert_eq!(size(0.5, 1280, 720), Ok((640, 360)));
232 assert_eq!(size(1.0, 1280, 720), Ok((1280, 720)));
233 assert_eq!(size(0.5, 1281, 721), Ok((641, 361)));
235 assert_eq!(size(0.25, 1282, 722), Ok((321, 181)));
236 for scale in [0.0, -1.0, 8.5, f32::NAN, f32::INFINITY] {
237 assert_eq!(
238 QualityProfile::preview()
239 .with_resolution_scale(scale)
240 .validate(),
241 Err(EngineError::InvalidQualityProfile),
242 "{scale}"
243 );
244 }
245 assert_eq!(
246 QualityProfile::preview()
247 .with_resolution_scale(QualityProfile::MAX_RESOLUTION_SCALE)
248 .validate(),
249 Ok(())
250 );
251 }
252}