1use serde::Serialize;
2
3const DEFAULT_SCROLLER_TARGET_TILE_BACKING_DIMENSION: f64 = 2496.0;
4const DEFAULT_MIN_LOGICAL_TILE_SIZE: f64 = 256.0;
5const DEFAULT_TILE_OVERSCAN_COLUMNS: i32 = 0;
6const DEFAULT_TILE_OVERSCAN_ROWS: i32 = 0;
7const DEFAULT_MIN_UNTILED_RENDER_DPR: f64 = 1.0;
8const DEFAULT_PREWARM_VIEWPORT_PAD_X_MULTIPLIER: f64 = 1.0;
9const DEFAULT_PREWARM_VIEWPORT_PAD_Y_MULTIPLIER: f64 = 1.5;
10const DEFAULT_PREWARM_VIEWPORT_PAD_MIN_X: f64 = 512.0;
11const DEFAULT_PREWARM_VIEWPORT_PAD_MIN_Y: f64 = 512.0;
12
13#[derive(Clone, Copy, Debug)]
14pub struct ScrollerTilingPolicy {
15 pub target_tile_backing_dimension: f64,
16 pub max_tile_backing_width: Option<f64>,
17 pub max_tile_backing_height: Option<f64>,
18 pub max_tile_backing_area: Option<f64>,
19 pub min_logical_tile_size: f64,
20 pub tile_overscan_columns: i32,
21 pub tile_overscan_rows: i32,
22 pub min_untiled_render_dpr: f64,
23 pub prewarm_viewport_pad_x_multiplier: f64,
24 pub prewarm_viewport_pad_y_multiplier: f64,
25 pub prewarm_viewport_pad_min_x: f64,
26 pub prewarm_viewport_pad_min_y: f64,
27 pub max_surfaces_per_layer: Option<usize>,
28}
29
30impl Default for ScrollerTilingPolicy {
31 fn default() -> Self {
32 Self {
33 target_tile_backing_dimension: DEFAULT_SCROLLER_TARGET_TILE_BACKING_DIMENSION,
34 max_tile_backing_width: None,
35 max_tile_backing_height: None,
36 max_tile_backing_area: None,
37 min_logical_tile_size: DEFAULT_MIN_LOGICAL_TILE_SIZE,
38 tile_overscan_columns: DEFAULT_TILE_OVERSCAN_COLUMNS,
39 tile_overscan_rows: DEFAULT_TILE_OVERSCAN_ROWS,
40 min_untiled_render_dpr: DEFAULT_MIN_UNTILED_RENDER_DPR,
41 prewarm_viewport_pad_x_multiplier: DEFAULT_PREWARM_VIEWPORT_PAD_X_MULTIPLIER,
42 prewarm_viewport_pad_y_multiplier: DEFAULT_PREWARM_VIEWPORT_PAD_Y_MULTIPLIER,
43 prewarm_viewport_pad_min_x: DEFAULT_PREWARM_VIEWPORT_PAD_MIN_X,
44 prewarm_viewport_pad_min_y: DEFAULT_PREWARM_VIEWPORT_PAD_MIN_Y,
45 max_surfaces_per_layer: None,
46 }
47 }
48}
49
50impl ScrollerTilingPolicy {
51 pub(crate) fn prewarm_padding(
54 self,
55 viewport_width: f64,
56 viewport_height: f64,
57 horizontal_scrollable: bool,
58 vertical_scrollable: bool,
59 ) -> (f64, f64) {
60 (
61 if horizontal_scrollable {
62 (viewport_width * self.prewarm_viewport_pad_x_multiplier)
63 .max(self.prewarm_viewport_pad_min_x)
64 } else {
65 0.0
66 },
67 if vertical_scrollable {
68 (viewport_height * self.prewarm_viewport_pad_y_multiplier)
69 .max(self.prewarm_viewport_pad_min_y)
70 } else {
71 0.0
72 },
73 )
74 }
75}
76
77#[derive(Clone, Debug, Serialize)]
78#[serde(rename_all = "camelCase")]
79pub struct SurfaceCanvasDescriptor {
81 pub id: String,
82 pub key: String,
83 pub left: f64,
84 pub top: f64,
85 pub width: f64,
86 pub height: f64,
87 pub replay_priority: i32,
88 pub surface_signature: String,
89 pub transform_signature: String,
90 pub host_signature: String,
91}
92
93#[derive(Clone, Debug, Serialize)]
94#[serde(rename_all = "camelCase")]
95pub struct LayerCanvasPlan {
97 pub layer_id: usize,
98 pub active: bool,
99 pub surfaces: Vec<SurfaceCanvasDescriptor>,
100}
101
102#[derive(Clone, Copy, Debug)]
103struct LogicalTileDimensions {
104 width: f64,
105 height: f64,
106}
107
108pub fn single_surface_plan(
110 layer_id: usize,
111 host_signature: String,
112 width: f64,
113 height: f64,
114) -> LayerCanvasPlan {
115 let width = width.max(0.0);
116 let height = height.max(0.0);
117 LayerCanvasPlan {
118 layer_id,
119 active: true,
120 surfaces: vec![SurfaceCanvasDescriptor {
121 id: layer_id.to_string(),
122 key: "single".to_string(),
123 left: 0.0,
124 top: 0.0,
125 width,
126 height,
127 replay_priority: 0,
128 surface_signature: format!("single:{width}x{height}@{host_signature}"),
129 transform_signature: "0,0".to_string(),
130 host_signature,
131 }],
132 }
133}
134
135pub fn scroller_canvas_plan(
137 layer_id: usize,
138 host_signature: String,
139 content_width: f64,
140 content_height: f64,
141 viewport_width: f64,
142 viewport_height: f64,
143 scroll_x: f64,
144 scroll_y: f64,
145 device_pixel_ratio: f64,
146) -> LayerCanvasPlan {
147 scroller_canvas_plan_with_policy(
148 layer_id,
149 host_signature,
150 content_width,
151 content_height,
152 viewport_width,
153 viewport_height,
154 scroll_x,
155 scroll_y,
156 device_pixel_ratio,
157 ScrollerTilingPolicy::default(),
158 )
159}
160
161pub fn scroller_canvas_plan_with_policy(
162 layer_id: usize,
163 host_signature: String,
164 content_width: f64,
165 content_height: f64,
166 viewport_width: f64,
167 viewport_height: f64,
168 scroll_x: f64,
169 scroll_y: f64,
170 device_pixel_ratio: f64,
171 policy: ScrollerTilingPolicy,
172) -> LayerCanvasPlan {
173 let content_width = content_width.max(0.0);
174 let content_height = content_height.max(0.0);
175 let viewport_width = clamp_dimension(viewport_width, content_width);
176 let viewport_height = clamp_dimension(viewport_height, content_height);
177 let scroll_x = clamp_offset(scroll_x, content_width, viewport_width);
178 let scroll_y = clamp_offset(scroll_y, content_height, viewport_height);
179 let dpr = device_pixel_ratio.max(1.0);
180
181 if can_render_as_single_surface(content_width, content_height, dpr, policy) {
182 return single_surface_plan(layer_id, host_signature, content_width, content_height);
183 }
184
185 let horizontal_scrollable = content_width > viewport_width + 0.5;
186 let vertical_scrollable = content_height > viewport_height + 0.5;
187 let tile_dimensions = compute_logical_tile_dimensions(
188 content_width,
189 content_height,
190 horizontal_scrollable,
191 vertical_scrollable,
192 dpr,
193 policy,
194 );
195 if content_width <= tile_dimensions.width && content_height <= tile_dimensions.height {
196 return single_surface_plan(layer_id, host_signature, content_width, content_height);
197 }
198
199 let max_column = ((content_width / tile_dimensions.width).ceil() as i32 - 1).max(0);
200 let max_row = ((content_height / tile_dimensions.height).ceil() as i32 - 1).max(0);
201 let (pad_x, pad_y) = policy.prewarm_padding(
202 viewport_width,
203 viewport_height,
204 horizontal_scrollable,
205 vertical_scrollable,
206 );
207 let padded_viewport_width = viewport_width + pad_x * 2.0;
208 let padded_viewport_height = viewport_height + pad_y * 2.0;
209 let padded_scroll_x = clamp_offset(scroll_x - pad_x, content_width, padded_viewport_width);
210 let padded_scroll_y = clamp_offset(scroll_y - pad_y, content_height, padded_viewport_height);
211 let overscan_columns = if horizontal_scrollable {
212 policy.tile_overscan_columns.max(0)
213 } else {
214 0
215 };
216 let overscan_rows = if vertical_scrollable {
217 policy.tile_overscan_rows.max(0)
218 } else {
219 0
220 };
221 let active_columns = (max_column + 1).min(
222 visible_tile_span(padded_viewport_width, tile_dimensions.width, true)
223 + overscan_columns * 2,
224 );
225 let active_rows = (max_row + 1).min(
226 visible_tile_span(padded_viewport_height, tile_dimensions.height, true) + overscan_rows * 2,
227 );
228 let min_active_columns = (max_column + 1).min(visible_tile_span(
229 viewport_width,
230 tile_dimensions.width,
231 true,
232 ));
233 let min_active_rows = (max_row + 1).min(visible_tile_span(
234 viewport_height,
235 tile_dimensions.height,
236 true,
237 ));
238 let (active_columns, active_rows) = clamp_active_tile_window(
239 active_columns,
240 active_rows,
241 min_active_columns,
242 min_active_rows,
243 horizontal_scrollable,
244 vertical_scrollable,
245 policy.max_surfaces_per_layer,
246 );
247 let start_column = clamp_window_start(
248 (padded_scroll_x / tile_dimensions.width).floor() as i32 - overscan_columns,
249 max_column,
250 active_columns,
251 );
252 let start_row = clamp_window_start(
253 (padded_scroll_y / tile_dimensions.height).floor() as i32 - overscan_rows,
254 max_row,
255 active_rows,
256 );
257 let end_column = max_column.min(start_column + active_columns - 1);
258 let end_row = max_row.min(start_row + active_rows - 1);
259
260 let mut surfaces = Vec::new();
261 for row in start_row..=end_row {
262 for column in start_column..=end_column {
263 let slot_column = column.rem_euclid(active_columns);
264 let slot_row = row.rem_euclid(active_rows);
265 let left = column as f64 * tile_dimensions.width;
266 let top = row as f64 * tile_dimensions.height;
267 let width = (content_width - left).min(tile_dimensions.width).max(1.0);
268 let height = (content_height - top).min(tile_dimensions.height).max(1.0);
269 let replay_priority = tile_replay_priority(
270 left,
271 top,
272 width,
273 height,
274 scroll_x,
275 scroll_y,
276 viewport_width,
277 viewport_height,
278 tile_dimensions.width,
279 tile_dimensions.height,
280 );
281 let key = format!("{slot_column}:{slot_row}");
282 let id = format!("layer-{layer_id}-tile-{slot_column}-{slot_row}");
283 let surface_signature = format!(
284 "tile:{slot_column},{slot_row}:{width}x{height}:{viewport_width}x{viewport_height}:{host_signature}"
285 );
286 let transform_signature = format!("{left},{top}");
287 surfaces.push(SurfaceCanvasDescriptor {
288 id,
289 key,
290 left,
291 top,
292 width,
293 height,
294 replay_priority,
295 surface_signature,
296 transform_signature,
297 host_signature: host_signature.clone(),
298 });
299 }
300 }
301 surfaces.sort_by(|left, right| {
302 left.key
303 .cmp(&right.key)
304 .then_with(|| left.transform_signature.cmp(&right.transform_signature))
305 });
306
307 LayerCanvasPlan {
308 layer_id,
309 active: true,
310 surfaces,
311 }
312}
313
314fn compute_logical_tile_size(device_pixel_ratio: f64, policy: ScrollerTilingPolicy) -> f64 {
315 let tile_size = (policy.target_tile_backing_dimension.max(1.0) / device_pixel_ratio).floor();
316 tile_size.max(policy.min_logical_tile_size.max(1.0))
317}
318
319fn compute_logical_tile_dimensions(
320 content_width: f64,
321 content_height: f64,
322 horizontal_scrollable: bool,
323 vertical_scrollable: bool,
324 device_pixel_ratio: f64,
325 policy: ScrollerTilingPolicy,
326) -> LogicalTileDimensions {
327 let target_tile_size = compute_logical_tile_size(device_pixel_ratio, policy);
328 let max_width = max_logical_tile_width(device_pixel_ratio, policy);
329 let max_height = max_logical_tile_height(device_pixel_ratio, policy);
330 let mut width = target_tile_size.min(max_width).max(1.0);
331 let mut height = target_tile_size.min(max_height).max(1.0);
332
333 if vertical_scrollable && !horizontal_scrollable {
334 width = content_width.max(1.0).min(max_width).max(1.0);
335 height = constrain_axis_by_area(height, width, device_pixel_ratio, policy);
336 } else if horizontal_scrollable && !vertical_scrollable {
337 height = content_height.max(1.0).min(max_height).max(1.0);
338 width = constrain_axis_by_area(width, height, device_pixel_ratio, policy);
339 } else {
340 (width, height) = constrain_tile_area(width, height, device_pixel_ratio, policy);
341 }
342
343 LogicalTileDimensions { width, height }
344}
345
346fn can_render_as_single_surface(
347 content_width: f64,
348 content_height: f64,
349 desired_dpr: f64,
350 policy: ScrollerTilingPolicy,
351) -> bool {
352 let safe_width = content_width.max(1.0);
353 let safe_height = content_height.max(1.0);
354 let backing_width = max_tile_backing_width(policy);
355 let backing_height = max_tile_backing_height(policy);
356 let backing_area = max_tile_backing_area(policy);
357 let single_surface_dpr = desired_dpr
358 .min(backing_width / safe_width)
359 .min(backing_height / safe_height)
360 .min((backing_area / (safe_width * safe_height)).sqrt());
361 single_surface_dpr >= desired_dpr.max(policy.min_untiled_render_dpr.max(0.0))
364}
365
366fn max_tile_backing_width(policy: ScrollerTilingPolicy) -> f64 {
367 finite_positive_or(
368 policy.max_tile_backing_width,
369 policy.target_tile_backing_dimension,
370 )
371}
372
373fn max_tile_backing_height(policy: ScrollerTilingPolicy) -> f64 {
374 finite_positive_or(
375 policy.max_tile_backing_height,
376 policy.target_tile_backing_dimension,
377 )
378}
379
380fn max_tile_backing_area(policy: ScrollerTilingPolicy) -> f64 {
381 finite_positive_or(
382 policy.max_tile_backing_area,
383 max_tile_backing_width(policy) * max_tile_backing_height(policy),
384 )
385}
386
387fn finite_positive_or(value: Option<f64>, fallback: f64) -> f64 {
388 value
389 .filter(|value| value.is_finite() && *value > 0.0)
390 .unwrap_or(fallback)
391 .max(1.0)
392}
393
394fn max_logical_tile_width(device_pixel_ratio: f64, policy: ScrollerTilingPolicy) -> f64 {
395 (max_tile_backing_width(policy) / device_pixel_ratio.max(1.0))
396 .floor()
397 .max(1.0)
398}
399
400fn max_logical_tile_height(device_pixel_ratio: f64, policy: ScrollerTilingPolicy) -> f64 {
401 (max_tile_backing_height(policy) / device_pixel_ratio.max(1.0))
402 .floor()
403 .max(1.0)
404}
405
406fn backing_extent(logical_extent: f64, device_pixel_ratio: f64) -> f64 {
407 (logical_extent.max(1.0) * device_pixel_ratio.max(1.0))
408 .ceil()
409 .max(1.0)
410}
411
412fn backing_area(width: f64, height: f64, device_pixel_ratio: f64) -> f64 {
413 backing_extent(width, device_pixel_ratio) * backing_extent(height, device_pixel_ratio)
414}
415
416fn constrain_axis_by_area(
417 axis: f64,
418 cross_axis: f64,
419 device_pixel_ratio: f64,
420 policy: ScrollerTilingPolicy,
421) -> f64 {
422 let max_area = max_tile_backing_area(policy);
423 let cross_backing = backing_extent(cross_axis, device_pixel_ratio);
424 let max_axis = (max_area / cross_backing / device_pixel_ratio.max(1.0))
425 .floor()
426 .max(1.0);
427 axis.min(max_axis).max(1.0)
428}
429
430fn constrain_tile_area(
431 mut width: f64,
432 mut height: f64,
433 device_pixel_ratio: f64,
434 policy: ScrollerTilingPolicy,
435) -> (f64, f64) {
436 let max_area = max_tile_backing_area(policy);
437 let area = backing_area(width, height, device_pixel_ratio);
438 if area <= max_area {
439 return (width, height);
440 }
441
442 let scale = (max_area / area).sqrt().min(1.0);
443 width = (width * scale).floor().max(1.0);
444 height = (height * scale).floor().max(1.0);
445 if backing_area(width, height, device_pixel_ratio) > max_area {
446 height = constrain_axis_by_area(height, width, device_pixel_ratio, policy);
447 }
448 if backing_area(width, height, device_pixel_ratio) > max_area {
449 width = constrain_axis_by_area(width, height, device_pixel_ratio, policy);
450 }
451 (width.max(1.0), height.max(1.0))
452}
453
454fn tile_replay_priority(
455 tile_left: f64,
456 tile_top: f64,
457 tile_width: f64,
458 tile_height: f64,
459 viewport_left: f64,
460 viewport_top: f64,
461 viewport_width: f64,
462 viewport_height: f64,
463 tile_step_width: f64,
464 tile_step_height: f64,
465) -> i32 {
466 let tile_right = tile_left + tile_width;
467 let tile_bottom = tile_top + tile_height;
468 let viewport_right = viewport_left + viewport_width;
469 let viewport_bottom = viewport_top + viewport_height;
470
471 let intersects = tile_right >= viewport_left
472 && tile_left <= viewport_right
473 && tile_bottom >= viewport_top
474 && tile_top <= viewport_bottom;
475 if intersects {
476 return 0;
477 }
478
479 let dx = if tile_right < viewport_left {
480 viewport_left - tile_right
481 } else if tile_left > viewport_right {
482 tile_left - viewport_right
483 } else {
484 0.0
485 };
486 let dy = if tile_bottom < viewport_top {
487 viewport_top - tile_bottom
488 } else if tile_top > viewport_bottom {
489 tile_top - viewport_bottom
490 } else {
491 0.0
492 };
493
494 let x_priority = if dx > 0.0 {
495 (dx / tile_step_width.max(1.0)).ceil() as i32
496 } else {
497 0
498 };
499 let y_priority = if dy > 0.0 {
500 (dy / tile_step_height.max(1.0)).ceil() as i32
501 } else {
502 0
503 };
504 (x_priority.max(y_priority) + 1).max(1)
505}
506
507fn clamp_active_tile_window(
508 mut active_columns: i32,
509 mut active_rows: i32,
510 min_active_columns: i32,
511 min_active_rows: i32,
512 horizontal_scrollable: bool,
513 vertical_scrollable: bool,
514 max_surfaces: Option<usize>,
515) -> (i32, i32) {
516 let Some(max_surfaces) = max_surfaces else {
517 return (active_columns.max(1), active_rows.max(1));
518 };
519 let max_surfaces = max_surfaces.max(1) as i32;
520
521 while active_columns * active_rows > max_surfaces {
524 let can_trim_rows = vertical_scrollable && active_rows > min_active_rows.max(1);
525 let can_trim_columns = horizontal_scrollable && active_columns > min_active_columns.max(1);
526 if can_trim_rows && (!can_trim_columns || active_rows >= active_columns) {
527 active_rows -= 1;
528 continue;
529 }
530 if can_trim_columns {
531 active_columns -= 1;
532 continue;
533 }
534 if can_trim_rows {
535 active_rows -= 1;
536 continue;
537 }
538 break;
539 }
540
541 while active_columns * active_rows > max_surfaces && (active_columns > 1 || active_rows > 1) {
544 if active_rows >= active_columns && active_rows > 1 {
545 active_rows -= 1;
546 } else if active_columns > 1 {
547 active_columns -= 1;
548 } else {
549 active_rows -= 1;
550 }
551 }
552
553 (active_columns.max(1), active_rows.max(1))
554}
555
556fn clamp_dimension(value: f64, fallback: f64) -> f64 {
557 if value.is_finite() {
558 value.max(0.0).min(fallback.max(0.0))
559 } else {
560 fallback.max(0.0)
561 }
562}
563
564fn clamp_offset(value: f64, content: f64, viewport: f64) -> f64 {
565 if content <= viewport {
566 return 0.0;
567 }
568 if !value.is_finite() {
569 return 0.0;
570 }
571 value.max(0.0).min((content - viewport).max(0.0))
572}
573
574fn visible_tile_span(viewport_size: f64, tile_size: f64, include_extra_slot: bool) -> i32 {
575 let base = (viewport_size.max(0.0) / tile_size).ceil() as i32;
576 let extra = if include_extra_slot { 1 } else { 0 };
577 (base + extra).max(1)
578}
579
580fn clamp_window_start(value: i32, max_index: i32, window_size: i32) -> i32 {
581 clamp_index(value, (max_index - window_size + 1).max(0))
582}
583
584fn clamp_index(value: i32, max: i32) -> i32 {
585 value.max(0).min(max)
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591
592 #[test]
593 fn single_surface_must_fit_at_requested_screen_density() {
594 let policy = ScrollerTilingPolicy::default();
595 assert!(can_render_as_single_surface(2000.0, 600.0, 1.0, policy));
596 assert!(!can_render_as_single_surface(2000.0, 600.0, 2.0, policy));
597 assert!(!can_render_as_single_surface(1000.0, 600.0, 3.0, policy));
598 assert!(can_render_as_single_surface(1248.0, 600.0, 2.0, policy));
599 }
600
601 #[test]
602 fn screen_density_tiles_stay_within_physical_budgets_and_cover_the_viewport() {
603 for dpr in [1.0, 1.25, 1.5, 2.0, 3.0] {
604 for cap in [2048.0, 2496.0, 4096.0] {
605 let mut policy = ScrollerTilingPolicy::default();
606 policy.target_tile_backing_dimension = cap;
607 policy.max_tile_backing_width = Some(cap);
608 policy.max_tile_backing_height = Some(cap);
609 policy.max_tile_backing_area = Some(cap * cap);
610 for (width, height, viewport_w, viewport_h) in [
611 (20000.0, 534.0, 1322.0, 534.0),
612 (1322.0, 20000.0, 1322.0, 930.0),
613 (20000.0, 20000.0, 390.0, 844.0),
614 (2000.0, 1200.0, 2000.0, 1200.0),
615 ] {
616 for fraction in [0.0, 0.5, 1.0] {
617 let x = (width - viewport_w) * fraction;
618 let y = (height - viewport_h) * fraction;
619 let plan = scroller_canvas_plan_with_policy(
620 1,
621 "density-test".into(),
622 width,
623 height,
624 viewport_w,
625 viewport_h,
626 x,
627 y,
628 dpr,
629 policy,
630 );
631 for tile in &plan.surfaces {
632 assert!(backing_extent(tile.width, dpr) <= cap);
633 assert!(backing_extent(tile.height, dpr) <= cap);
634 assert!(backing_area(tile.width, tile.height, dpr) <= cap * cap);
635 }
636 for u in [0.001, 0.25, 0.5, 0.75, 0.999] {
638 for v in [0.001, 0.25, 0.5, 0.75, 0.999] {
639 let px = x + viewport_w * u;
640 let py = y + viewport_h * v;
641 assert!(
642 plan.surfaces.iter().any(|tile| {
643 px >= tile.left
644 && px <= tile.left + tile.width
645 && py >= tile.top
646 && py <= tile.top + tile.height
647 }),
648 "uncovered ({px}, {py}) at {dpr}x, cap={cap}"
649 );
650 }
651 }
652 }
653 }
654 }
655 }
656 }
657
658 #[test]
659 fn vertical_only_scroller_does_not_add_horizontal_overscan() {
660 let mut policy = ScrollerTilingPolicy::default();
661 policy.tile_overscan_columns = 3;
662 policy.tile_overscan_rows = 0;
663 let plan = scroller_canvas_plan_with_policy(
664 1,
665 "test".to_string(),
666 600.0,
667 10_000.0,
668 600.0,
669 600.0,
670 0.0,
671 0.0,
672 1.0,
673 policy,
674 );
675
676 assert!(plan.surfaces.iter().all(|surface| surface.left == 0.0));
677 }
678
679 #[test]
680 fn vertical_only_scroller_uses_full_width_tiles_when_cap_allows() {
681 let mut policy = ScrollerTilingPolicy::default();
682 policy.target_tile_backing_dimension = 4096.0;
683 policy.max_tile_backing_width = Some(4096.0);
684 policy.max_tile_backing_height = Some(4096.0);
685 policy.max_tile_backing_area = Some(4096.0 * 4096.0);
686 policy.prewarm_viewport_pad_y_multiplier = 0.5;
687 policy.prewarm_viewport_pad_min_y = 384.0;
688 let plan = scroller_canvas_plan_with_policy(
689 1,
690 "test".to_string(),
691 1376.0,
692 120_000.0,
693 1376.0,
694 904.0,
695 0.0,
696 0.0,
697 2.0,
698 policy,
699 );
700
701 assert!(plan.surfaces.iter().all(|surface| surface.left == 0.0));
702 assert!(plan.surfaces.iter().all(|surface| surface.width == 1376.0));
703 assert!(plan.surfaces.len() <= 3);
704 }
705
706 #[test]
707 fn non_scrolling_axis_still_splits_when_backing_cap_requires_it() {
708 let mut policy = ScrollerTilingPolicy::default();
709 policy.target_tile_backing_dimension = 2048.0;
710 policy.max_tile_backing_width = Some(2048.0);
711 policy.max_tile_backing_height = Some(2048.0);
712 policy.max_tile_backing_area = Some(2048.0 * 2048.0);
713 let plan = scroller_canvas_plan_with_policy(
714 1,
715 "test".to_string(),
716 3000.0,
717 20_000.0,
718 3000.0,
719 600.0,
720 0.0,
721 0.0,
722 1.0,
723 policy,
724 );
725
726 assert!(plan.surfaces.iter().any(|surface| surface.left > 0.0));
727 assert!(plan.surfaces.iter().all(|surface| surface.width <= 2048.0));
728 }
729
730 #[test]
731 fn tile_area_cap_reduces_scroll_axis_extent() {
732 let mut policy = ScrollerTilingPolicy::default();
733 policy.target_tile_backing_dimension = 4096.0;
734 policy.max_tile_backing_width = Some(4096.0);
735 policy.max_tile_backing_height = Some(4096.0);
736 policy.max_tile_backing_area = Some(4_000_000.0);
737 let plan = scroller_canvas_plan_with_policy(
738 1,
739 "test".to_string(),
740 3000.0,
741 20_000.0,
742 3000.0,
743 600.0,
744 0.0,
745 0.0,
746 1.0,
747 policy,
748 );
749
750 assert!(plan
751 .surfaces
752 .iter()
753 .all(|surface| { (surface.width.ceil() * surface.height.ceil()) <= 4_000_000.0 }));
754 assert!(plan.surfaces.iter().all(|surface| surface.height <= 1333.0));
755 }
756
757 #[test]
758 fn vertical_overscan_is_axis_gated() {
759 let mut policy = ScrollerTilingPolicy::default();
760 policy.tile_overscan_rows = 1;
761 let vertical_plan = scroller_canvas_plan_with_policy(
762 1,
763 "test".to_string(),
764 600.0,
765 10_000.0,
766 600.0,
767 600.0,
768 0.0,
769 0.0,
770 1.0,
771 policy,
772 );
773 let horizontal_plan = scroller_canvas_plan_with_policy(
774 1,
775 "test".to_string(),
776 10_000.0,
777 600.0,
778 600.0,
779 600.0,
780 0.0,
781 0.0,
782 1.0,
783 policy,
784 );
785
786 assert!(vertical_plan.surfaces.len() > horizontal_plan.surfaces.len());
787 }
788
789 #[test]
790 fn max_surfaces_clamps_requested_tile_window() {
791 let mut policy = ScrollerTilingPolicy::default();
792 policy.tile_overscan_rows = 2;
793 policy.max_surfaces_per_layer = Some(3);
794 let plan = scroller_canvas_plan_with_policy(
795 1,
796 "test".to_string(),
797 600.0,
798 20_000.0,
799 600.0,
800 600.0,
801 0.0,
802 5_000.0,
803 1.0,
804 policy,
805 );
806
807 assert!(plan.surfaces.len() <= 3);
808 }
809
810 #[test]
811 fn overlapping_vertical_tile_keeps_physical_slot_key() {
812 let policy = ScrollerTilingPolicy::default();
813 let first = scroller_canvas_plan_with_policy(
814 1,
815 "test".to_string(),
816 600.0,
817 20_000.0,
818 600.0,
819 600.0,
820 0.0,
821 0.0,
822 1.0,
823 policy,
824 );
825 let second = scroller_canvas_plan_with_policy(
826 1,
827 "test".to_string(),
828 600.0,
829 20_000.0,
830 600.0,
831 600.0,
832 0.0,
833 3_200.0,
834 1.0,
835 policy,
836 );
837
838 let overlapping_surface = first
839 .surfaces
840 .iter()
841 .find(|surface| surface.top > 0.0)
842 .expect("first plan should have a second row");
843 let matching_surface = second
844 .surfaces
845 .iter()
846 .find(|surface| surface.top == overlapping_surface.top)
847 .expect("second plan should keep the overlapping row warm");
848
849 assert_eq!(matching_surface.key, overlapping_surface.key);
850 assert_eq!(matching_surface.id, overlapping_surface.id);
851 }
852
853 #[test]
854 fn replay_priority_prefers_visible_tiles() {
855 let policy = ScrollerTilingPolicy::default();
856 let plan = scroller_canvas_plan_with_policy(
857 1,
858 "test".to_string(),
859 600.0,
860 20_000.0,
861 600.0,
862 600.0,
863 0.0,
864 5_000.0,
865 1.0,
866 policy,
867 );
868
869 let visible_tiles = plan
870 .surfaces
871 .iter()
872 .filter(|surface| surface.top + surface.height >= 5_000.0 && surface.top <= 5_600.0)
873 .count();
874 assert!(visible_tiles > 0);
875 assert!(plan
876 .surfaces
877 .iter()
878 .filter(|surface| surface.top + surface.height >= 5_000.0 && surface.top <= 5_600.0)
879 .all(|surface| surface.replay_priority == 0));
880 assert!(plan
881 .surfaces
882 .iter()
883 .any(|surface| surface.replay_priority > 0));
884 }
885}