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}