concinnity_core/render/fullscreen.rs
1//! Backend-agnostic fullscreen-pass encoder seam, the first pilot of a hardware
2//! abstraction layer over the three render backends. The bloom
3//! prefilter -> downsample -> upsample chain is structurally identical on every
4//! backend, so its orchestration lives here once and each backend implements
5//! `BloomEncoder` to bind + draw one sub-pass in its own command stream.
6//!
7//! Two associated types absorb the only real divergence, so the trait names no
8//! backend types: `Rec` hides the per-backend command recorder, and `Args`
9//! carries the per-invocation binding context (DirectX passes the scene-colour
10//! SRV its prefilter samples; Vulkan threads the frame-in-flight index that
11//! selects its per-frame framebuffers + descriptor sets). Everything else each
12//! impl reads from `&self`, consistent with the read-only parallel-encode
13//! contract.
14//!
15//! Implemented by DirectX + Vulkan. Metal keeps its hand-rolled `encode_bloom`,
16//! already factored through its own `fullscreen_pass`, so this seam is unused
17//! (dead code) on a Metal build.
18
19use crate::gfx::render_types::TextDrawCall;
20use crate::math::{ceil, floor};
21use alloc::string::String;
22
23/// Convert a `TextDrawCall.clip_rect` (a rectangle `[x, y, w, h]` in overlay
24/// units, already mapped through the overlay transform by
25/// `gfx::text::band_to_window`) into an integer scissor rect `(x, y, w, h)` in
26/// attachment pixels, clamped to the attachment's bounds. Returns `None` when the
27/// clamped rectangle is empty (a row scrolled fully out of its band), so the
28/// caller skips the draw entirely.
29///
30/// `ui` is the overlay's logical size (see `RenderBackend::logical_size`) and
31/// `attach` the pixel size of the target the text pass writes. The two are equal
32/// wherever a window's logical units are pixels (Windows, unscaled X11), leaving
33/// a pure clamp; on a hi-DPI surface (macOS retina, scaled Wayland) the
34/// attachment is larger by the backing scale and the rect scales up with it. A
35/// zero logical dimension (minimised / mid-resize) falls back to a 1.0 scale
36/// rather than dividing by zero.
37pub fn clip_rect_to_scissor(
38 clip: [f32; 4],
39 ui: (f32, f32),
40 attach: (u32, u32),
41) -> Option<(i32, i32, u32, u32)> {
42 let aw = attach.0 as f32;
43 let ah = attach.1 as f32;
44 let sx = if ui.0 > 0.0 { aw / ui.0 } else { 1.0 };
45 let sy = if ui.1 > 0.0 { ah / ui.1 } else { 1.0 };
46 let x0 = floor(clip[0] * sx).clamp(0.0, aw);
47 let y0 = floor(clip[1] * sy).clamp(0.0, ah);
48 let x1 = ceil((clip[0] + clip[2]) * sx).clamp(0.0, aw);
49 let y1 = ceil((clip[1] + clip[3]) * sy).clamp(0.0, ah);
50 if x1 <= x0 || y1 <= y0 {
51 return None;
52 }
53 Some((x0 as i32, y0 as i32, (x1 - x0) as u32, (y1 - y0) as u32))
54}
55
56/// Round `offset` up to the next multiple of `align` (a power of two).
57pub fn align_up(offset: u64, align: u64) -> u64 {
58 (offset + align - 1) & !(align - 1)
59}
60
61/// Total bytes a frame's text geometry occupies in a backend's per-frame upload
62/// buffer, once each label's vertex and index blocks start on an `align`-byte
63/// boundary. Every sub-allocation aligns its start up and a prior aligned start
64/// plus an aligned size stays aligned, so this sum is an exact upper bound on the
65/// buffer cursor after all of a frame's blocks are appended: a slot reserved to
66/// it can never overflow mid-frame.
67///
68/// `align` is per backend: the alignment its buffer bindings require of a
69/// sub-range's offset.
70pub fn text_upload_bytes(text_calls: &[TextDrawCall], align: u64) -> u64 {
71 text_calls
72 .iter()
73 .map(|c| {
74 let v = core::mem::size_of_val(c.vertices.as_slice()) as u64;
75 let i = core::mem::size_of_val(c.indices.as_slice()) as u64;
76 align_up(v, align) + align_up(i, align)
77 })
78 .sum()
79}
80
81/// Per-backend hooks the shared bloom driver encodes through.
82pub trait BloomEncoder {
83 /// Per-backend command recorder (DX `ID3D12GraphicsCommandList`, VK `vk::CommandBuffer`).
84 type Rec;
85 /// Per-invocation binding context (DX scene-colour SRV handle, VK frame index).
86 type Args;
87
88 /// Number of bloom mips; zero means bloom is off and the driver no-ops.
89 fn bloom_mip_count(&self) -> usize;
90 /// One-time per-encode preamble, run once before the sub-passes and on the
91 /// same recorder: state every sub-pass shares belongs here, not in the
92 /// per-mip hooks (DX root signature / heap / IA state and the post-process
93 /// root constants; VK the post-process push constants).
94 fn begin_bloom(&self, rec: &Self::Rec, args: &Self::Args);
95 /// Prefilter: scene colour -> mip 0 (soft-knee threshold + Karis average).
96 fn bloom_prefilter(&self, rec: &Self::Rec, args: &Self::Args);
97 /// Downsample: mip `dst - 1` -> mip `dst`.
98 fn bloom_downsample(&self, rec: &Self::Rec, args: &Self::Args, dst: usize);
99 /// Upsample: mip `dst + 1` -> mip `dst`, additively blended.
100 fn bloom_upsample(&self, rec: &Self::Rec, args: &Self::Args, dst: usize);
101}
102
103/// The bloom chain orchestration, previously hand-duplicated in each backend's
104/// `encode_bloom`. On return, mip 0 holds the accumulated glow the composite pass
105/// samples.
106pub fn encode_bloom_chain<E: BloomEncoder>(enc: &E, rec: &E::Rec, args: E::Args) {
107 let n = enc.bloom_mip_count();
108 if n == 0 {
109 return;
110 }
111 enc.begin_bloom(rec, &args);
112 // Prefilter: scene -> mip 0.
113 enc.bloom_prefilter(rec, &args);
114 // Downsample chain: mip i-1 -> mip i.
115 for dst in 1..n {
116 enc.bloom_downsample(rec, &args, dst);
117 }
118 // Upsample chain: mip i+1 -> mip i, walking back down to mip 0.
119 for dst in (0..n - 1).rev() {
120 enc.bloom_upsample(rec, &args, dst);
121 }
122}
123
124/// The composite pass: tonemap (+ optional LUT grade) the post-stack scene onto
125/// the swapchain image, then layer the text overlay on top in the same pass. Its
126/// begin -> composite-draw -> text-loop -> end shape is identical on every
127/// backend; the swapchain target lifecycle, the descriptor binding, and the
128/// text-geometry uploads stay backend-specific behind the trait. `Args`
129/// carries the per-frame binding context each backend needs (DX: the swapchain
130/// back-buffer + its RTV, the scene SRV, the window size, the frame slot; VK: the
131/// acquired image index + the frame slot).
132///
133/// Every backend uploads a frame's text geometry into one persistent buffer per
134/// frame-in-flight slot, reserved up front with [`text_upload_bytes`] and
135/// appended to per call, and binds sub-ranges of it: no GPU buffer is created
136/// per label per frame anywhere. DX and VK append inside `text_draw`; Metal
137/// (which drives its own composite loop rather than this trait) writes the whole
138/// frame's geometry into its slot before the render graph runs.
139pub trait CompositeEncoder {
140 /// Per-backend command recorder (DX `ID3D12GraphicsCommandList`, VK `vk::CommandBuffer`).
141 type Rec;
142 /// Per-invocation binding context (see the trait doc).
143 type Args;
144
145 /// Begin the pass: target the swapchain image (DX transitions it to
146 /// RENDER_TARGET + binds the RTV; VK begins the composite render pass) and set
147 /// the full-window viewport / scissor.
148 fn begin_composite(&self, rec: &Self::Rec, args: &Self::Args);
149 /// The fullscreen tonemap draw: bind the composite pipeline + its inputs
150 /// (scene, bloom, LUT) + push constants, draw the fullscreen triangle.
151 fn composite_draw(&self, rec: &Self::Rec, args: &Self::Args);
152 /// Bind the text pipeline + any one-time text state. Returns false when text
153 /// is inert (no pipeline or no atlases), so the driver skips the call loop.
154 fn begin_text(&self, rec: &Self::Rec, args: &Self::Args) -> bool;
155 /// Encode one text draw call: append its vertex/index geometry to this frame
156 /// slot's persistent upload buffer, bind the atlas plus the two sub-ranges,
157 /// and draw.
158 fn text_draw(
159 &self,
160 rec: &Self::Rec,
161 args: &Self::Args,
162 call: &TextDrawCall,
163 ) -> Result<(), String>;
164 /// End the pass: DX transitions the back-buffer back to PRESENT; VK ends the
165 /// render pass.
166 fn end_composite(&self, rec: &Self::Rec, args: &Self::Args);
167}
168
169/// The composite + text orchestration, previously hand-duplicated in each
170/// backend's `encode_composite_and_text`. An error mid-text propagates without
171/// closing the pass, matching the prior DX/VK behaviour (the frame fails either
172/// way: the target is just left mis-stated). This is unused on Metal, where a
173/// render encoder must be `endEncoding`-ed before the command buffer commits:
174/// skipping `end_composite` on a text error would crash at commit, so Metal's
175/// `encode_composite_and_text` ends the encoder on any `?` with a `ScopedEncoder`
176/// RAII guard instead.
177pub fn encode_composite_chain<E: CompositeEncoder>(
178 enc: &E,
179 rec: &E::Rec,
180 args: &E::Args,
181 text_calls: &[TextDrawCall],
182) -> Result<(), String> {
183 enc.begin_composite(rec, args);
184 enc.composite_draw(rec, args);
185 if !text_calls.is_empty() && enc.begin_text(rec, args) {
186 for call in text_calls {
187 enc.text_draw(rec, args, call)?;
188 }
189 }
190 enc.end_composite(rec, args);
191 Ok(())
192}
193
194/// A single-draw fullscreen post pass (SSR resolve, TAA resolve, ...): target a
195/// render target, bind a pipeline + inputs, draw one fullscreen triangle, restore.
196/// Unlike the bloom + composite chains (whose drivers hold a mip / text loop), a
197/// fullscreen pass has no loop, so the driver is a fixed begin -> draw -> end. The
198/// value is the shared per-backend lifecycle factored behind begin/end (DX: the
199/// PSR<->RENDER_TARGET barrier bracket + render-target bind; VK: the render-pass
200/// bracket), reused across every such pass instead of re-pasted per pass.
201///
202/// The inert-pass guard lives at each backend's call site: it resolves the pass's
203/// resources (returning early if a required one is absent) BEFORE constructing the
204/// encoder, so the driver always runs all three steps over a fully-resolved pass
205/// and can never leave a render pass / barrier half-open. There is no `Args`: each
206/// backend's encoder is a small struct holding the already-resolved references +
207/// per-call scalars, so the trait names no backend types (like `BloomEncoder`).
208///
209/// Implemented by DirectX + Vulkan. Metal keeps its own `fullscreen_pass` helper,
210/// which already factors this begin/draw/end skeleton, so this seam is unused
211/// (dead code) on a Metal build.
212pub trait FullscreenPass {
213 /// Per-backend command recorder (DX `ID3D12GraphicsCommandList`, VK `vk::CommandBuffer`).
214 type Rec;
215
216 /// Begin: bind the target render target + set the full-resolution viewport /
217 /// scissor. DX transitions the target PIXEL_SHADER_RESOURCE -> RENDER_TARGET,
218 /// binds its RTV + the SRV heap; VK begins the pass's render pass.
219 fn begin(&self, rec: &Self::Rec);
220 /// Bind the pipeline + inputs + per-frame params and draw the fullscreen
221 /// triangle (3 vertices; the vertex shader builds the triangle from the id).
222 fn draw(&self, rec: &Self::Rec);
223 /// End: DX transitions the target back to PIXEL_SHADER_RESOURCE; VK ends the
224 /// render pass.
225 fn end(&self, rec: &Self::Rec);
226}
227
228/// The fullscreen-pass orchestration. Trivial by design (a single draw), but kept
229/// as a driver so every fullscreen post pass shares one begin -> draw -> end
230/// contract across backends, matching `encode_bloom_chain` / `encode_composite_chain`.
231pub fn encode_fullscreen<E: FullscreenPass>(enc: &E, rec: &E::Rec) {
232 enc.begin(rec);
233 enc.draw(rec);
234 enc.end(rec);
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use crate::gfx::render_types::TextDrawCall;
241 use core::cell::RefCell;
242
243 use alloc::format;
244 use alloc::string::ToString;
245 use alloc::vec;
246 use alloc::vec::Vec;
247 #[test]
248 fn clip_inside_attachment_passes_through() {
249 // Logical units are attachment pixels (Windows, unscaled X11): 1:1.
250 assert_eq!(
251 clip_rect_to_scissor([100.0, 50.0, 300.0, 200.0], (1280.0, 720.0), (1280, 720)),
252 Some((100, 50, 300, 200))
253 );
254 }
255
256 #[test]
257 fn clip_scales_from_logical_units_to_a_hi_dpi_attachment() {
258 // A 2x backing scale (macOS retina, scaled Wayland): the band covers the
259 // same fraction of an attachment twice the logical size.
260 assert_eq!(
261 clip_rect_to_scissor([100.0, 50.0, 300.0, 200.0], (1024.0, 768.0), (2048, 1536)),
262 Some((200, 100, 600, 400))
263 );
264 // A non-integer scale still lands on whole pixels, rounded outward so a
265 // band never crops the glyphs it should show.
266 assert_eq!(
267 clip_rect_to_scissor([10.0, 10.0, 100.0, 100.0], (1000.0, 1000.0), (1500, 1500)),
268 Some((15, 15, 150, 150))
269 );
270 }
271
272 #[test]
273 fn clip_is_clamped_to_attachment_bounds() {
274 // A band hanging off the right / bottom edge is clamped to the target.
275 assert_eq!(
276 clip_rect_to_scissor([1200.0, 700.0, 400.0, 400.0], (1280.0, 720.0), (1280, 720)),
277 Some((1200, 700, 80, 20))
278 );
279 // A negative origin is clamped to zero, shrinking the width/height.
280 assert_eq!(
281 clip_rect_to_scissor([-40.0, -10.0, 100.0, 100.0], (1280.0, 720.0), (1280, 720)),
282 Some((0, 0, 60, 90))
283 );
284 // The clamp is against the attachment, after scaling.
285 assert_eq!(
286 clip_rect_to_scissor([600.0, 350.0, 200.0, 200.0], (640.0, 360.0), (1280, 720)),
287 Some((1200, 700, 80, 20))
288 );
289 }
290
291 #[test]
292 fn fully_offscreen_clip_is_skipped() {
293 // A band entirely past the attachment yields no scissor (skip the draw).
294 assert_eq!(
295 clip_rect_to_scissor([2000.0, 50.0, 100.0, 100.0], (1280.0, 720.0), (1280, 720)),
296 None
297 );
298 // A zero-area band is also skipped.
299 assert_eq!(
300 clip_rect_to_scissor([10.0, 10.0, 0.0, 50.0], (1280.0, 720.0), (1280, 720)),
301 None
302 );
303 }
304
305 #[test]
306 fn a_zero_logical_size_falls_back_to_an_unscaled_clip() {
307 // Minimised / mid-resize: no divide by zero, and the rect is still
308 // clamped into the attachment.
309 assert_eq!(
310 clip_rect_to_scissor([10.0, 20.0, 100.0, 100.0], (0.0, 0.0), (1280, 720)),
311 Some((10, 20, 100, 100))
312 );
313 }
314
315 // A text-only draw call for the composite driver: the drivers never inspect
316 // its contents, so the geometry is empty.
317 fn text_call() -> TextDrawCall {
318 TextDrawCall {
319 vertices: Vec::new(),
320 indices: Vec::new(),
321 atlas_slot: 0,
322 clip_rect: None,
323 layer: 0,
324 }
325 }
326
327 // A call carrying `glyphs` quads: 4 vertices + 6 indices each, the shape
328 // `gfx::text::build_text_calls` emits.
329 fn glyph_call(glyphs: usize) -> TextDrawCall {
330 TextDrawCall {
331 vertices: vec![
332 crate::gfx::render_types::TextVertex {
333 pos: [0.0; 2],
334 uv: [0.0; 2],
335 color: [0.0; 3],
336 mode: 0.0,
337 };
338 glyphs * 4
339 ],
340 indices: vec![0u16; glyphs * 6],
341 atlas_slot: 0,
342 clip_rect: None,
343 layer: 0,
344 }
345 }
346
347 #[test]
348 fn align_up_rounds_to_multiple() {
349 assert_eq!(align_up(0, 16), 0);
350 assert_eq!(align_up(1, 16), 16);
351 assert_eq!(align_up(16, 16), 16);
352 assert_eq!(align_up(17, 16), 32);
353 assert_eq!(align_up(257, 256), 512);
354 }
355
356 #[test]
357 fn text_upload_bytes_is_zero_without_calls() {
358 assert_eq!(text_upload_bytes(&[], 256), 0);
359 // An empty call still contributes nothing: both blocks are zero bytes.
360 assert_eq!(text_upload_bytes(&[text_call()], 256), 0);
361 }
362
363 #[test]
364 fn text_upload_bytes_aligns_each_block() {
365 // One glyph: 4 * 32 B of vertices (already a multiple of 16) and 12 B of
366 // indices (rounded up).
367 assert_eq!(text_upload_bytes(&[glyph_call(1)], 16), 128 + 16);
368 assert_eq!(text_upload_bytes(&[glyph_call(1)], 256), 256 + 256);
369 }
370
371 // The reserved size must be an upper bound on the cursor after a run of
372 // appends (an aligned start plus an aligned size stays aligned), so a slot
373 // reserved to it can never overflow mid-frame.
374 #[test]
375 fn text_upload_bytes_bounds_a_simulated_cursor() {
376 let calls = [glyph_call(3), glyph_call(1), glyph_call(17), glyph_call(0)];
377 for align in [16u64, 256] {
378 let total = text_upload_bytes(&calls, align);
379 let mut cursor = 0u64;
380 for c in &calls {
381 for block in [
382 core::mem::size_of_val(c.vertices.as_slice()) as u64,
383 core::mem::size_of_val(c.indices.as_slice()) as u64,
384 ] {
385 cursor = align_up(cursor, align) + block;
386 assert!(cursor <= total, "cursor {cursor} exceeded reserved {total}");
387 }
388 }
389 }
390 }
391
392 // A mock bloom encoder recording each sub-pass in call order. The trait's
393 // associated types name no backend types, so both are `()`.
394 struct MockBloom {
395 mips: usize,
396 log: RefCell<Vec<String>>,
397 }
398
399 impl BloomEncoder for MockBloom {
400 type Rec = ();
401 type Args = ();
402
403 fn bloom_mip_count(&self) -> usize {
404 self.mips
405 }
406 fn begin_bloom(&self, _rec: &(), _args: &()) {
407 self.log.borrow_mut().push("begin".into());
408 }
409 fn bloom_prefilter(&self, _rec: &(), _args: &()) {
410 self.log.borrow_mut().push("prefilter".into());
411 }
412 fn bloom_downsample(&self, _rec: &(), _args: &(), dst: usize) {
413 self.log.borrow_mut().push(format!("down{dst}"));
414 }
415 fn bloom_upsample(&self, _rec: &(), _args: &(), dst: usize) {
416 self.log.borrow_mut().push(format!("up{dst}"));
417 }
418 }
419
420 #[test]
421 fn bloom_chain_encodes_prefilter_downsample_upsample_in_order() {
422 // 3 mips: prefilter, then the downsample chain 1..3, then the upsample
423 // chain walking back down (1, 0).
424 let enc = MockBloom {
425 mips: 3,
426 log: RefCell::new(Vec::new()),
427 };
428 encode_bloom_chain(&enc, &(), ());
429 assert_eq!(
430 *enc.log.borrow(),
431 ["begin", "prefilter", "down1", "down2", "up1", "up0"]
432 );
433 }
434
435 #[test]
436 fn bloom_chain_begins_once_whatever_the_mip_count() {
437 // Backends push the shared post-process constants in `begin_bloom` and
438 // rely on them surviving every sub-pass, so the preamble must run
439 // exactly once per chain, ahead of the first draw.
440 for mips in 1..8 {
441 let enc = MockBloom {
442 mips,
443 log: RefCell::new(Vec::new()),
444 };
445 encode_bloom_chain(&enc, &(), ());
446 let log = enc.log.borrow();
447 assert_eq!(log.iter().filter(|e| *e == "begin").count(), 1);
448 assert_eq!(log[0], "begin");
449 }
450 }
451
452 #[test]
453 fn bloom_chain_with_zero_mips_is_a_noop() {
454 // Bloom off: the driver returns before touching the encoder at all.
455 let enc = MockBloom {
456 mips: 0,
457 log: RefCell::new(Vec::new()),
458 };
459 encode_bloom_chain(&enc, &(), ());
460 assert!(enc.log.borrow().is_empty());
461 }
462
463 // A mock composite encoder. `text_ready` is the `begin_text` return; when
464 // `fail_at` matches a text-draw index that draw returns an error.
465 struct MockComposite {
466 text_ready: bool,
467 fail_at: Option<usize>,
468 log: RefCell<Vec<String>>,
469 text_seen: RefCell<usize>,
470 }
471
472 impl MockComposite {
473 fn new(text_ready: bool, fail_at: Option<usize>) -> Self {
474 Self {
475 text_ready,
476 fail_at,
477 log: RefCell::new(Vec::new()),
478 text_seen: RefCell::new(0),
479 }
480 }
481 }
482
483 impl CompositeEncoder for MockComposite {
484 type Rec = ();
485 type Args = ();
486
487 fn begin_composite(&self, _rec: &(), _args: &()) {
488 self.log.borrow_mut().push("begin".into());
489 }
490 fn composite_draw(&self, _rec: &(), _args: &()) {
491 self.log.borrow_mut().push("draw".into());
492 }
493 fn begin_text(&self, _rec: &(), _args: &()) -> bool {
494 self.log.borrow_mut().push("begin_text".into());
495 self.text_ready
496 }
497 fn text_draw(&self, _rec: &(), _args: &(), _call: &TextDrawCall) -> Result<(), String> {
498 let mut n = self.text_seen.borrow_mut();
499 self.log.borrow_mut().push(format!("text{}", *n));
500 let fail = self.fail_at == Some(*n);
501 *n += 1;
502 if fail {
503 return Err("text upload failed".into());
504 }
505 Ok(())
506 }
507 fn end_composite(&self, _rec: &(), _args: &()) {
508 self.log.borrow_mut().push("end".into());
509 }
510 }
511
512 #[test]
513 fn composite_chain_orders_passes_then_text_then_end() {
514 let enc = MockComposite::new(true, None);
515 let calls = [text_call(), text_call()];
516 let r = encode_composite_chain(&enc, &(), &(), &calls);
517 assert!(r.is_ok());
518 assert_eq!(
519 *enc.log.borrow(),
520 ["begin", "draw", "begin_text", "text0", "text1", "end"]
521 );
522 }
523
524 #[test]
525 fn composite_chain_propagates_text_error_without_ending() {
526 // The first text draw fails: the error propagates and, matching the
527 // prior DX/VK behaviour, the pass is left open (no `end_composite`) and
528 // the remaining text calls are skipped.
529 let enc = MockComposite::new(true, Some(0));
530 let calls = [text_call(), text_call()];
531 let r = encode_composite_chain(&enc, &(), &(), &calls);
532 assert_eq!(r, Err("text upload failed".into()));
533 let log = enc.log.borrow();
534 assert_eq!(*log, ["begin", "draw", "begin_text", "text0"]);
535 assert!(!log.contains(&"end".to_string()), "pass must stay open");
536 }
537
538 #[test]
539 fn composite_chain_with_no_text_skips_the_text_loop() {
540 // Empty text: `begin_text` is never called, but the pass still ends.
541 let enc = MockComposite::new(true, None);
542 let r = encode_composite_chain(&enc, &(), &(), &[]);
543 assert!(r.is_ok());
544 assert_eq!(*enc.log.borrow(), ["begin", "draw", "end"]);
545 }
546
547 #[test]
548 fn composite_chain_skips_draws_when_text_is_inert() {
549 // `begin_text` returns false (no pipeline / atlases): no per-call draws,
550 // but the pass still ends cleanly.
551 let enc = MockComposite::new(false, None);
552 let calls = [text_call()];
553 let r = encode_composite_chain(&enc, &(), &(), &calls);
554 assert!(r.is_ok());
555 assert_eq!(*enc.log.borrow(), ["begin", "draw", "begin_text", "end"]);
556 }
557
558 // A mock single-draw fullscreen pass recording its lifecycle.
559 struct MockFullscreen {
560 log: RefCell<Vec<String>>,
561 }
562
563 impl FullscreenPass for MockFullscreen {
564 type Rec = ();
565
566 fn begin(&self, _rec: &()) {
567 self.log.borrow_mut().push("begin".into());
568 }
569 fn draw(&self, _rec: &()) {
570 self.log.borrow_mut().push("draw".into());
571 }
572 fn end(&self, _rec: &()) {
573 self.log.borrow_mut().push("end".into());
574 }
575 }
576
577 #[test]
578 fn fullscreen_encodes_begin_draw_end() {
579 let enc = MockFullscreen {
580 log: RefCell::new(Vec::new()),
581 };
582 encode_fullscreen(&enc, &());
583 assert_eq!(*enc.log.borrow(), ["begin", "draw", "end"]);
584 }
585}