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