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}