Skip to content

Commit dcf429b

Browse files
committed
Add support for iOS / UIKit, and clean up CoreGraphics impl
1 parent d5cf875 commit dcf429b

File tree

7 files changed

+174
-79
lines changed

7 files changed

+174
-79
lines changed

.github/workflows/ci.yml

+4-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ jobs:
4747
- { target: x86_64-unknown-redox, os: ubuntu-latest, }
4848
- { target: x86_64-unknown-freebsd, os: ubuntu-latest, }
4949
- { target: x86_64-unknown-netbsd, os: ubuntu-latest, options: --no-default-features, features: "x11,x11-dlopen,wayland,wayland-dlopen" }
50-
- { target: x86_64-apple-darwin, os: macos-latest, }
50+
- { target: aarch64-apple-darwin, os: macos-latest, }
5151
- { target: wasm32-unknown-unknown, os: ubuntu-latest, }
5252
exclude:
5353
# Orbital doesn't follow MSRV
@@ -56,6 +56,9 @@ jobs:
5656
include:
5757
- rust_version: nightly
5858
platform: { target: wasm32-unknown-unknown, os: ubuntu-latest, options: "-Zbuild-std=panic_abort,std", rustflags: "-Ctarget-feature=+atomics,+bulk-memory" }
59+
# Mac Catalyst is only Tier 2 since Rust 1.81
60+
- rust_version: 'nightly'
61+
platform: { target: aarch64-apple-ios-macabi, os: macos-latest }
5962

6063
env:
6164
RUST_BACKTRACE: 1

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Unreleased
22

3+
- Added support for iOS, tvOS, watchOS and visionOS.
4+
- Redo the way surfaces work on macOS to work directly with layers, which will allow initializing directly from a `CALayer` in the future.
5+
36
# 0.4.5
47

