Support for iPad device family.

Device family is deduced from the app bundle, but user can also override
it with --device-family=... option.

For now, device family affects rendered window size and bounds of UIScreen.

The iPhone device family is kept to be default.
We explicitly fallback to it in the following cases:
- App picker case
- App doesn't define any device family
- App define both (universal app)

Change-Id: I104889df0942d63823ca77d5c7ca847087149bf0
This commit is contained in:
ciciplusplus
2026-01-17 19:34:05 +01:00
parent 8b1198015e
commit 3cbe9b5ee5
6 changed files with 128 additions and 12 deletions

View File

@@ -30,6 +30,17 @@ View options:
This is a natural number that is at least 1.
Device options:
--device-family=...
Specifies which device family should be emulated: iPhone or iPad.
This only work if running app does support selected family
(or the option will be ignored).
If omitted, suitable device family would be deduced from application
bundle itself, falling back to iPhone if needed.
Accepted values are "iphone" and "ipad" respectively.
Game controller options:
--deadzone=...
Configures the size of the \"dead zone\" for analog stick inputs.

View File

@@ -13,6 +13,7 @@
use crate::fs::{BundleData, Fs, GuestPath, GuestPathBuf};
use crate::image::Image;
use crate::window::DeviceFamily;
use plist::dictionary::Dictionary;
use plist::Value;
use std::io::Cursor;
@@ -201,4 +202,17 @@ impl Bundle {
.map_or("UIInterfaceOrientationPortrait", |o| o.as_string().unwrap())]
})
}
pub fn device_family_array(&self) -> Vec<DeviceFamily> {
self.plist
.get("UIDeviceFamily")
.map(|v| {
v.as_array()
.unwrap()
.iter()
.map(|o| DeviceFamily::try_from(o.as_unsigned_integer().unwrap()).unwrap())
.collect()
})
.unwrap_or_else(|| vec![DeviceFamily::iPhone])
}
}

View File

