Skip to main content

optic_render/handles/
canvas.rs

1use optic_core::{ImgFilter, ImgFormat, ImgWrap, OpticError, OpticErrorKind, OpticResult, Size2D};
2
3use crate::handles::texture::{Texture2D, delete_texture};
4
5/// Describes the format and structure of an off-screen framebuffer (canvas).
6///
7/// Pass to [`Canvas::new`] to create the GPU framebuffer.
8#[derive(Clone, Debug)]
9pub struct CanvasDesc {
10    pub size: Size2D,
11    pub color_formats: Vec<ImgFormat>,
12    pub depth: bool,
13    pub depth_as_texture: bool,
14    pub depth_compare: bool,
15    pub stencil: bool,
16    pub samples: u32,
17    pub filter: ImgFilter,
18    pub wrap: ImgWrap,
19}
20
21impl Default for CanvasDesc {
22    fn default() -> Self {
23        Self {
24            size: Size2D::from(512, 512),
25            color_formats: vec![ImgFormat::RGBA(8)],
26            depth: true,
27            depth_as_texture: true,
28            depth_compare: false,
29            stencil: false,
30            samples: 0,
31            filter: ImgFilter::Linear,
32            wrap: ImgWrap::Extend,
33        }
34    }
35}
36
37/// An off-screen render target (framebuffer object) with optional MSAA.
38///
39/// Supports multiple colour attachments, depth/stencil, and MSAA resolve.
40/// Created via [`Canvas::new`] or [`GPU::ship_canvas`](crate::GPU::ship_canvas).
41///
42/// # Example
43///
44/// ```ignore
45/// use optic_render::handles::{Canvas, CanvasDesc};
46///
47/// let canvas = Canvas::new(&CanvasDesc::default())?;
48/// ```
49pub struct Canvas {
50    pub(crate) fbo_id: u32,
51    pub(crate) resolve_fbo_id: u32,
52    pub(crate) msaa_rbos: Vec<u32>,
53    pub(crate) depth_stencil_rbo: u32,
54    pub(crate) color_texs: Vec<Texture2D>,
55    pub(crate) depth_tex: Option<Texture2D>,
56    pub(crate) size: Size2D,
57    #[allow(dead_code)]
58    pub(crate) samples: u32,
59    pub(crate) has_stencil: bool,
60    pub(crate) has_depth: bool,
61    #[allow(dead_code)]
62    pub(crate) depth_as_texture: bool,
63    pub(crate) desc: CanvasDesc,
64}
65
66impl Canvas {
67    /// Creates a new framebuffer from a descriptor.
68    ///
69    /// Validates constraints:
70    /// - At least one colour format or depth must be specified
71    /// - Stencil requires depth
72    /// - `depth_compare` requires `depth_as_texture`
73    pub fn new(desc: &CanvasDesc) -> OpticResult<Self> {
74        if desc.color_formats.is_empty() && !desc.depth {
75            return Err(OpticError::new(
76                OpticErrorKind::Custom,
77                "Canvas: at least one color format or depth must be specified",
78            ));
79        }
80        if desc.stencil && !desc.depth {
81            return Err(OpticError::new(
82                OpticErrorKind::Custom,
83                "Canvas: stencil requires depth to be enabled",
84            ));
85        }
86        if desc.depth_compare && !desc.depth_as_texture {
87            return Err(OpticError::new(
88                OpticErrorKind::Custom,
89                "Canvas: depth_compare requires depth_as_texture",
90            ));
91        }
92
93        let size = desc.size;
94        let has_msaa = desc.samples > 1;
95        let msaa_s = if has_msaa { desc.samples } else { 0 };
96
97        let fbo_id = unsafe {
98            let mut id = 0;
99            gl::GenFramebuffers(1, &mut id);
100            gl::BindFramebuffer(gl::FRAMEBUFFER, id);
101            id
102        };
103
104        let mut color_texs: Vec<Texture2D> = Vec::new();
105        let mut msaa_rbos: Vec<u32> = Vec::new();
106        let mut depth_stencil_rbo = 0u32;
107        let mut depth_tex: Option<Texture2D> = None;
108        let mut resolve_fbo_id = 0u32;
109
110        for (i, fmt) in desc.color_formats.iter().enumerate() {
111            let attachment = gl::COLOR_ATTACHMENT0 + i as u32;
112            if has_msaa {
113                let rbo = create_rbo_storage_msaa(size, msaa_s, fmt, desc.stencil);
114                unsafe {
115                    gl::FramebufferRenderbuffer(gl::FRAMEBUFFER, attachment, gl::RENDERBUFFER, rbo);
116                }
117                msaa_rbos.push(rbo);
118            } else {
119                let tex_id = create_empty_tex(size, fmt, desc.filter, desc.wrap);
120                unsafe {
121                    gl::FramebufferTexture2D(gl::FRAMEBUFFER, attachment, gl::TEXTURE_2D, tex_id, 0);
122                }
123                color_texs.push(Texture2D::new(tex_id, size, *fmt, desc.filter, desc.wrap));
124            }
125        }
126
127        if !desc.color_formats.is_empty() {
128            let attachments: Vec<u32> = (0..desc.color_formats.len() as u32)
129                .map(|i| gl::COLOR_ATTACHMENT0 + i)
130                .collect();
131            unsafe {
132                gl::DrawBuffers(desc.color_formats.len() as i32, attachments.as_ptr());
133            }
134        } else {
135            unsafe { gl::DrawBuffer(gl::NONE); }
136        }
137
138        if desc.depth {
139            if has_msaa {
140                let (internal, att) = depth_rbo_params(desc.stencil);
141                let rbo = unsafe {
142                    let mut id = 0;
143                    gl::GenRenderbuffers(1, &mut id);
144                    gl::BindRenderbuffer(gl::RENDERBUFFER, id);
145                    gl::RenderbufferStorageMultisample(
146                        gl::RENDERBUFFER, msaa_s as i32, internal as u32, size.w as i32, size.h as i32,
147                    );
148                    gl::FramebufferRenderbuffer(gl::FRAMEBUFFER, att, gl::RENDERBUFFER, id);
149                    gl::BindRenderbuffer(gl::RENDERBUFFER, 0);
150                    id
151                };
152                depth_stencil_rbo = rbo;
153            } else if desc.depth_as_texture {
154                let tex_id = create_depth_tex(size, desc.stencil, desc.depth_compare);
155                let att = if desc.stencil {
156                    gl::DEPTH_STENCIL_ATTACHMENT
157                } else {
158                    gl::DEPTH_ATTACHMENT
159                };
160                unsafe {
161                    gl::FramebufferTexture2D(gl::FRAMEBUFFER, att, gl::TEXTURE_2D, tex_id, 0);
162                }
163                depth_tex = Some(Texture2D::new(
164                    tex_id, size, ImgFormat::R(32), ImgFilter::Closest, ImgWrap::Extend,
165                ));
166            } else {
167                let (internal, att) = depth_rbo_params(desc.stencil);
168                let rbo = unsafe {
169                    let mut id = 0;
170                    gl::GenRenderbuffers(1, &mut id);
171                    gl::BindRenderbuffer(gl::RENDERBUFFER, id);
172                    gl::RenderbufferStorage(gl::RENDERBUFFER, internal as u32, size.w as i32, size.h as i32);
173                    gl::FramebufferRenderbuffer(gl::FRAMEBUFFER, att, gl::RENDERBUFFER, id);
174                    gl::BindRenderbuffer(gl::RENDERBUFFER, 0);
175                    id
176                };
177                depth_stencil_rbo = rbo;
178            }
179        }
180
181        if has_msaa {
182            resolve_fbo_id = unsafe {
183                let mut id = 0;
184                gl::GenFramebuffers(1, &mut id);
185                gl::BindFramebuffer(gl::FRAMEBUFFER, id);
186                id
187            };
188
189            for (i, fmt) in desc.color_formats.iter().enumerate() {
190                let tex_id = create_empty_tex(size, fmt, desc.filter, desc.wrap);
191                unsafe {
192                    gl::FramebufferTexture2D(
193                        gl::FRAMEBUFFER,
194                        gl::COLOR_ATTACHMENT0 + i as u32,
195                        gl::TEXTURE_2D, tex_id, 0,
196                    );
197                }
198                color_texs.push(Texture2D::new(tex_id, size, *fmt, desc.filter, desc.wrap));
199            }
200
201            if !desc.color_formats.is_empty() {
202                let attachments: Vec<u32> = (0..desc.color_formats.len() as u32)
203                    .map(|i| gl::COLOR_ATTACHMENT0 + i)
204                    .collect();
205                unsafe {
206                    gl::DrawBuffers(desc.color_formats.len() as i32, attachments.as_ptr());
207                }
208            } else {
209                unsafe { gl::DrawBuffer(gl::NONE); }
210            }
211
212            if desc.depth && desc.depth_as_texture {
213                let tex_id = create_depth_tex(size, desc.stencil, desc.depth_compare);
214                let att = if desc.stencil {
215                    gl::DEPTH_STENCIL_ATTACHMENT
216                } else {
217                    gl::DEPTH_ATTACHMENT
218                };
219                unsafe {
220                    gl::FramebufferTexture2D(gl::FRAMEBUFFER, att, gl::TEXTURE_2D, tex_id, 0);
221                }
222                depth_tex = Some(Texture2D::new(
223                    tex_id, size, ImgFormat::R(32), ImgFilter::Closest, ImgWrap::Extend,
224                ));
225            }
226
227            unsafe { gl::BindFramebuffer(gl::FRAMEBUFFER, fbo_id); }
228        }
229
230        let complete = unsafe { gl::CheckFramebufferStatus(gl::FRAMEBUFFER) };
231        unsafe { gl::BindFramebuffer(gl::FRAMEBUFFER, 0); }
232
233        if complete != gl::FRAMEBUFFER_COMPLETE {
234            unsafe {
235                gl::DeleteFramebuffers(1, &fbo_id);
236                if resolve_fbo_id != 0 {
237                    gl::DeleteFramebuffers(1, &resolve_fbo_id);
238                }
239                for &rbo in &msaa_rbos {
240                    gl::DeleteRenderbuffers(1, &rbo);
241                }
242                if depth_stencil_rbo != 0 {
243                    gl::DeleteRenderbuffers(1, &depth_stencil_rbo);
244                }
245            }
246            for tex in &color_texs {
247                delete_texture(tex.id);
248            }
249            if let Some(ref tex) = depth_tex {
250                delete_texture(tex.id);
251            }
252            return Err(OpticError::new(
253                OpticErrorKind::Framebuffer,
254                &format!("framebuffer incomplete: status={complete:#x}"),
255            ));
256        }
257
258        Ok(Self {
259            fbo_id,
260            resolve_fbo_id,
261            msaa_rbos,
262            depth_stencil_rbo,
263            color_texs,
264            depth_tex,
265            size,
266            samples: desc.samples,
267            has_stencil: desc.stencil,
268            has_depth: desc.depth,
269            depth_as_texture: desc.depth_as_texture,
270            desc: desc.clone(),
271        })
272    }
273
274    /// Returns the canvas size.
275    pub fn size(&self) -> Size2D {
276        self.size
277    }
278
279    /// Returns a reference to the colour texture at the given attachment index.
280    pub fn color_tex(&self, index: usize) -> OpticResult<&Texture2D> {
281        self.color_texs.get(index).ok_or_else(|| {
282            OpticError::new(
283                OpticErrorKind::Custom,
284                &format!("Canvas color attachment index {index} out of range ({} attachments)", self.color_texs.len()),
285            )
286        })
287    }
288
289    /// Returns a reference to the depth texture, if present.
290    pub fn depth_tex(&self) -> Option<&Texture2D> {
291        self.depth_tex.as_ref()
292    }
293
294    /// Resizes the canvas by recreating the framebuffer with a new size.
295    pub fn set_size(&mut self, new_size: Size2D) -> OpticResult<()> {
296        let mut new_desc = self.desc.clone();
297        new_desc.size = new_size;
298        let new_canvas = Canvas::new(&new_desc)?;
299        *self = new_canvas;
300        Ok(())
301    }
302
303    /// Resolves MSAA colour/depth/stencil into the resolve framebuffer.
304    ///
305    /// No-op if the canvas does not use MSAA.
306    pub fn resolve(&self) {
307        if self.resolve_fbo_id == 0 {
308            return;
309        }
310        unsafe {
311            gl::BindFramebuffer(gl::READ_FRAMEBUFFER, self.fbo_id);
312            gl::BindFramebuffer(gl::DRAW_FRAMEBUFFER, self.resolve_fbo_id);
313            let mut mask = gl::COLOR_BUFFER_BIT;
314            if self.has_depth {
315                mask |= gl::DEPTH_BUFFER_BIT;
316            }
317            if self.has_stencil {
318                mask |= gl::STENCIL_BUFFER_BIT;
319            }
320            gl::BlitFramebuffer(
321                0, 0, self.size.w as i32, self.size.h as i32,
322                0, 0, self.size.w as i32, self.size.h as i32,
323                mask, gl::NEAREST,
324            );
325            gl::BindFramebuffer(gl::READ_FRAMEBUFFER, 0);
326            gl::BindFramebuffer(gl::DRAW_FRAMEBUFFER, 0);
327        }
328    }
329
330    /// Blits this canvas into the default framebuffer (screen), scaling to fit.
331    pub fn blit_to_screen(&self, window_size: Size2D) {
332        self.resolve_if_needed();
333        let src = if self.resolve_fbo_id != 0 {
334            self.resolve_fbo_id
335        } else {
336            self.fbo_id
337        };
338        unsafe {
339            gl::BindFramebuffer(gl::READ_FRAMEBUFFER, src);
340            gl::BindFramebuffer(gl::DRAW_FRAMEBUFFER, 0);
341            gl::BlitFramebuffer(
342                0, 0, self.size.w as i32, self.size.h as i32,
343                0, 0, window_size.w as i32, window_size.h as i32,
344                gl::COLOR_BUFFER_BIT, gl::LINEAR,
345            );
346            gl::BindFramebuffer(gl::READ_FRAMEBUFFER, 0);
347        }
348    }
349
350    /// Sets the viewport scissor within this canvas.
351    ///
352    /// The rectangle must be within the canvas bounds.
353    pub fn set_renderable_area(&self, x: i32, y: i32, size: Size2D) -> OpticResult<()> {
354        if x < 0
355            || y < 0
356            || size.w <= 0
357            || size.h <= 0
358            || x + size.w as i32 > self.size.w as i32
359            || y + size.h as i32 > self.size.h as i32
360        {
361            return Err(OpticError::new(
362                OpticErrorKind::Custom,
363                &format!(
364                    "Canvas::set_renderable_area rect ({},{},{},{}) exceeds canvas size ({},{})",
365                    x, y, size.w, size.h, self.size.w, self.size.h,
366                ),
367            ));
368        }
369        unsafe {
370            gl::Viewport(x, y, size.w as i32, size.h as i32);
371        }
372        Ok(())
373    }
374
375    /// Reads pixel data from a colour attachment back to the CPU.
376    pub fn read_pixels(&self, index: usize) -> OpticResult<Vec<u8>> {
377        self.resolve_if_needed();
378        let tex = self.color_tex(index)?;
379        let src = if self.resolve_fbo_id != 0 {
380            self.resolve_fbo_id
381        } else {
382            self.fbo_id
383        };
384        let mut prev_fbo = 0i32;
385        unsafe {
386            gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut prev_fbo);
387            gl::BindFramebuffer(gl::FRAMEBUFFER, src);
388        }
389        let (format, pix_type, pixel_size) = fmt_gl_params(&tex.fmt);
390        let total = (self.size.w as usize) * (self.size.h as usize) * pixel_size;
391        let mut pixels = vec![0u8; total];
392        unsafe {
393            gl::ReadPixels(
394                0, 0, self.size.w as i32, self.size.h as i32,
395                format, pix_type, pixels.as_mut_ptr() as *mut _,
396            );
397            gl::BindFramebuffer(gl::FRAMEBUFFER, prev_fbo as u32);
398        }
399        Ok(pixels)
400    }
401
402    /// Saves a colour attachment to an image file on disk.
403    ///
404    /// Supported formats depend on the colour attachment format (8/16/32 bpc,
405    /// 1–4 channels).
406    pub fn save_to_disk(&self, index: usize, path: &str) -> OpticResult<()> {
407        let data = self.read_pixels(index)?;
408        let tex = self.color_tex(index)?;
409        let channels = tex.fmt.channels() as u8;
410        let bit_depth = tex.fmt.bit_depth();
411        let ct = match (channels, bit_depth) {
412            (1, 8) => image::ColorType::L8,
413            (2, 8) => image::ColorType::La8,
414            (3, 8) => image::ColorType::Rgb8,
415            (4, 8) => image::ColorType::Rgba8,
416            (1, 16) => image::ColorType::L16,
417            (2, 16) => image::ColorType::La16,
418            (3, 16) => image::ColorType::Rgb16,
419            (4, 16) => image::ColorType::Rgba16,
420            (3, 32) => image::ColorType::Rgb32F,
421            (4, 32) => image::ColorType::Rgba32F,
422            _ => {
423                return Err(OpticError::new(
424                    OpticErrorKind::Custom,
425                    &format!("unsupported format for save_to_file: {}x{}bpp", channels, bit_depth),
426                ))
427            }
428        };
429        image::save_buffer(path, &data, self.size.w, self.size.h, ct).map_err(|e| {
430            OpticError::new(OpticErrorKind::File, &format!("failed to save image: {e}"))
431        })
432    }
433
434    /// Deletes all GL resources (FBOs, RBOs, textures).
435    pub fn delete(&mut self) {
436        unsafe {
437            gl::DeleteFramebuffers(1, &self.fbo_id);
438            if self.resolve_fbo_id != 0 {
439                gl::DeleteFramebuffers(1, &self.resolve_fbo_id);
440            }
441            for &rbo in &self.msaa_rbos {
442                gl::DeleteRenderbuffers(1, &rbo);
443            }
444            if self.depth_stencil_rbo != 0 {
445                gl::DeleteRenderbuffers(1, &self.depth_stencil_rbo);
446            }
447        }
448        for tex in std::mem::take(&mut self.color_texs) {
449            tex.delete();
450        }
451        if let Some(tex) = self.depth_tex.take() {
452            tex.delete();
453        }
454    }
455
456    fn resolve_if_needed(&self) {
457        if self.resolve_fbo_id != 0 {
458            self.resolve();
459        }
460    }
461}
462
463fn depth_rbo_params(stencil: bool) -> (i32, u32) {
464    if stencil {
465        (gl::DEPTH24_STENCIL8 as i32, gl::DEPTH_STENCIL_ATTACHMENT)
466    } else {
467        (gl::DEPTH_COMPONENT24 as i32, gl::DEPTH_ATTACHMENT)
468    }
469}
470
471fn create_empty_tex(size: Size2D, fmt: &ImgFormat, filter: ImgFilter, wrap: ImgWrap) -> u32 {
472    unsafe {
473        let mut id = 0;
474        gl::GenTextures(1, &mut id);
475        gl::BindTexture(gl::TEXTURE_2D, id);
476
477        let (min_fil, mag_fil) = match filter {
478            ImgFilter::Closest => (gl::NEAREST as i32, gl::NEAREST as i32),
479            ImgFilter::Linear => (gl::LINEAR as i32, gl::LINEAR as i32),
480        };
481        let wrap_gl = match wrap {
482            ImgWrap::Repeat => gl::REPEAT,
483            ImgWrap::Extend => gl::CLAMP_TO_EDGE,
484            ImgWrap::Clip => gl::CLAMP_TO_BORDER,
485        };
486        gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, min_fil);
487        gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, mag_fil);
488        gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, wrap_gl as i32);
489        gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, wrap_gl as i32);
490
491        let (base, sized, pix_type) = fmt_to_gl(fmt);
492        gl::TexImage2D(
493            gl::TEXTURE_2D, 0, sized as i32,
494            size.w as i32, size.h as i32, 0,
495            base, pix_type, std::ptr::null(),
496        );
497        gl::BindTexture(gl::TEXTURE_2D, 0);
498        id
499    }
500}
501
502fn create_rbo_storage_msaa(size: Size2D, samples: u32, fmt: &ImgFormat, _stencil: bool) -> u32 {
503    unsafe {
504        let mut id = 0;
505        gl::GenRenderbuffers(1, &mut id);
506        gl::BindRenderbuffer(gl::RENDERBUFFER, id);
507        let (_base, sized, _pix_type) = fmt_to_gl(fmt);
508        gl::RenderbufferStorageMultisample(
509            gl::RENDERBUFFER, samples as i32, sized as u32,
510            size.w as i32, size.h as i32,
511        );
512        gl::BindRenderbuffer(gl::RENDERBUFFER, 0);
513        id
514    }
515}
516
517fn create_depth_tex(size: Size2D, stencil: bool, compare: bool) -> u32 {
518    unsafe {
519        let mut id = 0;
520        gl::GenTextures(1, &mut id);
521        gl::BindTexture(gl::TEXTURE_2D, id);
522        gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::NEAREST as i32);
523        gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::NEAREST as i32);
524        gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as i32);
525        gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as i32);
526
527        let (internal, format, pix_type) = if stencil {
528            (gl::DEPTH24_STENCIL8 as i32, gl::DEPTH_STENCIL, gl::UNSIGNED_INT_24_8)
529        } else {
530            (gl::DEPTH_COMPONENT24 as i32, gl::DEPTH_COMPONENT, gl::FLOAT)
531        };
532
533        gl::TexImage2D(
534            gl::TEXTURE_2D, 0, internal,
535            size.w as i32, size.h as i32, 0,
536            format, pix_type, std::ptr::null(),
537        );
538
539        if compare {
540            gl::TexParameteri(
541                gl::TEXTURE_2D, gl::TEXTURE_COMPARE_MODE,
542                gl::COMPARE_REF_TO_TEXTURE as i32,
543            );
544            gl::TexParameteri(
545                gl::TEXTURE_2D, gl::TEXTURE_COMPARE_FUNC,
546                gl::LEQUAL as i32,
547            );
548        }
549
550        gl::BindTexture(gl::TEXTURE_2D, 0);
551        id
552    }
553}
554
555fn fmt_to_gl(fmt: &ImgFormat) -> (u32, u32, u32) {
556    match fmt {
557        ImgFormat::R(bd) => match bd {
558            32 => (gl::RED, gl::R32F, gl::FLOAT),
559            16 => (gl::RED, gl::R16, gl::UNSIGNED_SHORT),
560            _ => (gl::RED, gl::R8, gl::UNSIGNED_BYTE),
561        },
562        ImgFormat::RG(bd) => match bd {
563            32 => (gl::RG, gl::RG32F, gl::FLOAT),
564            16 => (gl::RG, gl::RG16, gl::UNSIGNED_SHORT),
565            _ => (gl::RG, gl::RG8, gl::UNSIGNED_BYTE),
566        },
567        ImgFormat::RGB(bd) => match bd {
568            32 => (gl::RGB, gl::RGB32F, gl::FLOAT),
569            16 => (gl::RGB, gl::RGB16, gl::UNSIGNED_SHORT),
570            _ => (gl::RGB, gl::RGB8, gl::UNSIGNED_BYTE),
571        },
572        ImgFormat::RGBA(bd) => match bd {
573            32 => (gl::RGBA, gl::RGBA32F, gl::FLOAT),
574            16 => (gl::RGBA, gl::RGBA16, gl::UNSIGNED_SHORT),
575            _ => (gl::RGBA, gl::RGBA8, gl::UNSIGNED_BYTE),
576        },
577    }
578}
579
580fn fmt_gl_params(fmt: &ImgFormat) -> (u32, u32, usize) {
581    let (base, _, pix_type) = fmt_to_gl(fmt);
582    let channels = fmt.channels() as usize;
583    let bytes_per_channel = (fmt.bit_depth() / 8) as usize;
584    (base, pix_type, channels * bytes_per_channel)
585}
586
587/// Either the screen (default framebuffer) or a canvas (FBO).
588pub enum RenderTarget<'a> {
589    Screen,
590    Canvas(&'a Canvas),
591}