Skip to main content

webrender/invalidation/
vert_buffer.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
5//! Quantized raster-space vertex buffer for output-space tile invalidation.
6//!
7//! Each primitive and clip gets its transformed, raster-space corners stored
8//! here as quantized i32 values. The tile descriptor stores a VertRange
9//! referencing into this buffer instead of a picture-space prim_clip_box or
10//! spatial node dependency.
11
12use api::units::*;
13use crate::spatial_tree::{SpatialTree, SpatialNodeIndex, CoordinateSpaceMapping};
14use crate::util::{MatrixHelpers, ScaleOffset};
15
16/// Sub-pixel quantization scale: quarter-pixel precision.
17pub const VERT_QUANTIZE_SCALE: f32 = 4.0;
18
19pub fn quantize(v: f32) -> i32 {
20    (v * VERT_QUANTIZE_SCALE).round() as i32
21}
22
23/// A reference into a per-tile vert_data buffer: offset (in i32 elements) and count.
24/// count is 4 for an axis-aligned rect (2 corners × 2 coords), 8 for a
25/// non-axis-aligned quad (4 corners × 2 coords), or 16 for a transform fingerprint
26/// emitted when a perspective-projected rect crosses the camera plane (4 corners ×
27/// homogeneous (x, y, z, w) coords).
28#[derive(Copy, Clone, Debug, Default, PartialEq, peek_poke::PeekPoke)]
29#[cfg_attr(feature = "capture", derive(serde::Serialize))]
30#[cfg_attr(feature = "replay", derive(serde::Deserialize))]
31pub struct VertRange {
32    pub offset: u32,
33    pub count: u32,
34}
35
36impl VertRange {
37    pub const INVALID: VertRange = VertRange { offset: 0, count: 0 };
38
39    pub fn is_valid(self) -> bool {
40        self.count > 0
41    }
42}
43
44/// Persistent per-tile-cache scratch and transform cache for computing
45/// raster-space corners.
46///
47/// Lives on TileCacheInstance and provides two optimisations:
48///
49/// 1. **Amortised unquantized scratch**: `unquantized` is never dropped between
50///    frames, so the heap allocation is paid once after warmup.
51///
52/// 2. **Spatial-node transform cache**: the relative transform from
53///    `prim_spatial_node` → `tile_cache_spatial_node` is cached so that
54///    consecutive primitives in the same scroll frame avoid repeated
55///    `get_relative_transform` calls.
56pub struct CornersCache {
57    /// Amortised scratch for unquantized corners.
58    /// Cleared once before computing prim + coverage + clips for each primitive.
59    unquantized: Vec<RasterPoint>,
60
61    /// The primitive spatial node for which `cached_mapping` was computed.
62    /// `None` means the cache is cold (reset at frame start).
63    cached_node: Option<SpatialNodeIndex>,
64
65    /// Cached mapping for `cached_node`. Valid only when
66    /// `cached_node == Some(current prim_spatial_node)`.
67    cached_mapping: CoordinateSpaceMapping<LayoutPixel, LayoutPixel>,
68}
69
70impl CornersCache {
71    pub fn new() -> Self {
72        CornersCache {
73            unquantized: Vec::new(),
74            cached_node: None,
75            cached_mapping: CoordinateSpaceMapping::Local,
76        }
77    }
78
79    /// Reset the transform cache. Call once at the start of each frame's
80    /// dependency update, before any primitives are processed.
81    pub fn pre_update(&mut self) {
82        self.cached_node = None;
83    }
84
85    /// Clear the unquantized scratch. Call once before computing corners for a
86    /// single primitive (before prim rect, coverage rect and all clips).
87    pub fn clear_scratch(&mut self) {
88        self.unquantized.clear();
89    }
90
91    /// Compute unquantized raster-space corners for `local_rect` and append
92    /// them to the scratch buffer. Returns a VertRange into the scratch, or
93    /// VertRange::INVALID if the transform is non-invertible.
94    ///
95    /// The relative transform for `prim_spatial_node` is cached across calls:
96    /// if the same node is passed as the previous call, `get_relative_transform`
97    /// is not recomputed.
98    pub fn compute_to_scratch(
99        &mut self,
100        local_rect: LayoutRect,
101        prim_spatial_node: SpatialNodeIndex,
102        tile_cache_spatial_node: SpatialNodeIndex,
103        local_to_raster: ScaleOffset,
104        spatial_tree: &SpatialTree,
105    ) -> VertRange {
106        if Some(prim_spatial_node) != self.cached_node {
107            let mapping = spatial_tree.get_relative_transform(
108                prim_spatial_node,
109                tile_cache_spatial_node,
110            );
111            self.cached_mapping = match mapping {
112                CoordinateSpaceMapping::ScaleOffset(ref so) if so.is_reflection() => {
113                    CoordinateSpaceMapping::Transform(so.to_transform())
114                }
115                other => other,
116            };
117            self.cached_node = Some(prim_spatial_node);
118        }
119        self.append_corners_from_mapping(local_rect, local_to_raster)
120    }
121
122    fn append_corners_from_mapping(
123        &mut self,
124        local_rect: LayoutRect,
125        local_to_raster: ScaleOffset,
126    ) -> VertRange {
127        match &self.cached_mapping {
128            CoordinateSpaceMapping::Local => {
129                let r: RasterRect = local_to_raster.map_rect(&local_rect);
130                let offset = self.unquantized.len() as u32;
131                self.unquantized.push(r.min);
132                self.unquantized.push(r.max);
133                VertRange { offset, count: 2 }
134            }
135            CoordinateSpaceMapping::ScaleOffset(so) => {
136                let r: RasterRect = so.then(&local_to_raster).map_rect(&local_rect);
137                let offset = self.unquantized.len() as u32;
138                self.unquantized.push(r.min);
139                self.unquantized.push(r.max);
140                VertRange { offset, count: 2 }
141            }
142            CoordinateSpaceMapping::Transform(m) => {
143                let raster_m = m.then(&local_to_raster.to_transform::<LayoutPixel, RasterPixel>());
144                let src = [
145                    local_rect.min,
146                    LayoutPoint::new(local_rect.max.x, local_rect.min.y),
147                    LayoutPoint::new(local_rect.min.x, local_rect.max.y),
148                    local_rect.max,
149                ];
150                let offset = self.unquantized.len() as u32;
151
152                // Fast path: no perspective component. transform_point2d can never
153                // fail for one corner while succeeding for another, so we don't need
154                // homogeneous coords or a fingerprint fallback.
155                if !raster_m.has_perspective_component() {
156                    for p in &src {
157                        match raster_m.transform_point2d(*p) {
158                            Some(pt) => self.unquantized.push(pt),
159                            None => {
160                                self.unquantized.truncate(offset as usize);
161                                return VertRange::INVALID;
162                            }
163                        }
164                    }
165                    return VertRange { offset, count: 4 };
166                }
167
168                // Perspective transform: compute homogeneous coords so we can
169                // distinguish "all corners in front of camera" (project them) from
170                // "rect crosses the camera plane" (push a stable fingerprint).
171                let homogens = [
172                    raster_m.transform_point2d_homogeneous(src[0]),
173                    raster_m.transform_point2d_homogeneous(src[1]),
174                    raster_m.transform_point2d_homogeneous(src[2]),
175                    raster_m.transform_point2d_homogeneous(src[3]),
176                ];
177                if homogens.iter().all(|h| h.w > 0.0) {
178                    for h in &homogens {
179                        self.unquantized.push(RasterPoint::new(h.x / h.w, h.y / h.w));
180                    }
181                    VertRange { offset, count: 4 }
182                } else {
183                    // At least one corner is at or behind the camera plane and can't be
184                    // projected to a finite 2D raster point. Falling back to INVALID would
185                    // make compare_prim see equal empty slices on every frame, silently
186                    // hiding transform animations (bug 2036730). Instead, push the
187                    // homogeneous (x, y, z, w) of each corner as a stable per-transform
188                    // fingerprint: equal across frames when the transform is unchanged
189                    // (no over-invalidation), but different when the transform animates
190                    // (correct invalidation). Two RasterPoints encode each corner.
191                    for h in &homogens {
192                        self.unquantized.push(RasterPoint::new(h.x, h.y));
193                        self.unquantized.push(RasterPoint::new(h.z, h.w));
194                    }
195                    VertRange { offset, count: 8 }
196                }
197            }
198        }
199    }
200
201    /// Quantize corners at `scratch_range` from the scratch buffer into `dst`.
202    /// Returns a VertRange into `dst`, or INVALID if `scratch_range` is invalid.
203    pub fn push_verts(&self, scratch_range: VertRange, dst: &mut Vec<i32>) -> VertRange {
204        if !scratch_range.is_valid() {
205            return VertRange::INVALID;
206        }
207        let start = scratch_range.offset as usize;
208        let end = (scratch_range.offset + scratch_range.count) as usize;
209        let corners = &self.unquantized[start..end];
210        debug_assert!(corners.len() == 2 || corners.len() == 4 || corners.len() == 8);
211        let offset = dst.len() as u32;
212        for p in corners {
213            dst.push(quantize(p.x));
214            dst.push(quantize(p.y));
215        }
216        VertRange { offset, count: (corners.len() * 2) as u32 }
217    }
218
219    /// Quantize corners at `scratch_range` into `dst`, clamping to `tile_rect`.
220    /// Returns a VertRange into `dst`, or INVALID if `scratch_range` is invalid.
221    pub fn push_verts_clamped(
222        &self,
223        scratch_range: VertRange,
224        tile_rect: &RasterRect,
225        dst: &mut Vec<i32>,
226    ) -> VertRange {
227        if !scratch_range.is_valid() {
228            return VertRange::INVALID;
229        }
230        let start = scratch_range.offset as usize;
231        let end = (scratch_range.offset + scratch_range.count) as usize;
232        let corners = &self.unquantized[start..end];
233        debug_assert!(corners.len() == 2 || corners.len() == 4 || corners.len() == 8);
234        let offset = dst.len() as u32;
235        if corners.len() == 8 {
236            // Transform fingerprint (homogeneous coords for a perspective-crossing rect).
237            // Clamping these to tile bounds would corrupt the fingerprint, so skip the clamp.
238            for p in corners {
239                dst.push(quantize(p.x));
240                dst.push(quantize(p.y));
241            }
242        } else {
243            for p in corners {
244                dst.push(quantize(p.x.max(tile_rect.min.x).min(tile_rect.max.x)));
245                dst.push(quantize(p.y.max(tile_rect.min.y).min(tile_rect.max.y)));
246            }
247        }
248        VertRange { offset, count: (corners.len() * 2) as u32 }
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use api::units::{LayoutPixel, LayoutPoint, LayoutRect, LayoutTransform};
256    use euclid::Angle;
257
258    /// Build a perspective(d) * rotateX(deg) * translate(0, ty, 0) row-vector matrix.
259    /// Mirrors the CSS-style transform in bug 2036730's repro: a tall rect rotated
260    /// around its top edge so the bottom corners fall past the perspective camera
261    /// plane.
262    fn perspective_rotate_x_translate_y(deg: f32, d: f32, ty: f32) -> LayoutTransform {
263        let translate = LayoutTransform::translation(0.0, ty, 0.0);
264        let rotate = LayoutTransform::rotation(1.0, 0.0, 0.0, Angle::degrees(deg));
265        let mut perspective = LayoutTransform::identity();
266        perspective.m34 = -1.0 / d;
267        translate.then(&rotate).then(&perspective)
268    }
269
270    /// Bug 2036730 regression test. When a perspective-projected rect has corners
271    /// with w <= 0 (the rect crosses the camera plane), compute_to_scratch must
272    /// emit a stable per-transform fingerprint instead of returning INVALID. Two
273    /// frames of an animated transform must therefore produce different scratch
274    /// contents so downstream tile invalidation detects the change. Pre-fix,
275    /// both frames returned VertRange::INVALID and compare_prim saw equal empty
276    /// slices — silently bypassing invalidation.
277    #[test]
278    fn perspective_camera_plane_fingerprint_differs_per_transform() {
279        // 200 x 2000 rect rotated 80deg around the top edge. With perspective
280        // distance 1000, the bottom corners reach z ≈ 2000*sin(80°) ≈ 1969,
281        // which is past the camera and gives w ≈ -0.97.
282        let local_rect = LayoutRect::new(
283            LayoutPoint::new(0.0, 0.0),
284            LayoutPoint::new(200.0, 2000.0),
285        );
286        let local_to_raster = ScaleOffset::identity();
287
288        let mut cache = CornersCache::new();
289
290        cache.cached_mapping = CoordinateSpaceMapping::Transform(
291            perspective_rotate_x_translate_y(80.0, 1000.0, 0.0),
292        );
293        cache.clear_scratch();
294        let r1 = cache.append_corners_from_mapping(local_rect, local_to_raster);
295        assert!(r1.is_valid(), "fingerprint must not collapse to INVALID");
296        assert_eq!(r1.count, 8, "fingerprint encodes 4 corners as 8 RasterPoints");
297        let scratch1: Vec<RasterPoint> = cache.unquantized.clone();
298
299        cache.cached_mapping = CoordinateSpaceMapping::Transform(
300            perspective_rotate_x_translate_y(80.0, 1000.0, -20.0),
301        );
302        cache.clear_scratch();
303        let r2 = cache.append_corners_from_mapping(local_rect, local_to_raster);
304        assert_eq!(r2.count, 8);
305        let scratch2: Vec<RasterPoint> = cache.unquantized.clone();
306
307        assert_ne!(
308            scratch1, scratch2,
309            "different perspective transforms must produce different fingerprints",
310        );
311    }
312
313    /// The same transform applied twice must produce the same fingerprint, so a
314    /// static perspective-crossing primitive does not trip spurious invalidations.
315    #[test]
316    fn perspective_camera_plane_fingerprint_stable_for_unchanged_transform() {
317        let local_rect = LayoutRect::new(
318            LayoutPoint::new(0.0, 0.0),
319            LayoutPoint::new(200.0, 2000.0),
320        );
321        let local_to_raster = ScaleOffset::identity();
322
323        let mut cache = CornersCache::new();
324
325        let m = perspective_rotate_x_translate_y(80.0, 1000.0, -40.0);
326
327        cache.cached_mapping = CoordinateSpaceMapping::Transform(m);
328        cache.clear_scratch();
329        let _ = cache.append_corners_from_mapping(local_rect, local_to_raster);
330        let scratch1: Vec<RasterPoint> = cache.unquantized.clone();
331
332        cache.cached_mapping = CoordinateSpaceMapping::Transform(m);
333        cache.clear_scratch();
334        let _ = cache.append_corners_from_mapping(local_rect, local_to_raster);
335        let scratch2: Vec<RasterPoint> = cache.unquantized.clone();
336
337        assert_eq!(
338            scratch1, scratch2,
339            "the same perspective transform must produce identical fingerprints",
340        );
341    }
342
343    /// A transform without a perspective component (rotate only) must take the
344    /// fast path and emit 4 projected corners, not the 8-element fingerprint.
345    #[test]
346    fn no_perspective_uses_projected_corners() {
347        let local_rect = LayoutRect::new(
348            LayoutPoint::new(0.0, 0.0),
349            LayoutPoint::new(100.0, 100.0),
350        );
351        let local_to_raster = ScaleOffset::identity();
352
353        let mut cache = CornersCache::new();
354        // Pure rotation around X axis — no perspective component (m14, m24, m34 = 0
355        // and m44 = 1), so the fast-path projection should be used.
356        cache.cached_mapping = CoordinateSpaceMapping::<LayoutPixel, LayoutPixel>::Transform(
357            LayoutTransform::rotation(1.0, 0.0, 0.0, Angle::degrees(45.0)),
358        );
359        cache.clear_scratch();
360        let r = cache.append_corners_from_mapping(local_rect, local_to_raster);
361        assert_eq!(r.count, 4, "non-perspective transform must emit 4 corners");
362    }
363}