use core::ptr::{NonNull, null_mut};
use denise::{BufferAge, Frame, PixelFormat, Rect, Size, Surface, SurfaceError};
use objc2_core_foundation::{
CFDictionary, CFNumber, CFRetained, CFString, CGFloat, CGPoint, CGRect, CGSize,
};
use objc2_core_graphics::{
CGBitmapContextCreate, CGBitmapContextCreateImage, CGColorSpace, CGContext, CGImageAlphaInfo,
CGImageByteOrderInfo,
};
use objc2_io_surface::{
IOSurfaceLockOptions, IOSurfaceRef, kIOSurfaceBytesPerElement, kIOSurfaceHeight,
kIOSurfacePixelFormat, kIOSurfaceWidth,
};
use crate::Error;
fn bitmap_info() -> u32 {
CGImageByteOrderInfo::Order32Little.0 | CGImageAlphaInfo::NoneSkipFirst.0
}
const PIXEL_FORMAT_BGRA: i32 = i32::from_be_bytes(*b"BGRA");
fn new_io_surface(size: Size) -> Result<CFRetained<IOSurfaceRef>, Error> {
let number = |value: i64| CFNumber::new_i64(value);
let keys: [&CFString; 4] = unsafe {
[
kIOSurfaceWidth,
kIOSurfaceHeight,
kIOSurfaceBytesPerElement,
kIOSurfacePixelFormat,
]
};
let owned = [
number(i64::from(size.width)),
number(i64::from(size.height)),
number(4),
number(i64::from(PIXEL_FORMAT_BGRA)),
];
let values: [&CFNumber; 4] = [&owned[0], &owned[1], &owned[2], &owned[3]];
let properties = CFDictionary::from_slices(&keys, &values);
unsafe { IOSurfaceRef::new(properties.as_opaque()) }.ok_or(Error::BitmapContext)
}
struct Buffer {
io_surface: CFRetained<IOSurfaceRef>,
context: CFRetained<CGContext>,
pixels: NonNull<u32>,
}
pub struct ViewSurface {
buffers: [Buffer; 2],
front: usize,
drawing: bool,
stride: u32,
size: Size,
scale_factor: f32,
}
impl Buffer {
fn new(size: Size, space: &CGColorSpace) -> Result<Self, Error> {
let io_surface = new_io_surface(size)?;
let bytes_per_row = io_surface.bytes_per_row();
let pixels = io_surface.base_address().cast::<u32>();
let context = unsafe {
CGBitmapContextCreate(
pixels.as_ptr().cast(),
size.width as usize,
size.height as usize,
8,
bytes_per_row,
Some(space),
bitmap_info(),
)
}
.ok_or(Error::BitmapContext)?;
Ok(Self {
io_surface,
context,
pixels,
})
}
}
impl ViewSurface {
fn back(&self) -> &Buffer {
&self.buffers[1 - self.front]
}
pub fn new(size: Size, scale_factor: f32) -> Result<Self, Error> {
if size.is_empty() {
return Err(Error::EmptySurface);
}
let space = CGColorSpace::new_device_rgb().ok_or(Error::ColorSpace)?;
let one = Buffer::new(size, &space)?;
let two = Buffer::new(size, &space)?;
let bytes_per_row = one.io_surface.bytes_per_row();
if bytes_per_row != two.io_surface.bytes_per_row() {
return Err(Error::BitmapContext);
}
if !bytes_per_row.is_multiple_of(4) {
return Err(Error::BitmapContext);
}
Ok(Self {
buffers: [one, two],
front: 0,
drawing: false,
stride: (bytes_per_row / 4) as u32,
size,
scale_factor,
})
}
pub fn resize(&mut self, size: Size, scale_factor: f32) -> Result<bool, Error> {
if size == self.size && scale_factor == self.scale_factor {
return Ok(false);
}
*self = Self::new(size, scale_factor)?;
Ok(true)
}
#[inline]
pub const fn stride(&self) -> u32 {
self.stride
}
pub unsafe fn draw_into(&self, context: &CGContext, bounds: CGRect) {
let Some(image) = CGBitmapContextCreateImage(Some(&self.buffers[self.front].context))
else {
return;
};
CGContext::save_g_state(Some(context));
CGContext::translate_ctm(
Some(context),
0.0,
bounds.origin.y * 2.0 + bounds.size.height,
);
CGContext::scale_ctm(Some(context), 1.0, -1.0);
CGContext::draw_image(Some(context), bounds, Some(&image));
CGContext::restore_g_state(Some(context));
}
#[inline]
pub fn io_surface(&self) -> &IOSurfaceRef {
&self.buffers[self.front].io_surface
}
#[inline]
pub fn context(&self) -> &CGContext {
&self.back().context
}
pub fn damage_to_points(&self, rect: Rect) -> CGRect {
let scale = self.scale_factor.max(0.01) as CGFloat;
let x = (rect.x as CGFloat / scale).floor();
let y = (rect.y as CGFloat / scale).floor();
let right = ((rect.x + rect.width) as CGFloat / scale).ceil();
let bottom = ((rect.y + rect.height) as CGFloat / scale).ceil();
CGRect::new(CGPoint::new(x, y), CGSize::new(right - x, bottom - y))
}
}
impl Drop for ViewSurface {
fn drop(&mut self) {
if self.drawing {
let _ = unsafe {
self.back()
.io_surface
.unlock(IOSurfaceLockOptions(0), null_mut())
};
}
}
}
impl Surface for ViewSurface {
fn size(&self) -> Size {
self.size
}
fn scale_factor(&self) -> f32 {
self.scale_factor
}
fn format(&self) -> PixelFormat {
PixelFormat::Xrgb8888
}
fn acquire(&mut self) -> Result<Frame<'_>, SurfaceError> {
if !self.drawing {
let taken = unsafe {
self.back()
.io_surface
.lock(IOSurfaceLockOptions(0), null_mut())
};
if taken != 0 {
return Err(SurfaceError::NotReady);
}
self.drawing = true;
}
let len = self.stride as usize * self.size.height as usize;
let pixels = unsafe { core::slice::from_raw_parts_mut(self.back().pixels.as_ptr(), len) };
Frame::new(
pixels,
self.size,
self.stride,
PixelFormat::Xrgb8888,
BufferAge::Frames(2),
)
}
fn present(&mut self, _damage: &[Rect]) -> Result<(), SurfaceError> {
if self.drawing {
let _ = unsafe {
self.back()
.io_surface
.unlock(IOSurfaceLockOptions(0), null_mut())
};
self.drawing = false;
self.front = 1 - self.front;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use denise::Color;
use denise_render::Canvas;
use objc2_core_graphics::{CGBitmapContextGetBytesPerRow, CGBitmapContextGetData};
#[test]
fn core_graphics_picks_the_stride_and_it_is_not_the_width() {
let surface = ViewSurface::new(Size::new(100, 40), 1.0).expect("surface");
assert!(
surface.stride() >= 100,
"a stride below the width cannot hold a row"
);
assert_eq!(
surface.stride() * 4 % 4,
0,
"the pitch must be a whole number of words"
);
}
#[test]
fn drawing_lands_where_the_stride_says_it_does() {
let mut surface = ViewSurface::new(Size::new(64, 32), 1.0).expect("surface");
{
let mut frame = surface.acquire().expect("frame");
let mut canvas = Canvas::new(&mut frame);
canvas.clear(Color::from_rgb888(0x00FF00));
canvas.fill_rect(Rect::new(0, 0, 1, 1), Color::from_rgb888(0xFF0000));
}
let mut frame = surface.acquire().expect("frame");
let row = frame.row_mut(1).expect("second row");
assert_eq!(
row[0] & 0x00FF_FFFF,
0x00FF00,
"row 1 is not the clear colour, so the stride is wrong"
);
}
#[test]
fn a_flipped_context_draws_the_surface_the_right_way_up() {
use objc2_core_graphics::CGBitmapContextCreate;
const N: u32 = 8;
let mut source = ViewSurface::new(Size::new(N, N), 1.0).expect("source");
{
let mut frame = source.acquire().expect("frame");
let mut canvas = Canvas::new(&mut frame);
for y in 0..N as i32 {
let shade = (y as u32 * 16) << 16;
canvas.fill_rect(Rect::new(0, y, N as i32, 1), Color::from_rgb888(shade));
}
}
source.present(&[]).expect("present");
let space = CGColorSpace::new_device_rgb().expect("colour space");
let dest = unsafe {
CGBitmapContextCreate(
core::ptr::null_mut(),
N as usize,
N as usize,
8,
0,
Some(&space),
bitmap_info(),
)
}
.expect("destination context");
CGContext::translate_ctm(Some(&dest), 0.0, N as CGFloat);
CGContext::scale_ctm(Some(&dest), 1.0, -1.0);
let bounds = CGRect::new(
CGPoint::new(0.0, 0.0),
CGSize::new(N as CGFloat, N as CGFloat),
);
unsafe { source.draw_into(&dest, bounds) };
let dest_stride = CGBitmapContextGetBytesPerRow(Some(&dest)) / 4;
let dest_data = CGBitmapContextGetData(Some(&dest)).cast::<u32>();
assert!(!dest_data.is_null());
let drawn = unsafe { core::slice::from_raw_parts(dest_data, dest_stride * N as usize) };
let stride = source.stride() as usize;
let front = unsafe {
core::slice::from_raw_parts(
source.buffers[source.front].pixels.as_ptr(),
stride * N as usize,
)
};
for y in 0..N {
let expected = front[y as usize * stride] & 0x00FF_FFFF;
let actual = drawn[y as usize * dest_stride] & 0x00FF_FFFF;
assert_eq!(
actual, expected,
"row {y} came out as {actual:06X}, wanted {expected:06X} — \
the image is mirrored, so the panel renders upside down"
);
}
}
#[test]
fn an_empty_surface_is_refused_rather_than_allocated() {
assert!(ViewSurface::new(Size::new(0, 40), 1.0).is_err());
assert!(ViewSurface::new(Size::new(40, 0), 1.0).is_err());
}
#[test]
fn damage_rounds_outwards_on_a_retina_view() {
let surface = ViewSurface::new(Size::new(200, 100), 2.0).expect("surface");
let rect = surface.damage_to_points(Rect::new(3, 5, 4, 4));
assert_eq!(rect.origin.x, 1.0);
assert_eq!(rect.origin.y, 2.0);
assert_eq!(rect.size.width, 3.0);
assert_eq!(rect.size.height, 3.0);
}
}