Skip to content

Commit

Permalink
Make iOS thread safe
Browse files Browse the repository at this point in the history
  • Loading branch information
madsmtm committed Aug 27, 2023
1 parent c14e670 commit e73b3a3
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 109 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ And please only add new entries to the top of this list, right below the `# Unre

# Unreleased

- Make iOS `MonitorHandle` and `VideoMode` usable from other threads.
- Fix window size sometimes being invalid when resizing on macOS.
- On Web, `ControlFlow::Poll` and `ControlFlow::WaitUntil` are now using the Prioritized Task Scheduling API. `setTimeout()` with a trick to circumvent throttling to 4ms is used as a fallback.
- On Web, never return a `MonitorHandle`.
Expand Down
5 changes: 4 additions & 1 deletion src/platform/ios.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::os::raw::c_void;

use icrate::Foundation::MainThreadMarker;
use objc2::rc::Id;

use crate::{
Expand Down Expand Up @@ -210,7 +211,9 @@ pub trait MonitorHandleExtIOS {
impl MonitorHandleExtIOS for MonitorHandle {
#[inline]
fn ui_screen(&self) -> *mut c_void {
Id::as_ptr(self.inner.ui_screen()) as *mut c_void
// SAFETY: The marker is only used to get the pointer of the screen
let mtm = unsafe { MainThreadMarker::new_unchecked() };
Id::as_ptr(self.inner.ui_screen(mtm)) as *mut c_void
}

#[inline]
Expand Down
11 changes: 0 additions & 11 deletions src/platform_impl/ios/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,6 @@
#![cfg(ios_platform)]
#![allow(clippy::let_unit_value)]

// TODO: (mtak-) UIKit requires main thread for virtually all function/method calls. This could be
// worked around in the future by using GCD (grand central dispatch) and/or caching of values like
// window size/position.
macro_rules! assert_main_thread {
($($t:tt)*) => {
if !::icrate::Foundation::is_main_thread() {
panic!($($t)*);
}
};
}

mod app_state;
mod event_loop;
mod ffi;
Expand Down
203 changes: 117 additions & 86 deletions src/platform_impl/ios/monitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

use std::{
collections::{BTreeSet, VecDeque},
fmt,
ops::{Deref, DerefMut},
fmt, hash, ptr,
};

use icrate::Foundation::{MainThreadMarker, NSInteger};
use icrate::Foundation::{MainThreadBound, MainThreadMarker, NSInteger};
use objc2::mutability::IsRetainable;
use objc2::rc::Id;

use super::uikit::{UIScreen, UIScreenMode};
Expand All @@ -16,32 +16,59 @@ use crate::{
platform_impl::platform::app_state,
};

// TODO(madsmtm): Remove or refactor this
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub(crate) struct ScreenModeSendSync(pub(crate) Id<UIScreenMode>);
// Workaround for `MainThreadBound` implementing almost no traits
#[derive(Debug)]
struct MainThreadBoundDelegateImpls<T>(MainThreadBound<Id<T>>);

impl<T: IsRetainable> Clone for MainThreadBoundDelegateImpls<T> {
fn clone(&self) -> Self {
Self(
self.0
.get_on_main(|inner, mtm| MainThreadBound::new(Id::clone(inner), mtm)),
)
}
}

impl<T: IsRetainable> hash::Hash for MainThreadBoundDelegateImpls<T> {
fn hash<H: hash::Hasher>(&self, state: &mut H) {
// SAFETY: Marker only used to get the pointer
let mtm = unsafe { MainThreadMarker::new_unchecked() };
Id::as_ptr(self.0.get(mtm)).hash(state);
}
}

impl<T: IsRetainable> PartialEq for MainThreadBoundDelegateImpls<T> {
fn eq(&self, other: &Self) -> bool {
// SAFETY: Marker only used to get the pointer
let mtm = unsafe { MainThreadMarker::new_unchecked() };
Id::as_ptr(self.0.get(mtm)) == Id::as_ptr(other.0.get(mtm))
}
}

unsafe impl Send for ScreenModeSendSync {}
unsafe impl Sync for ScreenModeSendSync {}
impl<T: IsRetainable> Eq for MainThreadBoundDelegateImpls<T> {}

#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct VideoMode {
pub(crate) size: (u32, u32),
pub(crate) bit_depth: u16,
pub(crate) refresh_rate_millihertz: u32,
pub(crate) screen_mode: ScreenModeSendSync,
screen_mode: MainThreadBoundDelegateImpls<UIScreenMode>,
pub(crate) monitor: MonitorHandle,
}

impl VideoMode {
fn new(uiscreen: Id<UIScreen>, screen_mode: Id<UIScreenMode>) -> VideoMode {
assert_main_thread!("`VideoMode` can only be created on the main thread on iOS");
fn new(
uiscreen: Id<UIScreen>,
screen_mode: Id<UIScreenMode>,
mtm: MainThreadMarker,
) -> VideoMode {
let refresh_rate_millihertz = refresh_rate_millihertz(&uiscreen);
let size = screen_mode.size();
VideoMode {
size: (size.width as u32, size.height as u32),
bit_depth: 32,
refresh_rate_millihertz,
screen_mode: ScreenModeSendSync(screen_mode),
screen_mode: MainThreadBoundDelegateImpls(MainThreadBound::new(screen_mode, mtm)),
monitor: MonitorHandle::new(uiscreen),
}
}
Expand All @@ -61,53 +88,50 @@ impl VideoMode {
pub fn monitor(&self) -> MonitorHandle {
self.monitor.clone()
}
}

#[derive(Clone, PartialEq, Eq, Hash)]
pub struct Inner {
uiscreen: Id<UIScreen>,
pub(super) fn screen_mode(&self, mtm: MainThreadMarker) -> &Id<UIScreenMode> {
self.screen_mode.0.get(mtm)
}
}

#[derive(Clone, PartialEq, Eq, Hash)]
pub struct MonitorHandle {
inner: Inner,
ui_screen: MainThreadBound<Id<UIScreen>>,
}

impl PartialOrd for MonitorHandle {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
impl Clone for MonitorHandle {
fn clone(&self) -> Self {
Self {
ui_screen: self
.ui_screen
.get_on_main(|inner, mtm| MainThreadBound::new(inner.clone(), mtm)),
}
}
}

impl Ord for MonitorHandle {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
// TODO: Make a better ordering
(self as *const Self).cmp(&(other as *const Self))
impl hash::Hash for MonitorHandle {
fn hash<H: hash::Hasher>(&self, state: &mut H) {
(self as *const Self).hash(state);
}
}

impl Deref for MonitorHandle {
type Target = Inner;

fn deref(&self) -> &Inner {
assert_main_thread!("`MonitorHandle` methods can only be run on the main thread on iOS");
&self.inner
impl PartialEq for MonitorHandle {
fn eq(&self, other: &Self) -> bool {
ptr::eq(self, other)
}
}

impl DerefMut for MonitorHandle {
fn deref_mut(&mut self) -> &mut Inner {
assert_main_thread!("`MonitorHandle` methods can only be run on the main thread on iOS");
&mut self.inner
impl Eq for MonitorHandle {}

impl PartialOrd for MonitorHandle {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}

unsafe impl Send for MonitorHandle {}
unsafe impl Sync for MonitorHandle {}

impl Drop for MonitorHandle {
fn drop(&mut self) {
assert_main_thread!("`MonitorHandle` can only be dropped on the main thread on iOS");
impl Ord for MonitorHandle {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
// TODO: Make a better ordering
(self as *const Self).cmp(&(other as *const Self))
}
}

Expand Down Expand Up @@ -135,59 +159,80 @@ impl fmt::Debug for MonitorHandle {
}

impl MonitorHandle {
pub(crate) fn new(uiscreen: Id<UIScreen>) -> Self {
assert_main_thread!("`MonitorHandle` can only be created on the main thread on iOS");
pub(crate) fn new(ui_screen: Id<UIScreen>) -> Self {
// Holding `Id<UIScreen>` implies we're on the main thread.
let mtm = MainThreadMarker::new().unwrap();
Self {
inner: Inner { uiscreen },
ui_screen: MainThreadBound::new(ui_screen, mtm),
}
}
}

impl Inner {
pub fn name(&self) -> Option<String> {
let main = UIScreen::main(MainThreadMarker::new().unwrap());
if self.uiscreen == main {
Some("Primary".to_string())
} else if self.uiscreen == main.mirroredScreen() {
Some("Mirrored".to_string())
} else {
UIScreen::screens(MainThreadMarker::new().unwrap())
.iter()
.position(|rhs| rhs == &*self.uiscreen)
.map(|idx| idx.to_string())
}
self.ui_screen.get_on_main(|ui_screen, mtm| {
let main = UIScreen::main(mtm);
if *ui_screen == main {
Some("Primary".to_string())
} else if *ui_screen == main.mirroredScreen() {
Some("Mirrored".to_string())
} else {
UIScreen::screens(mtm)
.iter()
.position(|rhs| rhs == &**ui_screen)
.map(|idx| idx.to_string())
}
})
}

pub fn size(&self) -> PhysicalSize<u32> {
let bounds = self.uiscreen.nativeBounds();
let bounds = self
.ui_screen
.get_on_main(|ui_screen, _| ui_screen.nativeBounds());
PhysicalSize::new(bounds.size.width as u32, bounds.size.height as u32)
}

pub fn position(&self) -> PhysicalPosition<i32> {
let bounds = self.uiscreen.nativeBounds();
let bounds = self
.ui_screen
.get_on_main(|ui_screen, _| ui_screen.nativeBounds());
(bounds.origin.x as f64, bounds.origin.y as f64).into()
}

pub fn scale_factor(&self) -> f64 {
self.uiscreen.nativeScale() as f64
self.ui_screen
.get_on_main(|ui_screen, _| ui_screen.nativeScale()) as f64
}

pub fn refresh_rate_millihertz(&self) -> Option<u32> {
Some(refresh_rate_millihertz(&self.uiscreen))
Some(
self.ui_screen
.get_on_main(|ui_screen, _| refresh_rate_millihertz(ui_screen)),
)
}

pub fn video_modes(&self) -> impl Iterator<Item = VideoMode> {
// Use Ord impl of RootVideoMode
let modes: BTreeSet<_> = self
.uiscreen
.availableModes()
.into_iter()
.map(|mode| RootVideoMode {
video_mode: VideoMode::new(self.uiscreen.clone(), mode),
})
.collect();

modes.into_iter().map(|mode| mode.video_mode)
self.ui_screen.get_on_main(|ui_screen, mtm| {
// Use Ord impl of RootVideoMode

let modes: BTreeSet<_> = ui_screen
.availableModes()
.into_iter()
.map(|mode| RootVideoMode {
video_mode: VideoMode::new(ui_screen.clone(), mode, mtm),
})
.collect();

modes.into_iter().map(|mode| mode.video_mode)
})
}

pub(crate) fn ui_screen(&self, mtm: MainThreadMarker) -> &Id<UIScreen> {
self.ui_screen.get(mtm)
}

pub fn preferred_video_mode(&self) -> VideoMode {
self.ui_screen.get_on_main(|ui_screen, mtm| {
VideoMode::new(ui_screen.clone(), ui_screen.preferredMode().unwrap(), mtm)
})
}
}

Expand Down Expand Up @@ -215,20 +260,6 @@ fn refresh_rate_millihertz(uiscreen: &UIScreen) -> u32 {
refresh_rate_millihertz as u32 * 1000
}

// MonitorHandleExtIOS
impl Inner {
pub(crate) fn ui_screen(&self) -> &Id<UIScreen> {
&self.uiscreen
}

pub fn preferred_video_mode(&self) -> VideoMode {
VideoMode::new(
self.uiscreen.clone(),
self.uiscreen.preferredMode().unwrap(),
)
}
}

pub fn uiscreens(mtm: MainThreadMarker) -> VecDeque<MonitorHandle> {
UIScreen::screens(mtm)
.into_iter()
Expand Down
8 changes: 4 additions & 4 deletions src/platform_impl/ios/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ declare_class!(

impl WinitUIWindow {
pub(crate) fn new(
_mtm: MainThreadMarker,
mtm: MainThreadMarker,
window_attributes: &WindowAttributes,
_platform_attributes: &PlatformSpecificWindowBuilderAttributes,
frame: CGRect,
Expand All @@ -476,12 +476,12 @@ impl WinitUIWindow {
match window_attributes.fullscreen.clone().map(Into::into) {
Some(Fullscreen::Exclusive(ref video_mode)) => {
let monitor = video_mode.monitor();
let screen = monitor.ui_screen();
screen.setCurrentMode(Some(&video_mode.screen_mode.0));
let screen = monitor.ui_screen(mtm);
screen.setCurrentMode(Some(video_mode.screen_mode(mtm)));
this.setScreen(screen);
}
Some(Fullscreen::Borderless(Some(ref monitor))) => {
let screen = monitor.ui_screen();
let screen = monitor.ui_screen(mtm);
this.setScreen(screen);
}
_ => (),
Expand Down
Loading

0 comments on commit e73b3a3

Please sign in to comment.