Skip to main content

embedded_3dgfx/
config.rs

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
173/// Resolve the runtime default cap profile.
174///
175/// Priority:
176/// 1. `desktop-unbounded` cargo feature => no caps
177/// 2. `EMBEDDED_3DGFX_CAPS=off` => no caps
178/// 3. `EMBEDDED_3DGFX_CAPS=m0|m3|m4|m33|m55` => corresponding balanced/perf profile
179/// 4. Fallback => `DEFAULT_PROFILE_CAPS` (`PROFILE_M33_BALANCED`)
180pub 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}