Skip to main content

cranpose_render_wgpu/
display_clip.rs

1//! Display clip region: framework-level culling of pixels the display
2//! physically never shows.
3//!
4//! The surface has a VISIBLE REGION — the part of it the panel actually
5//! displays. Everything outside that region is cullable for any app and
6//! any layout, because no layout can make an invisible pixel visible. The
7//! mechanism is fully general: the region's COMPLEMENT is tessellated
8//! into a conservative occluder mesh, drawn first into a small transient
9//! depth attachment on the full-frame pass (depth write on, color writes
10//! off, no discard — early-Z/LRZ eligible), and every content pipeline
11//! runs a depth-tested variant (compare `Less`, write off). Content emits
12//! clip z 0.5 in those variants, the occluder writes 0.0, the clear is
13//! 1.0 — so occluded pixels fail the test before the fragment shader
14//! runs.
15//!
16//! [`DisplayVisibleRegion`] names the region; providers plug in as
17//! variants plus a [`tessellate_complement`] arm, without touching the
18//! cull machinery. The first provider is the round display: Android's
19//! `AConfiguration` screenRound yields [`DisplayVisibleRegion::InscribedCircle`].
20//! Future providers — display cutouts/insets reported by the platform, or
21//! an explicitly declared clip — are new variants. A rectangular display
22//! is [`DisplayVisibleRegion::Full`]: the mechanism is structurally inert,
23//! zero cost, and rendering is bitwise identical to a renderer without
24//! this capability.
25
26use std::borrow::Cow;
27
28/// The region of the surface the display physically shows. The renderer
29/// may refuse to shade anything outside it.
30///
31/// This is PLATFORM (or otherwise host-declared) truth about the display,
32/// never derived from app content — apps cannot invent one for their own
33/// scene; they get the cull for free on any layout.
34#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
35pub enum DisplayVisibleRegion {
36    /// The whole surface is visible (every rectangular display). The cull
37    /// machinery never engages: no depth attachment, no occluder, no
38    /// pipeline variants — bitwise-identical rendering.
39    #[default]
40    Full,
41    /// Only the circle inscribed in the surface rect is visible — the
42    /// round-display panel shape (center at the surface midpoint, radius
43    /// `min(width, height) / 2`).
44    InscribedCircle,
45}
46
47impl DisplayVisibleRegion {
48    /// Whether the region leaves anything to cull at all.
49    #[cfg(not(target_arch = "wasm32"))]
50    pub(crate) fn cullable(self) -> bool {
51        self != Self::Full
52    }
53}
54
55/// Depth format of the display-clip attachment: universally supported,
56/// 2 bytes per pixel (~333 KB transient at 408²), and with
57/// `LoadOp::Clear` + `StoreOp::Discard` it lives and dies in GMEM on tiled
58/// GPUs without ever touching main memory.
59pub(crate) const DISPLAY_CLIP_DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth16Unorm;
60
61/// The clear value of the display-clip depth attachment (far plane).
62#[cfg(not(target_arch = "wasm32"))]
63pub(crate) const DISPLAY_CLIP_DEPTH_CLEAR: f32 = 1.0;
64
65/// Depth state every CONTENT pipeline uses in its display-clip variant:
66/// test `Less` against the occluder (which wrote 0.0), never write.
67/// `None` for the ordinary no-depth variant, which stays byte-identical
68/// to the pipelines that existed before the cull.
69pub(crate) fn content_depth_state(depth: bool) -> Option<wgpu::DepthStencilState> {
70    depth.then(|| wgpu::DepthStencilState {
71        format: DISPLAY_CLIP_DEPTH_FORMAT,
72        depth_write_enabled: Some(false),
73        depth_compare: Some(wgpu::CompareFunction::Less),
74        stencil: wgpu::StencilState::default(),
75        bias: wgpu::DepthBiasState::default(),
76    })
77}
78
79/// The exact clip-position tail every framework vertex stage emits today.
80/// [`with_content_z`] rewrites it in the display-clip pipeline variants;
81/// the ordinary variants compile the untouched text, so their output
82/// cannot drift by construction.
83const FLAT_Z_TAIL: &str = "(x, y, 0.0, 1.0);";
84const MID_Z_TAIL: &str = "(x, y, 0.5, 1.0);";
85
86/// Rewrites a framework vertex stage's emitted clip z from 0.0 to the
87/// display-clip content depth 0.5 — only for the depth pipeline variant.
88/// 0.5 keeps a wide, unambiguous margin between content and both the
89/// occluder (0.0) and the clear (1.0), which is what lets conservative
90/// early-Z / LRZ hardware reject occluded fragments confidently. (Runtime
91/// user shaders are NOT rewritten; their conventional z 0.0 still fails
92/// `0.0 < 0.0` against the occluder, so they cull correctly, just without
93/// the margin.)
94///
95/// Exact-text substitution, same discipline as `shape_shader_source`: a
96/// drifted literal makes this a silent no-op, so the debug assertion (and
97/// a unit test below) pin the pattern.
98pub(crate) fn with_content_z(source: Cow<'static, str>, depth: bool) -> Cow<'static, str> {
99    if !depth {
100        return source;
101    }
102    debug_assert!(
103        source.contains(FLAT_Z_TAIL),
104        "vertex stage no longer emits `{FLAT_Z_TAIL}`; the display-clip z substitution missed"
105    );
106    Cow::Owned(source.replace(FLAT_Z_TAIL, MID_Z_TAIL))
107}
108
109/// The occluder's own shader: positions arrive pre-baked in NDC, z is the
110/// near plane 0.0, and the fragment stage is trivial — no discard, no
111/// texture reads — so the draw is early-Z friendly and the pipeline masks
112/// off every color write.
113#[cfg(not(target_arch = "wasm32"))]
114pub(crate) const OCCLUDER_SHADER: &str = "\
115@vertex
116fn mask_vs(@location(0) position: vec2<f32>) -> @builtin(position) vec4<f32> {
117    return vec4<f32>(position, 0.0, 1.0);
118}
119
120@fragment
121fn mask_fs() -> @location(0) vec4<f32> {
122    return vec4<f32>(0.0, 0.0, 0.0, 0.0);
123}
124";
125
126/// Triangles per corner fan of the inscribed-circle tessellation. Four
127/// corners × 8 = 32 triangles per frame — the entire per-frame geometry
128/// cost of that region's cull.
129#[cfg(not(target_arch = "wasm32"))]
130const SEGMENTS_PER_CORNER: usize = 8;
131
132/// Conservative tessellations are built against the region inflated by
133/// this margin, so f32 vertex rounding and rasterization snapping can
134/// never push an occluder edge over a pixel the panel actually shows. The
135/// cost is a sub-pixel band of cullable pixels left unculled —
136/// conservative in the safe direction.
137#[cfg(not(target_arch = "wasm32"))]
138const SAFETY_PX: f64 = 0.5;
139
140/// The tessellated complement of a visible region for one surface size.
141#[cfg(not(target_arch = "wasm32"))]
142pub(crate) struct ComplementMesh {
143    /// Triangle-list vertices, NDC xy.
144    pub(crate) vertices: Vec<[f32; 2]>,
145    /// Approximate pixels the occluder rejects per full-screen layer of
146    /// overdraw: the area outside the visible region. (A tessellation may
147    /// under-cover that area; this is a log figure, not an accounting
148    /// one.)
149    pub(crate) masked_px: u64,
150}
151
152/// Tessellates the COMPLEMENT of `region` on a `width`×`height` surface
153/// into the occluder mesh the depth pre-pass draws.
154///
155/// THE CONTRACT every region arm must uphold: the mesh is CONSERVATIVE —
156/// every triangle lies strictly outside the visible region, so the
157/// occluder can never cover a pixel whose center the display shows.
158/// Under-coverage is the only permitted error (unculled cullable pixels
159/// cost fill, never correctness). An arm that cannot guarantee this for a
160/// given size returns `None` and the caller leaves the cull off.
161///
162/// [`DisplayVisibleRegion::Full`] has an empty complement and always
163/// returns `None`.
164#[cfg(not(target_arch = "wasm32"))]
165pub(crate) fn tessellate_complement(
166    region: DisplayVisibleRegion,
167    width: u32,
168    height: u32,
169) -> Option<ComplementMesh> {
170    match region {
171        DisplayVisibleRegion::Full => None,
172        DisplayVisibleRegion::InscribedCircle => {
173            tessellate_inscribed_circle_complement(width, height)
174        }
175    }
176}
177
178/// The inscribed-circle arm of [`tessellate_complement`]: for each of the
179/// four corners, a triangle fan from the corner point to a polyline
180/// CIRCUMSCRIBED about the inscribed circle (every chord tangent to
181/// radius `r + SAFETY_PX`, vertices at `r_safe / cos(Δθ/2)`), clamped to
182/// the corner's tangent cone. Every triangle therefore lies strictly
183/// outside the circle.
184///
185/// The construction is verified numerically before it is accepted: the
186/// distance from the circle center to every triangle must exceed `r`. A
187/// failure returns `None` — fail-safe for exotic surface sizes, per the
188/// contract above.
189#[cfg(not(target_arch = "wasm32"))]
190fn tessellate_inscribed_circle_complement(width: u32, height: u32) -> Option<ComplementMesh> {
191    use std::f64::consts::PI;
192
193    if width < 16 || height < 16 {
194        return None;
195    }
196    let (w, h) = (f64::from(width), f64::from(height));
197    let (cx, cy) = (w / 2.0, h / 2.0);
198    let r = w.min(h) / 2.0;
199    let r_safe = r + SAFETY_PX;
200
201    // Corner order pairs each corner with the quadrant of the circle that
202    // faces it, in atan2-normalized-to-[0, 2π) angles (y grows down, so
203    // e.g. the bottom-right corner faces the (+x, +y) quadrant [0, π/2]).
204    let corners = [
205        (w, h, 0.0),              // bottom-right: quadrant [0, π/2]
206        (0.0, h, PI / 2.0),       // bottom-left:  quadrant [π/2, π]
207        (0.0, 0.0, PI),           // top-left:     quadrant [π, 3π/2]
208        (w, 0.0, 3.0 * PI / 2.0), // top-right:    quadrant [3π/2, 2π]
209    ];
210
211    let mut triangles: Vec<[[f64; 2]; 3]> = Vec::with_capacity(4 * SEGMENTS_PER_CORNER);
212    for (px, py, quadrant_start) in corners {
213        let (dx, dy) = (px - cx, py - cy);
214        let d = dx.hypot(dy);
215        if d <= r_safe {
216            // The corner itself is (numerically) inside the safe circle:
217            // nothing invisible to occlude here.
218            continue;
219        }
220        let mut phi = dy.atan2(dx);
221        if phi < 0.0 {
222            phi += 2.0 * PI;
223        }
224        // Tangent cone: the corner can only "see" (and thus safely fan to)
225        // arc points within ±acos(r_safe/d) of its own direction. For a
226        // square surface that is the whole quadrant; for elongated ones it
227        // shrinks, deliberately under-covering the side bands.
228        let beta = (r_safe / d).acos();
229        let theta_lo = quadrant_start.max(phi - beta);
230        let theta_hi = (quadrant_start + PI / 2.0).min(phi + beta);
231        if theta_hi - theta_lo < 1e-6 {
232            continue;
233        }
234        let step = (theta_hi - theta_lo) / SEGMENTS_PER_CORNER as f64;
235        // Chord between consecutive vertices at radius r_v stays at
236        // distance r_v·cos(step/2) = r_safe from the center: tangent to
237        // the safe circle, strictly outside the real one.
238        let r_v = r_safe / (step / 2.0).cos();
239        let vertex_at = |theta: f64| [cx + r_v * theta.cos(), cy + r_v * theta.sin()];
240        for i in 0..SEGMENTS_PER_CORNER {
241            let a = vertex_at(theta_lo + step * i as f64);
242            let b = vertex_at(theta_lo + step * (i + 1) as f64);
243            triangles.push([[px, py], a, b]);
244        }
245    }
246    if triangles.is_empty() {
247        return None;
248    }
249
250    // Conservative-containment proof, run once per surface size: the
251    // circle center must be strictly farther than r from every triangle.
252    // (The center is inside the circle; if it were inside a triangle the
253    // distance would be 0 and this rejects the whole mesh.)
254    for triangle in &triangles {
255        if distance_point_to_triangle([cx, cy], triangle) <= r {
256            log::warn!(
257                "[display-clip] occluder verification failed at {width}x{height}; cull stays off"
258            );
259            return None;
260        }
261    }
262
263    let to_ndc = |[x, y]: [f64; 2]| [(x / w * 2.0 - 1.0) as f32, (1.0 - y / h * 2.0) as f32];
264    let vertices = triangles
265        .iter()
266        .flat_map(|t| t.iter().copied().map(to_ndc))
267        .collect();
268    let masked_px = (w * h - PI * r * r).max(0.0).round() as u64;
269    Some(ComplementMesh {
270        vertices,
271        masked_px,
272    })
273}
274
275/// Distance from `p` to the closed triangle `t` (0 when inside).
276#[cfg(not(target_arch = "wasm32"))]
277fn distance_point_to_triangle(p: [f64; 2], t: &[[f64; 2]; 3]) -> f64 {
278    // Inside test via consistent edge orientation.
279    let cross = |a: [f64; 2], b: [f64; 2], c: [f64; 2]| {
280        (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
281    };
282    let d0 = cross(t[0], t[1], p);
283    let d1 = cross(t[1], t[2], p);
284    let d2 = cross(t[2], t[0], p);
285    let has_neg = d0 < 0.0 || d1 < 0.0 || d2 < 0.0;
286    let has_pos = d0 > 0.0 || d1 > 0.0 || d2 > 0.0;
287    if !(has_neg && has_pos) {
288        return 0.0;
289    }
290    distance_point_to_segment(p, t[0], t[1])
291        .min(distance_point_to_segment(p, t[1], t[2]))
292        .min(distance_point_to_segment(p, t[2], t[0]))
293}
294
295#[cfg(not(target_arch = "wasm32"))]
296fn distance_point_to_segment(p: [f64; 2], a: [f64; 2], b: [f64; 2]) -> f64 {
297    let (abx, aby) = (b[0] - a[0], b[1] - a[1]);
298    let (apx, apy) = (p[0] - a[0], p[1] - a[1]);
299    let len_sq = abx * abx + aby * aby;
300    let t = if len_sq > 0.0 {
301        ((apx * abx + apy * aby) / len_sq).clamp(0.0, 1.0)
302    } else {
303        0.0
304    };
305    let (dx, dy) = (apx - t * abx, apy - t * aby);
306    dx.hypot(dy)
307}
308
309/// Whether a pixel center is inside the visible region — the reference
310/// predicate the parity suite tests the GPU path against, defined here so
311/// the tests stay parameterized by region rather than hard-coding any one
312/// shape.
313#[cfg(not(target_arch = "wasm32"))]
314#[doc(hidden)]
315pub fn pixel_is_visible(
316    region: DisplayVisibleRegion,
317    width: u32,
318    height: u32,
319    x: u32,
320    y: u32,
321) -> bool {
322    match region {
323        DisplayVisibleRegion::Full => true,
324        DisplayVisibleRegion::InscribedCircle => {
325            let dx = (f64::from(x) + 0.5) - f64::from(width) / 2.0;
326            let dy = (f64::from(y) + 0.5) - f64::from(height) / 2.0;
327            (dx * dx + dy * dy).sqrt() < f64::from(width.min(height)) / 2.0
328        }
329    }
330}
331
332#[cfg(all(test, not(target_arch = "wasm32")))]
333mod tests {
334    use super::*;
335
336    /// Recovers a vertex's pixel position from its NDC form.
337    fn from_ndc([x, y]: [f32; 2], width: u32, height: u32) -> [f64; 2] {
338        [
339            (f64::from(x) + 1.0) / 2.0 * f64::from(width),
340            (1.0 - f64::from(y)) / 2.0 * f64::from(height),
341        ]
342    }
343
344    fn triangles_of(mesh: &ComplementMesh, width: u32, height: u32) -> Vec<[[f64; 2]; 3]> {
345        mesh.vertices
346            .chunks_exact(3)
347            .map(|t| {
348                [
349                    from_ndc(t[0], width, height),
350                    from_ndc(t[1], width, height),
351                    from_ndc(t[2], width, height),
352                ]
353            })
354            .collect()
355    }
356
357    fn point_in_triangle(p: [f64; 2], t: &[[f64; 2]; 3]) -> bool {
358        distance_point_to_triangle(p, t) == 0.0
359    }
360
361    /// The full region has an empty complement: the mechanism must be
362    /// structurally inert for every rectangular display.
363    #[test]
364    fn full_region_tessellates_to_nothing() {
365        assert!(tessellate_complement(DisplayVisibleRegion::Full, 408, 408).is_none());
366    }
367
368    /// THE tessellation contract, checked at pixel granularity for every
369    /// cullable region: no pixel whose center is visible may be covered
370    /// by any occluder triangle — for square, odd, and elongated
371    /// surfaces.
372    #[test]
373    fn occluder_never_covers_a_visible_pixel() {
374        for region in [DisplayVisibleRegion::InscribedCircle] {
375            for (width, height) in [
376                (408u32, 408u32),
377                (407, 407),
378                (466, 466),
379                (320, 290),
380                (480, 360),
381                (1000, 200),
382                (64, 64),
383            ] {
384                let mesh = tessellate_complement(region, width, height)
385                    .unwrap_or_else(|| panic!("{region:?} must tessellate at {width}x{height}"));
386                let triangles = triangles_of(&mesh, width, height);
387                for y in 0..height {
388                    for x in 0..width {
389                        if !pixel_is_visible(region, width, height, x, y) {
390                            continue;
391                        }
392                        let p = [f64::from(x) + 0.5, f64::from(y) + 0.5];
393                        for triangle in &triangles {
394                            assert!(
395                                !point_in_triangle(p, triangle),
396                                "{region:?} occluder covers visible pixel ({x}, {y}) \
397                                 at {width}x{height}"
398                            );
399                        }
400                    }
401                }
402            }
403        }
404    }
405
406    /// The occluder must actually be worth drawing: on a square surface
407    /// the inscribed-circle tessellation covers nearly all of the
408    /// invisible corner region.
409    #[test]
410    fn inscribed_circle_occluder_covers_most_of_the_invisible_region() {
411        let region = DisplayVisibleRegion::InscribedCircle;
412        let (width, height) = (408u32, 408u32);
413        let mesh = tessellate_complement(region, width, height).expect("mesh must build");
414        let triangles = triangles_of(&mesh, width, height);
415        let mut invisible = 0u64;
416        let mut covered = 0u64;
417        for y in 0..height {
418            for x in 0..width {
419                if pixel_is_visible(region, width, height, x, y) {
420                    continue;
421                }
422                invisible += 1;
423                let p = [f64::from(x) + 0.5, f64::from(y) + 0.5];
424                if triangles.iter().any(|t| point_in_triangle(p, t)) {
425                    covered += 1;
426                }
427            }
428        }
429        assert!(
430            covered as f64 >= invisible as f64 * 0.9,
431            "occluder covers {covered} of {invisible} invisible pixels — the cull would be hollow"
432        );
433    }
434
435    /// Pins the exact-text z substitution against shader drift, for every
436    /// vertex stage that draws inside the fused pass.
437    #[test]
438    fn content_z_substitution_matches_every_fused_pass_vertex_stage() {
439        for (name, source) in [
440            ("shape", crate::shaders::SHADER),
441            // The trimmed solid entries are appended to the shape source
442            // under `CRANPOSE_SOLID_TRIM_VARYINGS`; their z tails must take
443            // the same rewrite so the trimmed depth pipelines cull too.
444            ("shape_solid_trim", crate::shaders::SOLID_TRIM_APPENDIX),
445            ("image", crate::shaders::IMAGE_SHADER),
446            ("glyph_atlas", crate::shaders::GLYPH_ATLAS_SHADER),
447            ("fullscreen_quad", crate::shaders::FULLSCREEN_QUAD_VS),
448        ] {
449            assert!(
450                source.contains(FLAT_Z_TAIL),
451                "{name} no longer emits `{FLAT_Z_TAIL}`; display-clip z substitution would no-op"
452            );
453            let substituted = with_content_z(Cow::Borrowed(source), true);
454            assert!(
455                !substituted.contains(FLAT_Z_TAIL) && substituted.contains(MID_Z_TAIL),
456                "{name} substitution failed"
457            );
458            assert_eq!(
459                with_content_z(Cow::Borrowed(source), false),
460                source,
461                "{name} flat variant must be the untouched text"
462            );
463        }
464    }
465}