Skip to main content

webrender/
image_tiling.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use crate::api::TileSize;
6use crate::api::units::*;
7use crate::segment::EdgeMask;
8use euclid::{point2, size2};
9use std::i32;
10use std::ops::Range;
11
12/// If repetitions are far enough apart that only one is within
13/// the primitive rect, then we can simplify the parameters and
14/// treat the primitive as not repeated.
15/// This can let us avoid unnecessary work later to handle some
16/// of the parameters.
17pub fn simplify_repeated_primitive(
18    stretch_size: &LayoutSize,
19    tile_spacing: &mut LayoutSize,
20    prim_rect: &mut LayoutRect,
21) {
22    let stride = *stretch_size + *tile_spacing;
23
24    if stride.width >= prim_rect.width() {
25        tile_spacing.width = 0.0;
26        prim_rect.max.x = f32::min(prim_rect.min.x + stretch_size.width, prim_rect.max.x);
27    }
28    if stride.height >= prim_rect.height() {
29        tile_spacing.height = 0.0;
30        prim_rect.max.y = f32::min(prim_rect.min.y + stretch_size.height, prim_rect.max.y);
31    }
32}
33
34pub struct Repetition {
35    pub origin: LayoutPoint,
36    pub edge_flags: EdgeMask,
37}
38
39pub struct RepetitionIterator {
40    current_x: i32,
41    x_count: i32,
42    current_y: i32,
43    y_count: i32,
44    row_flags: EdgeMask,
45    current_origin: LayoutPoint,
46    initial_origin: LayoutPoint,
47    stride: LayoutSize,
48}
49
50impl Iterator for RepetitionIterator {
51    type Item = Repetition;
52
53    fn next(&mut self) -> Option<Self::Item> {
54        if self.current_x >= self.x_count {
55            self.current_y += 1;
56            if self.current_y >= self.y_count {
57                return None;
58            }
59            self.current_x = 0;
60
61            self.row_flags = EdgeMask::empty();
62            if self.current_y == self.y_count - 1 {
63                self.row_flags |= EdgeMask::BOTTOM;
64            }
65
66            self.current_origin.x = self.initial_origin.x;
67            self.current_origin.y += self.stride.height;
68        }
69
70        let mut edge_flags = self.row_flags;
71        if self.current_x == 0 {
72            edge_flags |= EdgeMask::LEFT;
73        }
74
75        if self.current_x == self.x_count - 1 {
76            edge_flags |= EdgeMask::RIGHT;
77        }
78
79        let repetition = Repetition {
80            origin: self.current_origin,
81            edge_flags,
82        };
83
84        self.current_origin.x += self.stride.width;
85        self.current_x += 1;
86
87        Some(repetition)
88    }
89}
90
91pub fn repetitions(
92    prim_rect: &LayoutRect,
93    visible_rect: &LayoutRect,
94    stride: LayoutSize,
95) -> RepetitionIterator {
96    let visible_rect = match prim_rect.intersection(&visible_rect) {
97        Some(rect) => rect,
98        None => {
99            return RepetitionIterator {
100                current_origin: LayoutPoint::zero(),
101                initial_origin: LayoutPoint::zero(),
102                current_x: 0,
103                current_y: 0,
104                x_count: 0,
105                y_count: 0,
106                stride,
107                row_flags: EdgeMask::empty(),
108            }
109        }
110    };
111
112    assert!(stride.width > 0.0);
113    assert!(stride.height > 0.0);
114
115    let nx = if visible_rect.min.x > prim_rect.min.x {
116        f32::floor((visible_rect.min.x - prim_rect.min.x) / stride.width)
117    } else {
118        0.0
119    };
120
121    let ny = if visible_rect.min.y > prim_rect.min.y {
122        f32::floor((visible_rect.min.y - prim_rect.min.y) / stride.height)
123    } else {
124        0.0
125    };
126
127    let x0 = prim_rect.min.x + nx * stride.width;
128    let y0 = prim_rect.min.y + ny * stride.height;
129
130    let x_most = visible_rect.max.x;
131    let y_most = visible_rect.max.y;
132
133    let mut x_count = f32::ceil((x_most - x0) / stride.width);
134    let mut y_count = f32::ceil((y_most - y0) / stride.height);
135
136    // Sanity-check that we don't have anything that may cause the iterator
137    // to run indefinitely.
138    let valid = x_count.is_finite()
139        & y_count.is_finite()
140        & stride.is_finite();
141
142    if !valid {
143        x_count = 0.0;
144        y_count = 0.0;
145    }
146
147
148    let mut row_flags = EdgeMask::TOP;
149    if y_count as i32 == 1 {
150        row_flags |= EdgeMask::BOTTOM;
151    }
152
153
154    RepetitionIterator {
155        current_origin: LayoutPoint::new(x0, y0),
156        initial_origin: LayoutPoint::new(x0, y0),
157        current_x: 0,
158        current_y: 0,
159        x_count: x_count as i32,
160        y_count: y_count as i32,
161        row_flags,
162        stride,
163    }
164}
165
166#[derive(Debug)]
167pub struct Tile {
168    pub rect: LayoutRect,
169    pub offset: TileOffset,
170    pub edge_flags: EdgeMask,
171}
172
173#[derive(Debug)]
174pub struct TileIteratorExtent {
175    /// Range of visible tiles to iterate over in number of tiles.
176    tile_range: Range<i32>,
177    /// Range of tiles of the full image including tiles that are culled out.
178    image_tiles: Range<i32>,
179    /// Size of the first tile in layout space.
180    first_tile_layout_size: f32,
181    /// Size of the last tile in layout space.
182    last_tile_layout_size: f32,
183    /// Position of blob point (0, 0) in layout space.
184    layout_tiling_origin: f32,
185    /// Position of the top-left corner of the primitive rect in layout space.
186    layout_prim_start: f32,
187}
188
189#[derive(Debug)]
190pub struct TileIterator {
191    current_tile: TileOffset,
192    x: TileIteratorExtent,
193    y: TileIteratorExtent,
194    regular_tile_size: LayoutSize,
195}
196
197impl Iterator for TileIterator {
198    type Item = Tile;
199
200    fn next(&mut self) -> Option<Self::Item> {
201        // If we reach the end of a row, reset to the beginning of the next row.
202        if self.current_tile.x >= self.x.tile_range.end {
203            self.current_tile.y += 1;
204            self.current_tile.x = self.x.tile_range.start;
205        }
206
207        // Stop iterating if we reach the last tile. We may start here if there
208        // were no tiles to iterate over.
209        if self.current_tile.x >= self.x.tile_range.end || self.current_tile.y >= self.y.tile_range.end {
210            return None;
211        }
212
213        let tile_offset = self.current_tile;
214
215        let mut segment_rect = LayoutRect::from_origin_and_size(
216            LayoutPoint::new(
217                self.x.layout_tiling_origin + tile_offset.x as f32 * self.regular_tile_size.width,
218                self.y.layout_tiling_origin + tile_offset.y as f32 * self.regular_tile_size.height,
219            ),
220            self.regular_tile_size,
221        );
222
223        let mut edge_flags = EdgeMask::empty();
224
225        if tile_offset.x == self.x.image_tiles.start {
226            edge_flags |= EdgeMask::LEFT;
227            segment_rect.min.x = self.x.layout_prim_start;
228            // TODO(nical) we may not need to do this.
229            segment_rect.max.x = segment_rect.min.x + self.x.first_tile_layout_size;
230        }
231        if tile_offset.x == self.x.image_tiles.end - 1 {
232            edge_flags |= EdgeMask::RIGHT;
233            segment_rect.max.x = segment_rect.min.x + self.x.last_tile_layout_size;
234        }
235
236        if tile_offset.y == self.y.image_tiles.start {
237            segment_rect.min.y = self.y.layout_prim_start;
238            segment_rect.max.y = segment_rect.min.y + self.y.first_tile_layout_size;
239            edge_flags |= EdgeMask::TOP;
240        }
241        if tile_offset.y == self.y.image_tiles.end - 1 {
242            segment_rect.max.y = segment_rect.min.y + self.y.last_tile_layout_size;
243            edge_flags |= EdgeMask::BOTTOM;
244        }
245
246        assert!(tile_offset.y < self.y.tile_range.end);
247        let tile = Tile {
248            rect: segment_rect,
249            offset: tile_offset,
250            edge_flags,
251        };
252
253        self.current_tile.x += 1;
254
255        Some(tile)
256    }
257}
258
259pub fn tiles(
260    prim_rect: &LayoutRect,
261    visible_rect: &LayoutRect,
262    image_rect: &DeviceIntRect,
263    device_tile_size: i32,
264) -> TileIterator {
265    // The image resource is tiled. We have to generate an image primitive
266    // for each tile.
267    // We need to do this because the image is broken up into smaller tiles in the texture
268    // cache and the image shader is not able to work with this type of sparse representation.
269
270    // The tiling logic works as follows:
271    //
272    //  +-#################-+  -+
273    //  | #//|    |    |//# |   | image size
274    //  | #//|    |    |//# |   |
275    //  +-#--+----+----+--#-+   |  -+
276    //  | #//|    |    |//# |   |   | regular tile size
277    //  | #//|    |    |//# |   |   |
278    //  +-#--+----+----+--#-+   |  -+-+
279    //  | #//|////|////|//# |   |     | "leftover" height
280    //  | ################# |  -+  ---+
281    //  +----+----+----+----+
282    //
283    // In the ascii diagram above, a large image is split into tiles of almost regular size.
284    // The tiles on the edges (hatched in the diagram) can be smaller than the regular tiles
285    // and are handled separately in the code (we'll call them boundary tiles).
286    //
287    // Each generated segment corresponds to a tile in the texture cache, with the
288    // assumption that the boundary tiles are sized to fit their own irregular size in the
289    // texture cache.
290    //
291    // Because we can have very large virtual images we iterate over the visible portion of
292    // the image in layer space instead of iterating over all device tiles.
293
294    let visible_rect = match prim_rect.intersection(&visible_rect) {
295        Some(rect) => rect,
296        None => {
297            return TileIterator {
298                current_tile: TileOffset::zero(),
299                x: TileIteratorExtent {
300                    tile_range: 0..0,
301                    image_tiles: 0..0,
302                    first_tile_layout_size: 0.0,
303                    last_tile_layout_size: 0.0,
304                    layout_tiling_origin: 0.0,
305                    layout_prim_start: prim_rect.min.x,
306                },
307                y: TileIteratorExtent {
308                    tile_range: 0..0,
309                    image_tiles: 0..0,
310                    first_tile_layout_size: 0.0,
311                    last_tile_layout_size: 0.0,
312                    layout_tiling_origin: 0.0,
313                    layout_prim_start: prim_rect.min.y,
314                },
315                regular_tile_size: LayoutSize::zero(),
316            }
317        }
318    };
319
320    // Size of regular tiles in layout space.
321    let layout_tile_size = LayoutSize::new(
322        device_tile_size as f32 / image_rect.width() as f32 * prim_rect.width(),
323        device_tile_size as f32 / image_rect.height() as f32 * prim_rect.height(),
324    );
325
326    // The decomposition logic is exactly the same on each axis so we reduce
327    // this to a 1-dimensional problem in an attempt to make the code simpler.
328
329    let x_extent = tiles_1d(
330        layout_tile_size.width,
331        visible_rect.x_range(),
332        prim_rect.min.x,
333        image_rect.x_range(),
334        device_tile_size,
335    );
336
337    let y_extent = tiles_1d(
338        layout_tile_size.height,
339        visible_rect.y_range(),
340        prim_rect.min.y,
341        image_rect.y_range(),
342        device_tile_size,
343    );
344
345    TileIterator {
346        current_tile: point2(
347            x_extent.tile_range.start,
348            y_extent.tile_range.start,
349        ),
350        x: x_extent,
351        y: y_extent,
352        regular_tile_size: layout_tile_size,
353    }
354}
355
356/// Decompose tiles along an arbitrary axis.
357///
358/// This does most of the heavy lifting needed for `tiles` but in a single dimension for
359/// the sake of simplicity since the problem is independent on the x and y axes.
360fn tiles_1d(
361    layout_tile_size: f32,
362    layout_visible_range: Range<f32>,
363    layout_prim_start: f32,
364    device_image_range: Range<i32>,
365    device_tile_size: i32,
366) -> TileIteratorExtent {
367    // A few sanity checks.
368    debug_assert!(layout_tile_size > 0.0);
369    debug_assert!(layout_visible_range.end >= layout_visible_range.start);
370    debug_assert!(device_image_range.end > device_image_range.start);
371    debug_assert!(device_tile_size > 0);
372
373    // Sizes of the boundary tiles in pixels.
374    let first_tile_device_size = first_tile_size_1d(&device_image_range, device_tile_size);
375    let last_tile_device_size = last_tile_size_1d(&device_image_range, device_tile_size);
376
377    // [start..end[ Range of tiles of this row/column (in number of tiles) without
378    // taking culling into account.
379    let image_tiles = tile_range_1d(&device_image_range, device_tile_size);
380
381    // Layout offset of tile (0, 0) with respect to the top-left corner of the display item.
382    let layout_offset = device_image_range.start as f32 * layout_tile_size / device_tile_size as f32;
383    // Position in layout space of tile (0, 0).
384    let layout_tiling_origin = layout_prim_start - layout_offset;
385
386    // [start..end[ Range of the visible tiles (because of culling).
387    let visible_tiles_start = f32::floor((layout_visible_range.start - layout_tiling_origin) / layout_tile_size) as i32;
388    let visible_tiles_end = f32::ceil((layout_visible_range.end - layout_tiling_origin) / layout_tile_size) as i32;
389
390    // Combine the above two to get the tiles in the image that are visible this frame.
391    let mut tiles_start = i32::max(image_tiles.start, visible_tiles_start);
392    let tiles_end = i32::min(image_tiles.end, visible_tiles_end);
393    if tiles_start > tiles_end {
394        tiles_start = tiles_end;
395    }
396
397    // The size in layout space of the boundary tiles.
398    let first_tile_layout_size = if tiles_start == image_tiles.start {
399        first_tile_device_size as f32 * layout_tile_size / device_tile_size as f32
400    } else {
401        // boundary tile was culled out, so the new first tile is a regularly sized tile.
402        layout_tile_size
403    };
404
405    // Same here.
406    let last_tile_layout_size = if tiles_end == image_tiles.end {
407        last_tile_device_size as f32 * layout_tile_size / device_tile_size as f32
408    } else {
409        layout_tile_size
410    };
411
412    TileIteratorExtent {
413        tile_range: tiles_start..tiles_end,
414        image_tiles,
415        first_tile_layout_size,
416        last_tile_layout_size,
417        layout_tiling_origin,
418        layout_prim_start,
419    }
420}
421
422/// Compute the range of tiles (in number of tiles) that intersect the provided
423/// image range (in pixels) in an arbitrary dimension.
424///
425/// ```ignore
426///
427///         0
428///         :
429///   #-+---+---+---+---+---+--#
430///   # |   |   |   |   |   |  #
431///   #-+---+---+---+---+---+--#
432/// ^       :                   ^
433///
434///  +------------------------+  image_range
435///        +---+  regular_tile_size
436///
437/// ```
438fn tile_range_1d(
439    image_range: &Range<i32>,
440    regular_tile_size: i32,
441) -> Range<i32> {
442    // Integer division truncates towards zero so with negative values if the first/last
443    // tile isn't a full tile we can get offset by one which we account for here.
444
445    let mut start = image_range.start / regular_tile_size;
446    if image_range.start % regular_tile_size < 0 {
447        start -= 1;
448    }
449
450    let mut end = image_range.end / regular_tile_size;
451    if image_range.end % regular_tile_size > 0 {
452        end += 1;
453    }
454
455    start..end
456}
457
458// Sizes of the first boundary tile in pixels.
459//
460// It can be smaller than the regular tile size if the image is not a multiple
461// of the regular tile size.
462fn first_tile_size_1d(
463    image_range: &Range<i32>,
464    regular_tile_size: i32,
465) -> i32 {
466    // We have to account for how the % operation behaves for negative values.
467    let image_size = image_range.end - image_range.start;
468    i32::min(
469        match image_range.start % regular_tile_size {
470            //             .      #------+------+      .
471            //             .      #//////|      |      .
472            0 => regular_tile_size,
473            //   (zero) -> 0      .   #--+------+      .
474            //             .      .   #//|      |      .
475            // %(m):                  ~~>
476            m if m > 0 => regular_tile_size - m,
477            //             .      .   #--+------+      0 <- (zero)
478            //             .      .   #//|      |      .
479            // %(m):                  <~~
480            m => -m,
481        },
482        image_size
483    )
484}
485
486// Sizes of the last boundary tile in pixels.
487//
488// It can be smaller than the regular tile size if the image is not a multiple
489// of the regular tile size.
490fn last_tile_size_1d(
491    image_range: &Range<i32>,
492    regular_tile_size: i32,
493) -> i32 {
494    // We have to account for how the modulo operation behaves for negative values.
495    let image_size = image_range.end - image_range.start;
496    i32::min(
497        match image_range.end % regular_tile_size {
498            //                    +------+------#      .
499            // tiles:      .      |      |//////#      .
500            0 => regular_tile_size,
501            //             .      +------+--#   .      0 <- (zero)
502            //             .      |      |//#   .      .
503            // modulo (m):                   <~~
504            m if m < 0 => regular_tile_size + m,
505            //   (zero) -> 0      +------+--#   .      .
506            //             .      |      |//#   .      .
507            // modulo (m):                ~~>
508            m => m,
509        },
510        image_size,
511    )
512}
513
514pub fn compute_tile_rect(
515    image_rect: &DeviceIntRect,
516    regular_tile_size: TileSize,
517    tile: TileOffset,
518) -> DeviceIntRect {
519    let regular_tile_size = regular_tile_size as i32;
520    DeviceIntRect::from_origin_and_size(
521        point2(
522            compute_tile_origin_1d(image_rect.x_range(), regular_tile_size, tile.x as i32),
523            compute_tile_origin_1d(image_rect.y_range(), regular_tile_size, tile.y as i32),
524        ),
525        size2(
526            compute_tile_size_1d(image_rect.x_range(), regular_tile_size, tile.x as i32),
527            compute_tile_size_1d(image_rect.y_range(), regular_tile_size, tile.y as i32),
528        ),
529    )
530}
531
532fn compute_tile_origin_1d(
533    img_range: Range<i32>,
534    regular_tile_size: i32,
535    tile_offset: i32,
536) -> i32 {
537    let tile_range = tile_range_1d(&img_range, regular_tile_size);
538    if tile_offset == tile_range.start {
539        img_range.start
540    } else {
541        tile_offset * regular_tile_size
542    }
543}
544
545// Compute the width and height in pixels of a tile depending on its position in the image.
546pub fn compute_tile_size(
547    image_rect: &DeviceIntRect,
548    regular_tile_size: TileSize,
549    tile: TileOffset,
550) -> DeviceIntSize {
551    let regular_tile_size = regular_tile_size as i32;
552    size2(
553        compute_tile_size_1d(image_rect.x_range(), regular_tile_size, tile.x as i32),
554        compute_tile_size_1d(image_rect.y_range(), regular_tile_size, tile.y as i32),
555    )
556}
557
558fn compute_tile_size_1d(
559    img_range: Range<i32>,
560    regular_tile_size: i32,
561    tile_offset: i32,
562) -> i32 {
563    let tile_range = tile_range_1d(&img_range, regular_tile_size);
564
565    // Most tiles are going to have base_size as width and height,
566    // except for tiles around the edges that are shrunk to fit the image data.
567    let actual_size = if tile_offset == tile_range.start {
568        first_tile_size_1d(&img_range, regular_tile_size)
569    } else if tile_offset == tile_range.end - 1 {
570        last_tile_size_1d(&img_range, regular_tile_size)
571    } else {
572        regular_tile_size
573    };
574
575    assert!(actual_size > 0);
576
577    actual_size
578}
579
580pub fn compute_tile_range(
581    visible_area: &DeviceIntRect,
582    tile_size: u16,
583) -> TileRange {
584    let tile_size = tile_size as i32;
585    let x_range = tile_range_1d(&visible_area.x_range(), tile_size);
586    let y_range = tile_range_1d(&visible_area.y_range(), tile_size);
587
588    TileRange {
589        min: point2(x_range.start, y_range.start),
590        max: point2(x_range.end, y_range.end),
591    }
592}
593
594pub fn for_each_tile_in_range(
595    range: &TileRange,
596    mut callback: impl FnMut(TileOffset),
597) {
598    for y in range.y_range() {
599        for x in range.x_range() {
600            callback(point2(x, y));
601        }
602    }
603}
604
605pub fn compute_valid_tiles_if_bounds_change(
606    prev_rect: &DeviceIntRect,
607    new_rect: &DeviceIntRect,
608    tile_size: u16,
609) -> Option<TileRange> {
610    let intersection = match prev_rect.intersection(new_rect) {
611        Some(rect) => rect,
612        None => {
613            return Some(TileRange::zero());
614        }
615    };
616
617    let left = prev_rect.min.x != new_rect.min.x;
618    let right = prev_rect.max.x != new_rect.max.x;
619    let top = prev_rect.min.y != new_rect.min.y;
620    let bottom = prev_rect.max.y != new_rect.max.y;
621
622    if !left && !right && !top && !bottom {
623        // Bounds have not changed.
624        return None;
625    }
626
627    let tw = 1.0 / (tile_size as f32);
628    let th = 1.0 / (tile_size as f32);
629
630    let tiles = intersection
631        .cast::<f32>()
632        .scale(tw, th);
633
634    let min_x = if left { f32::ceil(tiles.min.x) } else { f32::floor(tiles.min.x) };
635    let min_y = if top { f32::ceil(tiles.min.y) } else { f32::floor(tiles.min.y) };
636    let max_x = if right { f32::floor(tiles.max.x) } else { f32::ceil(tiles.max.x) };
637    let max_y = if bottom { f32::floor(tiles.max.y) } else { f32::ceil(tiles.max.y) };
638
639    Some(TileRange {
640        min: point2(min_x as i32, min_y as i32),
641        max: point2(max_x as i32, max_y as i32),
642    })
643}
644
645#[cfg(test)]
646mod tests {
647    use super::*;
648    use std::collections::HashSet;
649    use euclid::rect;
650
651    // this checks some additional invariants
652    fn checked_for_each_tile(
653        prim_rect: &LayoutRect,
654        visible_rect: &LayoutRect,
655        device_image_rect: &DeviceIntRect,
656        device_tile_size: i32,
657        callback: &mut dyn FnMut(&LayoutRect, TileOffset, EdgeMask),
658    ) {
659        let mut coverage = LayoutRect::zero();
660        let mut seen_tiles = HashSet::new();
661        for tile in tiles(
662            prim_rect,
663            visible_rect,
664            device_image_rect,
665            device_tile_size,
666        ) {
667            // make sure we don't get sent duplicate tiles
668            assert!(!seen_tiles.contains(&tile.offset));
669            seen_tiles.insert(tile.offset);
670            coverage = coverage.union(&tile.rect);
671            assert!(prim_rect.contains_box(&tile.rect));
672            callback(&tile.rect, tile.offset, tile.edge_flags);
673        }
674        assert!(prim_rect.contains_box(&coverage));
675        assert!(coverage.contains_box(&visible_rect.intersection(&prim_rect).unwrap_or(LayoutRect::zero())));
676    }
677
678    #[test]
679    fn basic() {
680        let mut count = 0;
681        checked_for_each_tile(&rect(0., 0., 1000., 1000.).to_box2d(),
682            &rect(75., 75., 400., 400.).to_box2d(),
683            &rect(0, 0, 400, 400).to_box2d(),
684            36,
685            &mut |_tile_rect, _tile_offset, _tile_flags| {
686                count += 1;
687            },
688        );
689        assert_eq!(count, 36);
690    }
691
692    #[test]
693    fn empty() {
694        let mut count = 0;
695        checked_for_each_tile(&rect(0., 0., 74., 74.).to_box2d(),
696            &rect(75., 75., 400., 400.).to_box2d(),
697            &rect(0, 0, 400, 400).to_box2d(),
698            36,
699            &mut |_tile_rect, _tile_offset, _tile_flags| {
700              count += 1;
701            },
702        );
703        assert_eq!(count, 0);
704    }
705
706    #[test]
707    fn test_tiles_1d() {
708        // Exactly one full tile at positive offset.
709        let result = tiles_1d(64.0, -10000.0..10000.0, 0.0, 0..64, 64);
710        assert_eq!(result.tile_range.start, 0);
711        assert_eq!(result.tile_range.end, 1);
712        assert_eq!(result.first_tile_layout_size, 64.0);
713        assert_eq!(result.last_tile_layout_size, 64.0);
714
715        // Exactly one full tile at negative offset.
716        let result = tiles_1d(64.0, -10000.0..10000.0, -64.0, -64..0, 64);
717        assert_eq!(result.tile_range.start, -1);
718        assert_eq!(result.tile_range.end, 0);
719        assert_eq!(result.first_tile_layout_size, 64.0);
720        assert_eq!(result.last_tile_layout_size, 64.0);
721
722        // Two full tiles at negative and positive offsets.
723        let result = tiles_1d(64.0, -10000.0..10000.0, -64.0, -64..64, 64);
724        assert_eq!(result.tile_range.start, -1);
725        assert_eq!(result.tile_range.end, 1);
726        assert_eq!(result.first_tile_layout_size, 64.0);
727        assert_eq!(result.last_tile_layout_size, 64.0);
728
729        // One partial tile at positive offset, non-zero origin, culled out.
730        let result = tiles_1d(64.0, -100.0..10.0, 64.0, 64..310, 64);
731        assert_eq!(result.tile_range.start, result.tile_range.end);
732
733        // Two tiles at negative and positive offsets, one of which is culled out.
734        // The remaining tile is partially culled but it should still generate a full tile.
735        let result = tiles_1d(64.0, 10.0..10000.0, -64.0, -64..64, 64);
736        assert_eq!(result.tile_range.start, 0);
737        assert_eq!(result.tile_range.end, 1);
738        assert_eq!(result.first_tile_layout_size, 64.0);
739        assert_eq!(result.last_tile_layout_size, 64.0);
740        let result = tiles_1d(64.0, -10000.0..-10.0, -64.0, -64..64, 64);
741        assert_eq!(result.tile_range.start, -1);
742        assert_eq!(result.tile_range.end, 0);
743        assert_eq!(result.first_tile_layout_size, 64.0);
744        assert_eq!(result.last_tile_layout_size, 64.0);
745
746        // Stretched tile in layout space device tile size is 64 and layout tile size is 128.
747        // So the resulting tile sizes in layout space should be multiplied by two.
748        let result = tiles_1d(128.0, -10000.0..10000.0, -64.0, -64..32, 64);
749        assert_eq!(result.tile_range.start, -1);
750        assert_eq!(result.tile_range.end, 1);
751        assert_eq!(result.first_tile_layout_size, 128.0);
752        assert_eq!(result.last_tile_layout_size, 64.0);
753
754        // Two visible tiles (the rest is culled out).
755        let result = tiles_1d(10.0, 0.0..20.0, 0.0, 0..64, 64);
756        assert_eq!(result.tile_range.start, 0);
757        assert_eq!(result.tile_range.end, 1);
758        assert_eq!(result.first_tile_layout_size, 10.0);
759        assert_eq!(result.last_tile_layout_size, 10.0);
760
761        // Two visible tiles at negative layout offsets (the rest is culled out).
762        let result = tiles_1d(10.0, -20.0..0.0, -20.0, 0..64, 64);
763        assert_eq!(result.tile_range.start, 0);
764        assert_eq!(result.tile_range.end, 1);
765        assert_eq!(result.first_tile_layout_size, 10.0);
766        assert_eq!(result.last_tile_layout_size, 10.0);
767    }
768
769    #[test]
770    fn test_tile_range_1d() {
771        assert_eq!(tile_range_1d(&(0..256), 256), 0..1);
772        assert_eq!(tile_range_1d(&(0..257), 256), 0..2);
773        assert_eq!(tile_range_1d(&(-1..257), 256), -1..2);
774        assert_eq!(tile_range_1d(&(-256..256), 256), -1..1);
775        assert_eq!(tile_range_1d(&(-20..-10), 6), -4..-1);
776        assert_eq!(tile_range_1d(&(20..100), 256), 0..1);
777    }
778
779    #[test]
780    fn test_first_last_tile_size_1d() {
781        assert_eq!(first_tile_size_1d(&(0..10), 64), 10);
782        assert_eq!(first_tile_size_1d(&(-20..0), 64), 20);
783
784        assert_eq!(last_tile_size_1d(&(0..10), 64), 10);
785        assert_eq!(last_tile_size_1d(&(-20..0), 64), 20);
786    }
787
788    #[test]
789    fn doubly_partial_tiles() {
790        // In the following tests the image is a single tile and none of the sides of the tile
791        // align with the tile grid.
792        // This can only happen when we have a single non-aligned partial tile and no regular
793        // tiles.
794        assert_eq!(first_tile_size_1d(&(300..310), 64), 10);
795        assert_eq!(first_tile_size_1d(&(-20..-10), 64), 10);
796
797        assert_eq!(last_tile_size_1d(&(300..310), 64), 10);
798        assert_eq!(last_tile_size_1d(&(-20..-10), 64), 10);
799
800
801        // One partial tile at positve offset, non-zero origin.
802        let result = tiles_1d(64.0, -10000.0..10000.0, 0.0, 300..310, 64);
803        assert_eq!(result.tile_range.start, 4);
804        assert_eq!(result.tile_range.end, 5);
805        assert_eq!(result.first_tile_layout_size, 10.0);
806        assert_eq!(result.last_tile_layout_size, 10.0);
807    }
808
809    #[test]
810    fn smaller_than_tile_size_at_origin() {
811        let r = compute_tile_rect(
812            &rect(0, 0, 80, 80).to_box2d(),
813            256,
814            point2(0, 0),
815        );
816
817        assert_eq!(r, rect(0, 0, 80, 80).to_box2d());
818    }
819
820    #[test]
821    fn smaller_than_tile_size_with_offset() {
822        let r = compute_tile_rect(
823            &rect(20, 20, 80, 80).to_box2d(),
824            256,
825            point2(0, 0),
826        );
827
828        assert_eq!(r, rect(20, 20, 80, 80).to_box2d());
829    }
830}