1use crate::error::{BudgetKind, RenderError};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum QualityTier {
5 Fastest,
6 Balanced,
7 Quality,
8}
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum MaterialProfile {
12 Unlit,
13 Lambert,
14 SimpleSpecular,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct RenderDefaults {
19 pub quality_tier: QualityTier,
20 pub material_profile: MaterialProfile,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum DegradationStep {
25 RaisePriorityFloor(u8),
26 MeshDecimationStride(usize),
27 DowngradeQuality,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct DegradationPolicy<'a> {
32 pub steps: &'a [DegradationStep],
33}
34
35impl Default for RenderDefaults {
36 fn default() -> Self {
37 Self {
38 quality_tier: QualityTier::Balanced,
39 material_profile: MaterialProfile::Lambert,
40 }
41 }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct ProfileCaps {
46 pub max_draw_primitives: usize,
47 pub max_meshes_per_frame: usize,
48 pub max_textures: usize,
49 pub max_width: usize,
50 pub max_height: usize,
51 pub max_triangles_per_mesh: usize,
52 pub max_vertices_per_mesh: usize,
53}
54
55impl ProfileCaps {
56 pub const fn validate_framebuffer(
57 &self,
58 width: usize,
59 height: usize,
60 ) -> Result<(), RenderError> {
61 if width > self.max_width || height > self.max_height {
62 return Err(RenderError::OutOfBudget(
63 BudgetKind::FramebufferDimensions {
64 width,
65 height,
66 max_width: self.max_width,
67 max_height: self.max_height,
68 },
69 ));
70 }
71 Ok(())
72 }
73}
74
75pub const PROFILE_M0_MIN: ProfileCaps = ProfileCaps {
76 max_draw_primitives: 256,
77 max_meshes_per_frame: 16,
78 max_textures: 1,
79 max_width: 128,
80 max_height: 128,
81 max_triangles_per_mesh: 512,
82 max_vertices_per_mesh: 512,
83};
84
85pub const PROFILE_M0_BALANCED: ProfileCaps = ProfileCaps {
86 max_draw_primitives: 512,
87 max_meshes_per_frame: 32,
88 max_textures: 2,
89 max_width: 160,
90 max_height: 128,
91 max_triangles_per_mesh: 1_024,
92 max_vertices_per_mesh: 1_024,
93};
94
95pub const PROFILE_M3_MIN: ProfileCaps = ProfileCaps {
96 max_draw_primitives: 1_024,
97 max_meshes_per_frame: 64,
98 max_textures: 4,
99 max_width: 240,
100 max_height: 240,
101 max_triangles_per_mesh: 2_048,
102 max_vertices_per_mesh: 2_048,
103};
104
105pub const PROFILE_M3_BALANCED: ProfileCaps = ProfileCaps {
106 max_draw_primitives: 1_536,
107 max_meshes_per_frame: 96,
108 max_textures: 6,
109 max_width: 320,
110 max_height: 240,
111 max_triangles_per_mesh: 3_072,
112 max_vertices_per_mesh: 3_072,
113};
114
115pub const PROFILE_M4_BALANCED: ProfileCaps = ProfileCaps {
116 max_draw_primitives: 2_048,
117 max_meshes_per_frame: 128,
118 max_textures: 8,
119 max_width: 320,
120 max_height: 240,
121 max_triangles_per_mesh: 4_096,
122 max_vertices_per_mesh: 4_096,
123};
124
125pub const PROFILE_M33_BALANCED: ProfileCaps = ProfileCaps {
126 max_draw_primitives: 2_048,
127 max_meshes_per_frame: 128,
128 max_textures: 8,
129 max_width: 320,
130 max_height: 240,
131 max_triangles_per_mesh: 4_096,
132 max_vertices_per_mesh: 4_096,
133};
134
135pub const PROFILE_M33_SECURE: ProfileCaps = ProfileCaps {
136 max_draw_primitives: 2_048,
137 max_meshes_per_frame: 128,
138 max_textures: 8,
139 max_width: 320,
140 max_height: 240,
141 max_triangles_per_mesh: 4_096,
142 max_vertices_per_mesh: 4_096,
143};
144
145pub const PROFILE_M55_PERF: ProfileCaps = ProfileCaps {
146 max_draw_primitives: 4_096,
147 max_meshes_per_frame: 256,
148 max_textures: 16,
149 max_width: 480,
150 max_height: 320,
151 max_triangles_per_mesh: 8_192,
152 max_vertices_per_mesh: 8_192,
153};
154
155pub const DEFAULT_PROFILE_CAPS: ProfileCaps = PROFILE_M33_BALANCED;
156
157pub fn render_defaults_for_profile(profile: ProfileCaps) -> RenderDefaults {
158 if profile.max_draw_primitives <= PROFILE_M3_BALANCED.max_draw_primitives {
159 return RenderDefaults {
160 quality_tier: QualityTier::Fastest,
161 material_profile: MaterialProfile::Unlit,
162 };
163 }
164 if profile.max_draw_primitives >= PROFILE_M55_PERF.max_draw_primitives {
165 return RenderDefaults {
166 quality_tier: QualityTier::Quality,
167 material_profile: MaterialProfile::SimpleSpecular,
168 };
169 }
170 RenderDefaults::default()
171}
172
173pub fn default_profile_caps() -> Option<ProfileCaps> {
181 if cfg!(feature = "desktop-unbounded") {
182 return None;
183 }
184
185 #[cfg(feature = "std")]
186 {
187 if let Ok(raw) = std::env::var("EMBEDDED_3DGFX_CAPS") {
188 let value = raw.trim().to_ascii_lowercase();
189 return match value.as_str() {
190 "off" | "none" | "unbounded" => None,
191 "m0" | "m0_min" => Some(PROFILE_M0_MIN),
192 "m0_balanced" => Some(PROFILE_M0_BALANCED),
193 "m3" | "m3_balanced" => Some(PROFILE_M3_BALANCED),
194 "m4" | "m4_balanced" => Some(PROFILE_M4_BALANCED),
195 "m33" | "m33_balanced" => Some(PROFILE_M33_BALANCED),
196 "m55" | "m55_perf" => Some(PROFILE_M55_PERF),
197 _ => Some(DEFAULT_PROFILE_CAPS),
198 };
199 }
200 }
201
202 Some(DEFAULT_PROFILE_CAPS)
203}
204
205pub fn apply_default_caps(engine: &mut crate::engine::K3dengine) {
206 if let Some(caps) = default_profile_caps() {
207 engine.set_caps(caps);
208 engine.apply_render_defaults(render_defaults_for_profile(caps));
209 } else {
210 engine.clear_caps();
211 engine.apply_render_defaults(RenderDefaults::default());
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn test_render_defaults_for_m0_prefers_fastest_unlit() {
221 let d = render_defaults_for_profile(PROFILE_M0_BALANCED);
222 assert_eq!(d.quality_tier, QualityTier::Fastest);
223 assert_eq!(d.material_profile, MaterialProfile::Unlit);
224 }
225
226 #[test]
227 fn test_render_defaults_for_m3_prefers_fastest_unlit() {
228 let d = render_defaults_for_profile(PROFILE_M3_BALANCED);
229 assert_eq!(d.quality_tier, QualityTier::Fastest);
230 assert_eq!(d.material_profile, MaterialProfile::Unlit);
231 }
232
233 #[test]
234 fn test_render_defaults_for_m55_prefers_quality_specular() {
235 let d = render_defaults_for_profile(PROFILE_M55_PERF);
236 assert_eq!(d.quality_tier, QualityTier::Quality);
237 assert_eq!(d.material_profile, MaterialProfile::SimpleSpecular);
238 }
239
240 #[test]
241 fn test_render_defaults_balanced_and_framebuffer_validation() {
242 let balanced = render_defaults_for_profile(PROFILE_M33_BALANCED);
243 assert_eq!(balanced.quality_tier, QualityTier::Balanced);
244 assert_eq!(balanced.material_profile, MaterialProfile::Lambert);
245
246 let caps = PROFILE_M0_BALANCED;
247 assert!(caps.validate_framebuffer(100, 100).is_ok());
248 assert!(caps.validate_framebuffer(1000, 100).is_err());
249 assert!(caps.validate_framebuffer(100, 1000).is_err());
250
251 let steps = [
252 DegradationStep::RaisePriorityFloor(16),
253 DegradationStep::MeshDecimationStride(2),
254 DegradationStep::DowngradeQuality,
255 ];
256 let policy = DegradationPolicy { steps: &steps };
257 assert_eq!(policy.steps.len(), 3);
258 }
259
260 #[test]
261 fn test_default_profile_caps_and_apply() {
262 if let Some(caps) = default_profile_caps() {
263 assert_eq!(
264 caps.max_draw_primitives,
265 DEFAULT_PROFILE_CAPS.max_draw_primitives
266 );
267 let mut engine = crate::engine::K3dengine::new(16, 16);
268 apply_default_caps(&mut engine);
269 }
270 }
271}