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