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}