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