@bornengine/engine 0.4.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +231 -0
- package/native/android/Cargo.lock +1848 -0
- package/native/android/Cargo.toml +24 -0
- package/native/android/src/lib.rs +702 -0
- package/native/ios/Cargo.lock +1690 -0
- package/native/ios/Cargo.toml +32 -0
- package/native/ios/src/lib.rs +1267 -0
- package/native/linux/Cargo.lock +3279 -0
- package/native/linux/Cargo.toml +29 -0
- package/native/linux/src/lib.rs +1331 -0
- package/native/macos/Cargo.lock +3310 -0
- package/native/macos/Cargo.toml +46 -0
- package/native/macos/src/lib.rs +1302 -0
- package/native/shared/Cargo.lock +1899 -0
- package/native/shared/Cargo.toml +62 -0
- package/native/shared/assets/default_font.ttf +0 -0
- package/native/shared/build.rs +270 -0
- package/native/shared/shaders/common/clouds.wgsl +122 -0
- package/native/shared/shaders/common/fog.wgsl +16 -0
- package/native/shared/shaders/common/foliage_wind.wgsl +98 -0
- package/native/shared/shaders/common/imposter.wgsl +112 -0
- package/native/shared/shaders/common/pbr.wgsl +186 -0
- package/native/shared/shaders/common/shadows.wgsl +186 -0
- package/native/shared/shaders/common/sky.wgsl +8 -0
- package/native/shared/shaders/common/tonemap.wgsl +25 -0
- package/native/shared/shaders/impulse_field.wgsl +57 -0
- package/native/shared/shaders/material_abi.wgsl +383 -0
- package/native/shared/shaders/materials/test_minimal.wgsl +42 -0
- package/native/shared/src/anim_mixer.rs +61 -0
- package/native/shared/src/attach.rs +263 -0
- package/native/shared/src/audio/decode.rs +123 -0
- package/native/shared/src/audio/mod.rs +863 -0
- package/native/shared/src/audio/render.rs +892 -0
- package/native/shared/src/audio/spsc.rs +156 -0
- package/native/shared/src/audio/stream.rs +226 -0
- package/native/shared/src/custom_shaders.rs +104 -0
- package/native/shared/src/decals.rs +245 -0
- package/native/shared/src/drs.rs +211 -0
- package/native/shared/src/engine.rs +261 -0
- package/native/shared/src/ffi.rs +116 -0
- package/native/shared/src/ffi_core/assets.rs +388 -0
- package/native/shared/src/ffi_core/audio_ffi.rs +184 -0
- package/native/shared/src/ffi_core/draw.rs +334 -0
- package/native/shared/src/ffi_core/game_loop.rs +577 -0
- package/native/shared/src/ffi_core/input.rs +234 -0
- package/native/shared/src/ffi_core/mod.rs +127 -0
- package/native/shared/src/ffi_core/models.rs +1154 -0
- package/native/shared/src/ffi_core/ragdoll_ffi.rs +261 -0
- package/native/shared/src/ffi_core/scene.rs +626 -0
- package/native/shared/src/ffi_core/vfx.rs +212 -0
- package/native/shared/src/ffi_core/visual.rs +691 -0
- package/native/shared/src/frame_callbacks.rs +122 -0
- package/native/shared/src/geometry.rs +236 -0
- package/native/shared/src/handles.rs +182 -0
- package/native/shared/src/input.rs +448 -0
- package/native/shared/src/jolt_sys.rs +822 -0
- package/native/shared/src/lib.rs +55 -0
- package/native/shared/src/models.rs +1093 -0
- package/native/shared/src/models_gltf.rs +1280 -0
- package/native/shared/src/particles.rs +391 -0
- package/native/shared/src/physics_jolt.rs +1908 -0
- package/native/shared/src/picking.rs +298 -0
- package/native/shared/src/postfx.rs +345 -0
- package/native/shared/src/profiler.rs +492 -0
- package/native/shared/src/ragdoll.rs +474 -0
- package/native/shared/src/renderer/atmosphere_lut.rs +573 -0
- package/native/shared/src/renderer/brdf_lut.rs +154 -0
- package/native/shared/src/renderer/draw2d.rs +143 -0
- package/native/shared/src/renderer/formats.rs +822 -0
- package/native/shared/src/renderer/froxel.rs +421 -0
- package/native/shared/src/renderer/gi_bake.rs +653 -0
- package/native/shared/src/renderer/graph.rs +462 -0
- package/native/shared/src/renderer/hiz.rs +269 -0
- package/native/shared/src/renderer/hot_reload.rs +390 -0
- package/native/shared/src/renderer/impulse_field.rs +456 -0
- package/native/shared/src/renderer/lighting.rs +154 -0
- package/native/shared/src/renderer/material_instancing.rs +171 -0
- package/native/shared/src/renderer/material_pipeline.rs +700 -0
- package/native/shared/src/renderer/material_system.rs +1996 -0
- package/native/shared/src/renderer/material_system_tests.rs +601 -0
- package/native/shared/src/renderer/material_system_wasm.rs +41 -0
- package/native/shared/src/renderer/mod.rs +12556 -0
- package/native/shared/src/renderer/model_draw.rs +641 -0
- package/native/shared/src/renderer/occlusion.rs +429 -0
- package/native/shared/src/renderer/planar_pass.rs +593 -0
- package/native/shared/src/renderer/planar_reflection.rs +499 -0
- package/native/shared/src/renderer/post_pass.rs +249 -0
- package/native/shared/src/renderer/postfx_chain.rs +728 -0
- package/native/shared/src/renderer/pt_pass.rs +577 -0
- package/native/shared/src/renderer/scene_pass.rs +607 -0
- package/native/shared/src/renderer/shader_include.rs +205 -0
- package/native/shared/src/renderer/shader_library.rs +135 -0
- package/native/shared/src/renderer/shaders/ao.rs +570 -0
- package/native/shared/src/renderer/shaders/core.rs +1243 -0
- package/native/shared/src/renderer/shaders/env.rs +907 -0
- package/native/shared/src/renderer/shaders/gi.rs +810 -0
- package/native/shared/src/renderer/shaders/mod.rs +19 -0
- package/native/shared/src/renderer/shaders/post.rs +1558 -0
- package/native/shared/src/renderer/shaders/pt.rs +1859 -0
- package/native/shared/src/renderer/shaders/ssgi.rs +1586 -0
- package/native/shared/src/renderer/shadow_pass.rs +731 -0
- package/native/shared/src/renderer/ssgi_pass.rs +392 -0
- package/native/shared/src/renderer/ssr_pass.rs +188 -0
- package/native/shared/src/renderer/texture_store.rs +473 -0
- package/native/shared/src/renderer/transient.rs +591 -0
- package/native/shared/src/renderer/types.rs +941 -0
- package/native/shared/src/renderer/util.rs +152 -0
- package/native/shared/src/scene.rs +1362 -0
- package/native/shared/src/sdf_cache.rs +274 -0
- package/native/shared/src/shadows.rs +1036 -0
- package/native/shared/src/staging.rs +102 -0
- package/native/shared/src/string_header.rs +266 -0
- package/native/shared/src/text_renderer.rs +502 -0
- package/native/shared/src/textures.rs +197 -0
- package/native/tvos/Cargo.lock +1693 -0
- package/native/tvos/Cargo.toml +36 -0
- package/native/tvos/metal-patched/Cargo.toml +178 -0
- package/native/tvos/metal-patched/LICENSE-APACHE +201 -0
- package/native/tvos/metal-patched/LICENSE-MIT +25 -0
- package/native/tvos/metal-patched/src/acceleration_structure.rs +667 -0
- package/native/tvos/metal-patched/src/acceleration_structure_pass.rs +108 -0
- package/native/tvos/metal-patched/src/argument.rs +366 -0
- package/native/tvos/metal-patched/src/blitpass.rs +102 -0
- package/native/tvos/metal-patched/src/buffer.rs +71 -0
- package/native/tvos/metal-patched/src/capturedescriptor.rs +76 -0
- package/native/tvos/metal-patched/src/capturemanager.rs +113 -0
- package/native/tvos/metal-patched/src/commandbuffer.rs +192 -0
- package/native/tvos/metal-patched/src/commandqueue.rs +44 -0
- package/native/tvos/metal-patched/src/computepass.rs +107 -0
- package/native/tvos/metal-patched/src/constants.rs +152 -0
- package/native/tvos/metal-patched/src/counters.rs +119 -0
- package/native/tvos/metal-patched/src/depthstencil.rs +190 -0
- package/native/tvos/metal-patched/src/device.rs +2134 -0
- package/native/tvos/metal-patched/src/drawable.rs +39 -0
- package/native/tvos/metal-patched/src/encoder.rs +2041 -0
- package/native/tvos/metal-patched/src/heap.rs +281 -0
- package/native/tvos/metal-patched/src/indirect_encoder.rs +344 -0
- package/native/tvos/metal-patched/src/lib.rs +657 -0
- package/native/tvos/metal-patched/src/library.rs +902 -0
- package/native/tvos/metal-patched/src/mps.rs +575 -0
- package/native/tvos/metal-patched/src/pipeline/compute.rs +475 -0
- package/native/tvos/metal-patched/src/pipeline/mod.rs +71 -0
- package/native/tvos/metal-patched/src/pipeline/render.rs +762 -0
- package/native/tvos/metal-patched/src/renderpass.rs +443 -0
- package/native/tvos/metal-patched/src/resource.rs +182 -0
- package/native/tvos/metal-patched/src/sampler.rs +165 -0
- package/native/tvos/metal-patched/src/sync.rs +178 -0
- package/native/tvos/metal-patched/src/texture.rs +352 -0
- package/native/tvos/metal-patched/src/types.rs +90 -0
- package/native/tvos/metal-patched/src/vertexdescriptor.rs +250 -0
- package/native/tvos/src/audio_backend.rs +197 -0
- package/native/tvos/src/lib.rs +1891 -0
- package/native/visionos/Cargo.lock +1693 -0
- package/native/visionos/Cargo.toml +40 -0
- package/native/visionos/src/audio_backend.rs +197 -0
- package/native/visionos/src/lib.rs +1887 -0
- package/native/watchos/Cargo.lock +16 -0
- package/native/watchos/Cargo.toml +19 -0
- package/native/watchos/shaders/bloom_postfx.metal +99 -0
- package/native/watchos/src/BloomWatchApp.swift +1267 -0
- package/native/watchos/src/BloomWatchAudio.swift +179 -0
- package/native/watchos/src/audio.rs +55 -0
- package/native/watchos/src/draw_list.rs +229 -0
- package/native/watchos/src/ffi_stubs.rs +915 -0
- package/native/watchos/src/ffi_stubs_manual.rs +35 -0
- package/native/watchos/src/lib.rs +1124 -0
- package/native/watchos/src/models.rs +746 -0
- package/native/watchos/src/postfx.rs +95 -0
- package/native/watchos/src/scene.rs +534 -0
- package/native/watchos/src/textures.rs +184 -0
- package/native/web/Cargo.lock +1657 -0
- package/native/web/Cargo.toml +43 -0
- package/native/web/bloom_glue.js +695 -0
- package/native/web/build.sh +131 -0
- package/native/web/index.html +35 -0
- package/native/web/jolt_bridge.js +1519 -0
- package/native/web/src/input_ffi.rs +286 -0
- package/native/web/src/lib.rs +1796 -0
- package/native/web/src/material_ffi.rs +710 -0
- package/native/web/src/parity_ffi.rs +343 -0
- package/native/web/src/physics_ffi.rs +643 -0
- package/native/web/src/ragdoll_ffi.rs +250 -0
- package/native/web/src/render_settings.rs +98 -0
- package/native/windows/Cargo.lock +1815 -0
- package/native/windows/Cargo.toml +68 -0
- package/native/windows/src/lib.rs +1486 -0
- package/package.json +4279 -0
- package/src/audio/index.ts +315 -0
- package/src/core/colors.ts +63 -0
- package/src/core/index.ts +1206 -0
- package/src/core/keys.ts +63 -0
- package/src/core/types.ts +104 -0
- package/src/index.ts +171 -0
- package/src/math/index.ts +516 -0
- package/src/mobile/index.ts +294 -0
- package/src/models/index.ts +1258 -0
- package/src/physics/index.ts +1134 -0
- package/src/scene/index.ts +698 -0
- package/src/shapes/index.ts +120 -0
- package/src/text/index.ts +48 -0
- package/src/textures/index.ts +187 -0
- package/src/vfx/index.ts +191 -0
- package/src/world/index.ts +24 -0
- package/src/world/loader.ts +423 -0
- package/src/world/prefab.ts +217 -0
- package/src/world/render.ts +172 -0
- package/src/world/saver.ts +108 -0
- package/src/world/serialize.ts +301 -0
- package/src/world/terrain.ts +355 -0
- package/src/world/types.ts +160 -0
- package/src/world/validate.ts +319 -0
- package/src/world/version.ts +114 -0
|
@@ -0,0 +1,1267 @@
|
|
|
1
|
+
// `static mut` is intentional throughout this FFI surface — Perry calls
|
|
2
|
+
// us from a single OS thread (the UIKit main run-loop), so the engine
|
|
3
|
+
// singleton + view/window scratch state never race. The 2024 lint
|
|
4
|
+
// flagging `&UI_VIEW`-style accesses is a real concern in
|
|
5
|
+
// multi-threaded code, but inapplicable here. Suppress at the crate
|
|
6
|
+
// root to avoid a dozen noise lines in every build. Mirrors
|
|
7
|
+
// native/macos/src/lib.rs.
|
|
8
|
+
#![allow(static_mut_refs)]
|
|
9
|
+
|
|
10
|
+
use bloom_shared::engine::EngineState;
|
|
11
|
+
use bloom_shared::renderer::Renderer;
|
|
12
|
+
use bloom_shared::string_header::str_from_header;
|
|
13
|
+
|
|
14
|
+
use objc2::encode::{Encode, Encoding, RefEncode};
|
|
15
|
+
use objc2::rc::{Allocated, Retained};
|
|
16
|
+
use objc2::runtime::{AnyClass, AnyObject, Bool, Sel};
|
|
17
|
+
use objc2::{msg_send, sel};
|
|
18
|
+
|
|
19
|
+
use raw_window_handle::{RawDisplayHandle, RawWindowHandle, UiKitDisplayHandle, UiKitWindowHandle};
|
|
20
|
+
|
|
21
|
+
use std::ffi::c_void;
|
|
22
|
+
use std::sync::OnceLock;
|
|
23
|
+
|
|
24
|
+
static mut ENGINE: OnceLock<EngineState> = OnceLock::new();
|
|
25
|
+
static mut UI_WINDOW: Option<Retained<AnyObject>> = None;
|
|
26
|
+
static mut UI_VIEW: Option<Retained<AnyObject>> = None;
|
|
27
|
+
static mut TOUCH_MAP: [*const c_void; 10] = [std::ptr::null(); 10];
|
|
28
|
+
static mut BUNDLE_PATH: Option<String> = None;
|
|
29
|
+
static mut SCREEN_SCALE: f64 = 1.0;
|
|
30
|
+
/// UIInterfaceOrientationMask for the root view controller.
|
|
31
|
+
/// Default: all (0x1E). Set to landscape (0x18) from bloom_init_window when width > height.
|
|
32
|
+
static mut ORIENTATION_MASK: u64 = 0x1E; // UIInterfaceOrientationMaskAll
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
/// Resolve a relative asset path to the app bundle path.
|
|
36
|
+
fn resolve_path(path: &str) -> String {
|
|
37
|
+
if path.starts_with('/') {
|
|
38
|
+
return path.to_string();
|
|
39
|
+
}
|
|
40
|
+
unsafe {
|
|
41
|
+
if let Some(ref base) = BUNDLE_PATH {
|
|
42
|
+
format!("{}/{}", base, path)
|
|
43
|
+
} else {
|
|
44
|
+
path.to_string()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fn engine() -> &'static mut EngineState {
|
|
50
|
+
unsafe { ENGINE.get_mut().expect("Engine not initialized") }
|
|
51
|
+
}
|
|
52
|
+
/// Asset-path hook for define_core_ffi! — routes through this platform's
|
|
53
|
+
/// resolve_path (relative paths don't resolve from the app working dir here).
|
|
54
|
+
fn bloom_resolve_asset_path(path: &str) -> std::borrow::Cow<'_, str> {
|
|
55
|
+
std::borrow::Cow::Owned(resolve_path(path))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The full shared (non-physics) FFI surface. See bloom_shared::ffi_core
|
|
59
|
+
// docs for the contract; tools/validate-ffi.js checks parity in CI.
|
|
60
|
+
bloom_shared::define_core_ffi!();
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
// ============================================================
|
|
64
|
+
// CG types with objc2 Encode
|
|
65
|
+
// ============================================================
|
|
66
|
+
|
|
67
|
+
#[repr(C)]
|
|
68
|
+
#[derive(Copy, Clone)]
|
|
69
|
+
struct CGPoint { x: f64, y: f64 }
|
|
70
|
+
|
|
71
|
+
unsafe impl Encode for CGPoint {
|
|
72
|
+
const ENCODING: Encoding = Encoding::Struct("CGPoint", &[Encoding::Double, Encoding::Double]);
|
|
73
|
+
}
|
|
74
|
+
unsafe impl RefEncode for CGPoint {
|
|
75
|
+
const ENCODING_REF: Encoding = Encoding::Pointer(&Self::ENCODING);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#[repr(C)]
|
|
79
|
+
#[derive(Copy, Clone)]
|
|
80
|
+
struct CGSize { width: f64, height: f64 }
|
|
81
|
+
|
|
82
|
+
unsafe impl Encode for CGSize {
|
|
83
|
+
const ENCODING: Encoding = Encoding::Struct("CGSize", &[Encoding::Double, Encoding::Double]);
|
|
84
|
+
}
|
|
85
|
+
unsafe impl RefEncode for CGSize {
|
|
86
|
+
const ENCODING_REF: Encoding = Encoding::Pointer(&Self::ENCODING);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#[repr(C)]
|
|
90
|
+
#[derive(Copy, Clone)]
|
|
91
|
+
struct CGRect { origin: CGPoint, size: CGSize }
|
|
92
|
+
|
|
93
|
+
unsafe impl Encode for CGRect {
|
|
94
|
+
const ENCODING: Encoding = Encoding::Struct("CGRect", &[CGPoint::ENCODING, CGSize::ENCODING]);
|
|
95
|
+
}
|
|
96
|
+
unsafe impl RefEncode for CGRect {
|
|
97
|
+
const ENCODING_REF: Encoding = Encoding::Pointer(&Self::ENCODING);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ============================================================
|
|
101
|
+
// ObjC runtime
|
|
102
|
+
// ============================================================
|
|
103
|
+
|
|
104
|
+
extern "C" {
|
|
105
|
+
fn objc_allocateClassPair(superclass: *const AnyClass, name: *const u8, extra_bytes: usize) -> *mut AnyClass;
|
|
106
|
+
fn objc_registerClassPair(cls: *mut AnyClass);
|
|
107
|
+
fn class_addMethod(cls: *mut AnyClass, sel: Sel, imp: *const c_void, types: *const u8) -> bool;
|
|
108
|
+
|
|
109
|
+
fn CFRunLoopRunInMode(mode: *const c_void, seconds: f64, return_after: u8) -> i32;
|
|
110
|
+
static kCFRunLoopDefaultMode: *const c_void;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fn pump_run_loop(seconds: f64) {
|
|
114
|
+
unsafe { CFRunLoopRunInMode(kCFRunLoopDefaultMode, seconds, 0); }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ============================================================
|
|
118
|
+
// Register BloomMetalView — UIView subclass with +layerClass = CAMetalLayer
|
|
119
|
+
// ============================================================
|
|
120
|
+
|
|
121
|
+
unsafe extern "C" fn bloom_layer_class(_cls: *const c_void, _sel: Sel) -> *const c_void {
|
|
122
|
+
AnyClass::get(c"CAMetalLayer").unwrap() as *const AnyClass as *const c_void
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
unsafe extern "C" fn bloom_touches_began(_this: *mut c_void, _sel: Sel, touches: *const AnyObject, _event: *const AnyObject) {
|
|
126
|
+
handle_touches(touches, TouchPhase::Began);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
unsafe extern "C" fn bloom_touches_moved(_this: *mut c_void, _sel: Sel, touches: *const AnyObject, _event: *const AnyObject) {
|
|
130
|
+
handle_touches(touches, TouchPhase::Moved);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
unsafe extern "C" fn bloom_touches_ended(_this: *mut c_void, _sel: Sel, touches: *const AnyObject, _event: *const AnyObject) {
|
|
134
|
+
handle_touches(touches, TouchPhase::Ended);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
unsafe extern "C" fn bloom_touches_cancelled(_this: *mut c_void, _sel: Sel, touches: *const AnyObject, _event: *const AnyObject) {
|
|
138
|
+
handle_touches(touches, TouchPhase::Ended);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
enum TouchPhase { Began, Moved, Ended }
|
|
142
|
+
|
|
143
|
+
unsafe fn handle_touches(touches: *const AnyObject, phase: TouchPhase) {
|
|
144
|
+
if touches.is_null() { return; }
|
|
145
|
+
|
|
146
|
+
let view_ptr: *const AnyObject = match UI_VIEW.as_ref() {
|
|
147
|
+
Some(v) => Retained::as_ptr(v),
|
|
148
|
+
None => std::ptr::null(),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
let enumerator: Retained<AnyObject> = msg_send![&*touches, objectEnumerator];
|
|
152
|
+
loop {
|
|
153
|
+
let touch: *const AnyObject = msg_send![&*enumerator, nextObject];
|
|
154
|
+
if touch.is_null() { break; }
|
|
155
|
+
|
|
156
|
+
let touch_id = touch as *const c_void;
|
|
157
|
+
let loc: CGPoint = msg_send![&*touch, locationInView: view_ptr];
|
|
158
|
+
|
|
159
|
+
let index = match phase {
|
|
160
|
+
TouchPhase::Began => {
|
|
161
|
+
let mut slot = None;
|
|
162
|
+
for i in 0..10 {
|
|
163
|
+
if TOUCH_MAP[i].is_null() {
|
|
164
|
+
TOUCH_MAP[i] = touch_id;
|
|
165
|
+
slot = Some(i);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
match slot {
|
|
170
|
+
Some(i) => i,
|
|
171
|
+
None => continue,
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
TouchPhase::Moved => {
|
|
175
|
+
match TOUCH_MAP.iter().position(|&p| p == touch_id) {
|
|
176
|
+
Some(i) => i,
|
|
177
|
+
None => continue,
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
TouchPhase::Ended => {
|
|
181
|
+
match TOUCH_MAP.iter().position(|&p| p == touch_id) {
|
|
182
|
+
Some(i) => {
|
|
183
|
+
TOUCH_MAP[i] = std::ptr::null();
|
|
184
|
+
i
|
|
185
|
+
}
|
|
186
|
+
None => continue,
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
if let Some(eng) = ENGINE.get_mut() {
|
|
192
|
+
let active = !matches!(phase, TouchPhase::Ended);
|
|
193
|
+
// Scale touch from points to pixels to match getScreenWidth/Height
|
|
194
|
+
let sx = loc.x * SCREEN_SCALE;
|
|
195
|
+
let sy = loc.y * SCREEN_SCALE;
|
|
196
|
+
eng.input.set_touch(index, sx, sy, active);
|
|
197
|
+
|
|
198
|
+
if index == 0 {
|
|
199
|
+
eng.input.set_mouse_position(sx, sy);
|
|
200
|
+
if active {
|
|
201
|
+
eng.input.set_mouse_button_down(0);
|
|
202
|
+
} else {
|
|
203
|
+
eng.input.set_mouse_button_up(0);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
fn register_metal_view_class() {
|
|
211
|
+
if AnyClass::get(c"BloomMetalView").is_some() { return; }
|
|
212
|
+
|
|
213
|
+
unsafe {
|
|
214
|
+
let superclass = AnyClass::get(c"UIView").unwrap();
|
|
215
|
+
let cls = objc_allocateClassPair(superclass as *const AnyClass, b"BloomMetalView\0".as_ptr(), 0);
|
|
216
|
+
if cls.is_null() { return; }
|
|
217
|
+
|
|
218
|
+
extern "C" { fn object_getClass(obj: *const c_void) -> *mut AnyClass; }
|
|
219
|
+
let meta = object_getClass(cls as *const c_void);
|
|
220
|
+
|
|
221
|
+
class_addMethod(meta, sel!(layerClass), bloom_layer_class as *const c_void, b"#8@0:8\0".as_ptr());
|
|
222
|
+
|
|
223
|
+
let touch_types = b"v32@0:8@16@24\0".as_ptr();
|
|
224
|
+
class_addMethod(cls, sel!(touchesBegan:withEvent:), bloom_touches_began as *const c_void, touch_types);
|
|
225
|
+
class_addMethod(cls, sel!(touchesMoved:withEvent:), bloom_touches_moved as *const c_void, touch_types);
|
|
226
|
+
class_addMethod(cls, sel!(touchesEnded:withEvent:), bloom_touches_ended as *const c_void, touch_types);
|
|
227
|
+
class_addMethod(cls, sel!(touchesCancelled:withEvent:), bloom_touches_cancelled as *const c_void, touch_types);
|
|
228
|
+
|
|
229
|
+
objc_registerClassPair(cls);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ============================================================
|
|
234
|
+
// Register BloomViewController — UIViewController with orientation lock
|
|
235
|
+
// ============================================================
|
|
236
|
+
|
|
237
|
+
unsafe extern "C" fn bloom_vc_supported_orientations(_this: *const c_void, _sel: Sel) -> u64 {
|
|
238
|
+
unsafe { ORIENTATION_MASK }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
fn register_view_controller_class() {
|
|
242
|
+
if AnyClass::get(c"BloomViewController").is_some() { return; }
|
|
243
|
+
|
|
244
|
+
unsafe {
|
|
245
|
+
let superclass = AnyClass::get(c"UIViewController").unwrap();
|
|
246
|
+
let cls = objc_allocateClassPair(superclass as *const AnyClass, b"BloomViewController\0".as_ptr(), 0);
|
|
247
|
+
if cls.is_null() { return; }
|
|
248
|
+
|
|
249
|
+
let sel = Sel::register(c"supportedInterfaceOrientations");
|
|
250
|
+
class_addMethod(cls, sel, bloom_vc_supported_orientations as *const c_void, b"Q16@0:8\0".as_ptr());
|
|
251
|
+
|
|
252
|
+
objc_registerClassPair(cls);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ============================================================
|
|
257
|
+
// Scene delegate — creates UIWindow + Metal view + wgpu engine
|
|
258
|
+
// ============================================================
|
|
259
|
+
|
|
260
|
+
unsafe extern "C" fn scene_will_connect(
|
|
261
|
+
_this: *mut c_void,
|
|
262
|
+
_sel: Sel,
|
|
263
|
+
scene: *const AnyObject,
|
|
264
|
+
_session: *const AnyObject,
|
|
265
|
+
_options: *const AnyObject,
|
|
266
|
+
) {
|
|
267
|
+
if scene.is_null() { return; }
|
|
268
|
+
|
|
269
|
+
// Get screen bounds from UIScreen.mainScreen (simpler than scene.screen)
|
|
270
|
+
let screen_cls = AnyClass::get(c"UIScreen").unwrap();
|
|
271
|
+
let screen: Retained<AnyObject> = msg_send![screen_cls, mainScreen];
|
|
272
|
+
let bounds: CGRect = msg_send![&*screen, bounds];
|
|
273
|
+
let scale: f64 = msg_send![&*screen, scale];
|
|
274
|
+
|
|
275
|
+
let pixel_width = (bounds.size.width * scale) as u32;
|
|
276
|
+
let pixel_height = (bounds.size.height * scale) as u32;
|
|
277
|
+
|
|
278
|
+
// Create UIWindow attached to the scene
|
|
279
|
+
let window_cls = AnyClass::get(c"UIWindow").unwrap();
|
|
280
|
+
let window: Allocated<AnyObject> = msg_send![window_cls, alloc];
|
|
281
|
+
let window: Retained<AnyObject> = msg_send![window, initWithWindowScene: scene];
|
|
282
|
+
|
|
283
|
+
// Create BloomViewController (with orientation lock support)
|
|
284
|
+
let vc_cls = AnyClass::get(c"BloomViewController")
|
|
285
|
+
.unwrap_or_else(|| AnyClass::get(c"UIViewController").unwrap());
|
|
286
|
+
let vc: Allocated<AnyObject> = msg_send![vc_cls, alloc];
|
|
287
|
+
let vc: Retained<AnyObject> = msg_send![vc, init];
|
|
288
|
+
|
|
289
|
+
// Create BloomMetalView
|
|
290
|
+
let view_cls = AnyClass::get(c"BloomMetalView").unwrap();
|
|
291
|
+
let view: Allocated<AnyObject> = msg_send![view_cls, alloc];
|
|
292
|
+
let view: Retained<AnyObject> = msg_send![view, initWithFrame: bounds];
|
|
293
|
+
|
|
294
|
+
// Set background to green (diagnostic — visible if Metal isn't rendering)
|
|
295
|
+
let color_cls = AnyClass::get(c"UIColor").unwrap();
|
|
296
|
+
let green: Retained<AnyObject> = msg_send![color_cls, greenColor];
|
|
297
|
+
let _: () = msg_send![&*view, setBackgroundColor: &*green];
|
|
298
|
+
|
|
299
|
+
// Configure CAMetalLayer
|
|
300
|
+
let layer: Retained<AnyObject> = msg_send![&*view, layer];
|
|
301
|
+
let drawable_size = CGSize { width: pixel_width as f64, height: pixel_height as f64 };
|
|
302
|
+
let _: () = msg_send![&*layer, setDrawableSize: drawable_size];
|
|
303
|
+
let _: () = msg_send![&*layer, setContentsScale: scale];
|
|
304
|
+
let _: () = msg_send![&*layer, setOpaque: Bool::NO]; // Non-opaque so green shows if Metal fails
|
|
305
|
+
|
|
306
|
+
// Enable touches
|
|
307
|
+
let _: () = msg_send![&*view, setUserInteractionEnabled: Bool::YES];
|
|
308
|
+
let _: () = msg_send![&*view, setMultipleTouchEnabled: Bool::YES];
|
|
309
|
+
|
|
310
|
+
// Set up window hierarchy
|
|
311
|
+
let _: () = msg_send![&*vc, setView: &*view];
|
|
312
|
+
let _: () = msg_send![&*window, setRootViewController: &*vc];
|
|
313
|
+
let _: () = msg_send![&*window, makeKeyAndVisible];
|
|
314
|
+
|
|
315
|
+
// Store references
|
|
316
|
+
UI_VIEW = Some(view.clone());
|
|
317
|
+
UI_WINDOW = Some(window);
|
|
318
|
+
|
|
319
|
+
// Create wgpu surface
|
|
320
|
+
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
|
321
|
+
backends: wgpu::Backends::METAL,
|
|
322
|
+
..wgpu::InstanceDescriptor::new_without_display_handle()
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
let view_ptr = Retained::as_ptr(&view) as *mut c_void;
|
|
326
|
+
let handle = UiKitWindowHandle::new(
|
|
327
|
+
std::ptr::NonNull::new(view_ptr).unwrap(),
|
|
328
|
+
);
|
|
329
|
+
let raw = RawWindowHandle::UiKit(handle);
|
|
330
|
+
let surface = instance.create_surface_unsafe(wgpu::SurfaceTargetUnsafe::RawHandle {
|
|
331
|
+
raw_display_handle: Some(RawDisplayHandle::UiKit(UiKitDisplayHandle::new())),
|
|
332
|
+
raw_window_handle: raw,
|
|
333
|
+
}).expect("Failed to create wgpu surface");
|
|
334
|
+
|
|
335
|
+
let adapter = pollster_block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
|
|
336
|
+
compatible_surface: Some(&surface),
|
|
337
|
+
power_preference: wgpu::PowerPreference::HighPerformance,
|
|
338
|
+
..Default::default()
|
|
339
|
+
})).expect("No GPU adapter found");
|
|
340
|
+
|
|
341
|
+
// Ticket 007b: opt into HW ray-query on RT-capable Apple Silicon
|
|
342
|
+
// devices. `BLOOM_FORCE_SW_GI=1` forces the SW path for bench parity.
|
|
343
|
+
let supported = adapter.features();
|
|
344
|
+
let force_sw_gi = std::env::var("BLOOM_FORCE_SW_GI")
|
|
345
|
+
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
|
346
|
+
.unwrap_or(false);
|
|
347
|
+
let rt_mask = wgpu::Features::EXPERIMENTAL_RAY_QUERY;
|
|
348
|
+
let mut required_features = wgpu::Features::empty();
|
|
349
|
+
// Ticket 011: request TIMESTAMP_QUERY when supported so the profiler
|
|
350
|
+
// can record GPU timings. Optional — profiler falls back to CPU-only
|
|
351
|
+
// when the adapter doesn't grant it.
|
|
352
|
+
if supported.contains(wgpu::Features::TIMESTAMP_QUERY) {
|
|
353
|
+
required_features |= wgpu::Features::TIMESTAMP_QUERY;
|
|
354
|
+
}
|
|
355
|
+
// Cooked BC7 textures (bloom-cook) upload compressed when the
|
|
356
|
+
// adapter has BC support; without it they CPU-decode at load.
|
|
357
|
+
if supported.contains(wgpu::Features::TEXTURE_COMPRESSION_BC) {
|
|
358
|
+
required_features |= wgpu::Features::TEXTURE_COMPRESSION_BC;
|
|
359
|
+
}
|
|
360
|
+
if !force_sw_gi && supported.contains(rt_mask) {
|
|
361
|
+
required_features |= rt_mask;
|
|
362
|
+
}
|
|
363
|
+
let experimental_features = if required_features.intersects(rt_mask) {
|
|
364
|
+
unsafe { wgpu::ExperimentalFeatures::enabled() }
|
|
365
|
+
} else {
|
|
366
|
+
wgpu::ExperimentalFeatures::disabled()
|
|
367
|
+
};
|
|
368
|
+
let mut required_limits = wgpu::Limits::default();
|
|
369
|
+
if required_features.intersects(rt_mask) {
|
|
370
|
+
required_limits = required_limits
|
|
371
|
+
.using_minimum_supported_acceleration_structure_values();
|
|
372
|
+
}
|
|
373
|
+
let (device, queue) = match pollster_block_on(adapter.request_device(
|
|
374
|
+
&wgpu::DeviceDescriptor {
|
|
375
|
+
label: Some("bloom_device"),
|
|
376
|
+
required_features,
|
|
377
|
+
required_limits,
|
|
378
|
+
experimental_features,
|
|
379
|
+
..Default::default()
|
|
380
|
+
},
|
|
381
|
+
)) {
|
|
382
|
+
Ok(dq) => dq,
|
|
383
|
+
Err(e) => {
|
|
384
|
+
// Some real devices (e.g. iPhone 16 Pro / A18) advertise
|
|
385
|
+
// EXPERIMENTAL_RAY_QUERY on the adapter but reject it at device
|
|
386
|
+
// creation through wgpu's Metal backend, which would otherwise
|
|
387
|
+
// abort the app on launch. Retry with a minimal, always-supported
|
|
388
|
+
// configuration. The renderer keys off device.features() (see
|
|
389
|
+
// renderer/mod.rs), so it transparently falls back to the non-RT
|
|
390
|
+
// path when ray-query isn't granted.
|
|
391
|
+
eprintln!("[bloom-ios] request_device with preferred features failed ({e:?}); retrying with adapter limits and no ray-query/experimental features");
|
|
392
|
+
// Request exactly the adapter's reported limits (not wgpu's
|
|
393
|
+
// Limits::default()): some iOS GPUs cap individual limits — e.g.
|
|
394
|
+
// max_inter_stage_shader_variables — below wgpu's defaults, so
|
|
395
|
+
// request_device rejects the default-limits request too. Asking for
|
|
396
|
+
// adapter.limits() can never exceed what the device supports.
|
|
397
|
+
pollster_block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
|
398
|
+
label: Some("bloom_device"),
|
|
399
|
+
required_features: wgpu::Features::empty(),
|
|
400
|
+
required_limits: adapter.limits(),
|
|
401
|
+
experimental_features: wgpu::ExperimentalFeatures::disabled(),
|
|
402
|
+
..Default::default()
|
|
403
|
+
})).expect("Failed to create device (minimal fallback)")
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
let surface_caps = surface.get_capabilities(&adapter);
|
|
408
|
+
let format = surface_caps.formats.iter()
|
|
409
|
+
.find(|f| f.is_srgb()).copied()
|
|
410
|
+
.unwrap_or(surface_caps.formats[0]);
|
|
411
|
+
|
|
412
|
+
let surface_config = wgpu::SurfaceConfiguration {
|
|
413
|
+
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
|
414
|
+
format,
|
|
415
|
+
width: pixel_width,
|
|
416
|
+
height: pixel_height,
|
|
417
|
+
present_mode: wgpu::PresentMode::Fifo,
|
|
418
|
+
alpha_mode: surface_caps.alpha_modes[0],
|
|
419
|
+
view_formats: vec![],
|
|
420
|
+
desired_maximum_frame_latency: 2,
|
|
421
|
+
};
|
|
422
|
+
surface.configure(&device, &surface_config);
|
|
423
|
+
|
|
424
|
+
let renderer = Renderer::new(device, queue, surface, surface_config, pixel_width, pixel_height);
|
|
425
|
+
let _ = ENGINE.set(EngineState::new(renderer));
|
|
426
|
+
|
|
427
|
+
// Write debug info to app container tmp
|
|
428
|
+
{
|
|
429
|
+
let ns_cls = AnyClass::get(c"NSTemporaryDirectory").unwrap_or(AnyClass::get(c"NSString").unwrap());
|
|
430
|
+
// Use NSFileManager to get tmp dir
|
|
431
|
+
extern "C" { fn NSTemporaryDirectory() -> *const AnyObject; }
|
|
432
|
+
let tmp_dir: *const AnyObject = NSTemporaryDirectory();
|
|
433
|
+
if !tmp_dir.is_null() {
|
|
434
|
+
let utf8: *const u8 = msg_send![&*tmp_dir, UTF8String];
|
|
435
|
+
if !utf8.is_null() {
|
|
436
|
+
let cstr_val = std::ffi::CStr::from_ptr(utf8 as *const i8);
|
|
437
|
+
if let Ok(tmp_path) = cstr_val.to_str() {
|
|
438
|
+
let _ = std::fs::write(format!("{}bloom_debug.txt", tmp_path),
|
|
439
|
+
format!("scene_will_connect OK\npixels={}x{} scale={}\nwindow_scene={}\n",
|
|
440
|
+
pixel_width, pixel_height, scale, !scene.is_null()));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ENGINE is now set — bloom_init_window polls for this on the game thread
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/// Called by the perry runtime's main() before UIApplicationMain to register
|
|
450
|
+
/// ObjC classes needed for the scene lifecycle.
|
|
451
|
+
#[no_mangle]
|
|
452
|
+
pub unsafe extern "C" fn perry_register_native_classes() {
|
|
453
|
+
register_metal_view_class();
|
|
454
|
+
register_view_controller_class();
|
|
455
|
+
register_scene_delegate();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/// Called by the runtime's scene delegate when the UIWindowScene connects.
|
|
459
|
+
/// This runs on the main thread — safe for all UIKit operations.
|
|
460
|
+
#[no_mangle]
|
|
461
|
+
pub unsafe extern "C" fn perry_scene_will_connect(scene: *const c_void) {
|
|
462
|
+
let screen_cls = AnyClass::get(c"UIScreen").unwrap();
|
|
463
|
+
let screen: Retained<AnyObject> = msg_send![screen_cls, mainScreen];
|
|
464
|
+
let bounds: CGRect = msg_send![&*screen, bounds];
|
|
465
|
+
let scale: f64 = msg_send![&*screen, scale];
|
|
466
|
+
|
|
467
|
+
let pixel_width = (bounds.size.width * scale) as u32;
|
|
468
|
+
let pixel_height = (bounds.size.height * scale) as u32;
|
|
469
|
+
|
|
470
|
+
// Store scale for touch coordinate conversion (points → pixels)
|
|
471
|
+
SCREEN_SCALE = scale;
|
|
472
|
+
|
|
473
|
+
// Create UIWindow — attached to scene if available, otherwise plain
|
|
474
|
+
let window_cls = AnyClass::get(c"UIWindow").unwrap();
|
|
475
|
+
let window: Retained<AnyObject> = if !scene.is_null() {
|
|
476
|
+
let w: Allocated<AnyObject> = msg_send![window_cls, alloc];
|
|
477
|
+
msg_send![w, initWithWindowScene: scene as *const AnyObject]
|
|
478
|
+
} else {
|
|
479
|
+
let w: Allocated<AnyObject> = msg_send![window_cls, alloc];
|
|
480
|
+
msg_send![w, initWithFrame: bounds]
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
// Create BloomViewController (with orientation lock support)
|
|
484
|
+
let vc_cls = AnyClass::get(c"BloomViewController")
|
|
485
|
+
.unwrap_or_else(|| AnyClass::get(c"UIViewController").unwrap());
|
|
486
|
+
let vc: Allocated<AnyObject> = msg_send![vc_cls, alloc];
|
|
487
|
+
let vc: Retained<AnyObject> = msg_send![vc, init];
|
|
488
|
+
|
|
489
|
+
// Create BloomMetalView
|
|
490
|
+
let view_cls = AnyClass::get(c"BloomMetalView").unwrap();
|
|
491
|
+
let view: Allocated<AnyObject> = msg_send![view_cls, alloc];
|
|
492
|
+
let view: Retained<AnyObject> = msg_send![view, initWithFrame: bounds];
|
|
493
|
+
|
|
494
|
+
// Set background to black
|
|
495
|
+
let color_cls = AnyClass::get(c"UIColor").unwrap();
|
|
496
|
+
let black: Retained<AnyObject> = msg_send![color_cls, blackColor];
|
|
497
|
+
let _: () = msg_send![&*view, setBackgroundColor: &*black];
|
|
498
|
+
|
|
499
|
+
// Configure CAMetalLayer
|
|
500
|
+
let layer: Retained<AnyObject> = msg_send![&*view, layer];
|
|
501
|
+
let drawable_size = CGSize { width: pixel_width as f64, height: pixel_height as f64 };
|
|
502
|
+
let _: () = msg_send![&*layer, setDrawableSize: drawable_size];
|
|
503
|
+
let _: () = msg_send![&*layer, setContentsScale: scale];
|
|
504
|
+
let _: () = msg_send![&*layer, setOpaque: Bool::YES];
|
|
505
|
+
|
|
506
|
+
// Enable touches
|
|
507
|
+
let _: () = msg_send![&*view, setUserInteractionEnabled: Bool::YES];
|
|
508
|
+
let _: () = msg_send![&*view, setMultipleTouchEnabled: Bool::YES];
|
|
509
|
+
|
|
510
|
+
// Set up window hierarchy
|
|
511
|
+
let _: () = msg_send![&*vc, setView: &*view];
|
|
512
|
+
let _: () = msg_send![&*window, setRootViewController: &*vc];
|
|
513
|
+
let _: () = msg_send![&*window, makeKeyAndVisible];
|
|
514
|
+
|
|
515
|
+
// Store references
|
|
516
|
+
UI_VIEW = Some(view.clone());
|
|
517
|
+
UI_WINDOW = Some(window);
|
|
518
|
+
|
|
519
|
+
// Create wgpu surface and engine
|
|
520
|
+
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
|
521
|
+
backends: wgpu::Backends::METAL,
|
|
522
|
+
..wgpu::InstanceDescriptor::new_without_display_handle()
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
let view_ptr = Retained::as_ptr(&view) as *mut c_void;
|
|
526
|
+
let handle = UiKitWindowHandle::new(
|
|
527
|
+
std::ptr::NonNull::new(view_ptr).unwrap(),
|
|
528
|
+
);
|
|
529
|
+
let raw = RawWindowHandle::UiKit(handle);
|
|
530
|
+
let surface = instance.create_surface_unsafe(wgpu::SurfaceTargetUnsafe::RawHandle {
|
|
531
|
+
raw_display_handle: Some(RawDisplayHandle::UiKit(UiKitDisplayHandle::new())),
|
|
532
|
+
raw_window_handle: raw,
|
|
533
|
+
}).expect("Failed to create wgpu surface");
|
|
534
|
+
|
|
535
|
+
let adapter = pollster_block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
|
|
536
|
+
compatible_surface: Some(&surface),
|
|
537
|
+
power_preference: wgpu::PowerPreference::HighPerformance,
|
|
538
|
+
..Default::default()
|
|
539
|
+
})).expect("No GPU adapter found");
|
|
540
|
+
|
|
541
|
+
// Ticket 007b: opt into HW ray-query on RT-capable Apple Silicon
|
|
542
|
+
// devices. `BLOOM_FORCE_SW_GI=1` forces the SW path for bench parity.
|
|
543
|
+
let supported = adapter.features();
|
|
544
|
+
let force_sw_gi = std::env::var("BLOOM_FORCE_SW_GI")
|
|
545
|
+
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
|
546
|
+
.unwrap_or(false);
|
|
547
|
+
let rt_mask = wgpu::Features::EXPERIMENTAL_RAY_QUERY;
|
|
548
|
+
let mut required_features = wgpu::Features::empty();
|
|
549
|
+
// Ticket 011: request TIMESTAMP_QUERY when supported so the profiler
|
|
550
|
+
// can record GPU timings. Optional — profiler falls back to CPU-only
|
|
551
|
+
// when the adapter doesn't grant it.
|
|
552
|
+
if supported.contains(wgpu::Features::TIMESTAMP_QUERY) {
|
|
553
|
+
required_features |= wgpu::Features::TIMESTAMP_QUERY;
|
|
554
|
+
}
|
|
555
|
+
// Cooked BC7 textures (bloom-cook) upload compressed when the
|
|
556
|
+
// adapter has BC support; without it they CPU-decode at load.
|
|
557
|
+
if supported.contains(wgpu::Features::TEXTURE_COMPRESSION_BC) {
|
|
558
|
+
required_features |= wgpu::Features::TEXTURE_COMPRESSION_BC;
|
|
559
|
+
}
|
|
560
|
+
if !force_sw_gi && supported.contains(rt_mask) {
|
|
561
|
+
required_features |= rt_mask;
|
|
562
|
+
}
|
|
563
|
+
let experimental_features = if required_features.intersects(rt_mask) {
|
|
564
|
+
unsafe { wgpu::ExperimentalFeatures::enabled() }
|
|
565
|
+
} else {
|
|
566
|
+
wgpu::ExperimentalFeatures::disabled()
|
|
567
|
+
};
|
|
568
|
+
let mut required_limits = wgpu::Limits::default();
|
|
569
|
+
if required_features.intersects(rt_mask) {
|
|
570
|
+
required_limits = required_limits
|
|
571
|
+
.using_minimum_supported_acceleration_structure_values();
|
|
572
|
+
}
|
|
573
|
+
let (device, queue) = match pollster_block_on(adapter.request_device(
|
|
574
|
+
&wgpu::DeviceDescriptor {
|
|
575
|
+
label: Some("bloom_device"),
|
|
576
|
+
required_features,
|
|
577
|
+
required_limits,
|
|
578
|
+
experimental_features,
|
|
579
|
+
..Default::default()
|
|
580
|
+
},
|
|
581
|
+
)) {
|
|
582
|
+
Ok(dq) => dq,
|
|
583
|
+
Err(e) => {
|
|
584
|
+
// Some real devices (e.g. iPhone 16 Pro / A18) advertise
|
|
585
|
+
// EXPERIMENTAL_RAY_QUERY on the adapter but reject it at device
|
|
586
|
+
// creation through wgpu's Metal backend, which would otherwise
|
|
587
|
+
// abort the app on launch. Retry with a minimal, always-supported
|
|
588
|
+
// configuration. The renderer keys off device.features() (see
|
|
589
|
+
// renderer/mod.rs), so it transparently falls back to the non-RT
|
|
590
|
+
// path when ray-query isn't granted.
|
|
591
|
+
eprintln!("[bloom-ios] request_device with preferred features failed ({e:?}); retrying with adapter limits and no ray-query/experimental features");
|
|
592
|
+
// Request exactly the adapter's reported limits (not wgpu's
|
|
593
|
+
// Limits::default()): some iOS GPUs cap individual limits — e.g.
|
|
594
|
+
// max_inter_stage_shader_variables — below wgpu's defaults, so
|
|
595
|
+
// request_device rejects the default-limits request too. Asking for
|
|
596
|
+
// adapter.limits() can never exceed what the device supports.
|
|
597
|
+
pollster_block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
|
598
|
+
label: Some("bloom_device"),
|
|
599
|
+
required_features: wgpu::Features::empty(),
|
|
600
|
+
required_limits: adapter.limits(),
|
|
601
|
+
experimental_features: wgpu::ExperimentalFeatures::disabled(),
|
|
602
|
+
..Default::default()
|
|
603
|
+
})).expect("Failed to create device (minimal fallback)")
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
let surface_caps = surface.get_capabilities(&adapter);
|
|
608
|
+
let format = surface_caps.formats.iter()
|
|
609
|
+
.find(|f| f.is_srgb()).copied()
|
|
610
|
+
.unwrap_or(surface_caps.formats[0]);
|
|
611
|
+
|
|
612
|
+
let surface_config = wgpu::SurfaceConfiguration {
|
|
613
|
+
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
|
614
|
+
format,
|
|
615
|
+
width: pixel_width,
|
|
616
|
+
height: pixel_height,
|
|
617
|
+
present_mode: wgpu::PresentMode::Fifo,
|
|
618
|
+
alpha_mode: surface_caps.alpha_modes[0],
|
|
619
|
+
view_formats: vec![],
|
|
620
|
+
desired_maximum_frame_latency: 2,
|
|
621
|
+
};
|
|
622
|
+
surface.configure(&device, &surface_config);
|
|
623
|
+
|
|
624
|
+
let renderer = Renderer::new(device, queue, surface, surface_config, pixel_width, pixel_height);
|
|
625
|
+
let _ = ENGINE.set(EngineState::new(renderer));
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
fn register_scene_delegate() {
|
|
629
|
+
if AnyClass::get(c"PerrySceneDelegate").is_some() { return; }
|
|
630
|
+
|
|
631
|
+
unsafe {
|
|
632
|
+
let superclass = AnyClass::get(c"UIResponder").unwrap();
|
|
633
|
+
let cls = objc_allocateClassPair(superclass as *const AnyClass, b"PerrySceneDelegate\0".as_ptr(), 0);
|
|
634
|
+
if cls.is_null() { return; }
|
|
635
|
+
|
|
636
|
+
// scene:willConnectToSession:connectionOptions:
|
|
637
|
+
let sel = Sel::register(c"scene:willConnectToSession:connectionOptions:");
|
|
638
|
+
let types = b"v48@0:8@16@24@32\0".as_ptr();
|
|
639
|
+
class_addMethod(cls, sel, scene_will_connect as *const c_void, types);
|
|
640
|
+
|
|
641
|
+
// Add UIWindowSceneDelegate protocol
|
|
642
|
+
extern "C" { fn objc_getProtocol(name: *const u8) -> *const c_void; }
|
|
643
|
+
let protocol = objc_getProtocol(b"UIWindowSceneDelegate\0".as_ptr());
|
|
644
|
+
if !protocol.is_null() {
|
|
645
|
+
extern "C" { fn class_addProtocol(cls: *mut AnyClass, protocol: *const c_void) -> bool; }
|
|
646
|
+
class_addProtocol(cls, protocol);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
objc_registerClassPair(cls);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// ============================================================
|
|
654
|
+
// Minimal pollster
|
|
655
|
+
// ============================================================
|
|
656
|
+
|
|
657
|
+
fn pollster_block_on<F: std::future::Future>(future: F) -> F::Output {
|
|
658
|
+
use std::task::{Context, Poll, Wake, Waker};
|
|
659
|
+
use std::pin::Pin;
|
|
660
|
+
use std::sync::Arc;
|
|
661
|
+
struct NoopWaker;
|
|
662
|
+
impl Wake for NoopWaker { fn wake(self: Arc<Self>) {} }
|
|
663
|
+
let waker = Waker::from(Arc::new(NoopWaker));
|
|
664
|
+
let mut cx = Context::from_waker(&waker);
|
|
665
|
+
let mut future = unsafe { Pin::new_unchecked(Box::new(future)) };
|
|
666
|
+
loop {
|
|
667
|
+
match future.as_mut().poll(&mut cx) {
|
|
668
|
+
Poll::Ready(result) => return result,
|
|
669
|
+
Poll::Pending => {
|
|
670
|
+
pump_run_loop(0.001);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// ============================================================
|
|
677
|
+
// FFI entry points
|
|
678
|
+
// ============================================================
|
|
679
|
+
|
|
680
|
+
#[no_mangle]
|
|
681
|
+
pub extern "C" fn bloom_init_window(_width: f64, _height: f64, title_ptr: *const u8, _fullscreen: f64) {
|
|
682
|
+
let _title = str_from_header(title_ptr);
|
|
683
|
+
|
|
684
|
+
// Set orientation mask based on requested dimensions
|
|
685
|
+
// width > height → landscape, otherwise all orientations
|
|
686
|
+
if _width > _height {
|
|
687
|
+
unsafe { ORIENTATION_MASK = 0x18; } // UIInterfaceOrientationMaskLandscape
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Register ObjC classes for the scene delegate (window/view creation)
|
|
691
|
+
register_metal_view_class();
|
|
692
|
+
register_view_controller_class();
|
|
693
|
+
register_scene_delegate();
|
|
694
|
+
|
|
695
|
+
// Signal the main thread that our ObjC classes are ready.
|
|
696
|
+
// UIApplicationMain (on main thread) waits for this before starting.
|
|
697
|
+
extern "C" { fn perry_ios_classes_registered(); }
|
|
698
|
+
unsafe { perry_ios_classes_registered(); }
|
|
699
|
+
|
|
700
|
+
// Debug: write marker to confirm we reached this point
|
|
701
|
+
let _ = std::fs::write("/tmp/bloom_checkpoint_1.txt", "classes registered\n");
|
|
702
|
+
|
|
703
|
+
// Get app bundle path for resolving relative asset paths
|
|
704
|
+
unsafe {
|
|
705
|
+
let bundle_cls = AnyClass::get(c"NSBundle").unwrap();
|
|
706
|
+
let main_bundle: Retained<AnyObject> = msg_send![bundle_cls, mainBundle];
|
|
707
|
+
let resource_path: *const AnyObject = msg_send![&*main_bundle, resourcePath];
|
|
708
|
+
if !resource_path.is_null() {
|
|
709
|
+
let utf8: *const u8 = msg_send![&*resource_path, UTF8String];
|
|
710
|
+
if !utf8.is_null() {
|
|
711
|
+
let cstr = std::ffi::CStr::from_ptr(utf8 as *const i8);
|
|
712
|
+
if let Ok(s) = cstr.to_str() {
|
|
713
|
+
BUNDLE_PATH = Some(s.to_string());
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// With --features ios-game-loop, this function runs on the game thread.
|
|
720
|
+
// UIApplicationMain runs on the main thread. The runtime's scene delegate
|
|
721
|
+
// calls perry_scene_will_connect() which creates the window, view, wgpu
|
|
722
|
+
// surface, and ENGINE — all on the main thread. We just wait for it.
|
|
723
|
+
unsafe {
|
|
724
|
+
for _ in 0..1000 {
|
|
725
|
+
if ENGINE.get().is_some() { break; }
|
|
726
|
+
std::thread::sleep(std::time::Duration::from_millis(10));
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if unsafe { ENGINE.get().is_none() } {
|
|
731
|
+
panic!("[bloom-ios] Engine not initialized after 10s. \
|
|
732
|
+
Compile with: perry compile --target ios-simulator --features ios-game-loop");
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/// Attach the engine to a host-owned `UIView*` instead of creating its
|
|
737
|
+
/// own UIWindow (PerryTS/perry#5519). `handle` is the raw `UIView*` the
|
|
738
|
+
/// host (Perry UI's `BloomView`) owns; `width`/`height` are its size in
|
|
739
|
+
/// points. Returns 1.0 on success, 0.0 on a null/invalid handle or if
|
|
740
|
+
/// surface bring-up failed. Idempotent once attached.
|
|
741
|
+
///
|
|
742
|
+
/// HiDPI: callers wanting full backing resolution should pass the pixel
|
|
743
|
+
/// size (points × `UIScreen.scale`); this path uses `width`/`height` as
|
|
744
|
+
/// the drawable size directly.
|
|
745
|
+
#[no_mangle]
|
|
746
|
+
pub extern "C" fn bloom_attach_native(handle: i64, width: f64, height: f64) -> f64 {
|
|
747
|
+
if handle == 0 {
|
|
748
|
+
return 0.0;
|
|
749
|
+
}
|
|
750
|
+
if unsafe { ENGINE.get() }.is_some() {
|
|
751
|
+
return 1.0;
|
|
752
|
+
}
|
|
753
|
+
let Some(view_nn) = std::ptr::NonNull::new(handle as *mut c_void) else {
|
|
754
|
+
return 0.0;
|
|
755
|
+
};
|
|
756
|
+
let target = {
|
|
757
|
+
let h = UiKitWindowHandle::new(view_nn);
|
|
758
|
+
wgpu::SurfaceTargetUnsafe::RawHandle {
|
|
759
|
+
raw_display_handle: Some(RawDisplayHandle::UiKit(UiKitDisplayHandle::new())),
|
|
760
|
+
raw_window_handle: RawWindowHandle::UiKit(h),
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
match unsafe {
|
|
764
|
+
bloom_shared::attach::attach_engine(
|
|
765
|
+
target,
|
|
766
|
+
bloom_shared::attach::AttachParams {
|
|
767
|
+
backends: wgpu::Backends::METAL,
|
|
768
|
+
logical_w: width as u32,
|
|
769
|
+
logical_h: height as u32,
|
|
770
|
+
physical_w: (width as u32).max(1),
|
|
771
|
+
physical_h: (height as u32).max(1),
|
|
772
|
+
format: bloom_shared::attach::FormatPreference::Srgb,
|
|
773
|
+
},
|
|
774
|
+
)
|
|
775
|
+
} {
|
|
776
|
+
Ok(es) => {
|
|
777
|
+
unsafe {
|
|
778
|
+
let _ = ENGINE.set(es);
|
|
779
|
+
}
|
|
780
|
+
1.0
|
|
781
|
+
}
|
|
782
|
+
Err(_) => 0.0,
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/// Resize the engine's surface (#70 parity; used by host-driven
|
|
787
|
+
/// BloomViews on layout changes). `phys_*` physical px, `log_*` logical.
|
|
788
|
+
#[no_mangle]
|
|
789
|
+
pub extern "C" fn bloom_resize(phys_w: f64, phys_h: f64, log_w: f64, log_h: f64) {
|
|
790
|
+
if let Some(eng) = unsafe { ENGINE.get_mut() } {
|
|
791
|
+
eng.renderer.resize(phys_w as u32, phys_h as u32, log_w as u32, log_h as u32);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/// HWND host-embed (#70) — Windows only; a no-op here for FFI-manifest
|
|
796
|
+
/// parity. Non-Windows hosts attach via `bloom_attach_native`.
|
|
797
|
+
#[no_mangle]
|
|
798
|
+
pub extern "C" fn bloom_attach_hwnd(_hwnd_bits: f64, _width: f64, _height: f64) {}
|
|
799
|
+
|
|
800
|
+
#[no_mangle]
|
|
801
|
+
pub extern "C" fn bloom_close_window() {
|
|
802
|
+
unsafe { UI_VIEW = None; UI_WINDOW = None; }
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
#[no_mangle]
|
|
806
|
+
pub extern "C" fn bloom_window_should_close() -> f64 {
|
|
807
|
+
if engine().should_close { 1.0 } else { 0.0 }
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/// Poll the first connected game controller (MFi / Xbox / PlayStation) through
|
|
811
|
+
/// the GameController framework and feed it into the shared input state.
|
|
812
|
+
///
|
|
813
|
+
/// iOS had NO gamepad support before this despite iPhone/iPad supporting MFi and
|
|
814
|
+
/// console controllers — the shared gamepad FFI read `engine().input`, which
|
|
815
|
+
/// nothing populated. Resolved dynamically via the ObjC runtime so no
|
|
816
|
+
/// GameController link dep is needed; a no-op when absent. Identical to the
|
|
817
|
+
/// macOS poll (compile-verified there); button/axis indices match the
|
|
818
|
+
/// tvOS/visionOS map. Values are ObjC `float`, read as `f32` (reading them as
|
|
819
|
+
/// `f64` is an ABI mismatch).
|
|
820
|
+
///
|
|
821
|
+
/// NOTE: compile-verified on the macOS twin; not yet run on an iOS device.
|
|
822
|
+
fn poll_game_controllers() {
|
|
823
|
+
use objc2::runtime::{AnyClass, AnyObject, Bool};
|
|
824
|
+
macro_rules! btn_down {
|
|
825
|
+
($btn:expr, $eng:expr, $idx:expr) => {{
|
|
826
|
+
let b: Retained<AnyObject> = $btn;
|
|
827
|
+
let pressed: Bool = msg_send![&*b, isPressed];
|
|
828
|
+
if pressed.as_bool() { $eng.input.set_gamepad_button_down($idx); }
|
|
829
|
+
}};
|
|
830
|
+
}
|
|
831
|
+
unsafe {
|
|
832
|
+
let gc_cls = match AnyClass::get(c"GCController") {
|
|
833
|
+
Some(c) => c,
|
|
834
|
+
None => return,
|
|
835
|
+
};
|
|
836
|
+
let controllers: Retained<AnyObject> = msg_send![gc_cls, controllers];
|
|
837
|
+
let count: usize = msg_send![&*controllers, count];
|
|
838
|
+
if count == 0 {
|
|
839
|
+
return; // no pad: leave any injected gamepad state untouched
|
|
840
|
+
}
|
|
841
|
+
let controller: Retained<AnyObject> = msg_send![&*controllers, objectAtIndex: 0usize];
|
|
842
|
+
let extended: *mut AnyObject = msg_send![&*controller, extendedGamepad];
|
|
843
|
+
if extended.is_null() {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
let extended: &AnyObject = &*extended;
|
|
847
|
+
|
|
848
|
+
let eng = engine();
|
|
849
|
+
eng.input.reset_gamepad();
|
|
850
|
+
eng.input.gamepad_available = true;
|
|
851
|
+
|
|
852
|
+
let ls: Retained<AnyObject> = msg_send![extended, leftThumbstick];
|
|
853
|
+
let ls_x: Retained<AnyObject> = msg_send![&*ls, xAxis];
|
|
854
|
+
let ls_y: Retained<AnyObject> = msg_send![&*ls, yAxis];
|
|
855
|
+
let lx: f32 = msg_send![&*ls_x, value];
|
|
856
|
+
let ly: f32 = msg_send![&*ls_y, value];
|
|
857
|
+
eng.input.set_gamepad_axis(0, lx);
|
|
858
|
+
eng.input.set_gamepad_axis(1, -ly);
|
|
859
|
+
let rs: Retained<AnyObject> = msg_send![extended, rightThumbstick];
|
|
860
|
+
let rs_x: Retained<AnyObject> = msg_send![&*rs, xAxis];
|
|
861
|
+
let rs_y: Retained<AnyObject> = msg_send![&*rs, yAxis];
|
|
862
|
+
let rx: f32 = msg_send![&*rs_x, value];
|
|
863
|
+
let ry: f32 = msg_send![&*rs_y, value];
|
|
864
|
+
eng.input.set_gamepad_axis(2, rx);
|
|
865
|
+
eng.input.set_gamepad_axis(3, -ry);
|
|
866
|
+
|
|
867
|
+
btn_down!(msg_send![extended, buttonA], eng, 0);
|
|
868
|
+
btn_down!(msg_send![extended, buttonB], eng, 1);
|
|
869
|
+
btn_down!(msg_send![extended, buttonX], eng, 2);
|
|
870
|
+
btn_down!(msg_send![extended, buttonY], eng, 3);
|
|
871
|
+
btn_down!(msg_send![extended, leftShoulder], eng, 4);
|
|
872
|
+
btn_down!(msg_send![extended, rightShoulder], eng, 5);
|
|
873
|
+
let dpad: Retained<AnyObject> = msg_send![extended, dpad];
|
|
874
|
+
btn_down!(msg_send![&*dpad, up], eng, 12);
|
|
875
|
+
btn_down!(msg_send![&*dpad, down], eng, 13);
|
|
876
|
+
btn_down!(msg_send![&*dpad, left], eng, 14);
|
|
877
|
+
btn_down!(msg_send![&*dpad, right], eng, 15);
|
|
878
|
+
|
|
879
|
+
let lt: Retained<AnyObject> = msg_send![extended, leftTrigger];
|
|
880
|
+
let rt: Retained<AnyObject> = msg_send![extended, rightTrigger];
|
|
881
|
+
let ltv: f32 = msg_send![&*lt, value];
|
|
882
|
+
let rtv: f32 = msg_send![&*rt, value];
|
|
883
|
+
eng.input.set_gamepad_axis(4, ltv);
|
|
884
|
+
eng.input.set_gamepad_axis(5, rtv);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
#[no_mangle]
|
|
889
|
+
pub extern "C" fn bloom_begin_drawing() {
|
|
890
|
+
// No run loop pumping needed — UIApplicationMain handles the main run loop
|
|
891
|
+
// on its own thread. The game runs on the game thread.
|
|
892
|
+
|
|
893
|
+
// Poll a connected controller before either begin_frame path below.
|
|
894
|
+
poll_game_controllers();
|
|
895
|
+
|
|
896
|
+
// Update drawable size to match actual view bounds (handles orientation changes)
|
|
897
|
+
unsafe {
|
|
898
|
+
if let Some(view) = &UI_VIEW {
|
|
899
|
+
let view_ptr = Retained::as_ptr(view);
|
|
900
|
+
let layer: Retained<AnyObject> = msg_send![&*view_ptr, layer];
|
|
901
|
+
let view_bounds: CGRect = msg_send![&*view_ptr, bounds];
|
|
902
|
+
let scale = SCREEN_SCALE;
|
|
903
|
+
let pw = (view_bounds.size.width * scale) as u32;
|
|
904
|
+
let ph = (view_bounds.size.height * scale) as u32;
|
|
905
|
+
let eng = engine();
|
|
906
|
+
if pw > 0 && ph > 0 && (pw != eng.renderer.width() || ph != eng.renderer.height()) {
|
|
907
|
+
let ds = CGSize { width: pw as f64, height: ph as f64 };
|
|
908
|
+
let _: () = msg_send![&*layer, setDrawableSize: ds];
|
|
909
|
+
eng.renderer.resize(pw, ph, pw, ph);
|
|
910
|
+
} else {
|
|
911
|
+
eng.begin_frame();
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
engine().begin_frame();
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
#[no_mangle]
|
|
920
|
+
pub extern "C" fn bloom_end_drawing() {
|
|
921
|
+
engine().end_frame();
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// ============================================================
|
|
925
|
+
// Keyboard input
|
|
926
|
+
// ============================================================
|
|
927
|
+
|
|
928
|
+
// ============================================================
|
|
929
|
+
// Mouse input
|
|
930
|
+
// ============================================================
|
|
931
|
+
|
|
932
|
+
// ============================================================
|
|
933
|
+
// Shape drawing
|
|
934
|
+
// ============================================================
|
|
935
|
+
|
|
936
|
+
// ============================================================
|
|
937
|
+
// Text
|
|
938
|
+
// ============================================================
|
|
939
|
+
|
|
940
|
+
// ============================================================
|
|
941
|
+
// Textures
|
|
942
|
+
// ============================================================
|
|
943
|
+
|
|
944
|
+
// ============================================================
|
|
945
|
+
// Camera 2D
|
|
946
|
+
// ============================================================
|
|
947
|
+
|
|
948
|
+
// ============================================================
|
|
949
|
+
// 3D Camera and Drawing
|
|
950
|
+
// ============================================================
|
|
951
|
+
|
|
952
|
+
// ============================================================
|
|
953
|
+
// Joint test (skeletal animation debug)
|
|
954
|
+
// ============================================================
|
|
955
|
+
|
|
956
|
+
// ============================================================
|
|
957
|
+
// Lighting
|
|
958
|
+
// ============================================================
|
|
959
|
+
|
|
960
|
+
// ============================================================
|
|
961
|
+
// Models
|
|
962
|
+
// ============================================================
|
|
963
|
+
|
|
964
|
+
// ============================================================
|
|
965
|
+
// Phase 1c — material system FFI
|
|
966
|
+
// ============================================================
|
|
967
|
+
|
|
968
|
+
// ============================================================
|
|
969
|
+
// CoreAudio (iOS) — RemoteIO Audio Unit
|
|
970
|
+
// ============================================================
|
|
971
|
+
|
|
972
|
+
type AudioUnit = *mut c_void;
|
|
973
|
+
type OSStatus = i32;
|
|
974
|
+
type AudioUnitPropertyID = u32;
|
|
975
|
+
type AudioUnitScope = u32;
|
|
976
|
+
type AudioUnitElement = u32;
|
|
977
|
+
|
|
978
|
+
#[repr(C)]
|
|
979
|
+
#[derive(Clone, Copy)]
|
|
980
|
+
struct AudioComponentDescription {
|
|
981
|
+
component_type: u32,
|
|
982
|
+
component_sub_type: u32,
|
|
983
|
+
component_manufacturer: u32,
|
|
984
|
+
component_flags: u32,
|
|
985
|
+
component_flags_mask: u32,
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
#[repr(C)]
|
|
989
|
+
#[derive(Clone, Copy)]
|
|
990
|
+
struct AudioStreamBasicDescription {
|
|
991
|
+
sample_rate: f64,
|
|
992
|
+
format_id: u32,
|
|
993
|
+
format_flags: u32,
|
|
994
|
+
bytes_per_packet: u32,
|
|
995
|
+
frames_per_packet: u32,
|
|
996
|
+
bytes_per_frame: u32,
|
|
997
|
+
channels_per_frame: u32,
|
|
998
|
+
bits_per_channel: u32,
|
|
999
|
+
reserved: u32,
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
#[repr(C)]
|
|
1003
|
+
struct AudioBufferList {
|
|
1004
|
+
number_buffers: u32,
|
|
1005
|
+
buffers: [AudioBufferData; 1],
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
#[repr(C)]
|
|
1009
|
+
struct AudioBufferData {
|
|
1010
|
+
number_channels: u32,
|
|
1011
|
+
data_byte_size: u32,
|
|
1012
|
+
data: *mut c_void,
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
type AURenderCallback = unsafe extern "C" fn(
|
|
1016
|
+
in_ref_con: *mut c_void,
|
|
1017
|
+
io_action_flags: *mut u32,
|
|
1018
|
+
in_time_stamp: *const c_void,
|
|
1019
|
+
in_bus_number: u32,
|
|
1020
|
+
in_number_frames: u32,
|
|
1021
|
+
io_data: *mut AudioBufferList,
|
|
1022
|
+
) -> OSStatus;
|
|
1023
|
+
|
|
1024
|
+
#[repr(C)]
|
|
1025
|
+
struct AURenderCallbackStruct {
|
|
1026
|
+
input_proc: AURenderCallback,
|
|
1027
|
+
input_proc_ref_con: *mut c_void,
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
type AudioComponent = *mut c_void;
|
|
1031
|
+
|
|
1032
|
+
#[link(name = "AudioToolbox", kind = "framework")]
|
|
1033
|
+
extern "C" {
|
|
1034
|
+
fn AudioComponentFindNext(component: AudioComponent, desc: *const AudioComponentDescription) -> AudioComponent;
|
|
1035
|
+
fn AudioComponentInstanceNew(component: AudioComponent, out: *mut AudioUnit) -> OSStatus;
|
|
1036
|
+
fn AudioUnitSetProperty(
|
|
1037
|
+
unit: AudioUnit,
|
|
1038
|
+
property_id: AudioUnitPropertyID,
|
|
1039
|
+
scope: AudioUnitScope,
|
|
1040
|
+
element: AudioUnitElement,
|
|
1041
|
+
data: *const c_void,
|
|
1042
|
+
data_size: u32,
|
|
1043
|
+
) -> OSStatus;
|
|
1044
|
+
fn AudioUnitInitialize(unit: AudioUnit) -> OSStatus;
|
|
1045
|
+
fn AudioOutputUnitStart(unit: AudioUnit) -> OSStatus;
|
|
1046
|
+
fn AudioOutputUnitStop(unit: AudioUnit) -> OSStatus;
|
|
1047
|
+
fn AudioComponentInstanceDispose(unit: AudioUnit) -> OSStatus;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
const K_AUDIO_UNIT_TYPE_OUTPUT: u32 = u32::from_be_bytes(*b"auou");
|
|
1051
|
+
const K_AUDIO_UNIT_SUB_TYPE_REMOTE_IO: u32 = u32::from_be_bytes(*b"rioc");
|
|
1052
|
+
const K_AUDIO_UNIT_MANUFACTURER_APPLE: u32 = u32::from_be_bytes(*b"appl");
|
|
1053
|
+
|
|
1054
|
+
const K_AUDIO_UNIT_PROPERTY_STREAM_FORMAT: AudioUnitPropertyID = 8;
|
|
1055
|
+
const K_AUDIO_UNIT_PROPERTY_SET_RENDER_CALLBACK: AudioUnitPropertyID = 23;
|
|
1056
|
+
const K_AUDIO_UNIT_SCOPE_INPUT: AudioUnitScope = 1;
|
|
1057
|
+
|
|
1058
|
+
const K_AUDIO_FORMAT_LINEAR_PCM: u32 = u32::from_be_bytes(*b"lpcm");
|
|
1059
|
+
const K_AUDIO_FORMAT_FLAG_IS_FLOAT: u32 = 1;
|
|
1060
|
+
const K_AUDIO_FORMAT_FLAG_IS_PACKED: u32 = 8;
|
|
1061
|
+
|
|
1062
|
+
struct AudioUnitInstance { unit: AudioUnit }
|
|
1063
|
+
unsafe impl Send for AudioUnitInstance {}
|
|
1064
|
+
unsafe impl Sync for AudioUnitInstance {}
|
|
1065
|
+
|
|
1066
|
+
static mut AUDIO_UNIT: Option<AudioUnitInstance> = None;
|
|
1067
|
+
// Render half of the audio system — owned by the CoreAudio render thread
|
|
1068
|
+
// after bloom_init_audio hands it off via AudioMixer::take_renderer.
|
|
1069
|
+
// See native/shared/src/audio/mod.rs for the threading contract.
|
|
1070
|
+
static mut AUDIO_RENDERER: Option<bloom_shared::audio::AudioRenderer> = None;
|
|
1071
|
+
|
|
1072
|
+
unsafe extern "C" fn audio_render_callback(
|
|
1073
|
+
_in_ref_con: *mut c_void,
|
|
1074
|
+
_io_action_flags: *mut u32,
|
|
1075
|
+
_in_time_stamp: *const c_void,
|
|
1076
|
+
_in_bus_number: u32,
|
|
1077
|
+
in_number_frames: u32,
|
|
1078
|
+
io_data: *mut AudioBufferList,
|
|
1079
|
+
) -> OSStatus {
|
|
1080
|
+
let buffer_list = &mut *io_data;
|
|
1081
|
+
let buffer = &mut buffer_list.buffers[0];
|
|
1082
|
+
let num_samples = in_number_frames as usize * 2;
|
|
1083
|
+
let output = std::slice::from_raw_parts_mut(buffer.data as *mut f32, num_samples);
|
|
1084
|
+
match AUDIO_RENDERER.as_mut() {
|
|
1085
|
+
Some(r) => r.mix(output),
|
|
1086
|
+
None => output.iter_mut().for_each(|s| *s = 0.0),
|
|
1087
|
+
}
|
|
1088
|
+
0
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
#[no_mangle]
|
|
1092
|
+
pub extern "C" fn bloom_init_audio() {
|
|
1093
|
+
unsafe {
|
|
1094
|
+
// Configure the audio session BEFORE bringing up RemoteIO. Without a
|
|
1095
|
+
// category the session defaults to SoloAmbient: audio is silenced by
|
|
1096
|
+
// the ring/silent switch and stops when the screen locks (audit gap).
|
|
1097
|
+
// `Playback` keeps game audio audible over the silent switch. Done via
|
|
1098
|
+
// the ObjC runtime so the crate needs no AVFAudio link dependency; the
|
|
1099
|
+
// category constants' NSString VALUES equal their symbol names, so we
|
|
1100
|
+
// build the string directly rather than linking the extern constant.
|
|
1101
|
+
// NOTE: written blind (no iOS SDK here) — verify on device.
|
|
1102
|
+
if let Some(av_cls) = objc2::runtime::AnyClass::get(c"AVAudioSession") {
|
|
1103
|
+
let session: Retained<objc2::runtime::AnyObject> = msg_send![av_cls, sharedInstance];
|
|
1104
|
+
let cat = objc2_foundation::NSString::from_str("AVAudioSessionCategoryPlayback");
|
|
1105
|
+
let null_err: *mut *mut objc2::runtime::AnyObject = std::ptr::null_mut();
|
|
1106
|
+
let _: objc2::runtime::Bool = msg_send![&*session, setCategory: &*cat, error: null_err];
|
|
1107
|
+
let _: objc2::runtime::Bool = msg_send![&*session, setActive: true, error: null_err];
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Hand the render half to the audio thread before the callback
|
|
1111
|
+
// can fire. Idempotent: a second init keeps the existing renderer.
|
|
1112
|
+
if AUDIO_RENDERER.is_none() {
|
|
1113
|
+
AUDIO_RENDERER = engine().audio.take_renderer();
|
|
1114
|
+
}
|
|
1115
|
+
let desc = AudioComponentDescription {
|
|
1116
|
+
component_type: K_AUDIO_UNIT_TYPE_OUTPUT,
|
|
1117
|
+
component_sub_type: K_AUDIO_UNIT_SUB_TYPE_REMOTE_IO,
|
|
1118
|
+
component_manufacturer: K_AUDIO_UNIT_MANUFACTURER_APPLE,
|
|
1119
|
+
component_flags: 0,
|
|
1120
|
+
component_flags_mask: 0,
|
|
1121
|
+
};
|
|
1122
|
+
|
|
1123
|
+
let component = AudioComponentFindNext(std::ptr::null_mut(), &desc);
|
|
1124
|
+
if component.is_null() { return; }
|
|
1125
|
+
|
|
1126
|
+
let mut unit: AudioUnit = std::ptr::null_mut();
|
|
1127
|
+
if AudioComponentInstanceNew(component, &mut unit) != 0 { return; }
|
|
1128
|
+
|
|
1129
|
+
let stream_desc = AudioStreamBasicDescription {
|
|
1130
|
+
sample_rate: 44100.0,
|
|
1131
|
+
format_id: K_AUDIO_FORMAT_LINEAR_PCM,
|
|
1132
|
+
format_flags: K_AUDIO_FORMAT_FLAG_IS_FLOAT | K_AUDIO_FORMAT_FLAG_IS_PACKED,
|
|
1133
|
+
bytes_per_packet: 8,
|
|
1134
|
+
frames_per_packet: 1,
|
|
1135
|
+
bytes_per_frame: 8,
|
|
1136
|
+
channels_per_frame: 2,
|
|
1137
|
+
bits_per_channel: 32,
|
|
1138
|
+
reserved: 0,
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
AudioUnitSetProperty(
|
|
1142
|
+
unit, K_AUDIO_UNIT_PROPERTY_STREAM_FORMAT, K_AUDIO_UNIT_SCOPE_INPUT, 0,
|
|
1143
|
+
&stream_desc as *const _ as *const c_void,
|
|
1144
|
+
std::mem::size_of::<AudioStreamBasicDescription>() as u32,
|
|
1145
|
+
);
|
|
1146
|
+
|
|
1147
|
+
let callback_struct = AURenderCallbackStruct {
|
|
1148
|
+
input_proc: audio_render_callback,
|
|
1149
|
+
input_proc_ref_con: std::ptr::null_mut(),
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
AudioUnitSetProperty(
|
|
1153
|
+
unit, K_AUDIO_UNIT_PROPERTY_SET_RENDER_CALLBACK, K_AUDIO_UNIT_SCOPE_INPUT, 0,
|
|
1154
|
+
&callback_struct as *const _ as *const c_void,
|
|
1155
|
+
std::mem::size_of::<AURenderCallbackStruct>() as u32,
|
|
1156
|
+
);
|
|
1157
|
+
|
|
1158
|
+
AudioUnitInitialize(unit);
|
|
1159
|
+
AudioOutputUnitStart(unit);
|
|
1160
|
+
AUDIO_UNIT = Some(AudioUnitInstance { unit });
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
#[no_mangle]
|
|
1165
|
+
pub extern "C" fn bloom_close_audio() {
|
|
1166
|
+
unsafe {
|
|
1167
|
+
if let Some(au) = AUDIO_UNIT.take() {
|
|
1168
|
+
AudioOutputUnitStop(au.unit);
|
|
1169
|
+
AudioComponentInstanceDispose(au.unit);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// ============================================================
|
|
1175
|
+
// Music
|
|
1176
|
+
// ============================================================
|
|
1177
|
+
|
|
1178
|
+
// ============================================================
|
|
1179
|
+
// Gamepad input
|
|
1180
|
+
// ============================================================
|
|
1181
|
+
|
|
1182
|
+
// ============================================================
|
|
1183
|
+
// Touch input
|
|
1184
|
+
// ============================================================
|
|
1185
|
+
|
|
1186
|
+
// ============================================================
|
|
1187
|
+
// Utility
|
|
1188
|
+
// ============================================================
|
|
1189
|
+
|
|
1190
|
+
#[no_mangle]
|
|
1191
|
+
pub extern "C" fn bloom_toggle_fullscreen() {}
|
|
1192
|
+
|
|
1193
|
+
#[no_mangle]
|
|
1194
|
+
pub extern "C" fn bloom_set_window_title(_title_ptr: *const u8) {}
|
|
1195
|
+
|
|
1196
|
+
#[no_mangle]
|
|
1197
|
+
pub extern "C" fn bloom_set_window_icon(_path_ptr: *const u8) {}
|
|
1198
|
+
|
|
1199
|
+
#[no_mangle]
|
|
1200
|
+
pub extern "C" fn bloom_disable_cursor() {
|
|
1201
|
+
engine().input.cursor_disabled = true;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
#[no_mangle]
|
|
1205
|
+
pub extern "C" fn bloom_enable_cursor() {
|
|
1206
|
+
engine().input.cursor_disabled = false;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// E4: Clipboard (stub on this platform)
|
|
1210
|
+
#[no_mangle]
|
|
1211
|
+
pub extern "C" fn bloom_set_clipboard_text(_text_ptr: *const u8) {}
|
|
1212
|
+
#[no_mangle]
|
|
1213
|
+
pub extern "C" fn bloom_get_clipboard_text() -> *const u8 { std::ptr::null() }
|
|
1214
|
+
|
|
1215
|
+
// E5b: File dialogs (stub on this platform)
|
|
1216
|
+
#[no_mangle]
|
|
1217
|
+
pub extern "C" fn bloom_open_file_dialog(_filter_ptr: *const u8, _title_ptr: *const u8) -> *const u8 { std::ptr::null() }
|
|
1218
|
+
#[no_mangle]
|
|
1219
|
+
pub extern "C" fn bloom_save_file_dialog(_default_name_ptr: *const u8, _title_ptr: *const u8) -> *const u8 { std::ptr::null() }
|
|
1220
|
+
|
|
1221
|
+
// ============================================================
|
|
1222
|
+
// Input injection + platform detection
|
|
1223
|
+
// ============================================================
|
|
1224
|
+
#[no_mangle]
|
|
1225
|
+
pub extern "C" fn bloom_get_platform() -> f64 { 2.0 }
|
|
1226
|
+
|
|
1227
|
+
/// Preferred OS language packed as `c0*256+c1` (ISO-639 primary subtag). See macos lib for format.
|
|
1228
|
+
#[no_mangle]
|
|
1229
|
+
pub extern "C" fn bloom_get_language() -> f64 {
|
|
1230
|
+
fn pack(code: &str) -> f64 { let l = code.to_ascii_lowercase(); let b = l.as_bytes(); if b.len() >= 2 { (b[0] as f64) * 256.0 + (b[1] as f64) } else { 25966.0 } }
|
|
1231
|
+
let langs = objc2_foundation::NSLocale::preferredLanguages();
|
|
1232
|
+
match langs.firstObject() { Some(s) => pack(&s.to_string()), None => 25966.0 }
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// ============================================================
|
|
1236
|
+
// Thread-safe staging (for async asset loading via Perry threads)
|
|
1237
|
+
// ============================================================
|
|
1238
|
+
|
|
1239
|
+
|
|
1240
|
+
// Q6: Multi-hit picking
|
|
1241
|
+
// ============================================================
|
|
1242
|
+
|
|
1243
|
+
// ============================================================
|
|
1244
|
+
// Render quality toggles (individual + preset) — ticket 011
|
|
1245
|
+
// Mirror of the macOS FFI surface added in commit 95da6af; previously
|
|
1246
|
+
// macOS-only, now exposed on every native platform so non-macOS builds
|
|
1247
|
+
// don't fail at runtime (missing symbol) when the TS API invokes them.
|
|
1248
|
+
// ============================================================
|
|
1249
|
+
|
|
1250
|
+
// ============================================================
|
|
1251
|
+
// Profiler — CPU phase timings (always available) + GPU timestamps
|
|
1252
|
+
// (when the adapter supports TIMESTAMP_QUERY). Disabled by default.
|
|
1253
|
+
// ============================================================
|
|
1254
|
+
|
|
1255
|
+
// ============================================================
|
|
1256
|
+
// Physics (Jolt 5.x) — FFI surface generated from shared macro
|
|
1257
|
+
// ============================================================
|
|
1258
|
+
|
|
1259
|
+
#[cfg(feature = "jolt")]
|
|
1260
|
+
#[inline]
|
|
1261
|
+
fn bloom_jolt_ffi_physics() -> &'static mut bloom_shared::physics_jolt::JoltPhysics {
|
|
1262
|
+
&mut engine().jolt
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
#[cfg(feature = "jolt")]
|
|
1266
|
+
bloom_shared::define_physics_ffi!();
|
|
1267
|
+
|