1use crate::effect_cost::{ARITHMETIC_UNITS, EffectCost, FETCH_UNITS, TRANSCENDENTAL_UNITS};
13
14pub(crate) const PLANNING_THRESHOLD_UNIT_PIXELS: f64 = 64.0 * 1_048_576.0;
17
18pub(crate) const MAX_BANDS: u32 = 64;
20
21pub(crate) const COMPOSITE_LAYER_COST: EffectCost = EffectCost::fixed(4, 0);
23pub(crate) const VECTOR_COST: EffectCost = EffectCost::fixed(2, 0);
25
26#[derive(Copy, Clone, Debug, Default, PartialEq)]
28pub struct FrameCost {
29 pub fetches: f64,
31 pub transcendentals: f64,
33 pub arithmetic: f64,
35}
36
37impl FrameCost {
38 #[must_use]
40 #[allow(clippy::cast_precision_loss)]
41 pub fn unit_pixels(&self) -> f64 {
42 self.fetches * FETCH_UNITS as f64
43 + self.transcendentals * TRANSCENDENTAL_UNITS as f64
44 + self.arithmetic * ARITHMETIC_UNITS as f64
45 }
46
47 #[allow(clippy::cast_precision_loss)]
48 pub(crate) fn add(&mut self, cost: EffectCost, pixels: u64) {
49 let pixels = pixels as f64;
50 self.fetches += cost.fetches as f64 * pixels;
51 self.transcendentals += cost.transcendentals as f64 * pixels;
52 self.arithmetic += cost.arithmetic as f64 * pixels;
53 }
54}
55
56#[derive(Copy, Clone, Debug, Default, PartialEq)]
58pub struct FrameSubmissions {
59 pub submissions: u32,
61 pub predicted_ms: Option<f64>,
63 pub cost: FrameCost,
65}
66
67#[derive(Copy, Clone, Debug, PartialEq, Eq)]
68pub(crate) struct PassPlacement {
69 pub(crate) cut_before: bool,
71 pub(crate) bands: u32,
73}
74
75#[derive(Debug, Default)]
76pub(crate) struct SubmissionPlanner {
77 plan: Option<(f64, f64)>,
79 current_ms: f64,
81 finished_ms: Vec<f64>,
83 band_ms: f64,
85 cost: FrameCost,
86 frame: bool,
88}
89
90impl SubmissionPlanner {
91 pub(crate) fn plan(&mut self, ms_per_unit_pixel: f64, budget_ms: f64) {
92 self.plan = Some((ms_per_unit_pixel, budget_ms.max(f64::MIN_POSITIVE)));
93 }
94
95 pub(crate) const fn mark_frame(&mut self) {
96 self.frame = true;
97 }
98
99 pub(crate) fn pass(&mut self, cost: EffectCost, pixels: u64, bandable: bool) -> PassPlacement {
102 self.cost.add(cost, pixels);
103 let Some((rate, budget)) = self.plan else {
104 return PassPlacement {
105 cut_before: false,
106 bands: 1,
107 };
108 };
109 #[allow(clippy::cast_precision_loss)]
110 let ms = cost.units() as f64 * pixels as f64 * rate;
111 if ms > budget {
112 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
113 let bands = if bandable {
114 ((ms / budget).ceil() as u32).clamp(1, MAX_BANDS)
115 } else {
116 1
117 };
118 let cut_before = self.current_ms > 0.0;
119 if cut_before {
120 self.finished_ms.push(self.current_ms);
121 }
122 self.band_ms = ms / f64::from(bands);
123 self.current_ms = self.band_ms;
124 return PassPlacement { cut_before, bands };
125 }
126 let cut_before = self.current_ms > 0.0 && self.current_ms + ms > budget;
127 if cut_before {
128 self.finished_ms.push(self.current_ms);
129 self.current_ms = 0.0;
130 }
131 self.current_ms += ms;
132 PassPlacement {
133 cut_before,
134 bands: 1,
135 }
136 }
137
138 pub(crate) fn next_band(&mut self) {
139 self.finished_ms.push(self.current_ms);
140 self.current_ms = self.band_ms;
141 }
142
143 pub(crate) const fn is_planned(&self) -> bool {
144 self.plan.is_some()
145 }
146
147 pub(crate) const fn is_frame(&self) -> bool {
148 self.frame
149 }
150
151 pub(crate) const fn cost(&self) -> FrameCost {
152 self.cost
153 }
154
155 pub(crate) fn buffer_ms(&self) -> Option<Vec<f64>> {
157 self.plan.map(|_| {
158 let mut buffers = self.finished_ms.clone();
159 buffers.push(self.current_ms);
160 buffers
161 })
162 }
163}
164
165const CALIBRATION_SIDE: u32 = 512;
166
167const CALIBRATION_BODY: &str = "\
169fn effect(uv: vec2f, src: vec4f) -> vec4f {
170 var acc = src;
171 for (var i = 0u; i < 64u; i++) {
172 let s = sample_src(fract(uv + vec2f(f32(i) * 0.0019, f32(i) * 0.0007)));
173 acc = vec4f(sin(acc.r * 1.37 + s.g), acc.g, acc.b, acc.a);
174 }
175 return acc;
176}
177";
178
179fn calibration_definition() -> crate::EffectDef {
180 crate::EffectDef {
181 id: "alpha2.internal.calibrate".into(),
182 version: 1,
183 kind: crate::EffectKind::Effect,
184 inputs: Vec::new(),
185 params: Vec::new(),
186 resources: Vec::new(),
187 wgsl_body: CALIBRATION_BODY.into(),
188 tier_min: alpha2_gpu::CapabilityTier::TierB,
189 license: None,
190 }
191}
192
193impl crate::Renderer {
194 pub(crate) fn plan_submissions(
198 &self,
199 encoder: &mut crate::renderer::RenderEncoder,
200 cost: FrameCost,
201 ) {
202 encoder.planner().mark_frame();
203 if cost.unit_pixels() <= PLANNING_THRESHOLD_UNIT_PIXELS {
204 return;
205 }
206 let rate = self.gpu.rate();
207 if rate.ms_per_unit_pixel().is_none()
208 && rate.begin_calibration()
209 && self.calibrate().is_err()
210 {
211 rate.abandon_calibration();
212 }
213 let ms_per_unit_pixel = rate
214 .ms_per_unit_pixel()
215 .unwrap_or(alpha2_gpu::UNCALIBRATED_MS_PER_UNIT_PIXEL);
216 encoder.planner().plan(
217 ms_per_unit_pixel,
218 f64::from(alpha2_gpu::submission_budget_ms()),
219 );
220 }
221
222 fn calibrate(&self) -> Result<(), crate::RenderError> {
228 let effect = self
229 .calibration
230 .get_or_init(|| crate::registry::registered_effect(calibration_definition()));
231 let id = crate::EffectId::new(effect.definition.id.clone(), effect.definition.version);
232 let size = crate::RenderSize::new(CALIBRATION_SIDE, CALIBRATION_SIDE)?;
233 let format = self.intermediate_format();
234 let params = crate::EffectParams::new();
235 let resources = crate::EffectResources::new();
236 let mut first = None;
237 let mut last = None;
238 for _ in 0..2 {
239 let mut encoder = self.command_encoder("alpha2 calibration encoder");
240 let source =
241 self.textures
242 .acquire(self.gpu.device(), size, format, "alpha2 calibration source");
243 let target =
244 self.textures
245 .acquire(self.gpu.device(), size, format, "alpha2 calibration target");
246 let mut uniforms = self.uniform_buffers.acquire(self.gpu.device(), 4096);
247 self.encode_registered_pass(
248 &mut encoder,
249 effect,
250 crate::runtime_effect::EffectPass {
251 id: &id,
252 kind: crate::RenderNodeKind::Effect,
253 source_a: source.view(),
254 source_b: None,
255 inputs: &[],
256 size,
257 params: ¶ms,
258 resources: &resources,
259 runtime: crate::EffectRuntime::default(),
260 output: target.view(),
261 },
262 &mut uniforms,
263 )?;
264 uniforms.flush(self.gpu.queue());
265 let submitted = self.submit_measured([encoder.finish()], false)?;
266 uniforms.recycle();
267 #[cfg(not(target_arch = "wasm32"))]
268 self.gpu
269 .device()
270 .poll(wgpu::PollType::Wait {
271 submission_index: Some(submitted.index.clone()),
272 timeout: Some(self.gpu_wait_timeout),
273 })
274 .map_err(crate::error::poll_error)?;
275 first.get_or_insert(submitted.sequences.start);
276 last = Some(submitted);
277 }
278 let submitted = last.expect("two calibration draws");
279 #[cfg(not(target_arch = "wasm32"))]
280 let (sequences, draws) = (submitted.sequences.clone(), 1.0);
281 #[cfg(target_arch = "wasm32")]
282 let (sequences, draws) = (
283 first.unwrap_or(submitted.sequences.start)..submitted.sequences.end,
284 2.0,
285 );
286 #[allow(clippy::cast_precision_loss)]
287 let unit_pixels =
288 effect.cost.units() as f64 * f64::from(CALIBRATION_SIDE) * f64::from(CALIBRATION_SIDE);
289 let clock = std::sync::Arc::clone(&self.submission_clock);
290 let rate = std::sync::Arc::clone(self.gpu.rate());
291 let seed = move || match clock.gpu_ms(sequences) {
292 Some(ms) => rate.seed(ms / draws / unit_pixels),
293 None => rate.abandon_calibration(),
294 };
295 #[cfg(not(target_arch = "wasm32"))]
297 seed();
298 #[cfg(target_arch = "wasm32")]
299 self.gpu.on_submitted_work_done(seed);
300 Ok(())
301 }
302}
303
304pub(crate) fn band_rows(band: u32, bands: u32, height: u32) -> (u32, u32) {
307 let edge = |index: u32| -> u32 {
308 if index >= bands {
309 return height;
310 }
311 let row = u64::from(height) * u64::from(index) / u64::from(bands);
312 u32::try_from(row).unwrap_or(height) & !1
313 };
314 let top = edge(band);
315 (top, edge(band + 1).saturating_sub(top))
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn passes_fill_a_buffer_up_to_the_budget_and_a_heavy_one_is_banded() {
324 let mut planner = SubmissionPlanner::default();
325 let pass = EffectCost {
326 fetches: 0,
327 transcendentals: 0,
328 arithmetic: 10,
329 };
330 assert_eq!(planner.pass(pass, 100, true).bands, 1);
331 assert_eq!(planner.buffer_ms(), None);
332 planner.plan(1.0, 2_500.0);
333 assert!(!planner.pass(pass, 100, true).cut_before);
335 assert!(!planner.pass(pass, 100, true).cut_before);
336 assert!(planner.pass(pass, 100, true).cut_before);
337 let placed = planner.pass(pass, 1_000, true);
339 assert_eq!(
340 placed,
341 PassPlacement {
342 cut_before: true,
343 bands: 4
344 }
345 );
346 for _ in 1..placed.bands {
347 planner.next_band();
348 }
349 assert_eq!(
350 planner.pass(pass, 1_000, false),
351 PassPlacement {
352 cut_before: true,
353 bands: 1
354 }
355 );
356 assert_eq!(
357 planner.buffer_ms(),
358 Some(vec![
359 2_000.0, 1_000.0, 2_500.0, 2_500.0, 2_500.0, 2_500.0, 10_000.0
360 ])
361 );
362 assert!((planner.cost().unit_pixels() - 24_000.0).abs() < 1e-9);
363 }
364
365 #[test]
366 fn bands_cover_every_row_once_on_even_edges() {
367 for (bands, height) in [(1, 7), (3, 288), (7, 101), (64, 4320)] {
368 let mut next = 0;
369 for band in 0..bands {
370 let (top, rows) = band_rows(band, bands, height);
371 assert_eq!(top, next);
372 assert_eq!(top % 2, 0);
373 next = top + rows;
374 }
375 assert_eq!(next, height);
376 }
377 }
378}