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