1use oriera_core::{FrameCtx, System};
12use oriera_ecs::{Position, World};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
17pub enum Quality {
18 Low,
19 Medium,
20 High,
21}
22
23impl Quality {
24 fn batch_factor(self) -> u32 {
27 match self {
28 Quality::High => 1,
29 Quality::Medium => 4,
30 Quality::Low => 16,
31 }
32 }
33
34 fn step_down(self) -> Option<Quality> {
35 match self {
36 Quality::High => Some(Quality::Medium),
37 Quality::Medium => Some(Quality::Low),
38 Quality::Low => None,
39 }
40 }
41
42 fn step_up(self) -> Option<Quality> {
43 match self {
44 Quality::Low => Some(Quality::Medium),
45 Quality::Medium => Some(Quality::High),
46 Quality::High => None,
47 }
48 }
49}
50
51const DEGRADE_AFTER: u32 = 30;
54const RECOVER_AFTER: u32 = 120;
55const RECOVER_HEADROOM: f32 = 0.6;
57
58pub struct RenderSystem {
61 pub draw_calls: u32,
62 pub visible: u32,
63 pub quality: Quality,
66 over_streak: u32,
67 under_streak: u32,
68}
69
70impl RenderSystem {
71 pub fn new() -> Self {
72 Self {
73 draw_calls: 0,
74 visible: 0,
75 quality: Quality::High,
76 over_streak: 0,
77 under_streak: 0,
78 }
79 }
80
81 pub fn observe_frame(&mut self, avg_ms: f32, budget_ms: f32) {
87 if avg_ms > budget_ms {
88 self.over_streak += 1;
89 self.under_streak = 0;
90 if self.over_streak >= DEGRADE_AFTER {
91 if let Some(q) = self.quality.step_down() {
92 tracing::info!(from = ?self.quality, to = ?q, "L5 quality degrade");
93 self.quality = q;
94 }
95 self.over_streak = 0;
96 }
97 } else if avg_ms < budget_ms * RECOVER_HEADROOM {
98 self.under_streak += 1;
99 self.over_streak = 0;
100 if self.under_streak >= RECOVER_AFTER {
101 if let Some(q) = self.quality.step_up() {
102 tracing::info!(from = ?self.quality, to = ?q, "L5 quality recover");
103 self.quality = q;
104 }
105 self.under_streak = 0;
106 }
107 } else {
108 self.over_streak = 0;
109 self.under_streak = 0;
110 }
111 }
112}
113
114impl System<World> for RenderSystem {
115 fn name(&self) -> &str {
116 "render"
117 }
118 fn run(&mut self, _ctx: &FrameCtx, world: &mut World) {
119 self.visible = world.query::<Position>().count() as u32;
120 let factor = self.quality.batch_factor();
121 self.draw_calls = self.visible.div_ceil(factor).max(1);
122 }
123}
124
125impl Default for RenderSystem {
126 fn default() -> Self {
127 Self::new()
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use oriera_common::FrameIndex;
135
136 #[test]
137 fn counts_visible_positions() {
138 let mut world = World::new();
139 for i in 0..5 {
140 let e = world.spawn();
141 world.insert(
142 e,
143 Position {
144 x: i as f32,
145 y: 0.0,
146 },
147 );
148 }
149 let ctx = FrameCtx {
150 frame: FrameIndex(0),
151 dt: 0.0,
152 now: oriera_common::Timestamp(0),
153 };
154 let mut sys = RenderSystem::new();
155 sys.run(&ctx, &mut world);
156 assert_eq!(sys.visible, 5);
157 assert_eq!(sys.draw_calls, 5); }
159
160 #[test]
161 fn batching_reduces_draw_calls_at_lower_tiers() {
162 let mut world = World::new();
163 for i in 0..32 {
164 let e = world.spawn();
165 world.insert(
166 e,
167 Position {
168 x: i as f32,
169 y: 0.0,
170 },
171 );
172 }
173 let ctx = FrameCtx {
174 frame: FrameIndex(0),
175 dt: 0.0,
176 now: oriera_common::Timestamp(0),
177 };
178 let mut sys = RenderSystem::new();
179
180 sys.quality = Quality::Medium;
181 sys.run(&ctx, &mut world);
182 assert_eq!(sys.visible, 32);
183 assert_eq!(sys.draw_calls, 8); sys.quality = Quality::Low;
186 sys.run(&ctx, &mut world);
187 assert_eq!(sys.draw_calls, 2); }
189
190 #[test]
191 fn sustained_overruns_degrade_quality() {
192 let mut sys = RenderSystem::new();
193 for _ in 0..DEGRADE_AFTER {
194 sys.observe_frame(20.0, 16.7);
195 }
196 assert_eq!(sys.quality, Quality::Medium);
197 for _ in 0..DEGRADE_AFTER {
198 sys.observe_frame(20.0, 16.7);
199 }
200 assert_eq!(sys.quality, Quality::Low);
201 for _ in 0..DEGRADE_AFTER {
203 sys.observe_frame(20.0, 16.7);
204 }
205 assert_eq!(sys.quality, Quality::Low);
206 }
207
208 #[test]
209 fn in_budget_frames_reset_the_degrade_streak() {
210 let mut sys = RenderSystem::new();
211 for _ in 0..(DEGRADE_AFTER - 1) {
212 sys.observe_frame(20.0, 16.7);
213 }
214 sys.observe_frame(10.0, 16.7); for _ in 0..(DEGRADE_AFTER - 1) {
216 sys.observe_frame(20.0, 16.7);
217 }
218 assert_eq!(sys.quality, Quality::High); }
220
221 #[test]
222 fn recovery_needs_headroom_and_patience() {
223 let mut sys = RenderSystem::new();
224 sys.quality = Quality::Low;
225 for _ in 0..RECOVER_AFTER {
227 sys.observe_frame(11.0, 16.7);
228 }
229 assert_eq!(sys.quality, Quality::Low);
230 for _ in 0..RECOVER_AFTER {
232 sys.observe_frame(5.0, 16.7);
233 }
234 assert_eq!(sys.quality, Quality::Medium);
235 for _ in 0..(RECOVER_AFTER - 1) {
237 sys.observe_frame(5.0, 16.7);
238 }
239 sys.observe_frame(20.0, 16.7);
240 for _ in 0..(RECOVER_AFTER - 1) {
241 sys.observe_frame(5.0, 16.7);
242 }
243 assert_eq!(sys.quality, Quality::Medium); }
245}