Skip to main content

denise_macos/
surface.rs

1//! A [`Surface`] backed by a CoreGraphics bitmap context.
2//!
3//! CoreGraphics owns the pixels, not us. `CGBitmapContextCreate` with a null data
4//! pointer allocates and keeps them for the lifetime of the context, which removes
5//! the whole question of whether a `CGImage` the window server has not finished
6//! with is still pointing at a `Vec` that moved. It also means CoreGraphics picks
7//! the row pitch — and it does not pick the width.
8
9use core::ptr::{NonNull, null_mut};
10
11use denise::{BufferAge, Frame, PixelFormat, Rect, Size, Surface, SurfaceError};
12use objc2_core_foundation::{
13    CFDictionary, CFNumber, CFRetained, CFString, CGFloat, CGPoint, CGRect, CGSize,
14};
15use objc2_core_graphics::{
16    CGBitmapContextCreate, CGBitmapContextCreateImage, CGColorSpace, CGContext, CGImageAlphaInfo,
17    CGImageByteOrderInfo,
18};
19use objc2_io_surface::{
20    IOSurfaceLockOptions, IOSurfaceRef, kIOSurfaceBytesPerElement, kIOSurfaceHeight,
21    kIOSurfacePixelFormat, kIOSurfaceWidth,
22};
23
24use crate::Error;
25
26/// Every pixel Denise produces is `0xAARRGGBB` in a native-endian word. On a
27/// little-endian machine that is `B, G, R, A` in memory, which CoreGraphics spells
28/// as "skip the first component, 32-bit little-endian" — the same layout DRM calls
29/// `XRGB8888` and Win32 calls a `BI_RGB` DIB. Getting this wrong does not fail; it
30/// swaps red and blue, which is the sort of bug that survives review.
31fn bitmap_info() -> u32 {
32    CGImageByteOrderInfo::Order32Little.0 | CGImageAlphaInfo::NoneSkipFirst.0
33}
34
35/// `'BGRA'` as IOSurface spells it: the four-character code for 32-bit
36/// little-endian BGRA, which is the same memory layout as `bitmap_info` above
37/// and as every other surface in this project.
38const PIXEL_FORMAT_BGRA: i32 = i32::from_be_bytes(*b"BGRA");
39
40/// Allocates an IOSurface for `size`, letting it choose its own row alignment.
41///
42/// The properties are the minimum that produces a CPU-writable 32-bit surface:
43/// anything omitted is derived. `BytesPerRow` in particular is deliberately not
44/// requested — IOSurface aligns rows to suit the hardware, and asking for a
45/// tighter pitch than it wants is how you get a surface it will not accelerate.
46fn new_io_surface(size: Size) -> Result<CFRetained<IOSurfaceRef>, Error> {
47    let number = |value: i64| CFNumber::new_i64(value);
48    // SAFETY: reading IOSurface's own property-key statics, which are constants
49    // the framework guarantees for the life of the process.
50    let keys: [&CFString; 4] = unsafe {
51        [
52            kIOSurfaceWidth,
53            kIOSurfaceHeight,
54            kIOSurfaceBytesPerElement,
55            kIOSurfacePixelFormat,
56        ]
57    };
58    let owned = [
59        number(i64::from(size.width)),
60        number(i64::from(size.height)),
61        number(4),
62        number(i64::from(PIXEL_FORMAT_BGRA)),
63    ];
64    let values: [&CFNumber; 4] = [&owned[0], &owned[1], &owned[2], &owned[3]];
65
66    let properties = CFDictionary::from_slices(&keys, &values);
67    // SAFETY: every key is one of IOSurface's own, and every value is the type
68    // that key is documented to take.
69    unsafe { IOSurfaceRef::new(properties.as_opaque()) }.ok_or(Error::BitmapContext)
70}
71
72/// One of the two buffers, and everything needed to draw into it.
73struct Buffer {
74    io_surface: CFRetained<IOSurfaceRef>,
75    context: CFRetained<CGContext>,
76    /// The pixels inside `io_surface`. The address is stable for the surface's
77    /// lifetime; the lock taken around each frame is about coherency, not about
78    /// where the buffer is.
79    pixels: NonNull<u32>,
80}
81
82/// A pixel buffer a Cocoa view draws from.
83///
84/// **Two `IOSurface`s, shown alternately.** One is handed to a `CALayer` as its
85/// contents, where CoreAnimation reads it in place — no copy, and a cost that
86/// does not scale with the size of the window. The obvious alternative, a
87/// `CGImage` per frame, is copied whole on every commit however little of it
88/// changed: on a 1040×720 surface with one spinner animating, that is the
89/// difference between 9.2% of a core and 2%.
90///
91/// The pair is not for tearing, though it helps there too. It is because
92/// assigning the *same* object to `contents` tells CoreAnimation nothing: the
93/// property has not changed, so it has no reason to look at the buffer again,
94/// and the window shows the first frame for ever while the application draws
95/// happily into memory nobody is reading. Two surfaces means every present
96/// assigns a different object, which is a change it cannot miss. The private
97/// `-[CALayer setContentsChanged]` is the other way, and not one a published
98/// crate should take.
99///
100/// So the buffer handed back by [`Surface::acquire`] is two frames old, not one,
101/// and [`BufferAge::Frames(2)`](denise::BufferAge::Frames) is what says so — which is exactly the case
102/// `DamageTracker` exists to widen for.
103pub struct ViewSurface {
104    buffers: [Buffer; 2],
105    /// The buffer the layer is showing. The other one is the next frame's.
106    front: usize,
107    /// Whether a frame is out, and therefore whether the back buffer is locked.
108    drawing: bool,
109    /// Words per row, which is `CGBitmapContextGetBytesPerRow / 4` and is *not*
110    /// `size.width`: CoreGraphics aligns rows, typically to 32 bytes.
111    stride: u32,
112    size: Size,
113    scale_factor: f32,
114}
115
116impl Buffer {
117    /// One `IOSurface` and a bitmap context that draws straight into it.
118    fn new(size: Size, space: &CGColorSpace) -> Result<Self, Error> {
119        let io_surface = new_io_surface(size)?;
120        let bytes_per_row = io_surface.bytes_per_row();
121        let pixels = io_surface.base_address().cast::<u32>();
122
123        // SAFETY: the buffer belongs to `io_surface`, which this struct holds,
124        // and is at least `bytes_per_row * height` bytes. Passing it explicitly
125        // — rather than a null `data` — is what puts the rasteriser's output
126        // inside the surface the compositor reads.
127        let context = unsafe {
128            CGBitmapContextCreate(
129                pixels.as_ptr().cast(),
130                size.width as usize,
131                size.height as usize,
132                8,
133                bytes_per_row,
134                Some(space),
135                bitmap_info(),
136            )
137        }
138        .ok_or(Error::BitmapContext)?;
139
140        Ok(Self {
141            io_surface,
142            context,
143            pixels,
144        })
145    }
146}
147
148impl ViewSurface {
149    /// The buffer being drawn into: the one the layer is *not* showing.
150    fn back(&self) -> &Buffer {
151        &self.buffers[1 - self.front]
152    }
153
154    /// Allocates a surface `size` physical pixels across.
155    ///
156    /// `scale_factor` is the view's backing scale — 2.0 on a Retina display — and
157    /// is reported to the application rather than applied here. Denise lays out in
158    /// physical pixels, so a Retina view asks for twice as many of them.
159    pub fn new(size: Size, scale_factor: f32) -> Result<Self, Error> {
160        if size.is_empty() {
161            return Err(Error::EmptySurface);
162        }
163        let space = CGColorSpace::new_device_rgb().ok_or(Error::ColorSpace)?;
164
165        let one = Buffer::new(size, &space)?;
166        let two = Buffer::new(size, &space)?;
167        let bytes_per_row = one.io_surface.bytes_per_row();
168        // Two surfaces of one size get one pitch, and the `&mut [u32]` handed out
169        // by `acquire` is built from a single `stride` for both.
170        if bytes_per_row != two.io_surface.bytes_per_row() {
171            return Err(Error::BitmapContext);
172        }
173        // A pitch that is not a whole number of words would make that slice
174        // unsound. CoreGraphics has never produced one for a 32-bit format, and
175        // this is cheaper than trusting that.
176        if !bytes_per_row.is_multiple_of(4) {
177            return Err(Error::BitmapContext);
178        }
179
180        Ok(Self {
181            buffers: [one, two],
182            front: 0,
183            drawing: false,
184            stride: (bytes_per_row / 4) as u32,
185            size,
186            scale_factor,
187        })
188    }
189
190    /// Reallocates for a new size or backing scale, discarding the contents.
191    ///
192    /// The caller owes a full repaint afterwards. Nothing here can produce one:
193    /// the tree owns damage, and it is the one that has to be told.
194    pub fn resize(&mut self, size: Size, scale_factor: f32) -> Result<bool, Error> {
195        if size == self.size && scale_factor == self.scale_factor {
196            return Ok(false);
197        }
198        *self = Self::new(size, scale_factor)?;
199        Ok(true)
200    }
201
202    /// Words per row. See [`Frame::stride`] for why this is not the width.
203    #[inline]
204    pub const fn stride(&self) -> u32 {
205        self.stride
206    }
207
208    /// Draws the surface into a Cocoa view's graphics context.
209    ///
210    /// `bounds` is the view's rectangle in points, so a Retina view scales the
211    /// image down by the backing factor here — the pixels stay physical all the
212    /// way from layout to this call.
213    ///
214    /// # Safety
215    ///
216    /// `context` must be a live `CGContext` whose coordinate system is the one
217    /// AppKit installs for a **flipped** view. In an unflipped one the image
218    /// arrives upside down, silently.
219    pub unsafe fn draw_into(&self, context: &CGContext, bounds: CGRect) {
220        let Some(image) = CGBitmapContextCreateImage(Some(&self.buffers[self.front].context))
221        else {
222            return;
223        };
224
225        // A `CGImage` is placed bottom-up, and a flipped view's context already
226        // runs y downwards, so drawing straight into it produces a mirror. Undo
227        // the flip for the duration of the draw and put it back.
228        //
229        // The translation is `2y + h` rather than `h` so this is right for a
230        // sub-rectangle as well as for the whole view: mirroring about the rect's
231        // own centre line, not the context's.
232        CGContext::save_g_state(Some(context));
233        CGContext::translate_ctm(
234            Some(context),
235            0.0,
236            bounds.origin.y * 2.0 + bounds.size.height,
237        );
238        CGContext::scale_ctm(Some(context), 1.0, -1.0);
239        CGContext::draw_image(Some(context), bounds, Some(&image));
240        CGContext::restore_g_state(Some(context));
241    }
242
243    /// The buffer to hand a `CALayer` as its contents.
244    ///
245    /// `IOSurfaceRef` is toll-free bridged to the `IOSurface` class, which is
246    /// what makes it assignable to `contents` at all.
247    #[inline]
248    pub fn io_surface(&self) -> &IOSurfaceRef {
249        &self.buffers[self.front].io_surface
250    }
251
252    /// The bitmap context, for a caller that wants to draw over Denise's output
253    /// with CoreGraphics itself.
254    #[inline]
255    pub fn context(&self) -> &CGContext {
256        &self.back().context
257    }
258
259    /// Converts a damage rectangle in physical pixels to the view's points.
260    ///
261    /// Rounded outwards, because a rectangle that lands between two points has to
262    /// invalidate both or leave a seam down one edge.
263    ///
264    /// No vertical flip: the view is flipped, so its coordinates already run
265    /// top-left downwards, the same as Denise's.
266    pub fn damage_to_points(&self, rect: Rect) -> CGRect {
267        let scale = self.scale_factor.max(0.01) as CGFloat;
268        let x = (rect.x as CGFloat / scale).floor();
269        let y = (rect.y as CGFloat / scale).floor();
270        let right = ((rect.x + rect.width) as CGFloat / scale).ceil();
271        let bottom = ((rect.y + rect.height) as CGFloat / scale).ceil();
272
273        CGRect::new(CGPoint::new(x, y), CGSize::new(right - x, bottom - y))
274    }
275}
276
277impl Drop for ViewSurface {
278    fn drop(&mut self) {
279        if self.drawing {
280            // SAFETY: as in `present`.
281            let _ = unsafe {
282                self.back()
283                    .io_surface
284                    .unlock(IOSurfaceLockOptions(0), null_mut())
285            };
286        }
287    }
288}
289
290impl Surface for ViewSurface {
291    fn size(&self) -> Size {
292        self.size
293    }
294
295    fn scale_factor(&self) -> f32 {
296        self.scale_factor
297    }
298
299    fn format(&self) -> PixelFormat {
300        PixelFormat::Xrgb8888
301    }
302
303    fn acquire(&mut self) -> Result<Frame<'_>, SurfaceError> {
304        // Locked for the duration of the drawing and no longer. Holding it
305        // across frames looks like it should be free — one writer, and `Frame`
306        // already excludes a second — and it is not: the lock is what the
307        // compositor waits on to read the surface, so a lock that is never
308        // released is a window that draws one frame and then freezes.
309        //
310        // SAFETY: a null seed pointer is documented as "do not report the seed".
311        if !self.drawing {
312            // SAFETY: a null seed pointer is documented as "do not report the
313            // seed", and the matching unlock happens in `present` or `drop`.
314            let taken = unsafe {
315                self.back()
316                    .io_surface
317                    .lock(IOSurfaceLockOptions(0), null_mut())
318            };
319            if taken != 0 {
320                return Err(SurfaceError::NotReady);
321            }
322            self.drawing = true;
323        }
324
325        let len = self.stride as usize * self.size.height as usize;
326        // SAFETY: `pixels` is the allocation CoreGraphics made for `context`, which
327        // this struct owns and which is `stride * height` words by construction.
328        // Nothing else holds a reference to it: `draw_into` takes `&self` and only
329        // snapshots through CoreGraphics, and `Frame` borrows `self` mutably for as
330        // long as it lives.
331        let pixels = unsafe { core::slice::from_raw_parts_mut(self.back().pixels.as_ptr(), len) };
332        Frame::new(
333            pixels,
334            self.size,
335            self.stride,
336            PixelFormat::Xrgb8888,
337            // Two buffers alternating: what is handed back was last drawn two
338            // frames ago, and `DamageTracker` widens for exactly that.
339            BufferAge::Frames(2),
340        )
341    }
342
343    fn present(&mut self, _damage: &[Rect]) -> Result<(), SurfaceError> {
344        // Handing the buffer back to whoever wants to read it, and making it the
345        // one the layer is shown next. The swap is what changes the object a
346        // `CALayer` is given, which is the only thing that makes CoreAnimation
347        // look at the pixels again.
348        if self.drawing {
349            // SAFETY: the lock was taken in `acquire`, with the same options.
350            let _ = unsafe {
351                self.back()
352                    .io_surface
353                    .unlock(IOSurfaceLockOptions(0), null_mut())
354            };
355            self.drawing = false;
356            self.front = 1 - self.front;
357        }
358
359        // Nothing to do: the view draws from this surface when AppKit asks it to,
360        // and telling AppKit *what* to ask about is `DeniseView`'s job, because it
361        // is the only one holding the view.
362        Ok(())
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369    use denise::Color;
370    use denise_render::Canvas;
371    use objc2_core_graphics::{CGBitmapContextGetBytesPerRow, CGBitmapContextGetData};
372
373    #[test]
374    fn core_graphics_picks_the_stride_and_it_is_not_the_width() {
375        // 100 pixels is 400 bytes, which CoreGraphics rounds up to its own
376        // alignment. If this ever stops being true the test is still correct;
377        // what it is really asserting is that we ask rather than assume.
378        let surface = ViewSurface::new(Size::new(100, 40), 1.0).expect("surface");
379        assert!(
380            surface.stride() >= 100,
381            "a stride below the width cannot hold a row"
382        );
383        assert_eq!(
384            surface.stride() * 4 % 4,
385            0,
386            "the pitch must be a whole number of words"
387        );
388    }
389
390    #[test]
391    fn drawing_lands_where_the_stride_says_it_does() {
392        let mut surface = ViewSurface::new(Size::new(64, 32), 1.0).expect("surface");
393        {
394            let mut frame = surface.acquire().expect("frame");
395            let mut canvas = Canvas::new(&mut frame);
396            canvas.clear(Color::from_rgb888(0x00FF00));
397            canvas.fill_rect(Rect::new(0, 0, 1, 1), Color::from_rgb888(0xFF0000));
398        }
399        // Read back through the same pointer the view draws from.
400        let mut frame = surface.acquire().expect("frame");
401        let row = frame.row_mut(1).expect("second row");
402        assert_eq!(
403            row[0] & 0x00FF_FFFF,
404            0x00FF00,
405            "row 1 is not the clear colour, so the stride is wrong"
406        );
407    }
408
409    /// The one thing about this backend that fails silently.
410    ///
411    /// A `CGImage` is bottom-up and a flipped `NSView`'s context already mirrors
412    /// the y axis, so the two are supposed to cancel. If they do not, the panel
413    /// renders upside down and nothing anywhere reports an error — it just looks
414    /// like somebody laid the widgets out wrong.
415    ///
416    /// So: draw a surface whose every row is a different colour into a second
417    /// bitmap context carrying the same transform AppKit installs for a flipped
418    /// view, and require the destination's memory to match the source's row for
419    /// row. Both are `CGBitmapContext`s and share a memory convention, so equal
420    /// memory means equal pixels on screen.
421    #[test]
422    fn a_flipped_context_draws_the_surface_the_right_way_up() {
423        use objc2_core_graphics::CGBitmapContextCreate;
424
425        const N: u32 = 8;
426        let mut source = ViewSurface::new(Size::new(N, N), 1.0).expect("source");
427        {
428            let mut frame = source.acquire().expect("frame");
429            let mut canvas = Canvas::new(&mut frame);
430            for y in 0..N as i32 {
431                // Row y gets red = y * 16, which no other row has.
432                let shade = (y as u32 * 16) << 16;
433                canvas.fill_rect(Rect::new(0, y, N as i32, 1), Color::from_rgb888(shade));
434            }
435        }
436        // Published, which with two buffers is what makes this the one a host
437        // reads: `draw_into` shows the last *presented* frame, not the one being
438        // drawn into.
439        source.present(&[]).expect("present");
440
441        let space = CGColorSpace::new_device_rgb().expect("colour space");
442        // SAFETY: a null `data` asks CoreGraphics to allocate, as in `new`.
443        let dest = unsafe {
444            CGBitmapContextCreate(
445                core::ptr::null_mut(),
446                N as usize,
447                N as usize,
448                8,
449                0,
450                Some(&space),
451                bitmap_info(),
452            )
453        }
454        .expect("destination context");
455
456        // Exactly what AppKit does to the context of a flipped view: move the
457        // origin to the top and run y downwards.
458        CGContext::translate_ctm(Some(&dest), 0.0, N as CGFloat);
459        CGContext::scale_ctm(Some(&dest), 1.0, -1.0);
460
461        let bounds = CGRect::new(
462            CGPoint::new(0.0, 0.0),
463            CGSize::new(N as CGFloat, N as CGFloat),
464        );
465        // SAFETY: `dest` is a live context carrying a flipped view's transform,
466        // which is what `draw_into` requires.
467        unsafe { source.draw_into(&dest, bounds) };
468
469        let dest_stride = CGBitmapContextGetBytesPerRow(Some(&dest)) / 4;
470        let dest_data = CGBitmapContextGetData(Some(&dest)).cast::<u32>();
471        assert!(!dest_data.is_null());
472        // SAFETY: `dest` owns `dest_stride * N` words and outlives this slice.
473        let drawn = unsafe { core::slice::from_raw_parts(dest_data, dest_stride * N as usize) };
474
475        // The front buffer, read back through a fresh frame over the same memory.
476        // `acquire` hands out the *back* one, so this reads the presented buffer
477        // directly rather than through it.
478        let stride = source.stride() as usize;
479        // SAFETY: the front buffer owns `stride * N` words and outlives this.
480        let front = unsafe {
481            core::slice::from_raw_parts(
482                source.buffers[source.front].pixels.as_ptr(),
483                stride * N as usize,
484            )
485        };
486        for y in 0..N {
487            let expected = front[y as usize * stride] & 0x00FF_FFFF;
488            let actual = drawn[y as usize * dest_stride] & 0x00FF_FFFF;
489            assert_eq!(
490                actual, expected,
491                "row {y} came out as {actual:06X}, wanted {expected:06X} — \
492                 the image is mirrored, so the panel renders upside down"
493            );
494        }
495    }
496
497    #[test]
498    fn an_empty_surface_is_refused_rather_than_allocated() {
499        assert!(ViewSurface::new(Size::new(0, 40), 1.0).is_err());
500        assert!(ViewSurface::new(Size::new(40, 0), 1.0).is_err());
501    }
502
503    #[test]
504    fn damage_rounds_outwards_on_a_retina_view() {
505        let surface = ViewSurface::new(Size::new(200, 100), 2.0).expect("surface");
506        // A rectangle starting on an odd physical pixel covers the point before it
507        // and the one after; rounding inwards would leave a one-point seam.
508        let rect = surface.damage_to_points(Rect::new(3, 5, 4, 4));
509        assert_eq!(rect.origin.x, 1.0);
510        assert_eq!(rect.origin.y, 2.0);
511        assert_eq!(rect.size.width, 3.0);
512        assert_eq!(rect.size.height, 3.0);
513    }
514}