Skip to content

Commit 765f0f4

Browse files
committed
iOS and tvOS export templates
- renzora_ios platform crate (staticlib) with Swift AppDelegate bridge - Xcode project linking Metal, GameController, AudioToolbox, and other frameworks - Build script supporting iOS device, iOS simulator, Apple TV, and Apple TV simulator - VFS detect_ios() loads game.rpak from app bundle via CoreFoundation - Export overlay injects rpak into .app bundle, outputs .ipa - TvOS platform variant with landscape-only Info.plist - Cargo aliases and make tasks for all 4 targets - README documentation for iOS/tvOS build and export workflow
1 parent f6e6740 commit 765f0f4

9 files changed

Lines changed: 692 additions & 0 deletions

File tree

crates/core/renzora_runtime/src/vfs.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ impl Vfs {
3333
}
3434
}
3535

36+
// iOS / tvOS: load rpak from app bundle
37+
#[cfg(any(target_os = "ios", target_os = "tvos"))]
38+
{
39+
if let Some(vfs) = Self::detect_ios() {
40+
return vfs;
41+
}
42+
}
43+
3644
#[cfg(not(target_arch = "wasm32"))]
3745
{
3846
// 1. Check for embedded rpak in current exe
@@ -155,6 +163,98 @@ impl Vfs {
155163
}
156164
}
157165

166+
/// On iOS/tvOS, load game.rpak from the app bundle's resource directory.
167+
#[cfg(any(target_os = "ios", target_os = "tvos"))]
168+
fn detect_ios() -> Option<Self> {
169+
use std::ffi::{CStr, c_char};
170+
171+
extern "C" {
172+
fn CFBundleGetMainBundle() -> *const std::ffi::c_void;
173+
fn CFBundleCopyResourceURL(
174+
bundle: *const std::ffi::c_void,
175+
resource_name: *const std::ffi::c_void,
176+
resource_type: *const std::ffi::c_void,
177+
sub_dir_name: *const std::ffi::c_void,
178+
) -> *const std::ffi::c_void;
179+
fn CFURLGetFileSystemRepresentation(
180+
url: *const std::ffi::c_void,
181+
resolve_against_base: bool,
182+
buffer: *mut u8,
183+
max_buf_len: isize,
184+
) -> bool;
185+
fn CFRelease(cf: *const std::ffi::c_void);
186+
}
187+
188+
// Helper to create a CFString from a Rust &str
189+
fn cfstring(s: &str) -> *const std::ffi::c_void {
190+
extern "C" {
191+
fn CFStringCreateWithBytes(
192+
alloc: *const std::ffi::c_void,
193+
bytes: *const u8,
194+
num_bytes: isize,
195+
encoding: u32,
196+
is_external: bool,
197+
) -> *const std::ffi::c_void;
198+
}
199+
const K_CF_STRING_ENCODING_UTF8: u32 = 0x08000100;
200+
unsafe {
201+
CFStringCreateWithBytes(
202+
std::ptr::null(),
203+
s.as_ptr(),
204+
s.len() as isize,
205+
K_CF_STRING_ENCODING_UTF8,
206+
false,
207+
)
208+
}
209+
}
210+
211+
unsafe {
212+
let bundle = CFBundleGetMainBundle();
213+
if bundle.is_null() {
214+
warn!("iOS: could not get main bundle");
215+
return None;
216+
}
217+
218+
let name = cfstring("game");
219+
let ext = cfstring("rpak");
220+
let url = CFBundleCopyResourceURL(bundle, name, ext, std::ptr::null());
221+
CFRelease(name);
222+
CFRelease(ext);
223+
224+
if url.is_null() {
225+
warn!("iOS: game.rpak not found in app bundle");
226+
return None;
227+
}
228+
229+
let mut buf = [0u8; 1024];
230+
let ok = CFURLGetFileSystemRepresentation(url, true, buf.as_mut_ptr(), buf.len() as isize);
231+
CFRelease(url);
232+
233+
if !ok {
234+
warn!("iOS: could not get filesystem path for game.rpak");
235+
return None;
236+
}
237+
238+
let c_path = CStr::from_ptr(buf.as_ptr() as *const c_char);
239+
let path_str = c_path.to_str().ok()?;
240+
let path = Path::new(path_str);
241+
242+
match RpakArchive::from_file(path) {
243+
Ok(archive) => {
244+
info!("Loaded iOS bundle rpak ({} files)", archive.len());
245+
Some(Self {
246+
archive: Some(Arc::new(archive)),
247+
project_root: None,
248+
})
249+
}
250+
Err(e) => {
251+
error!("Failed to load iOS bundle rpak: {}", e);
252+
None
253+
}
254+
}
255+
}
256+
}
257+
158258
/// Whether we have an rpak archive loaded.
159259
pub fn has_archive(&self) -> bool {
160260
self.archive.is_some()
@@ -196,6 +296,17 @@ impl Vfs {
196296
self.archive.clone()
197297
}
198298

299+
/// Load a VFS from raw rpak bytes (used by wasm fetch).
300+
pub fn from_rpak_bytes(bytes: &[u8]) -> Result<Self, String> {
301+
let archive = RpakArchive::from_bytes(bytes)
302+
.map_err(|e| format!("Failed to load rpak: {}", e))?;
303+
info!("Loaded rpak from bytes ({} files)", archive.len());
304+
Ok(Self {
305+
archive: Some(Arc::new(archive)),
306+
project_root: None,
307+
})
308+
}
309+
199310
/// Extract the entire archive to a temporary directory and return the path.
200311
/// Useful for systems that need filesystem paths (e.g., Bevy asset server).
201312
#[cfg(not(target_arch = "wasm32"))]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "renzora-ios"
3+
version = "0.2.0"
4+
edition = "2021"
5+
publish = false
6+
7+
[lib]
8+
name = "renzora_ios"
9+
crate-type = ["staticlib"]
10+
11+
[dependencies]
12+
renzora = { path = "../../..", default-features = false }
13+
bevy = { version = "0.18", features = ["jpeg"] }
14+
15+
[profile.release]
16+
lto = "thin"
17+
strip = true
18+
19+
[profile.dist]
20+
inherits = "release"
21+
lto = "fat"
22+
strip = true
23+
codegen-units = 1
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
use bevy::prelude::*;
2+
3+
/// Entry point called from the Swift/Objective-C bridge.
4+
///
5+
/// The iOS app host calls this from `AppDelegate` after UIKit is ready.
6+
/// Bevy's winit backend handles the Metal surface via the existing UIWindow.
7+
#[unsafe(no_mangle)]
8+
pub extern "C" fn renzora_main() {
9+
let mut app = renzora::build_runtime_app();
10+
app.run();
11+
}

0 commit comments

Comments
 (0)