@@ -22,6 +22,7 @@ use std::net::TcpListener;
use std::time::{Duration, Instant};
use crate::libc::pthread::cond::pthread_cond_t;
use crate::window::DeviceFamily;
pub use mutex::{MutexId, MutexType, PTHREAD_MUTEX_DEFAULT};
/// Index into the [Vec] of threads. Thread 0 is always the main thread.
@@ -274,6 +275,34 @@ impl Environment {
}
}
let device_family_override = options.device_family;
let device_family_array = bundle.device_family_array();
let device_family = match device_family_array.len() {
// iPhone only or iPad only
1 => {
let only_supported = device_family_array[0];
if let Some(dfo) = device_family_override {
if dfo != only_supported {
log!("Warning: User-defined {:?} device family override is not supported by the app! ignoring", dfo);
}
}
only_supported
}
// iPhone and iPad
2 => {
if let Some(dfo) = device_family_override {
assert!(device_family_array.contains(&dfo));
dfo
} else {
assert!(device_family_array.contains(&DeviceFamily::iPhone));
DeviceFamily::iPhone
}
}
_ => unreachable!(),
};
log!("{:?} device family is chosen.", device_family);
options.device_family = Some(device_family);
let window = if options.headless {
None
} else {

View File

@@ -44,9 +44,10 @@ pub const CLASSES: ClassExports = objc_classes! {
// While Apple's documentation says this changes with the interface
// orientation, https://useyourloaf.com/blog/uiscreen-bounds-in-ios-8/ says
// ths wasn't the case prior to iOS 8.
let (width, height) = env.window().device_family().portrait_size();
CGRect {
origin: CGPoint { x: 0.0, y: 0.0 },
size: CGSize { width: 320.0, height: 480.0 },
size: CGSize { width: width as f32, height: height as f32 },
}
}

View File

@@ -6,7 +6,7 @@
//! Parsing and management of user-configurable options, e.g. for input methods.
use crate::gles::GLESImplementation;
use crate::window::DeviceOrientation;
use crate::window::{DeviceFamily, DeviceOrientation};
use std::collections::HashMap;
use std::io::{BufRead, BufReader, Read};
use std::net::{SocketAddr, ToSocketAddrs};
@@ -35,6 +35,7 @@ pub enum Button {
#[derive(Clone)]
pub struct Options {
pub fullscreen: bool,
pub device_family: Option<DeviceFamily>,
pub initial_orientation: DeviceOrientation,
pub scale_hack: NonZeroU32,
pub deadzone: f32,
@@ -66,6 +67,7 @@ impl Default for Options {
fn default() -> Self {
Options {
fullscreen: false,
device_family: None,
initial_orientation: DeviceOrientation::Portrait,
scale_hack: NonZeroU32::new(1).unwrap(),
analog_stick_tilt_controls: true,
@@ -116,6 +118,10 @@ impl Options {
self.initial_orientation = DeviceOrientation::LandscapeLeft;
} else if arg == "--landscape-right" {
self.initial_orientation = DeviceOrientation::LandscapeRight;
} else if let Some(value) = arg.strip_prefix("--device-family=") {
let parsed =
DeviceFamily::try_from(value).map_err(|_| "Invalid device family".to_string())?;
self.device_family = Some(parsed);
} else if let Some(value) = arg.strip_prefix("--scale-hack=") {
self.scale_hack = value
.parse()

View File

@@ -28,18 +28,58 @@ use std::num::NonZeroU32;
use std::ptr::null_mut;
use std::time::{Duration, Instant};
#[allow(non_camel_case_types)]
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum DeviceFamily {
iPhone,
iPad,
}
impl DeviceFamily {
pub fn portrait_size(&self) -> (u32, u32) {
match self {
DeviceFamily::iPhone => (320, 480),
DeviceFamily::iPad => (768, 1024),
}
}
}
impl TryFrom<u64> for DeviceFamily {
type Error = ();
fn try_from(value: u64) -> Result<Self, Self::Error> {
match value {
1 => Ok(DeviceFamily::iPhone),
2 => Ok(DeviceFamily::iPad),
_ => Err(()),
}
}
}
impl TryFrom<&str> for DeviceFamily {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"iphone" => Ok(DeviceFamily::iPhone),
"ipad" => Ok(DeviceFamily::iPad),
_ => Err(()),
}
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum DeviceOrientation {
Portrait,
LandscapeLeft,
LandscapeRight,
}
fn size_for_orientation(orientation: DeviceOrientation, scale_hack: NonZeroU32) -> (u32, u32) {
fn size_for_orientation(
family: DeviceFamily,
orientation: DeviceOrientation,
scale_hack: NonZeroU32,
) -> (u32, u32) {
let (width, height) = family.portrait_size();
let scale_hack = scale_hack.get();
match orientation {
DeviceOrientation::Portrait => (320 * scale_hack, 480 * scale_hack),
DeviceOrientation::LandscapeLeft => (480 * scale_hack, 320 * scale_hack),
DeviceOrientation::LandscapeRight => (480 * scale_hack, 320 * scale_hack),
DeviceOrientation::Portrait => (width * scale_hack, height * scale_hack),
DeviceOrientation::LandscapeLeft => (height * scale_hack, width * scale_hack),
DeviceOrientation::LandscapeRight => (height * scale_hack, width * scale_hack),
}
}
fn rotate_fullscreen_size(orientation: DeviceOrientation, screen_size: (u32, u32)) -> (u32, u32) {
@@ -180,6 +220,7 @@ pub struct Window {
scale_hack: NonZeroU32,
internal_gl_ins: Option<Box<dyn GLESContext>>,
splash_image: Option<Image>,
device_family: DeviceFamily,
device_orientation: DeviceOrientation,
controller_ctx: sdl2::GameControllerSubsystem,
controllers: Vec<sdl2::controller::GameController>,
@@ -236,6 +277,7 @@ impl Window {
let scale_hack = options.scale_hack;
// TODO: some apps specify their orientation in Info.plist, we could use
// that here.
let device_family = options.device_family.unwrap_or(DeviceFamily::iPhone);
let device_orientation = options.initial_orientation;
let fullscreen = options.fullscreen;
@@ -261,7 +303,8 @@ impl Window {
.unwrap();
window
} else {
let (width, height) = size_for_orientation(device_orientation, scale_hack);
let (width, height) =
size_for_orientation(device_family, device_orientation, scale_hack);
let window = video_ctx
.window(title, width, height)
.position_centered()
@@ -320,6 +363,7 @@ impl Window {
scale_hack,
internal_gl_ins: None,
splash_image: launch_image,
device_family,
device_orientation,
controller_ctx,
controllers: Vec::new(),
@@ -377,8 +421,11 @@ impl Window {
independent_of_viewport: bool,
) -> (f32, f32) {
let (vx, vy, vw, vh) = if independent_of_viewport {
let (width, height) =
size_for_orientation(window.device_orientation, NonZeroU32::new(1).unwrap());
let (width, height) = size_for_orientation(
window.device_family,
window.device_orientation,
NonZeroU32::new(1).unwrap(),
);
(0, 0, width, height)
} else {
window.viewport()
@@ -1219,7 +1266,7 @@ impl Window {
set_sdl2_orientation(new_orientation);
rotate_fullscreen_size(new_orientation, self.window.size())
} else {
size_for_orientation(new_orientation, self.scale_hack)
size_for_orientation(self.device_family, new_orientation, self.scale_hack)
};
// macOS quirk: when resizing the window, the new framebuffer's size
@@ -1267,6 +1314,10 @@ impl Window {
}
}
pub fn device_family(&self) -> DeviceFamily {
self.device_family
}
/// Returns the current device orientation
pub fn current_rotation(&self) -> DeviceOrientation {
self.device_orientation
@@ -1277,7 +1328,11 @@ impl Window {
/// The aspect ratio, scale and orientation reflect the guest app's view of
/// the world.
pub fn size_unrotated_unscaled(&self) -> (u32, u32) {
size_for_orientation(DeviceOrientation::Portrait, NonZeroU32::new(1).unwrap())
size_for_orientation(
self.device_family,
DeviceOrientation::Portrait,
NonZeroU32::new(1).unwrap(),
)
}
/// Get the region of the on-screen window (x, y, width, height) used to
@@ -1287,7 +1342,7 @@ impl Window {
/// the world, but the scale and orientation might not.
pub fn viewport(&self) -> (u32, u32, u32, u32) {
let (app_width, app_height) =
size_for_orientation(self.device_orientation, self.scale_hack);
size_for_orientation(self.device_family, self.device_orientation, self.scale_hack);
if !self.fullscreen && !Self::rotatable_fullscreen() {
return (0, 0, app_width, app_height);
}