frust_shell_common/surface_mode.rs
1//! HOST-declared translucent-surface slot:
2//! [`declare_host_translucent_surface`]/[`SurfaceModeWatcher::current`], plus
3//! its outward-facing sibling — the RESOLVED slot
4//! ([`publish_resolved_surface_mode`]/[`resolved_surface_mode`]) an app or
5//! plugin reads to learn what the platform actually gave us, including a
6//! refusal (`translucencyRefused`). See `docs/SHELLS_ARCHITECTURE.md`'s
7//! surface-mode resolution flow.
8//!
9//! # The gap this closes
10//!
11//! Platform-view compositing (Mode B) needs the GPU surface itself to be
12//! created with an alpha channel (Android's `EGLConfig`, iOS's
13//! `CAMetalLayer.isOpaque`) so a native sibling view placed *behind* it can
14//! show through wherever frust paints nothing. That surface-format choice
15//! happens once, at surface-creation time, well before any app code runs — so
16//! there is no "widget asks for translucency" moment the way there is for,
17//! say, `set_app_theme`.
18//!
19//! # Callers: host glue only
20//!
21//! [`declare_host_translucent_surface`] is called **only** by
22//! `frust-shell-android::jni_glue`'s `native_set_surface_mode` and
23//! `frust-shell-ios::ffi_glue`'s `set_surface_mode` — the fixed JNI/C-ABI
24//! exports the generated host template's Kotlin/Swift calls during startup,
25//! from the same branch that sets `SurfaceHolder`'s `PixelFormat.TRANSLUCENT` /
26//! `CAMetalLayer.isOpaque = false` on the native window itself and arranges the
27//! native-sibling z-order; each shell's surface-creation path then reads
28//! [`SurfaceModeWatcher::current`] **before** configuring the surface. Setting
29//! the latch is a claim that the window is *already* configured translucent, so
30//! **calling it from anywhere else, or without that configuration in place, is
31//! a host-template bug**: an app opting in from Rust alone with no matching
32//! host window config is a reachable black-rectangle vector, which is why the
33//! call is deliberately not re-exported past `frust-shell-common`.
34//!
35//! # This latch is a HOST DECLARATION, not the outcome
36//!
37//! Declaring it doesn't decide the outcome. Each shell translates this latch
38//! into a `frust_render::SurfaceAlphaRequest`, and `frust-render` resolves
39//! *that* against the platform's advertised `CompositeAlphaMode`s at configure
40//! time — falling back to an opaque swapchain (with a `log::warn!`) when the
41//! platform advertises no translucent mode, and the GPU-tier blit fallback can
42//! further refuse a premultiply-expecting mode it can't reproduce. **The
43//! resolved truth lives at a different seam**:
44//! `frust_render::SurfaceRenderer::surface_resolved_translucent`, read by each
45//! shell after every surface (re)install and threaded into
46//! `RenderRoot::set_surface_translucent` — that seam governs paint.
47//!
48//! So: read this latch to decide what to *request*; never to decide whether to
49//! paint the Mode B contract (a transparent base clear, a `platform_view` hole
50//! punch). Keying paint off the declaration alone (ignoring the resolved seam)
51//! means a fallback clears to `TRANSPARENT` and `DestOut`-punches every slot
52//! rect on an OPAQUE swapchain — black rectangles instead of a graceful degrade
53//! to Mode A.
54//!
55//! # Latch contract (one-way, v1)
56//!
57//! [`declare_host_translucent_surface`] only ever moves the slot from
58//! [`SurfaceMode::Opaque`] to [`SurfaceMode::Translucent`] — there is no
59//! "undo" call, and once observed as `Translucent` it never reverts. The
60//! surface format is fixed at creation (the platform APIs above expose no
61//! supported runtime toggle), so "reverting" would mean destroying and
62//! recreating the whole surface — out of scope for v1, and no current use case
63//! needs it (an app either wants platform-view compositing for the process's
64//! lifetime, or it doesn't). A live flip would have to plumb a full
65//! surface-recreation round-trip through each shell's `SurfacePhase` state
66//! machine (`docs/ARCHITECTURE.md`'s frame pipelines) — not a slot-shape
67//! change.
68//!
69//! One-way applies to the DECLARATION only. The *resolved* state below is not
70//! one-way and is not fixed before the surface exists: every (re)install
71//! re-resolves it, and a failed install clears it — which is exactly why the
72//! shells re-read it per frame rather than caching it at construction.
73//!
74//! # The RESOLVED slot
75//!
76//! Everything above is the *inward* half: what the host declared, read by the
77//! shells to decide what to request. [`publish_resolved_surface_mode`]/
78//! [`resolved_surface_mode`] are the *outward* half — the resolved verdict
79//! ([`ResolvedSurfaceMode`]) travelling back out to app/plugin code, whose
80//! whole reason to exist is
81//! [`ResolvedSurfaceMode::RefusedTranslucent`]: the host declared Mode B and
82//! the platform resolved opaque anyway (its compositor offered no translucent
83//! `CompositeAlphaMode`; see `docs/NATIVE_WIDGETS_ARCHITECTURE.md`'s Mode-B
84//! paragraph). Without it that case is invisible *and* unsignalled — a
85//! native sibling arranged behind a now-opaque frust surface simply vanishes,
86//! with no way for app code to fall back deliberately.
87//!
88//! Publishing is again shell-glue-only (pinned by the same
89//! `crates/frust/tests/surface_mode_conformance.rs` scan): each mobile shell
90//! publishes whatever
91//! `frust_render::SurfaceRenderer::surface_resolved_translucent` reports for
92//! the live surface, on the same UI-thread beat it pushes
93//! `RenderRoot::set_surface_translucent` — seeded at handle construction,
94//! re-published on every later (re)install. Reading is open to anyone; the
95//! `frust` facade re-exports [`resolved_surface_mode`] +
96//! [`ResolvedSurfaceMode`] (never the publisher).
97//!
98//! **Polling contract, not a reactive one.** Nothing here wakes a frame: this
99//! slot is a plain process-global read, exactly like
100//! [`crate::theme_override::theme_override_active`]. App code reads it *during
101//! a rebuild* (or during paint/an event handler) and branches on what it finds;
102//! since both mobile shells run a continuous per-frame loop and re-publish
103//! every frame, a downgrade is observed on the rebuild after the install that
104//! caused it. A value read on a frame where nothing else is dirty does **not**
105//! by itself schedule another frame — pair a fallback decision with an actual
106//! state write (a signal set, a component-state change) if the UI must change
107//! shape because of it.
108//!
109//! # Layering and thread contract
110//!
111//! Same shape as [`crate::theme_override`]/[`crate::system_ui`]: process-global
112//! `Mutex` slots living in `frust-shell-common`, the crate every shell already
113//! polls this kind of state from, each callable from any thread with no
114//! ordering enforced. Unlike those two, neither slot is a per-frame
115//! generation/poll pair, so [`SurfaceModeWatcher`] carries no per-instance
116//! "last seen" cursor — `current` is an associated function, a plain peek.
117//!
118//! In practice [`declare_host_translucent_surface`] must be called before the
119//! shell's surface-creation path reads [`SurfaceModeWatcher::current`]
120//! (startup-time only — see the module docs above), and each shell publishes
121//! the resolved slot from its UI thread (the one owning the `RenderRoot`) while
122//! app code reads it from that same thread during a rebuild. Both orderings are
123//! caller responsibilities, not something this module enforces; the read is a
124//! plain lock-load either way, so an off-thread reader sees a consistent value,
125//! just possibly one frame old.
126
127use std::sync::Mutex;
128
129/// Whether a shell's GPU surface should be created with an alpha channel.
130/// See the module docs' Latch contract — this only ever moves
131/// `Opaque` → `Translucent`, never back.
132#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
133pub enum SurfaceMode {
134 /// Default: an opaque surface, matching every shell's pre-platform-views
135 /// behavior.
136 #[default]
137 Opaque,
138 /// Create the surface with an alpha channel so a native sibling view
139 /// placed behind it can show through unpainted regions.
140 Translucent,
141}
142
143/// The process-wide latch. No generation counter (unlike
144/// [`crate::theme_override`]/[`crate::system_ui`]'s slots) — see the module
145/// docs' Layering and thread contract for why a per-frame "changed since last
146/// poll" concept doesn't apply here.
147static SURFACE_MODE: Mutex<SurfaceMode> = Mutex::new(SurfaceMode::Opaque);
148
149/// Declare that the native host window has already been configured
150/// translucent (`PixelFormat.TRANSLUCENT`/`isOpaque = false`) so this
151/// shell's next GPU surface should be created with an alpha channel too.
152///
153/// **Called only by the generated host glue** (`jni_glue::native_set_surface_mode`
154/// on Android, `ffi_glue::set_surface_mode` on iOS), from the same branch
155/// that actually configured the window — see the module docs' Callers
156/// section. Calling this without that window configuration in place is a
157/// host-template bug, not a supported app-Rust opt-in.
158///
159/// Callable from any thread (see the module docs' Layering and thread
160/// contract), and
161/// idempotent — calling it more than once, or after the surface already
162/// latched translucent, has no additional effect.
163///
164/// Must be called before the running shell's surface-creation path reads
165/// [`SurfaceModeWatcher::current`] (see the module docs) — calling it after
166/// the surface already exists has no effect on that surface.
167///
168/// Declaring translucency does not guarantee the resolved outcome: the
169/// platform may refuse (see the module docs' *This latch is a HOST
170/// DECLARATION, not the outcome*), in which case the app degrades to the
171/// opaque Mode A contract.
172pub fn declare_host_translucent_surface() {
173 let mut slot = SURFACE_MODE.lock().unwrap_or_else(|e| e.into_inner());
174 *slot = SurfaceMode::Translucent;
175}
176
177/// Per-shell-instance reader over the process-wide latch. Kept as a type
178/// (mirroring [`crate::theme_override::ThemeOverrideWatcher`]/
179/// [`crate::system_ui::SystemUiWatcher`]'s shape) even though it carries no
180/// state of its own — [`current`](Self::current) is a plain peek, not a
181/// diffed poll, per the module docs' Layering and thread contract.
182#[derive(Debug, Default)]
183pub struct SurfaceModeWatcher;
184
185impl SurfaceModeWatcher {
186 /// A fresh (stateless) watcher.
187 pub fn new() -> Self {
188 Self
189 }
190
191 /// Read the latch's current value. Not a "since last call" diff — a
192 /// shell's surface-creation path calls this once, at surface-creation
193 /// time, and applies whatever it reads.
194 pub fn current() -> SurfaceMode {
195 *SURFACE_MODE.lock().unwrap_or_else(|e| e.into_inner())
196 }
197}
198
199/// What the platform **actually gave us**, as opposed to what the host
200/// declared ([`SurfaceMode`]) — see the module docs' *The RESOLVED slot*.
201///
202/// Read via [`resolved_surface_mode`] (re-exported by the `frust` facade);
203/// published only by the two mobile shells, once per frame, from the same
204/// value that drives `RenderRoot::set_surface_translucent`.
205#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
206pub enum ResolvedSurfaceMode {
207 /// No shell has published a resolution yet: no surface exists, or the
208 /// running shell doesn't participate (the desktop preview shell never
209 /// publishes — it has no Mode B host seam). **Not** a synonym for
210 /// `Opaque`: app code that must distinguish "opaque" from "don't know yet"
211 /// (e.g. deferring a fallback decision to the next frame) can.
212 #[default]
213 Unknown,
214 /// The live surface came up opaque, and that is what the host asked for —
215 /// the ordinary Mode A contract every pre-platform-views app runs under.
216 Opaque,
217 /// The live surface came up translucent: frust clears to a transparent
218 /// base and `platform_view` punches its slot rects (Mode B).
219 Translucent,
220 /// **The refusal** (`translucencyRefused`): the host declared Mode B, and
221 /// the platform resolved the surface opaque anyway — no matching
222 /// `CompositeAlphaMode` (the engine's premultiplied output has no other
223 /// refusal case; see `docs/NATIVE_WIDGETS_ARCHITECTURE.md`'s Mode-B
224 /// paragraph).
225 ///
226 /// frust's own paint side degrades correctly (opaque clear, no hole
227 /// punch), but the host's native-sibling z-order was fixed at build time:
228 /// a sibling arranged *behind* the now-opaque surface is invisible and
229 /// untappable. That is what this variant exists to tell app/plugin code —
230 /// so it can render its own fallback content instead of a dead rect.
231 RefusedTranslucent,
232}
233
234impl ResolvedSurfaceMode {
235 /// The pure mapping [`publish_resolved_surface_mode`] applies, split out so
236 /// it is testable without touching the process-global slots: a resolved
237 /// translucent surface is [`Translucent`](Self::Translucent); a resolved
238 /// opaque one is [`RefusedTranslucent`](Self::RefusedTranslucent) when the
239 /// host declared translucency and [`Opaque`](Self::Opaque) when it didn't.
240 ///
241 /// Never returns [`Unknown`](Self::Unknown) — that variant means "nobody
242 /// published", which is the slot's initial state, not a resolution.
243 pub const fn resolve(declared: SurfaceMode, resolved_translucent: bool) -> Self {
244 match (declared, resolved_translucent) {
245 (_, true) => Self::Translucent,
246 (SurfaceMode::Translucent, false) => Self::RefusedTranslucent,
247 (SurfaceMode::Opaque, false) => Self::Opaque,
248 }
249 }
250
251 /// `true` only for [`RefusedTranslucent`](Self::RefusedTranslucent) — the
252 /// one-liner a native-widget fallback branches on
253 /// (`if frust::resolved_surface_mode().translucency_refused() { ... }`).
254 pub const fn translucency_refused(self) -> bool {
255 matches!(self, Self::RefusedTranslucent)
256 }
257
258 /// `true` only for [`Translucent`](Self::Translucent), i.e. the frames
259 /// being painted right now really do follow the Mode B contract. Both
260 /// refusal and plain opacity answer `false`; so does
261 /// [`Unknown`](Self::Unknown), since no surface has reported in.
262 pub const fn is_translucent(self) -> bool {
263 matches!(self, Self::Translucent)
264 }
265}
266
267/// The process-wide RESOLVED slot, beside [`SURFACE_MODE`]'s declaration
268/// latch. Unlike that latch this is **not** one-way: every surface (re)install
269/// re-resolves, and a failed install downgrades — see the module docs.
270static RESOLVED_SURFACE_MODE: Mutex<ResolvedSurfaceMode> = Mutex::new(ResolvedSurfaceMode::Unknown);
271
272/// Publish the live surface's **resolved** translucency, mapped against the
273/// host declaration into a [`ResolvedSurfaceMode`] (see
274/// [`ResolvedSurfaceMode::resolve`]).
275///
276/// **Called only by the two mobile shells' `app.rs`** — the UI-thread beat
277/// that already reads the resolved flag and pushes
278/// `RenderRoot::set_surface_translucent` — pinned by
279/// `crates/frust/tests/surface_mode_conformance.rs` the same way
280/// [`declare_host_translucent_surface`] is. `resolved_translucent` is what
281/// `frust_render::SurfaceRenderer::surface_resolved_translucent` reports for
282/// the surface that is live *now*; a failed install reports `false`, which is
283/// the honest answer (nothing to punch a hole in).
284///
285/// Idempotent and re-callable in any direction: publishing the same value
286/// every frame is the expected usage, and a later install may legitimately
287/// move the slot back (`Translucent` → `RefusedTranslucent`, or the reverse
288/// once a re-created surface comes up capable again).
289pub fn publish_resolved_surface_mode(resolved_translucent: bool) {
290 let declared = SurfaceModeWatcher::current();
291 let mut slot = RESOLVED_SURFACE_MODE
292 .lock()
293 .unwrap_or_else(|e| e.into_inner());
294 *slot = ResolvedSurfaceMode::resolve(declared, resolved_translucent);
295}
296
297/// Read the resolved surface mode — [`ResolvedSurfaceMode::Unknown`] until a
298/// shell publishes one (no surface yet, or a shell with no Mode B seam, e.g.
299/// the desktop preview).
300///
301/// A **poll**, not a subscription: reading this never wakes a frame, and a
302/// change here never marks anything dirty on its own. Read it during a rebuild
303/// (the same place `theme_override`-style process-global state is read) and
304/// branch; see the module docs' *The RESOLVED slot* for the full contract.
305pub fn resolved_surface_mode() -> ResolvedSurfaceMode {
306 *RESOLVED_SURFACE_MODE
307 .lock()
308 .unwrap_or_else(|e| e.into_inner())
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314 use std::sync::Mutex as StdMutex;
315 use std::thread;
316
317 // Serializes every test in this module against the shared process-wide
318 // `SURFACE_MODE` static — mirrors `theme_override`'s `TEST_LOCK` pattern.
319 static TEST_LOCK: StdMutex<()> = StdMutex::new(());
320
321 fn reset_slot() {
322 let mut slot = SURFACE_MODE.lock().unwrap_or_else(|e| e.into_inner());
323 *slot = SurfaceMode::Opaque;
324 let mut resolved = RESOLVED_SURFACE_MODE
325 .lock()
326 .unwrap_or_else(|e| e.into_inner());
327 *resolved = ResolvedSurfaceMode::Unknown;
328 }
329
330 #[test]
331 fn defaults_to_opaque() {
332 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
333 reset_slot();
334 assert_eq!(SurfaceModeWatcher::current(), SurfaceMode::Opaque);
335 }
336
337 /// The default is Opaque
338 /// with no host call at all — same assertion as `defaults_to_opaque`
339 /// above, spelled out explicitly since it's the important
340 /// case (no `declare_host_translucent_surface()` call anywhere in this
341 /// test body).
342 #[test]
343 fn opaque_by_default_with_no_host_declaration() {
344 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
345 reset_slot();
346 assert_eq!(SurfaceModeWatcher::current(), SurfaceMode::Opaque);
347 }
348
349 #[test]
350 fn declaration_latches_translucent() {
351 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
352 reset_slot();
353
354 declare_host_translucent_surface();
355 assert_eq!(SurfaceModeWatcher::current(), SurfaceMode::Translucent);
356 }
357
358 #[test]
359 fn repeated_declarations_are_idempotent() {
360 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
361 reset_slot();
362
363 declare_host_translucent_surface();
364 declare_host_translucent_surface();
365 assert_eq!(SurfaceModeWatcher::current(), SurfaceMode::Translucent);
366 }
367
368 #[test]
369 fn declaration_from_a_spawned_thread_is_observed() {
370 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
371 reset_slot();
372
373 thread::spawn(|| {
374 declare_host_translucent_surface();
375 })
376 .join()
377 .unwrap();
378
379 assert_eq!(SurfaceModeWatcher::current(), SurfaceMode::Translucent);
380 }
381
382 // --- The RESOLVED slot ---------------------------------------------------
383
384 /// Nothing published yet reads `Unknown` — the desktop/host
385 /// case, and every mobile launch before the first surface resolution.
386 #[test]
387 fn resolved_defaults_to_unknown() {
388 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
389 reset_slot();
390
391 assert_eq!(resolved_surface_mode(), ResolvedSurfaceMode::Unknown);
392 }
393
394 /// No host declaration + an opaque resolution is plain `Opaque`, never a
395 /// refusal — a Mode A app must not look like it was refused anything.
396 #[test]
397 fn undeclared_opaque_resolution_is_opaque_not_refused() {
398 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
399 reset_slot();
400
401 publish_resolved_surface_mode(false);
402 assert_eq!(resolved_surface_mode(), ResolvedSurfaceMode::Opaque);
403 assert!(!resolved_surface_mode().translucency_refused());
404 }
405
406 #[test]
407 fn declared_translucent_resolution_is_translucent() {
408 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
409 reset_slot();
410
411 declare_host_translucent_surface();
412 publish_resolved_surface_mode(true);
413
414 assert_eq!(resolved_surface_mode(), ResolvedSurfaceMode::Translucent);
415 assert!(resolved_surface_mode().is_translucent());
416 assert!(!resolved_surface_mode().translucency_refused());
417 }
418
419 /// The whole point of the slot: declared Mode B, resolved opaque (the
420 /// platform offered no translucent `CompositeAlphaMode`).
421 #[test]
422 fn declared_but_opaque_resolution_is_refused() {
423 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
424 reset_slot();
425
426 declare_host_translucent_surface();
427 publish_resolved_surface_mode(false);
428
429 assert_eq!(
430 resolved_surface_mode(),
431 ResolvedSurfaceMode::RefusedTranslucent
432 );
433 assert!(resolved_surface_mode().translucency_refused());
434 assert!(!resolved_surface_mode().is_translucent());
435 }
436
437 /// Unlike the declaration latch, the resolved slot is **not** one-way: a
438 /// later (re)install may downgrade it, and a recovered surface may take it
439 /// back up. Both directions must land.
440 #[test]
441 fn resolved_slot_moves_in_both_directions() {
442 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
443 reset_slot();
444
445 declare_host_translucent_surface();
446
447 publish_resolved_surface_mode(true);
448 assert_eq!(resolved_surface_mode(), ResolvedSurfaceMode::Translucent);
449
450 // A failed/incapable reinstall downgrades …
451 publish_resolved_surface_mode(false);
452 assert_eq!(
453 resolved_surface_mode(),
454 ResolvedSurfaceMode::RefusedTranslucent
455 );
456
457 // … and a recreated, capable surface takes it back up.
458 publish_resolved_surface_mode(true);
459 assert_eq!(resolved_surface_mode(), ResolvedSurfaceMode::Translucent);
460 }
461
462 /// Re-publishing the same value every frame (the shells' actual usage) is
463 /// a no-op beyond the store.
464 #[test]
465 fn repeated_publishes_are_idempotent() {
466 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
467 reset_slot();
468
469 publish_resolved_surface_mode(true);
470 publish_resolved_surface_mode(true);
471 publish_resolved_surface_mode(true);
472
473 assert_eq!(resolved_surface_mode(), ResolvedSurfaceMode::Translucent);
474 }
475
476 #[test]
477 fn publish_from_a_spawned_thread_is_observed() {
478 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
479 reset_slot();
480 declare_host_translucent_surface();
481
482 thread::spawn(|| {
483 publish_resolved_surface_mode(false);
484 })
485 .join()
486 .unwrap();
487
488 assert_eq!(
489 resolved_surface_mode(),
490 ResolvedSurfaceMode::RefusedTranslucent
491 );
492 }
493
494 /// The pure mapping, exhaustively — no globals touched, so this one needs
495 /// no `TEST_LOCK`.
496 #[test]
497 fn resolve_maps_every_declaration_resolution_pair() {
498 assert_eq!(
499 ResolvedSurfaceMode::resolve(SurfaceMode::Opaque, false),
500 ResolvedSurfaceMode::Opaque
501 );
502 assert_eq!(
503 ResolvedSurfaceMode::resolve(SurfaceMode::Opaque, true),
504 ResolvedSurfaceMode::Translucent
505 );
506 assert_eq!(
507 ResolvedSurfaceMode::resolve(SurfaceMode::Translucent, true),
508 ResolvedSurfaceMode::Translucent
509 );
510 assert_eq!(
511 ResolvedSurfaceMode::resolve(SurfaceMode::Translucent, false),
512 ResolvedSurfaceMode::RefusedTranslucent
513 );
514 }
515
516 /// `Unknown` is the default and is never produced by a resolution — a
517 /// published value always says something definite.
518 #[test]
519 fn unknown_is_the_default_and_never_a_resolution() {
520 assert_eq!(ResolvedSurfaceMode::default(), ResolvedSurfaceMode::Unknown);
521 for declared in [SurfaceMode::Opaque, SurfaceMode::Translucent] {
522 for resolved in [false, true] {
523 assert_ne!(
524 ResolvedSurfaceMode::resolve(declared, resolved),
525 ResolvedSurfaceMode::Unknown
526 );
527 }
528 }
529 assert!(!ResolvedSurfaceMode::Unknown.is_translucent());
530 assert!(!ResolvedSurfaceMode::Unknown.translucency_refused());
531 }
532}