From d73e50bb9ec59bea86a9ce82b1c70ed81bc2aafd Mon Sep 17 00:00:00 2001 From: Sienna Meridian Satterwhite Date: Wed, 17 Dec 2025 22:40:51 +0000 Subject: [PATCH] chore: checkpoint before render engine vendoring Signed-off-by: Sienna Meridian Satterwhite --- Cargo.lock | 1 + Cargo.toml | 2 +- crates/app/Cargo.toml | 5 +- crates/app/src/input/input_handler.rs | 2 +- crates/app/src/input/mod.rs | 1 - crates/app/src/main.rs | 81 ++++- crates/app/src/setup.rs | 4 +- crates/app/tests/cube_sync_headless.rs | 5 +- crates/xtask/Cargo.toml | 11 + crates/xtask/README.md | 94 ++++++ crates/xtask/src/main.rs | 448 +++++++++++++++++++++++++ docs/ESTIMATION.md | 159 +++++++++ scripts/ios/Entitlements.plist | 14 + scripts/ios/Info.plist | 2 +- scripts/ios/deploy-simulator.sh | 2 +- 15 files changed, 806 insertions(+), 25 deletions(-) create mode 100644 crates/xtask/Cargo.toml create mode 100644 crates/xtask/README.md create mode 100644 crates/xtask/src/main.rs create mode 100644 docs/ESTIMATION.md create mode 100644 scripts/ios/Entitlements.plist diff --git a/Cargo.lock b/Cargo.lock index 32f0ca3..9103a65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7247,6 +7247,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bevy", + "bytes", "inventory", "libmarathon", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 003866d..acb9c74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ rusqlite = "0.37.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" toml = "0.9" -rkyv = { version = "0.8", features = ["uuid-1"] } +rkyv = { version = "0.8", features = ["uuid-1", "bytes-1"] } # Error handling thiserror = "2.0" diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 84dd387..38e860b 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -33,20 +33,21 @@ rand = "0.8" iroh = { version = "0.95", features = ["discovery-local-network"] } iroh-gossip = "0.95" futures-lite = "2.0" -bincode = "1.3" +rkyv = { workspace = true } bytes = "1.0" crossbeam-channel = "0.5.15" [target.'cfg(target_os = "ios")'.dependencies] objc = "0.2" raw-window-handle = "0.6" +tracing-oslog = "0.3" [dev-dependencies] iroh = { version = "0.95", features = ["discovery-local-network"] } iroh-gossip = "0.95" tempfile = "3" futures-lite = "2.0" -bincode = "1.3" +rkyv = { workspace = true } bytes = "1.0" [lib] diff --git a/crates/app/src/input/input_handler.rs b/crates/app/src/input/input_handler.rs index 810bddf..575d603 100644 --- a/crates/app/src/input/input_handler.rs +++ b/crates/app/src/input/input_handler.rs @@ -242,7 +242,7 @@ fn screen_to_world_ray( screen_pos: glam::Vec2, camera: &Camera, camera_transform: &GlobalTransform, - window: &Window, + _window: &Window, ) -> Option { // Convert screen position to viewport position (0..1 range) let viewport_pos = Vec2::new(screen_pos.x, screen_pos.y); diff --git a/crates/app/src/input/mod.rs b/crates/app/src/input/mod.rs index 47d7686..a1263ac 100644 --- a/crates/app/src/input/mod.rs +++ b/crates/app/src/input/mod.rs @@ -9,5 +9,4 @@ pub mod event_buffer; pub mod input_handler; -pub use event_buffer::InputEventBuffer; pub use input_handler::InputHandlerPlugin; diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index e9d10bc..43766e5 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -4,10 +4,12 @@ use bevy::prelude::*; use libmarathon::{ - engine::{EngineBridge, EngineCore}, + engine::{ + EngineBridge, + EngineCore, + }, persistence::PersistenceConfig, }; -use std::path::PathBuf; mod camera; mod cube; @@ -32,56 +34,102 @@ use session::*; use session_ui::*; fn main() { + // Note: eprintln doesn't work on iOS, but tracing-oslog will once initialized + eprintln!(">>> RUST ENTRY: main() started"); + // Initialize logging - tracing_subscriber::fmt() - .with_env_filter( - tracing_subscriber::EnvFilter::from_default_env() - .add_directive("wgpu=error".parse().unwrap()) - .add_directive("naga=warn".parse().unwrap()), - ) - .init(); + eprintln!(">>> Initializing tracing_subscriber"); + + #[cfg(target_os = "ios")] + { + use tracing_subscriber::prelude::*; + + let filter = tracing_subscriber::EnvFilter::builder() + .with_default_directive(tracing::Level::DEBUG.into()) + .from_env_lossy() + .add_directive("wgpu=error".parse().unwrap()) + .add_directive("naga=warn".parse().unwrap()) + .add_directive("winit=error".parse().unwrap()); + + tracing_subscriber::registry() + .with(filter) + .with(tracing_oslog::OsLogger::new("io.r3t.aspen", "default")) + .init(); + + info!("OSLog initialized successfully"); + } + + #[cfg(not(target_os = "ios"))] + { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive("wgpu=error".parse().unwrap()) + .add_directive("naga=warn".parse().unwrap()), + ) + .init(); + } + + eprintln!(">>> Tracing subscriber initialized"); // Application configuration const APP_NAME: &str = "Aspen"; // Get platform-appropriate database path + eprintln!(">>> Getting database path"); let db_path = libmarathon::platform::get_database_path(APP_NAME); let db_path_str = db_path.to_str().unwrap().to_string(); info!("Database path: {}", db_path_str); + eprintln!(">>> Database path: {}", db_path_str); // Create EngineBridge (for communication between Bevy and EngineCore) + eprintln!(">>> Creating EngineBridge"); let (engine_bridge, engine_handle) = EngineBridge::new(); info!("EngineBridge created"); + eprintln!(">>> EngineBridge created"); // Spawn EngineCore on tokio runtime (runs in background thread) + eprintln!(">>> Spawning EngineCore background thread"); std::thread::spawn(move || { + eprintln!(">>> [EngineCore thread] Thread started"); info!("Starting EngineCore on tokio runtime..."); + eprintln!(">>> [EngineCore thread] Creating tokio runtime"); let rt = tokio::runtime::Runtime::new().unwrap(); + eprintln!(">>> [EngineCore thread] Tokio runtime created"); rt.block_on(async { + eprintln!(">>> [EngineCore thread] Creating EngineCore"); let core = EngineCore::new(engine_handle, &db_path_str); + eprintln!(">>> [EngineCore thread] Running EngineCore"); core.run().await; }); }); info!("EngineCore spawned in background"); + eprintln!(">>> EngineCore thread spawned"); // Create Bevy app (without winit - we own the event loop) + eprintln!(">>> Creating Bevy App"); let mut app = App::new(); + eprintln!(">>> Bevy App created"); // Insert EngineBridge as a resource for Bevy systems to use + eprintln!(">>> Inserting EngineBridge resource"); app.insert_resource(engine_bridge); // Use DefaultPlugins but disable winit/window/input (we own those) + eprintln!(">>> Adding DefaultPlugins"); app.add_plugins( DefaultPlugins .build() - .disable::() // Using tracing-subscriber - .disable::() // We own winit - .disable::() // We own the window - .disable::() // We provide InputEvents directly - .disable::() // We handle gamepad input ourselves + .disable::() // Using tracing-subscriber + .disable::() // We own winit + .disable::() // We own the window + .disable::() // We provide InputEvents directly + .disable::(), // We handle gamepad input ourselves ); + eprintln!(">>> DefaultPlugins added"); // Marathon core plugins (networking, debug UI, persistence) + eprintln!(">>> Adding MarathonPlugin"); app.add_plugins(libmarathon::MarathonPlugin::new( APP_NAME, PersistenceConfig { @@ -91,8 +139,10 @@ fn main() { ..Default::default() }, )); + eprintln!(">>> MarathonPlugin added"); // App-specific bridge for polling engine events + eprintln!(">>> Adding app plugins"); app.add_plugins(EngineBridgePlugin); app.add_plugins(CameraPlugin); app.add_plugins(RenderingPlugin); @@ -102,6 +152,9 @@ fn main() { app.add_plugins(DebugUiPlugin); app.add_plugins(SessionUiPlugin); app.add_systems(Startup, initialize_offline_resources); + eprintln!(">>> All plugins added"); + eprintln!(">>> Running executor"); libmarathon::platform::run_executor(app).expect("Failed to run executor"); + eprintln!(">>> Executor returned (should never reach here)"); } diff --git a/crates/app/src/setup.rs b/crates/app/src/setup.rs index 85240b7..2ca14e8 100644 --- a/crates/app/src/setup.rs +++ b/crates/app/src/setup.rs @@ -285,7 +285,7 @@ fn spawn_bridge_tasks( loop { if let Some(msg) = bridge_out.try_recv_outgoing() { - if let Ok(bytes) = bincode::serialize(&msg) { + if let Ok(bytes) = rkyv::to_bytes::(&msg).map(|b| b.to_vec()) { if let Err(e) = sender.broadcast(Bytes::from(bytes)).await { error!("[Node {}] Broadcast failed: {}", node_id, e); } @@ -303,7 +303,7 @@ fn spawn_bridge_tasks( | Ok(Some(Ok(event))) => { if let iroh_gossip::api::Event::Received(msg) = event { if let Ok(versioned_msg) = - bincode::deserialize::(&msg.content) + rkyv::from_bytes::(&msg.content) { if let Err(e) = bridge_in.push_incoming(versioned_msg) { error!("[Node {}] Push incoming failed: {}", node_id, e); diff --git a/crates/app/tests/cube_sync_headless.rs b/crates/app/tests/cube_sync_headless.rs index 3c03382..52da6f2 100644 --- a/crates/app/tests/cube_sync_headless.rs +++ b/crates/app/tests/cube_sync_headless.rs @@ -114,6 +114,7 @@ mod test_utils { } /// Count entities with CubeMarker component + #[allow(dead_code)] pub fn count_cubes(world: &mut World) -> usize { let mut query = world.query::<&CubeMarker>(); query.iter(world).count() @@ -308,7 +309,7 @@ mod test_utils { "[Node {}] Sending message #{} via gossip", node_id, msg_count ); - match bincode::serialize(&versioned_msg) { + match rkyv::to_bytes::(&versioned_msg).map(|b| b.to_vec()) { | Ok(bytes) => { if let Err(e) = sender.broadcast(Bytes::from(bytes)).await { eprintln!("[Node {}] Failed to broadcast message: {}", node_id, e); @@ -349,7 +350,7 @@ mod test_utils { "[Node {}] Received message #{} from gossip", node_id, msg_count ); - match bincode::deserialize::(&msg.content) { + match rkyv::from_bytes::(&msg.content) { | Ok(versioned_msg) => { if let Err(e) = bridge_in.push_incoming(versioned_msg) { eprintln!( diff --git a/crates/xtask/Cargo.toml b/crates/xtask/Cargo.toml new file mode 100644 index 0000000..fed9608 --- /dev/null +++ b/crates/xtask/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "xtask" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +anyhow = "1.0" +clap = { version = "4.5", features = ["derive"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/crates/xtask/README.md b/crates/xtask/README.md new file mode 100644 index 0000000..8787059 --- /dev/null +++ b/crates/xtask/README.md @@ -0,0 +1,94 @@ +# xtask - Build Automation for Aspen + +This crate provides build automation tasks for the Aspen workspace using the [cargo-xtask pattern](https://github.com/matklad/cargo-xtask). + +## Usage + +All commands are run from the workspace root: + +```bash +# Build for iOS Simulator (release mode) +cargo xtask ios-build + +# Build for iOS Simulator (debug mode) +cargo xtask ios-build --debug + +# Deploy to iOS Simulator +cargo xtask ios-deploy + +# Deploy to a specific device +cargo xtask ios-deploy --device "iPad Air (5th generation)" + +# Build, deploy, and stream logs (most common) +cargo xtask ios-run + +# Build, deploy, and stream logs (debug mode) +cargo xtask ios-run --debug + +# Build, deploy, and stream logs (specific device) +cargo xtask ios-run --device "iPhone 15 Pro" +``` + +## Available Commands + +- **`ios-build`**: Builds the app for iOS Simulator (aarch64-apple-ios-sim) + - `--debug`: Build in debug mode (default is release) + +- **`ios-deploy`**: Deploys the app to iOS Simulator + - `--device`: Device name (default: "iPad Pro 12.9-inch M2") + +- **`ios-run`**: Builds, deploys, and streams logs (all-in-one) + - `--debug`: Build in debug mode + - `--device`: Device name (default: "iPad Pro 12.9-inch M2") + +## How It Works + +The xtask pattern creates a new binary target in your workspace that contains build automation code. This is better than shell scripts because: + +1. **Cross-platform**: Pure Rust, works everywhere Rust works +2. **Type-safe**: Configuration and arguments are type-checked +3. **Maintainable**: Can use the same error handling and libraries as your main code +4. **Fast**: Compiled Rust is much faster than shell scripts +5. **Integrated**: Can share code with your workspace + +## Adding New Tasks + +To add a new task: + +1. Add a new variant to the `Commands` enum in `src/main.rs` +2. Implement the corresponding function +3. Add the function call in the `match` statement in `main()` + +Example: + +```rust +#[derive(Subcommand)] +enum Commands { + // ... existing commands + + /// Run tests on iOS Simulator + IosTest { + /// Device name + #[arg(long, default_value = "iPad Pro 12.9-inch M2")] + device: String, + }, +} + +fn ios_test(device_name: &str) -> Result<()> { + // Implementation here + Ok(()) +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + match cli.command { + // ... existing matches + Commands::IosTest { device } => ios_test(&device), + } +} +``` + +## Dependencies + +- `anyhow`: Error handling +- `clap`: Command-line argument parsing diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs new file mode 100644 index 0000000..2871a77 --- /dev/null +++ b/crates/xtask/src/main.rs @@ -0,0 +1,448 @@ +use anyhow::{Context, Result}; +use clap::{Parser, Subcommand}; +use std::path::PathBuf; +use std::process::Command; +use std::fs; +use tracing::{info, warn, error}; + +#[derive(Parser)] +#[command(name = "xtask")] +#[command(about = "Build automation for Aspen", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Build for iOS Simulator + IosBuild { + /// Build in debug mode instead of release + #[arg(long)] + debug: bool, + }, + /// Deploy to iOS Simulator + IosDeploy { + /// Device name (defaults to "iPad Pro 12.9-inch M2") + #[arg(long, default_value = "iPad Pro 12.9-inch M2")] + device: String, + /// Use debug build instead of release + #[arg(long)] + debug: bool, + }, + /// Build, deploy, and stream logs from iOS Simulator + IosRun { + /// Device name (defaults to "iPad Pro 12.9-inch M2") + #[arg(long, default_value = "iPad Pro 12.9-inch M2")] + device: String, + /// Build in debug mode instead of release + #[arg(long)] + debug: bool, + }, + /// Build, deploy, and stream logs from physical iOS device + IosDevice { + /// Build in debug mode instead of release + #[arg(long)] + debug: bool, + }, +} + +fn main() -> Result<()> { + // Initialize tracing + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive("xtask=info".parse().unwrap()), + ) + .init(); + + let cli = Cli::parse(); + + match cli.command { + Commands::IosBuild { debug } => ios_build(debug), + Commands::IosDeploy { device, debug } => ios_deploy(&device, debug), + Commands::IosRun { device, debug } => ios_run(&device, debug), + Commands::IosDevice { debug } => ios_device(debug), + } +} + +fn project_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .ancestors() + .nth(2) + .expect("Failed to find project root") + .to_path_buf() +} + +fn package_app_bundle(profile: &str, target: &str) -> Result<()> { + let root = project_root(); + let app_name = "Aspen"; + + let binary_path = root.join(format!("target/{}/{}/app", target, profile)); + let bundle_path = root.join(format!("target/{}/{}/{}.app", target, profile, app_name)); + let info_plist_src = root.join("scripts/ios/Info.plist"); + + info!("Packaging app bundle"); + info!(" Binary: {}", binary_path.display()); + info!(" Bundle: {}", bundle_path.display()); + + // Verify binary exists + if !binary_path.exists() { + anyhow::bail!("Binary not found at {}", binary_path.display()); + } + + // Remove old bundle if it exists + if bundle_path.exists() { + fs::remove_dir_all(&bundle_path) + .context("Failed to remove old app bundle")?; + } + + // Create bundle directory + fs::create_dir_all(&bundle_path) + .context("Failed to create app bundle directory")?; + + // Copy binary + let bundle_binary = bundle_path.join("app"); + fs::copy(&binary_path, &bundle_binary) + .context("Failed to copy binary to bundle")?; + + // Set executable permissions + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&bundle_binary)?.permissions(); + perms.set_mode(0o755); + fs::set_permissions(&bundle_binary, perms) + .context("Failed to set executable permissions")?; + } + + // Copy Info.plist + fs::copy(&info_plist_src, bundle_path.join("Info.plist")) + .context("Failed to copy Info.plist")?; + + // Create PkgInfo + fs::write(bundle_path.join("PkgInfo"), b"APPL????") + .context("Failed to create PkgInfo")?; + + info!("App bundle packaged successfully"); + Ok(()) +} + +fn ios_build(debug: bool) -> Result<()> { + let root = project_root(); + let profile = if debug { "debug" } else { "release" }; + + info!("Building for iOS Simulator"); + info!("Project root: {}", root.display()); + + let mut cmd = Command::new("cargo"); + cmd.current_dir(&root) + .arg("build") + .arg("--package") + .arg("app") + .arg("--target") + .arg("aarch64-apple-ios-sim"); + + if !debug { + cmd.arg("--release"); + } + + let status = cmd.status().context("Failed to run cargo build")?; + + if !status.success() { + anyhow::bail!("Build failed"); + } + + // Package the app bundle + package_app_bundle(profile, "aarch64-apple-ios-sim")?; + + info!("Build complete: target/aarch64-apple-ios-sim/{}/Aspen.app", profile); + Ok(()) +} + +fn ios_deploy(device_name: &str, debug: bool) -> Result<()> { + let root = project_root(); + let profile = if debug { "debug" } else { "release" }; + let bundle_path = root.join(format!("target/aarch64-apple-ios-sim/{}/Aspen.app", profile)); + + info!("Deploying to iOS Simulator"); + info!("Device: {}", device_name); + info!("Bundle: {}", bundle_path.display()); + + // Find device UUID + info!("Finding device UUID"); + let output = Command::new("xcrun") + .args(["simctl", "list", "devices", "available"]) + .output() + .context("Failed to list devices")?; + + let devices_list = String::from_utf8_lossy(&output.stdout); + let device_uuid = devices_list + .lines() + .find(|line| line.contains(device_name)) + .and_then(|line| { + line.split('(') + .nth(1)? + .split(')') + .next() + }) + .context("Device not found")?; + + info!("Found device: {}", device_uuid); + + // Boot simulator + info!("Booting simulator"); + let boot_result = Command::new("xcrun") + .args(["simctl", "boot", device_uuid]) + .status(); + + if let Ok(status) = boot_result { + if !status.success() { + warn!("Simulator boot returned non-zero (device may already be booted)"); + } + } + + // Open Simulator.app + info!("Opening Simulator.app"); + Command::new("open") + .args(["-a", "Simulator"]) + .status() + .context("Failed to open Simulator")?; + + // Uninstall old version + info!("Uninstalling old version"); + let uninstall_result = Command::new("xcrun") + .args(["simctl", "uninstall", device_uuid, "G872CZV7WG.aspen"]) + .status(); + + if let Ok(status) = uninstall_result { + if !status.success() { + warn!("Uninstall returned non-zero (app may not have been installed)"); + } + } + + // Install app + info!("Installing app"); + let status = Command::new("xcrun") + .args(["simctl", "install", device_uuid]) + .arg(&bundle_path) + .status() + .context("Failed to install app")?; + + if !status.success() { + anyhow::bail!("Installation failed"); + } + + // Launch app + info!("Launching app"); + let output = Command::new("xcrun") + .args(["simctl", "launch", device_uuid, "G872CZV7WG.aspen"]) + .output() + .context("Failed to launch app")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + error!("Launch failed: {}", stderr); + anyhow::bail!("Launch failed"); + } + + info!("App launched successfully"); + Ok(()) +} + +fn ios_run(device_name: &str, debug: bool) -> Result<()> { + // Build + ios_build(debug)?; + + // Deploy + ios_deploy(device_name, debug)?; + + // Stream logs + info!("Streaming logs (Ctrl+C to stop)"); + + // Find device UUID again + let output = Command::new("xcrun") + .args(["simctl", "list", "devices", "available"]) + .output() + .context("Failed to list devices")?; + + let devices_list = String::from_utf8_lossy(&output.stdout); + let device_uuid = devices_list + .lines() + .find(|line| line.contains(device_name)) + .and_then(|line| { + line.split('(') + .nth(1)? + .split(')') + .next() + }) + .context("Device not found")?; + + // Stream logs using simctl spawn + // Note: Process name is "app" (CFBundleExecutable), not "Aspen" (display name) + let mut log_cmd = Command::new("xcrun"); + log_cmd.args([ + "simctl", + "spawn", + device_uuid, + "log", + "stream", + "--predicate", + "process == \"app\"", + "--style", + "compact", + ]); + + // When debug flag is set, show all log levels (info, debug, default, error, fault) + if debug { + log_cmd.args(["--info", "--debug"]); + info!("Streaming all log levels (info, debug, default, error, fault)"); + } else { + info!("Streaming default log levels (default, error, fault)"); + } + + let status = log_cmd.status().context("Failed to start log stream")?; + + // Log streaming can exit for legitimate reasons: + // - User quit the simulator + // - App was killed + // - User hit Ctrl+C + // Don't treat these as errors + match status.code() { + Some(0) => info!("Log streaming ended normally"), + Some(code) => info!("Log streaming ended with code {}", code), + None => info!("Log streaming ended (killed by signal)"), + } + + Ok(()) +} + +fn ios_device(debug: bool) -> Result<()> { + let root = project_root(); + let profile = if debug { "debug" } else { "release" }; + let target = "aarch64-apple-ios"; + + info!("Building for physical iOS device"); + info!("Project root: {}", root.display()); + + // Build + let mut cmd = Command::new("cargo"); + cmd.current_dir(&root) + .arg("build") + .arg("--package") + .arg("app") + .arg("--target") + .arg(target); + + if !debug { + cmd.arg("--release"); + } + + let status = cmd.status().context("Failed to run cargo build")?; + + if !status.success() { + anyhow::bail!("Build failed"); + } + + // Package the app bundle + package_app_bundle(profile, target)?; + + let bundle_path = root.join(format!("target/{}/{}/Aspen.app", target, profile)); + info!("Build complete: {}", bundle_path.display()); + + // Code sign with Apple Development certificate + info!("Signing app with Apple Development certificate"); + let status = Command::new("codesign") + .args(["--force", "--sign", "Apple Development: sienna@r3t.io (6A6PF29R8A)"]) + .arg(&bundle_path) + .status() + .context("Failed to code sign app")?; + + if !status.success() { + anyhow::bail!("Code signing failed. Make sure the certificate is trusted in Keychain Access."); + } + + info!("App signed successfully"); + + // Get connected device + info!("Finding connected device"); + let output = Command::new("xcrun") + .args(["devicectl", "list", "devices"]) + .output() + .context("Failed to list devices")?; + + let devices_list = String::from_utf8_lossy(&output.stdout); + let device_id = devices_list + .lines() + .find(|line| (line.contains("connected") || line.contains("available")) && line.contains("iPad")) + .and_then(|line| { + // Extract device ID from the line + // Format: "Name domain.coredevice.local ID connected/available Model" + line.split_whitespace().nth(2) + }) + .context("No connected iPad found. Make sure your iPad is connected and trusted.")?; + + info!("Found device: {}", device_id); + + // Install app + info!("Installing app to device"); + let status = Command::new("xcrun") + .args(["devicectl", "device", "install", "app"]) + .arg("--device") + .arg(device_id) + .arg(&bundle_path) + .status() + .context("Failed to install app")?; + + if !status.success() { + anyhow::bail!("Installation failed. Make sure Developer Mode is enabled on your iPad."); + } + + info!("App installed successfully"); + + // Launch app + info!("Launching app"); + let status = Command::new("xcrun") + .args(["devicectl", "device", "process", "launch"]) + .arg("--device") + .arg(device_id) + .arg("G872CZV7WG.aspen") + .status() + .context("Failed to launch app")?; + + if !status.success() { + anyhow::bail!("Launch failed"); + } + + info!("App launched successfully"); + + // Stream logs + info!("Streaming device logs (Ctrl+C to stop)"); + info!("Note: Device logs may be delayed. Use Console.app for real-time logs."); + + let mut log_cmd = Command::new("xcrun"); + log_cmd.args([ + "devicectl", + "device", + "stream", + "log", + "--device", + device_id, + "--predicate", + "process == \"app\"", + ]); + + if debug { + info!("Streaming all log levels"); + } + + let status = log_cmd.status().context("Failed to start log stream")?; + + match status.code() { + Some(0) => info!("Log streaming ended normally"), + Some(code) => info!("Log streaming ended with code {}", code), + None => info!("Log streaming ended (killed by signal)"), + } + + Ok(()) +} diff --git a/docs/ESTIMATION.md b/docs/ESTIMATION.md new file mode 100644 index 0000000..ed7f2d9 --- /dev/null +++ b/docs/ESTIMATION.md @@ -0,0 +1,159 @@ +# Estimation and Prioritization + +This document defines how r3t Studios sizes, estimates, and prioritizes work. Our approach is grounded in **Lean Software Development** principles, adapted for indie game development. + +--- + +## Lean Principles as Evaluation Questions + +Every sizing and prioritization decision should be evaluated through these questions: + +| Principle | Evaluation Questions | +|-----------|---------------------| +| **Eliminate Waste** | Does this reduce waste? Is this the simplest solution? Are we building something nobody needs? | +| **Amplify Learning** | What will we learn from this? Does this reduce uncertainty? Should we prototype first? | +| **Decide as Late as Possible** | Do we have enough information to commit? Can this decision be deferred responsibly? | +| **Deliver as Fast as Possible** | What's the smallest increment that delivers value? What's blocking flow? | +| **Empower the Team** | Do I have what I need to do this? Who else should be involved in this decision? | +| **Build Integrity In** | Are we building quality in from the start? Will this create technical debt? | +| **Optimize the Whole** | Does this improve the whole system or just one part? Are we sub-optimizing? | + +--- + +## Sizing + +We use **powers of 2** for effort estimation: `1, 2, 4, 8, 16, 32` + +This scale is intentionally coarse. Precision in estimation is waste—we optimize for fast, good-enough decisions. + +| Size | Points | Description | +|------|--------|-------------| +| **XS** | 1 | Trivial change. A few lines, minimal risk. Less than 2 hours. | +| **S** | 2 | Small task. Well-understood, limited scope. Half a day. | +| **M** | 4 | Medium task. Some complexity or unknowns. About 1 day. | +| **L** | 8 | Large task. Multiple components or significant complexity. 2-3 days. | +| **XL** | 16 | Very large. Should probably be an epic. ~1 week. | +| **XXL** | 32 | Epic-scale work. Multi-phase refactoring or major features. 2-4 weeks. | + +### Sizing Guidelines + +**Before assigning a size, ask:** + +- *Eliminate Waste*: Is this the smallest scope that delivers value? +- *Deliver Fast*: Can we break this down further? +- *Amplify Learning*: Is there uncertainty that inflates the estimate? Should we prototype/spike first? +- *Optimize the Whole*: Does the estimate account for integration, testing, and platform testing (iOS + macOS)? + +**If the size is XL (16) or larger:** + +This is epic territory. Break it into phases or sub-tasks. An XL/XXL indicates the work is not well-understood or needs careful sequencing. Consider writing an RFC for XXL work. + +--- + +## Priority + +Priority is based on **Cost of Delay**—the impact of not doing this work now. + +| Label | Priority | Cost of Delay Profile | Description | +|-------|----------|----------------------|-------------| +| **P0** | Critical | Expedite | Game-breaking bug, crash on launch, data loss, or blocks release. Drop everything. Every hour matters. | +| **P1** | High | Fixed Date / High Value | Release blocker, community-visible bug, or deadline-driven (demo, event, content creator access). Do this sprint. | +| **P2** | Medium | Standard | Normal feature work, engine improvements, content additions. Standard backlog work—prioritize by WSJF. | +| **P3** | Low | Intangible | Nice-to-have, polish, research, experiments. Do when capacity allows or inspiration strikes. | + +### Priority Guidelines + +**Before assigning a priority, ask:** + +- *Eliminate Waste*: Should we do this at all? What happens if we never do it? +- *Amplify Learning*: Does doing this first reduce uncertainty for other work? +- *Decide Late*: Do we need to commit now, or can we learn more first? (Especially important for game design decisions) +- *Optimize the Whole*: Does this unblock other high-value work? Does it improve both engine (Marathon) and game (Aspen)? + +--- + +## WSJF (Weighted Shortest Job First) + +For backlog ordering within a priority tier, we use WSJF: + +``` +WSJF = Cost of Delay / Job Size +``` + +Where **Cost of Delay** considers: + +- **Player Value**: Does this improve player experience, immersion, or enjoyment? +- **Time Criticality**: Content creator previews, demo deadlines, platform submission windows +- **Risk Reduction**: Tech debt payoff, engine stability, enables future content/features + +Higher WSJF = do it first. + +### Example (Game Dev Context) + +| Item | Player Value | Time Criticality | Risk Reduction | CoD | Size | WSJF | +|------|--------------|------------------|----------------|-----|------|------| +| Spatial Audio | 8 | 6 | 4 | 18 | 8 | 2.25 | +| Agent AI Polish | 6 | 2 | 2 | 10 | 16 | 0.63 | +| Fix iOS Crash | 10 | 10 | 8 | 28 | 4 | 7.0 | +| Low-Poly Tree Assets | 4 | 2 | 1 | 7 | 2 | 3.5 | + +**Order: iOS Crash (7.0), Tree Assets (3.5), Spatial Audio (2.25), AI Polish (0.63)** + +The crash gets fixed first (high value, small effort), then quick content wins, then larger features. + +--- + +## Area Labels and Work Types + +Our `area/` labels map to different types of work: + +| Area | Typical Work | Considerations | +|------|--------------|----------------| +| **area/core** | Engine foundation | High risk—test thoroughly, affects everything | +| **area/rendering** | Graphics, PBR, cameras | Test on iOS + macOS, check DPI scaling | +| **area/audio** | Spatial audio, sound | Requires device testing, earbuds vs speakers | +| **area/networking** | P2P, CRDT sync | Test with poor connections, edge cases | +| **area/platform** | iOS/macOS specific | Platform-specific testing required | +| **area/simulation** | Agent behaviors, AI | Needs playtesting, emergent behavior testing | +| **area/content** | Art, audio assets, dialogue | Artistic review, asset pipeline testing | +| **area/ui-ux** | Menus, HUD, accessibility | Needs user testing, various screen sizes | +| **area/tooling** | Build, CI/CD, dev tools | Verify on fresh checkout | +| **area/docs** | Technical specs, RFCs | Keep updated as code evolves | +| **area/infrastructure** | Deployment, hosting | Test in staging before production | +| **area/rfc** | Design proposals | Gather feedback before committing | + +--- + +## Quick Reference + +### When Sizing + +1. Compare to recent similar work +2. Use the Lean questions to validate scope +3. If XL or larger, consider making it an epic with sub-tasks +4. Bias toward smaller—deliver fast, learn, iterate +5. Remember: estimates are for ordering work, not commitments + +### When Prioritizing + +1. Is it game-breaking (crash/data loss)? If yes, **P0**—do it now +2. Is there a deadline (demo/release)? If yes, **P1**—this sprint +3. Otherwise, score WSJF and order the backlog as **P2** +4. Nice-to-haves and experiments go to **P3** +5. Re-evaluate priorities weekly—inspiration and context change + +### For Solo Work + +- **Batching**: Group similar work (all rendering tasks, all iOS fixes) to minimize context switching +- **Flow**: Protect deep work time—don't interrupt XL tasks with small P3 items +- **Momentum**: Sometimes doing a quick XS/S task builds momentum for harder work +- **Inspiration**: If you're excited about a P3 item, that's valuable—creative energy matters in game dev + +--- + +## References + +- Poppendieck, Mary & Tom. *Lean Software Development: An Agile Toolkit*. Addison-Wesley, 2003. +- Reinertsen, Donald G. *The Principles of Product Development Flow*. Celeritas Publishing, 2009. +- [Cost of Delay - Black Swan Farming](https://blackswanfarming.com/cost-of-delay/) +- [WSJF - Scaled Agile Framework](https://framework.scaledagile.com/wsjf/) diff --git a/scripts/ios/Entitlements.plist b/scripts/ios/Entitlements.plist new file mode 100644 index 0000000..9d77b4c --- /dev/null +++ b/scripts/ios/Entitlements.plist @@ -0,0 +1,14 @@ + + + + + application-identifier + G872CZV7WG.G872CZV7WG.aspen + get-task-allow + + keychain-access-groups + + G872CZV7WG.* + + + diff --git a/scripts/ios/Info.plist b/scripts/ios/Info.plist index c12717c..eece5a6 100644 --- a/scripts/ios/Info.plist +++ b/scripts/ios/Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable app CFBundleIdentifier - io.r3t.aspen + G872CZV7WG.aspen CFBundleInfoDictionaryVersion 6.0 CFBundleName diff --git a/scripts/ios/deploy-simulator.sh b/scripts/ios/deploy-simulator.sh index 44882f6..15a6826 100755 --- a/scripts/ios/deploy-simulator.sh +++ b/scripts/ios/deploy-simulator.sh @@ -8,7 +8,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" APP_NAME="Aspen" -BUNDLE_ID="io.r3t.aspen" +BUNDLE_ID="G872CZV7WG.aspen" TARGET="aarch64-apple-ios-sim" BUILD_MODE="release"