58
- Make the `wayland-sys` dependency optional. (#223)

Cargo.toml

+4-5
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,13 @@ x11rb = { version = "0.13.0", features = ["allow-unsafe-code", "shm"], optional
4545
version = "0.59.0"
4646
features = ["Win32_Graphics_Gdi", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_Foundation"]
4747

48-
[target.'cfg(target_os = "macos")'.dependencies]
48+
[target.'cfg(target_vendor = "apple")'.dependencies]
4949
bytemuck = { version = "1.12.3", features = ["extern_crate_alloc"] }
5050
core-graphics = "0.24.0"
5151
foreign-types = "0.5.0"
52-
objc2 = "0.5.1"
53-
objc2-foundation = { version = "0.2.0", features = ["dispatch", "NSThread"] }
54-
objc2-app-kit = { version = "0.2.0", features = ["NSResponder", "NSView", "NSWindow"] }
55-
objc2-quartz-core = { version = "0.2.0", features = ["CALayer", "CATransaction"] }
52+
objc2 = "0.5.2"
53+
objc2-foundation = { version = "0.2.2", features = ["dispatch", "NSThread"] }
54+
objc2-quartz-core = { version = "0.2.2", features = ["CALayer", "CATransaction"] }
5655

5756
[target.'cfg(target_arch = "wasm32")'.dependencies]
5857
js-sys = "0.3.63"

src/backend_dispatch.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,8 @@ make_dispatch! {
186186
Kms(Arc<backends::kms::KmsDisplayImpl<D>>, backends::kms::KmsImpl<D, W>, backends::kms::BufferImpl<'a, D, W>),
187187
#[cfg(target_os = "windows")]
188188
Win32(D, backends::win32::Win32Impl<D, W>, backends::win32::BufferImpl<'a, D, W>),
189-
#[cfg(target_os = "macos")]
190-
CG(D, backends::cg::CGImpl<D, W>, backends::cg::BufferImpl<'a, D, W>),
189+
#[cfg(target_vendor = "apple")]
190+
CoreGraphics(D, backends::cg::CGImpl<D, W>, backends::cg::BufferImpl<'a, D, W>),
191191
#[cfg(target_arch = "wasm32")]
192192
Web(backends::web::WebDisplayImpl<D>, backends::web::WebImpl<D, W>, backends::web::BufferImpl<'a, D, W>),
193193
#[cfg(target_os = "redox")]

src/backends/cg.rs

+158-68
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,23 @@ use crate::backend_interface::*;
22
use crate::error::InitError;
33
use crate::{Rect, SoftBufferError};
44
use core_graphics::base::{
5-
kCGBitmapByteOrder32Little, kCGImageAlphaNoneSkipFirst, kCGRenderingIntentDefault,
5+
kCGBitmapByteOrder32Little, kCGImageAlphaNoneSkipFirst, kCGRenderingIntentDefault, CGFloat,
66
};
77
use core_graphics::color_space::CGColorSpace;
88
use core_graphics::data_provider::CGDataProvider;
99
use core_graphics::image::CGImage;
10-
use objc2::runtime::AnyObject;
10+
use objc2::runtime::{AnyObject, Bool};
11+
use objc2::{msg_send, msg_send_id};
1112
use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawWindowHandle};
1213

1314
use foreign_types::ForeignType;
14-
use objc2::msg_send;
15-
use objc2::rc::Id;
16-
use objc2_app_kit::{NSAutoresizingMaskOptions, NSView, NSWindow};
17-
use objc2_foundation::{MainThreadBound, MainThreadMarker};
18-
use objc2_quartz_core::{kCAGravityTopLeft, CALayer, CATransaction};
15+
use objc2::rc::Retained;
16+
use objc2_foundation::{CGPoint, CGRect, CGSize, MainThreadMarker, NSObject};
17+
use objc2_quartz_core::{kCAGravityResize, CALayer, CATransaction};
1918

2019
use std::marker::PhantomData;
2120
use std::num::NonZeroU32;
21+
use std::ops::Deref;
2222
use std::sync::Arc;
2323

2424
struct Buffer(Vec<u32>);
@@ -30,64 +30,92 @@ impl AsRef<[u8]> for Buffer {
3030
}
3131

3232
pub struct CGImpl<D, W> {
33-
layer: MainThreadBound<Id<CALayer>>,
34-
window: MainThreadBound<Id<NSWindow>>,
33+
/// Our layer.
34+
layer: SendCALayer,
35+
/// The layer that our layer was created from.
36+
///
37+
/// Can also be retrieved from `layer.superlayer()`.
38+
root_layer: SendCALayer,
3539
color_space: SendCGColorSpace,
36-
size: Option<(NonZeroU32, NonZeroU32)>,
3740
window_handle: W,
3841
_display: PhantomData<D>,
3942
}
4043

41-
// TODO(madsmtm): Expose this in `objc2_app_kit`.
42-
fn set_layer(view: &NSView, layer: &CALayer) {
43-
unsafe { msg_send![view, setLayer: layer] }
44-
}
45-
4644
impl<D: HasDisplayHandle, W: HasWindowHandle> SurfaceInterface<D, W> for CGImpl<D, W> {
4745
type Context = D;
4846
type Buffer<'a> = BufferImpl<'a, D, W> where Self: 'a;
4947

5048
fn new(window_src: W, _display: &D) -> Result<Self, InitError<W>> {
51-
let raw = window_src.window_handle()?.as_raw();
52-
let handle = match raw {
53-
RawWindowHandle::AppKit(handle) => handle,
49+
// `NSView`/`UIView` can only be accessed from the main thread.
50+
let _mtm = MainThreadMarker::new().ok_or(SoftBufferError::PlatformError(
51+
Some("can only access Core Graphics handles from the main thread".to_string()),
52+
None,
53+
))?;
54+
55+
let root_layer = match window_src.window_handle()?.as_raw() {
56+
RawWindowHandle::AppKit(handle) => {
57+
// SAFETY: The pointer came from `WindowHandle`, which ensures that the
58+
// `AppKitWindowHandle` contains a valid pointer to an `NSView`.
59+
//
60+
// We use `NSObject` here to avoid importing `objc2-app-kit`.
61+
let view: &NSObject = unsafe { handle.ns_view.cast().as_ref() };
62+
63+
// Force the view to become layer backed
64+
let _: () = unsafe { msg_send![view, setWantsLayer: Bool::YES] };
65+
66+
// SAFETY: `-[NSView layer]` returns an optional `CALayer`
67+
let layer: Option<Retained<CALayer>> = unsafe { msg_send_id![view, layer] };
68+
layer.expect("failed making the view layer-backed")
69+
}
70+
RawWindowHandle::UiKit(handle) => {
71+
// SAFETY: The pointer came from `WindowHandle`, which ensures that the
72+
// `UiKitWindowHandle` contains a valid pointer to an `UIView`.
73+
//
74+
// We use `NSObject` here to avoid importing `objc2-ui-kit`.
75+
let view: &NSObject = unsafe { handle.ui_view.cast().as_ref() };
76+
77+
// SAFETY: `-[UIView layer]` returns `CALayer`
78+
let layer: Retained<CALayer> = unsafe { msg_send_id![view, layer] };
79+
layer
80+
}
5481
_ => return Err(InitError::Unsupported(window_src)),
5582
};
5683

57-
// `NSView` can only be accessed from the main thread.
58-
let mtm = MainThreadMarker::new().ok_or(SoftBufferError::PlatformError(
59-
Some("can only access AppKit / macOS handles from the main thread".to_string()),
60-
None,
61-
))?;
62-
let view = handle.ns_view.as_ptr();
63-
// SAFETY: The pointer came from `WindowHandle`, which ensures that
64-
// the `AppKitWindowHandle` contains a valid pointer to an `NSView`.
65-
// Unwrap is fine, since the pointer came from `NonNull`.
66-
let view: Id<NSView> = unsafe { Id::retain(view.cast()) }.unwrap();
84+
// Add a sublayer, to avoid interfering with the root layer, since setting the contents of
85+
// e.g. a view-controlled layer is brittle.
6786
let layer = CALayer::new();
68-
let subview = unsafe { NSView::initWithFrame(mtm.alloc(), view.frame()) };
69-
layer.setContentsGravity(unsafe { kCAGravityTopLeft });
70-
layer.setNeedsDisplayOnBoundsChange(false);
71-
set_layer(&subview, &layer);
72-
unsafe {
73-
subview.setAutoresizingMask(NSAutoresizingMaskOptions(
74-
NSAutoresizingMaskOptions::NSViewWidthSizable.0
75-
| NSAutoresizingMaskOptions::NSViewHeightSizable.0,
76-
))
77-
};
87+
root_layer.addSublayer(&layer);
7888

79-
let window = view.window().ok_or(SoftBufferError::PlatformError(
80-
Some("view must be inside a window".to_string()),
81-
None,
82-
))?;
89+
// Set the anchor point. Used to avoid having to calculate the center point when setting
90+
// `bounds` in `resize`.
91+
layer.setAnchorPoint(CGPoint::new(0.0, 0.0));
92+
93+
// Set initial scale factor. Updated in `resize`.
94+
layer.setContentsScale(root_layer.contentsScale());
95+
96+
// Set `bounds` and `position` so that the new layer is inside the superlayer.
97+
//
98+
// This differs from just setting the `bounds`, as it also takes into account any
99+
// translation that the superlayer may have that we want to preserve.
100+
layer.setFrame(root_layer.bounds());
83101

84-
unsafe { view.addSubview(&subview) };
102+
// Do not use auto-resizing mask, see comments in `resize` for details.
103+
// layer.setAutoresizingMask(kCALayerHeightSizable | kCALayerWidthSizable);
104+
105+
// Set the content gravity in a way that masks failure to redraw at the correct time.
106+
layer.setContentsGravity(unsafe { kCAGravityResize });
107+
108+
// Softbuffer uses a coordinate system with the origin in the top-left corner (doesn't
109+
// really matter unless we start setting the `position` of our layer).
110+
layer.setGeometryFlipped(true);
111+
112+
// Initialize color space here, to reduce work later on.
85113
let color_space = CGColorSpace::create_device_rgb();
114+
86115
Ok(Self {
87-
layer: MainThreadBound::new(layer, mtm),
88-
window: MainThreadBound::new(window, mtm),
116+
layer: SendCALayer(layer),
117+
root_layer: SendCALayer(root_layer),
89118
color_space: SendCGColorSpace(color_space),
90-
size: None,
91119
_display: PhantomData,
92120
window_handle: window_src,
93121
})
@@ -99,17 +127,70 @@ impl<D: HasDisplayHandle, W: HasWindowHandle> SurfaceInterface<D, W> for CGImpl<
99127
}
100128

101129
fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), SoftBufferError> {
102-
self.size = Some((width, height));
130+
let scale_factor = self.root_layer.contentsScale();
131+
let bounds = CGRect::new(
132+
CGPoint::new(0.0, 0.0),
133+
CGSize::new(
134+
width.get() as CGFloat / scale_factor,
135+
height.get() as CGFloat / scale_factor,
136+
),
137+
);
138+
139+
// _Usually_, the programmer should be resizing the surface together in lockstep with the
140+
// user action that initiated the resize, e.g. a window resize, where there would already be
141+
// a transaction ongoing.
142+
//
143+
// With the current version of Winit, though, that isn't the case, and we end up getting the
144+
// resize event emitted later, outside the callstack where the transaction was ongoing. The
145+
// user could also choose to resize e.g. on a different thread, or in loads of other
146+
// circumstances.
147+
//
148+
// This, in turn, means that the default animation with a delay of 0.25 seconds kicks in
149+
// when updating these values - this is definitely not what we want, so we disable those
150+
// animations here.
151+
CATransaction::begin();
152+
CATransaction::setDisableActions(true);
153+
154+
// Set the scale factor of the layer to match the root layer / super layer, in case it
155+
// changed (e.g. if moved to a different monitor, or monitor settings changed).
156+
self.layer.setContentsScale(scale_factor);
157+
158+
// Set the bounds on the layer.
159+
//
160+
// This is an explicit design decision: We set the bounds on the layer manually, instead of
161+
// letting it be automatically updated using `autoresizingMask`.
162+
//
163+
// The first reason for this is that it gives the user complete control over the size of the
164+
// layer (and underlying buffer, once properly implemented, see #83), which matches other
165+
// platforms.
166+
//
167+
// The second is that it is needed to work around a bug in macOS 14 and above, where views
168+
// using auto layout may end up setting fractional values as the bounds, and that in turn
169+
// doesn't propagate properly through the auto-resizing mask and with contents gravity.
170+
//
171+
// If we were to change this so that the layer resizes automatically, we should _not_ use
172+
// `layer.setAutoresizingMask(...)`, but instead register an observer on the super layer,
173+
// which then propagates the bounds change to the sublayer. It is unfortunate that we cannot
174+
// use the built-in functionality to do this, but not something you can avoid either way if
175+
// you're doing automatic resizing, since you'd _need_ to propagate the scale factor anyhow.
176+
self.layer.setBounds(bounds);
177+
178+
// See comment on `CATransaction::begin`.
179+
CATransaction::commit();
180+
103181
Ok(())
104182
}
105183

106184
fn buffer_mut(&mut self) -> Result<BufferImpl<'_, D, W>, SoftBufferError> {
107-
let (width, height) = self
108-
.size
109-
.expect("Must set size of surface before calling `buffer_mut()`");
185+
let scale_factor = self.layer.contentsScale();
186+
let bounds = self.layer.bounds();
187+
// The bounds and scale factor are set in `resize`, and should result in integer values when
188+
// combined like this.
189+
let width = (bounds.size.width * scale_factor) as usize;
190+
let height = (bounds.size.height * scale_factor) as usize;
110191

111192
Ok(BufferImpl {
112-
buffer: vec![0; width.get() as usize * height.get() as usize],
193+
buffer: vec![0; width * height],
113194
imp: self,
114195
})
115196
}
@@ -137,41 +218,38 @@ impl<'a, D: HasDisplayHandle, W: HasWindowHandle> BufferInterface for BufferImpl
137218

138219
fn present(self) -> Result<(), SoftBufferError> {
139220
let data_provider = CGDataProvider::from_buffer(Arc::new(Buffer(self.buffer)));
140-
let (width, height) = self.imp.size.unwrap();
221+
222+
let scale_factor = self.imp.layer.contentsScale();
223+
let bounds = self.imp.layer.bounds();
224+
// The bounds and scale factor are set in `resize`, and should result in integer values when
225+
// combined like this.
226+
let width = (bounds.size.width * scale_factor) as usize;
227+
let height = (bounds.size.height * scale_factor) as usize;
228+
141229
let image = CGImage::new(
142-
width.get() as usize,
143-
height.get() as usize,
230+
width,
231+
height,
144232
8,
145233
32,
146-
(width.get() * 4) as usize,
234+
width * 4,
147235
&self.imp.color_space.0,
148236
kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipFirst,
149237
&data_provider,
150238
false,
151239
kCGRenderingIntentDefault,
152240
);
153-
154-
// TODO: Use run_on_main() instead.
155-
let mtm = MainThreadMarker::new().ok_or(SoftBufferError::PlatformError(
156-
Some("can only access AppKit / macOS handles from the main thread".to_string()),
157-
None,
158-
))?;
241+
let contents = unsafe { (image.as_ptr() as *mut AnyObject).as_ref() };
159242

160243
// The CALayer has a default action associated with a change in the layer contents, causing
161244
// a quarter second fade transition to happen every time a new buffer is applied. This can
162245
// be mitigated by wrapping the operation in a transaction and disabling all actions.
163246
CATransaction::begin();
164247
CATransaction::setDisableActions(true);
165248

166-
let layer = self.imp.layer.get(mtm);
167-
layer.setContentsScale(self.imp.window.get(mtm).backingScaleFactor());
168-
169-
unsafe {
170-
layer.setContents((image.as_ptr() as *mut AnyObject).as_ref());
171-
};
249+
// SAFETY: The contents is `CGImage`, which is a valid class for `contents`.
250+
unsafe { self.imp.layer.setContents(contents) };
172251

173252
CATransaction::commit();
174-
175253
Ok(())
176254
}
177255

@@ -184,3 +262,15 @@ struct SendCGColorSpace(CGColorSpace);
184262
// SAFETY: `CGColorSpace` is immutable, and can freely be shared between threads.
185263
unsafe impl Send for SendCGColorSpace {}
186264
unsafe impl Sync for SendCGColorSpace {}
265+
266+
struct SendCALayer(Retained<CALayer>);
267+
// CALayer is thread safe
268+
unsafe impl Send for SendCALayer {}
269+
unsafe impl Sync for SendCALayer {}
270+
271+
impl Deref for SendCALayer {
272+
type Target = CALayer;
273+
fn deref(&self) -> &Self::Target {
274+
&self.0
275+
}
276+
}

src/backends/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::{ContextInterface, InitError};
22
use raw_window_handle::HasDisplayHandle;
33

4-
#[cfg(target_os = "macos")]
4+
#[cfg(target_vendor = "apple")]
55
pub(crate) mod cg;
66
#[cfg(kms_platform)]
77
pub(crate) mod kms;

src/lib.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ impl<D: HasDisplayHandle, W: HasWindowHandle> Surface<D, W> {
118118
/// ## Platform Dependent Behavior
119119
///
120120
/// - On X11, the window must be visible.
121-
/// - On macOS, Redox and Wayland, this function is unimplemented.
121+
/// - On Apple platforms, Redox and Wayland, this function is unimplemented.
122122
/// - On Web, this will fail if the content was supplied by
123123
/// a different origin depending on the sites CORS rules.
124124
pub fn fetch(&mut self) -> Result<Vec<u32>, SoftBufferError> {
@@ -194,7 +194,7 @@ impl<D: HasDisplayHandle, W: HasWindowHandle> HasWindowHandle for Surface<D, W>
194194
///
195195
/// Currently [`Buffer::present`] must block copying image data on:
196196
/// - Web
197-
/// - macOS
197+
/// - Apple platforms
198198
pub struct Buffer<'a, D, W> {
199199
buffer_impl: BufferDispatch<'a, D, W>,
200200
_marker: PhantomData<(Arc<D>, Cell<()>)>,

0 commit comments

Comments
 (0)