@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,1302 @@
|
|
|
1
|
+
// `static mut` is intentional throughout this FFI surface — Perry calls
|
|
2
|
+
// us from a single OS thread (the macOS run-loop), so the engine
|
|
3
|
+
// singleton + scratch state never race. The 2024 lint flagging
|
|
4
|
+
// `&LAST_PICK`-style accesses is a real concern in multi-threaded
|
|
5
|
+
// code, but inapplicable here. Suppress at the crate root to avoid
|
|
6
|
+
// 16+ noise lines in every build.
|
|
7
|
+
#![allow(static_mut_refs)]
|
|
8
|
+
|
|
9
|
+
use bloom_shared::engine::EngineState;
|
|
10
|
+
use bloom_shared::string_header::{str_from_header, alloc_perry_string};
|
|
11
|
+
|
|
12
|
+
use objc2::rc::Retained;
|
|
13
|
+
use objc2::{msg_send, MainThreadMarker, MainThreadOnly};
|
|
14
|
+
use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSEventMask, NSEventType, NSWindow, NSWindowStyleMask};
|
|
15
|
+
use objc2_foundation::{NSDate, NSDefaultRunLoopMode, NSPoint, NSRect, NSSize, NSString};
|
|
16
|
+
|
|
17
|
+
use raw_window_handle::{RawWindowHandle, AppKitWindowHandle};
|
|
18
|
+
use std::sync::OnceLock;
|
|
19
|
+
|
|
20
|
+
static mut ENGINE: OnceLock<EngineState> = OnceLock::new();
|
|
21
|
+
static mut WINDOW: Option<Retained<NSWindow>> = None;
|
|
22
|
+
// Set by bloom_init_window when BLOOM_HEADLESS=1. Skips the
|
|
23
|
+
// `!isVisible → should_close` shortcut (hidden windows aren't
|
|
24
|
+
// 'closed', just invisible) so headless --capture can run to
|
|
25
|
+
// completion.
|
|
26
|
+
static mut HEADLESS: bool = false;
|
|
27
|
+
static mut AUDIO_UNIT: Option<AudioUnitInstance> = None;
|
|
28
|
+
// Render half of the audio system. Moved here from EngineState by
|
|
29
|
+
// bloom_init_audio (AudioMixer::take_renderer) BEFORE the CoreAudio
|
|
30
|
+
// callback starts; after that it is owned exclusively by the audio
|
|
31
|
+
// render thread. The old design mixed through ENGINE from the callback —
|
|
32
|
+
// a cross-thread data race with every play/stop call on the main thread.
|
|
33
|
+
static mut AUDIO_RENDERER: Option<bloom_shared::audio::AudioRenderer> = None;
|
|
34
|
+
|
|
35
|
+
fn engine() -> &'static mut EngineState {
|
|
36
|
+
unsafe { ENGINE.get_mut().expect("Engine not initialized") }
|
|
37
|
+
}
|
|
38
|
+
/// Asset-path hook for define_core_ffi! — identity on desktop, where game
|
|
39
|
+
/// asset paths are valid relative to the working directory.
|
|
40
|
+
fn bloom_resolve_asset_path(path: &str) -> std::borrow::Cow<'_, str> {
|
|
41
|
+
std::borrow::Cow::Borrowed(path)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The full shared (non-physics) FFI surface. See bloom_shared::ffi_core
|
|
45
|
+
// docs for the contract; tools/validate-ffi.js checks parity in CI.
|
|
46
|
+
bloom_shared::define_core_ffi!();
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
/// Map macOS virtual key code to Bloom key code.
|
|
50
|
+
fn map_keycode(keycode: u16) -> usize {
|
|
51
|
+
match keycode {
|
|
52
|
+
0 => 65, // A
|
|
53
|
+
1 => 83, // S
|
|
54
|
+
2 => 68, // D
|
|
55
|
+
3 => 70, // F
|
|
56
|
+
4 => 72, // H
|
|
57
|
+
5 => 71, // G
|
|
58
|
+
6 => 90, // Z
|
|
59
|
+
7 => 88, // X
|
|
60
|
+
8 => 67, // C
|
|
61
|
+
9 => 86, // V
|
|
62
|
+
11 => 66, // B
|
|
63
|
+
12 => 81, // Q
|
|
64
|
+
13 => 87, // W
|
|
65
|
+
14 => 69, // E
|
|
66
|
+
15 => 82, // R
|
|
67
|
+
16 => 89, // Y
|
|
68
|
+
17 => 84, // T
|
|
69
|
+
18 => 49, // 1
|
|
70
|
+
19 => 50, // 2
|
|
71
|
+
20 => 51, // 3
|
|
72
|
+
21 => 52, // 4
|
|
73
|
+
22 => 54, // 6
|
|
74
|
+
23 => 53, // 5
|
|
75
|
+
24 => 61, // =
|
|
76
|
+
25 => 57, // 9
|
|
77
|
+
26 => 55, // 7
|
|
78
|
+
27 => 45, // -
|
|
79
|
+
28 => 56, // 8
|
|
80
|
+
29 => 48, // 0
|
|
81
|
+
30 => 93, // ]
|
|
82
|
+
31 => 79, // O
|
|
83
|
+
32 => 85, // U
|
|
84
|
+
33 => 91, // [
|
|
85
|
+
34 => 73, // I
|
|
86
|
+
35 => 80, // P
|
|
87
|
+
36 => 265, // Enter (mapped to Bloom ENTER = 265)
|
|
88
|
+
37 => 76, // L
|
|
89
|
+
38 => 74, // J
|
|
90
|
+
39 => 39, // '
|
|
91
|
+
40 => 75, // K
|
|
92
|
+
41 => 59, // ;
|
|
93
|
+
42 => 92, // backslash
|
|
94
|
+
43 => 44, // ,
|
|
95
|
+
44 => 47, // /
|
|
96
|
+
45 => 78, // N
|
|
97
|
+
46 => 77, // M
|
|
98
|
+
47 => 46, // .
|
|
99
|
+
48 => 9, // Tab
|
|
100
|
+
49 => 32, // Space
|
|
101
|
+
50 => 96, // `
|
|
102
|
+
51 => 8, // Backspace
|
|
103
|
+
53 => 27, // Escape
|
|
104
|
+
// Arrow keys
|
|
105
|
+
123 => 258, // Left
|
|
106
|
+
124 => 259, // Right
|
|
107
|
+
125 => 257, // Down
|
|
108
|
+
126 => 256, // Up
|
|
109
|
+
// Function keys
|
|
110
|
+
122 => 112, // F1
|
|
111
|
+
120 => 113, // F2
|
|
112
|
+
99 => 114, // F3
|
|
113
|
+
118 => 115, // F4
|
|
114
|
+
96 => 116, // F5
|
|
115
|
+
97 => 117, // F6
|
|
116
|
+
98 => 118, // F7
|
|
117
|
+
100 => 119, // F8
|
|
118
|
+
101 => 120, // F9
|
|
119
|
+
109 => 121, // F10
|
|
120
|
+
103 => 122, // F11
|
|
121
|
+
111 => 123, // F12
|
|
122
|
+
// Modifiers
|
|
123
|
+
56 => 280, // Left Shift
|
|
124
|
+
60 => 281, // Right Shift
|
|
125
|
+
59 => 282, // Left Control
|
|
126
|
+
62 => 283, // Right Control
|
|
127
|
+
58 => 284, // Left Alt/Option
|
|
128
|
+
61 => 285, // Right Alt/Option
|
|
129
|
+
55 => 286, // Left Command
|
|
130
|
+
54 => 287, // Right Command
|
|
131
|
+
_ => 0,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============================================================
|
|
136
|
+
// CoreAudio FFI types and setup
|
|
137
|
+
// ============================================================
|
|
138
|
+
|
|
139
|
+
type AudioUnit = *mut std::ffi::c_void;
|
|
140
|
+
type OSStatus = i32;
|
|
141
|
+
type AudioUnitPropertyID = u32;
|
|
142
|
+
type AudioUnitScope = u32;
|
|
143
|
+
type AudioUnitElement = u32;
|
|
144
|
+
|
|
145
|
+
#[repr(C)]
|
|
146
|
+
#[derive(Clone, Copy)]
|
|
147
|
+
struct AudioComponentDescription {
|
|
148
|
+
component_type: u32,
|
|
149
|
+
component_sub_type: u32,
|
|
150
|
+
component_manufacturer: u32,
|
|
151
|
+
component_flags: u32,
|
|
152
|
+
component_flags_mask: u32,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#[repr(C)]
|
|
156
|
+
#[derive(Clone, Copy)]
|
|
157
|
+
struct AudioStreamBasicDescription {
|
|
158
|
+
sample_rate: f64,
|
|
159
|
+
format_id: u32,
|
|
160
|
+
format_flags: u32,
|
|
161
|
+
bytes_per_packet: u32,
|
|
162
|
+
frames_per_packet: u32,
|
|
163
|
+
bytes_per_frame: u32,
|
|
164
|
+
channels_per_frame: u32,
|
|
165
|
+
bits_per_channel: u32,
|
|
166
|
+
reserved: u32,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#[repr(C)]
|
|
170
|
+
struct AudioBufferList {
|
|
171
|
+
number_buffers: u32,
|
|
172
|
+
buffers: [AudioBuffer; 1],
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#[repr(C)]
|
|
176
|
+
struct AudioBuffer {
|
|
177
|
+
number_channels: u32,
|
|
178
|
+
data_byte_size: u32,
|
|
179
|
+
data: *mut std::ffi::c_void,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
type AURenderCallback = unsafe extern "C" fn(
|
|
183
|
+
in_ref_con: *mut std::ffi::c_void,
|
|
184
|
+
io_action_flags: *mut u32,
|
|
185
|
+
in_time_stamp: *const std::ffi::c_void,
|
|
186
|
+
in_bus_number: u32,
|
|
187
|
+
in_number_frames: u32,
|
|
188
|
+
io_data: *mut AudioBufferList,
|
|
189
|
+
) -> OSStatus;
|
|
190
|
+
|
|
191
|
+
#[repr(C)]
|
|
192
|
+
struct AURenderCallbackStruct {
|
|
193
|
+
input_proc: AURenderCallback,
|
|
194
|
+
input_proc_ref_con: *mut std::ffi::c_void,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
type AudioComponent = *mut std::ffi::c_void;
|
|
198
|
+
|
|
199
|
+
#[link(name = "AudioToolbox", kind = "framework")]
|
|
200
|
+
extern "C" {
|
|
201
|
+
fn AudioComponentFindNext(component: AudioComponent, desc: *const AudioComponentDescription) -> AudioComponent;
|
|
202
|
+
fn AudioComponentInstanceNew(component: AudioComponent, out: *mut AudioUnit) -> OSStatus;
|
|
203
|
+
fn AudioUnitSetProperty(
|
|
204
|
+
unit: AudioUnit,
|
|
205
|
+
property_id: AudioUnitPropertyID,
|
|
206
|
+
scope: AudioUnitScope,
|
|
207
|
+
element: AudioUnitElement,
|
|
208
|
+
data: *const std::ffi::c_void,
|
|
209
|
+
data_size: u32,
|
|
210
|
+
) -> OSStatus;
|
|
211
|
+
fn AudioUnitInitialize(unit: AudioUnit) -> OSStatus;
|
|
212
|
+
fn AudioOutputUnitStart(unit: AudioUnit) -> OSStatus;
|
|
213
|
+
fn AudioOutputUnitStop(unit: AudioUnit) -> OSStatus;
|
|
214
|
+
fn AudioComponentInstanceDispose(unit: AudioUnit) -> OSStatus;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const K_AUDIO_UNIT_TYPE_OUTPUT: u32 = u32::from_be_bytes(*b"auou");
|
|
218
|
+
const K_AUDIO_UNIT_SUB_TYPE_DEFAULT_OUTPUT: u32 = u32::from_be_bytes(*b"def ");
|
|
219
|
+
const K_AUDIO_UNIT_MANUFACTURER_APPLE: u32 = u32::from_be_bytes(*b"appl");
|
|
220
|
+
|
|
221
|
+
const K_AUDIO_UNIT_PROPERTY_STREAM_FORMAT: AudioUnitPropertyID = 8;
|
|
222
|
+
const K_AUDIO_UNIT_PROPERTY_SET_RENDER_CALLBACK: AudioUnitPropertyID = 23;
|
|
223
|
+
const K_AUDIO_UNIT_SCOPE_INPUT: AudioUnitScope = 1;
|
|
224
|
+
|
|
225
|
+
const K_AUDIO_FORMAT_LINEAR_PCM: u32 = u32::from_be_bytes(*b"lpcm");
|
|
226
|
+
const K_AUDIO_FORMAT_FLAG_IS_FLOAT: u32 = 1;
|
|
227
|
+
const K_AUDIO_FORMAT_FLAG_IS_PACKED: u32 = 8;
|
|
228
|
+
|
|
229
|
+
struct AudioUnitInstance {
|
|
230
|
+
unit: AudioUnit,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Safety: AudioUnit is accessed only from audio thread callback + main thread init/deinit
|
|
234
|
+
unsafe impl Send for AudioUnitInstance {}
|
|
235
|
+
unsafe impl Sync for AudioUnitInstance {}
|
|
236
|
+
|
|
237
|
+
unsafe extern "C" fn audio_render_callback(
|
|
238
|
+
_in_ref_con: *mut std::ffi::c_void,
|
|
239
|
+
_io_action_flags: *mut u32,
|
|
240
|
+
_in_time_stamp: *const std::ffi::c_void,
|
|
241
|
+
_in_bus_number: u32,
|
|
242
|
+
in_number_frames: u32,
|
|
243
|
+
io_data: *mut AudioBufferList,
|
|
244
|
+
) -> OSStatus {
|
|
245
|
+
let buffer_list = &mut *io_data;
|
|
246
|
+
let buffer = &mut buffer_list.buffers[0];
|
|
247
|
+
let num_samples = in_number_frames as usize * 2; // stereo
|
|
248
|
+
let output = std::slice::from_raw_parts_mut(
|
|
249
|
+
buffer.data as *mut f32,
|
|
250
|
+
num_samples,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
match AUDIO_RENDERER.as_mut() {
|
|
254
|
+
Some(r) => r.mix(output),
|
|
255
|
+
None => output.iter_mut().for_each(|s| *s = 0.0),
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
0 // noErr
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ============================================================
|
|
262
|
+
// FFI entry points
|
|
263
|
+
// ============================================================
|
|
264
|
+
|
|
265
|
+
#[no_mangle]
|
|
266
|
+
pub extern "C" fn bloom_init_window(width: f64, height: f64, title_ptr: *const u8, fullscreen: f64) {
|
|
267
|
+
let title = str_from_header(title_ptr);
|
|
268
|
+
let mtm = MainThreadMarker::from(unsafe { MainThreadMarker::new_unchecked() });
|
|
269
|
+
|
|
270
|
+
// Headless mode: BLOOM_HEADLESS=1 keeps the NSWindow + CAMetalLayer
|
|
271
|
+
// alive (wgpu's Metal backend requires a CAMetalLayer-backed view)
|
|
272
|
+
// but hides the window and suppresses dock icon / focus. Needed
|
|
273
|
+
// so an agent can spin up the renderer in a batch loop without
|
|
274
|
+
// stealing the user's focus on every sample.
|
|
275
|
+
let headless = std::env::var("BLOOM_HEADLESS")
|
|
276
|
+
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
|
277
|
+
.unwrap_or(false);
|
|
278
|
+
unsafe { HEADLESS = headless; }
|
|
279
|
+
|
|
280
|
+
let app = NSApplication::sharedApplication(mtm);
|
|
281
|
+
if headless {
|
|
282
|
+
// Prohibited = no dock icon, no menu bar, no activation.
|
|
283
|
+
app.setActivationPolicy(NSApplicationActivationPolicy::Prohibited);
|
|
284
|
+
} else {
|
|
285
|
+
app.setActivationPolicy(NSApplicationActivationPolicy::Regular);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Far-off-screen origin keeps the window out of every display
|
|
289
|
+
// even if the OS insists on showing something.
|
|
290
|
+
let origin_x = if headless { -20000.0 } else { 100.0 };
|
|
291
|
+
let content_rect = NSRect::new(NSPoint::new(origin_x, 100.0), NSSize::new(width, height));
|
|
292
|
+
let style = NSWindowStyleMask::Titled
|
|
293
|
+
| NSWindowStyleMask::Closable
|
|
294
|
+
| NSWindowStyleMask::Miniaturizable
|
|
295
|
+
| NSWindowStyleMask::Resizable;
|
|
296
|
+
|
|
297
|
+
let window = unsafe {
|
|
298
|
+
NSWindow::initWithContentRect_styleMask_backing_defer(
|
|
299
|
+
NSWindow::alloc(mtm),
|
|
300
|
+
content_rect,
|
|
301
|
+
style,
|
|
302
|
+
objc2_app_kit::NSBackingStoreType(2), // NSBackingStoreBuffered
|
|
303
|
+
false,
|
|
304
|
+
)
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
let ns_title = NSString::from_str(title);
|
|
308
|
+
window.setTitle(&ns_title);
|
|
309
|
+
|
|
310
|
+
// Don't persist window size/fullscreen state across launches.
|
|
311
|
+
// NSWindow restoration was resurrecting a prior fullscreen toggle on
|
|
312
|
+
// the 4K display, which silently rendered benchmarks at 4× the
|
|
313
|
+
// requested pixel count.
|
|
314
|
+
unsafe { let _: () = msg_send![&window, setRestorable: false]; }
|
|
315
|
+
|
|
316
|
+
// BLOOM_NO_FULLSCREEN=1 hard-disables fullscreen capability: the
|
|
317
|
+
// window cannot be entered into fullscreen via the green button,
|
|
318
|
+
// cmd-ctrl-F, or inheriting a fullscreen Space from the launching
|
|
319
|
+
// terminal. Intended for benchmark harnesses where the 4K-display
|
|
320
|
+
// fullscreen path would otherwise silently quadruple render cost.
|
|
321
|
+
let no_fullscreen = std::env::var("BLOOM_NO_FULLSCREEN")
|
|
322
|
+
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
|
323
|
+
.unwrap_or(false);
|
|
324
|
+
if no_fullscreen {
|
|
325
|
+
window.setCollectionBehavior(objc2_app_kit::NSWindowCollectionBehavior::FullScreenNone);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if headless {
|
|
329
|
+
// Alpha 0 + off-screen origin + prohibited-activation app
|
|
330
|
+
// policy = fully invisible window that still backs the
|
|
331
|
+
// CAMetalLayer wgpu renders into. Don't call
|
|
332
|
+
// `makeKeyAndOrderFront` — that brings it to front.
|
|
333
|
+
unsafe {
|
|
334
|
+
let _: () = msg_send![&window, setAlphaValue: 0.0_f64];
|
|
335
|
+
let _: () = msg_send![&window, setIgnoresMouseEvents: true];
|
|
336
|
+
let _: () = msg_send![&window, orderOut: std::ptr::null::<objc2::runtime::AnyObject>()];
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
window.center();
|
|
340
|
+
window.makeKeyAndOrderFront(None);
|
|
341
|
+
#[allow(deprecated)]
|
|
342
|
+
app.activateIgnoringOtherApps(true);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Set up CAMetalLayer on the content view
|
|
346
|
+
let content_view = window.contentView().expect("No content view");
|
|
347
|
+
unsafe {
|
|
348
|
+
let _: () = msg_send![&content_view, setWantsLayer: true];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Build the wgpu Metal surface on the content view and bring up the
|
|
352
|
+
// engine. Surface / adapter / device / swapchain creation is shared
|
|
353
|
+
// with the host-attach path (PerryTS/perry#5519) — see
|
|
354
|
+
// `bloom_shared::attach::attach_engine`; the only macOS-specific work
|
|
355
|
+
// here is producing the AppKit raw-window-handle and the HiDPI scale.
|
|
356
|
+
//
|
|
357
|
+
// Retina/HiDPI: AppKit reports window dimensions in points, but the
|
|
358
|
+
// CAMetalLayer drawable needs physical pixels or AppKit bilinearly
|
|
359
|
+
// upscales a low-res image. `backingScaleFactor` is 2.0 on Retina,
|
|
360
|
+
// 1.0 otherwise (tracks the window's current screen).
|
|
361
|
+
let scale: f64 = unsafe { msg_send![&*window, backingScaleFactor] };
|
|
362
|
+
let scale = if scale > 0.0 { scale } else { 1.0 };
|
|
363
|
+
|
|
364
|
+
let target = {
|
|
365
|
+
let view_ptr = Retained::as_ptr(&content_view) as *mut std::ffi::c_void;
|
|
366
|
+
let handle = AppKitWindowHandle::new(std::ptr::NonNull::new(view_ptr).unwrap());
|
|
367
|
+
wgpu::SurfaceTargetUnsafe::RawHandle {
|
|
368
|
+
raw_display_handle: Some(raw_window_handle::RawDisplayHandle::AppKit(
|
|
369
|
+
raw_window_handle::AppKitDisplayHandle::new(),
|
|
370
|
+
)),
|
|
371
|
+
raw_window_handle: RawWindowHandle::AppKit(handle),
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
let engine_state = unsafe {
|
|
375
|
+
bloom_shared::attach::attach_engine(
|
|
376
|
+
target,
|
|
377
|
+
bloom_shared::attach::AttachParams {
|
|
378
|
+
backends: wgpu::Backends::METAL,
|
|
379
|
+
logical_w: width as u32,
|
|
380
|
+
logical_h: height as u32,
|
|
381
|
+
physical_w: ((width * scale) as u32).max(1),
|
|
382
|
+
physical_h: ((height * scale) as u32).max(1),
|
|
383
|
+
format: bloom_shared::attach::FormatPreference::Srgb,
|
|
384
|
+
},
|
|
385
|
+
)
|
|
386
|
+
.expect("Failed to attach engine")
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
unsafe {
|
|
390
|
+
let _ = ENGINE.set(engine_state);
|
|
391
|
+
WINDOW = Some(window);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Register Bloom's GPU screenshot capture with perry-geisterhand (if linked)
|
|
395
|
+
bloom_register_geisterhand_screenshot();
|
|
396
|
+
|
|
397
|
+
if fullscreen != 0.0 {
|
|
398
|
+
bloom_toggle_fullscreen();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/// Attach the engine to a host-owned `NSView` instead of creating an
|
|
403
|
+
/// NSWindow (PerryTS/perry#5519). The host (e.g. Perry UI's `BloomView`)
|
|
404
|
+
/// passes the raw `NSView*` as `handle`; `width`/`height` are the view's
|
|
405
|
+
/// size in points. The engine builds its Metal surface on the view's
|
|
406
|
+
/// CAMetalLayer and stores its singleton, after which the normal
|
|
407
|
+
/// `bloom_begin_drawing` / `bloom_end_drawing` loop renders into it.
|
|
408
|
+
///
|
|
409
|
+
/// Returns 1.0 on success, 0.0 on failure (null/invalid handle, called
|
|
410
|
+
/// off the main thread, or surface bring-up failed). Idempotent: a second
|
|
411
|
+
/// call is a no-op once the engine exists.
|
|
412
|
+
#[no_mangle]
|
|
413
|
+
pub extern "C" fn bloom_attach_native(handle: i64, width: f64, height: f64) -> f64 {
|
|
414
|
+
if handle == 0 {
|
|
415
|
+
return 0.0;
|
|
416
|
+
}
|
|
417
|
+
// wgpu's Metal surface + AppKit msg_sends require the main thread.
|
|
418
|
+
if MainThreadMarker::new().is_none() {
|
|
419
|
+
return 0.0;
|
|
420
|
+
}
|
|
421
|
+
if unsafe { ENGINE.get() }.is_some() {
|
|
422
|
+
return 1.0; // already attached
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
let view_ptr = handle as *mut std::ffi::c_void;
|
|
426
|
+
let Some(view_nn) = std::ptr::NonNull::new(view_ptr) else {
|
|
427
|
+
return 0.0;
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// Ensure the host view is layer-backed (CAMetalLayer) and resolve the
|
|
431
|
+
// backing scale from its window — or the main screen if it isn't in a
|
|
432
|
+
// window yet — so the drawable is sized in physical pixels.
|
|
433
|
+
let scale: f64 = unsafe {
|
|
434
|
+
let view: &objc2::runtime::AnyObject = &*(view_ptr as *const objc2::runtime::AnyObject);
|
|
435
|
+
let _: () = msg_send![view, setWantsLayer: true];
|
|
436
|
+
let window: *mut objc2::runtime::AnyObject = msg_send![view, window];
|
|
437
|
+
let s: f64 = if !window.is_null() {
|
|
438
|
+
msg_send![window, backingScaleFactor]
|
|
439
|
+
} else {
|
|
440
|
+
let screen: *mut objc2::runtime::AnyObject =
|
|
441
|
+
msg_send![objc2::class!(NSScreen), mainScreen];
|
|
442
|
+
if !screen.is_null() {
|
|
443
|
+
msg_send![screen, backingScaleFactor]
|
|
444
|
+
} else {
|
|
445
|
+
1.0
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
if s > 0.0 { s } else { 1.0 }
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
let target = {
|
|
452
|
+
let handle = AppKitWindowHandle::new(view_nn);
|
|
453
|
+
wgpu::SurfaceTargetUnsafe::RawHandle {
|
|
454
|
+
raw_display_handle: Some(raw_window_handle::RawDisplayHandle::AppKit(
|
|
455
|
+
raw_window_handle::AppKitDisplayHandle::new(),
|
|
456
|
+
)),
|
|
457
|
+
raw_window_handle: RawWindowHandle::AppKit(handle),
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
let engine_state = match unsafe {
|
|
461
|
+
bloom_shared::attach::attach_engine(
|
|
462
|
+
target,
|
|
463
|
+
bloom_shared::attach::AttachParams {
|
|
464
|
+
backends: wgpu::Backends::METAL,
|
|
465
|
+
logical_w: width as u32,
|
|
466
|
+
logical_h: height as u32,
|
|
467
|
+
physical_w: ((width * scale) as u32).max(1),
|
|
468
|
+
physical_h: ((height * scale) as u32).max(1),
|
|
469
|
+
format: bloom_shared::attach::FormatPreference::Srgb,
|
|
470
|
+
},
|
|
471
|
+
)
|
|
472
|
+
} {
|
|
473
|
+
Ok(es) => es,
|
|
474
|
+
Err(_) => return 0.0,
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
unsafe {
|
|
478
|
+
let _ = ENGINE.set(engine_state);
|
|
479
|
+
}
|
|
480
|
+
// Host owns the run loop in embedded mode; there is no NSWindow to
|
|
481
|
+
// store. Register the screenshot hook as the windowed path does.
|
|
482
|
+
bloom_register_geisterhand_screenshot();
|
|
483
|
+
1.0
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/// Resize the engine's surface (#70 parity; used by host-driven
|
|
487
|
+
/// BloomViews on layout changes). `phys_*` physical px, `log_*` logical.
|
|
488
|
+
#[no_mangle]
|
|
489
|
+
pub extern "C" fn bloom_resize(phys_w: f64, phys_h: f64, log_w: f64, log_h: f64) {
|
|
490
|
+
if let Some(eng) = unsafe { ENGINE.get_mut() } {
|
|
491
|
+
eng.renderer.resize(phys_w as u32, phys_h as u32, log_w as u32, log_h as u32);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/// HWND host-embed (#70) — Windows only; a no-op here for FFI-manifest
|
|
496
|
+
/// parity. Non-Windows hosts attach via `bloom_attach_native`.
|
|
497
|
+
#[no_mangle]
|
|
498
|
+
pub extern "C" fn bloom_attach_hwnd(_hwnd_bits: f64, _width: f64, _height: f64) {}
|
|
499
|
+
|
|
500
|
+
#[no_mangle]
|
|
501
|
+
pub extern "C" fn bloom_close_window() {
|
|
502
|
+
unsafe {
|
|
503
|
+
WINDOW = None;
|
|
504
|
+
}
|
|
505
|
+
// Dropping the NSWindow alone never flipped the loop's exit flag — a
|
|
506
|
+
// game's quit path calling closeWindow() would keep running headless.
|
|
507
|
+
engine().should_close = true;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
#[no_mangle]
|
|
511
|
+
pub extern "C" fn bloom_window_should_close() -> f64 {
|
|
512
|
+
if engine().should_close { 1.0 } else { 0.0 }
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/// Poll the first connected game controller (MFi / Xbox / PlayStation) through
|
|
516
|
+
/// the GameController framework and feed it into the shared input state.
|
|
517
|
+
///
|
|
518
|
+
/// Resolved dynamically via the ObjC runtime (`AnyClass::get`) so the crate
|
|
519
|
+
/// needs no GameController link dependency; a no-op when the framework or a
|
|
520
|
+
/// controller is absent. macOS had NO gamepad support before this — the FFI
|
|
521
|
+
/// read `engine().input`, which nothing populated, so `isGamepadAvailable`
|
|
522
|
+
/// was permanently false (audit finding). Button/axis indices match the
|
|
523
|
+
/// tvOS/visionOS map so a game's controls carry across Apple targets.
|
|
524
|
+
///
|
|
525
|
+
/// `GCControllerAxisInput.value` / `GCControllerButtonInput.value` are ObjC
|
|
526
|
+
/// `float`, so they are read as `f32` (reading them as `f64` is an ABI
|
|
527
|
+
/// mismatch — float and double return in different-width registers).
|
|
528
|
+
fn poll_game_controllers() {
|
|
529
|
+
use objc2::runtime::{AnyClass, AnyObject, Bool};
|
|
530
|
+
// Read `isPressed` off a `GCControllerButtonInput` and, if set, mark the
|
|
531
|
+
// mapped button down. `$btn` is any expression yielding the button object.
|
|
532
|
+
macro_rules! btn_down {
|
|
533
|
+
($btn:expr, $eng:expr, $idx:expr) => {{
|
|
534
|
+
let b: Retained<AnyObject> = $btn;
|
|
535
|
+
let pressed: Bool = msg_send![&*b, isPressed];
|
|
536
|
+
if pressed.as_bool() { $eng.input.set_gamepad_button_down($idx); }
|
|
537
|
+
}};
|
|
538
|
+
}
|
|
539
|
+
unsafe {
|
|
540
|
+
let gc_cls = match AnyClass::get(c"GCController") {
|
|
541
|
+
Some(c) => c,
|
|
542
|
+
None => return, // framework not present
|
|
543
|
+
};
|
|
544
|
+
let controllers: Retained<AnyObject> = msg_send![gc_cls, controllers];
|
|
545
|
+
let count: usize = msg_send![&*controllers, count];
|
|
546
|
+
if count == 0 {
|
|
547
|
+
return; // no pad: leave any injected gamepad state untouched
|
|
548
|
+
}
|
|
549
|
+
let controller: Retained<AnyObject> = msg_send![&*controllers, objectAtIndex: 0usize];
|
|
550
|
+
let extended: *mut AnyObject = msg_send![&*controller, extendedGamepad];
|
|
551
|
+
if extended.is_null() {
|
|
552
|
+
return; // e.g. a Siri Remote micro-gamepad; macOS games want a real pad
|
|
553
|
+
}
|
|
554
|
+
let extended: &AnyObject = &*extended;
|
|
555
|
+
|
|
556
|
+
let eng = engine();
|
|
557
|
+
// Clear last frame's polled inputs first: a poll only SETS the
|
|
558
|
+
// currently-pressed buttons/axes, so without this a released button
|
|
559
|
+
// stays down (begin_frame does not reset gamepad state).
|
|
560
|
+
eng.input.reset_gamepad();
|
|
561
|
+
eng.input.gamepad_available = true;
|
|
562
|
+
|
|
563
|
+
// Thumbsticks -> axes 0..3 (Y inverted so screen-up is +1).
|
|
564
|
+
let ls: Retained<AnyObject> = msg_send![extended, leftThumbstick];
|
|
565
|
+
let ls_x: Retained<AnyObject> = msg_send![&*ls, xAxis];
|
|
566
|
+
let ls_y: Retained<AnyObject> = msg_send![&*ls, yAxis];
|
|
567
|
+
let lx: f32 = msg_send![&*ls_x, value];
|
|
568
|
+
let ly: f32 = msg_send![&*ls_y, value];
|
|
569
|
+
eng.input.set_gamepad_axis(0, lx);
|
|
570
|
+
eng.input.set_gamepad_axis(1, -ly);
|
|
571
|
+
let rs: Retained<AnyObject> = msg_send![extended, rightThumbstick];
|
|
572
|
+
let rs_x: Retained<AnyObject> = msg_send![&*rs, xAxis];
|
|
573
|
+
let rs_y: Retained<AnyObject> = msg_send![&*rs, yAxis];
|
|
574
|
+
let rx: f32 = msg_send![&*rs_x, value];
|
|
575
|
+
let ry: f32 = msg_send![&*rs_y, value];
|
|
576
|
+
eng.input.set_gamepad_axis(2, rx);
|
|
577
|
+
eng.input.set_gamepad_axis(3, -ry);
|
|
578
|
+
|
|
579
|
+
// Face buttons A/B/X/Y -> 0/1/2/3.
|
|
580
|
+
btn_down!(msg_send![extended, buttonA], eng, 0);
|
|
581
|
+
btn_down!(msg_send![extended, buttonB], eng, 1);
|
|
582
|
+
btn_down!(msg_send![extended, buttonX], eng, 2);
|
|
583
|
+
btn_down!(msg_send![extended, buttonY], eng, 3);
|
|
584
|
+
// Shoulders -> 4/5.
|
|
585
|
+
btn_down!(msg_send![extended, leftShoulder], eng, 4);
|
|
586
|
+
btn_down!(msg_send![extended, rightShoulder], eng, 5);
|
|
587
|
+
// D-pad -> 12/13/14/15 (up/down/left/right).
|
|
588
|
+
let dpad: Retained<AnyObject> = msg_send![extended, dpad];
|
|
589
|
+
btn_down!(msg_send![&*dpad, up], eng, 12);
|
|
590
|
+
btn_down!(msg_send![&*dpad, down], eng, 13);
|
|
591
|
+
btn_down!(msg_send![&*dpad, left], eng, 14);
|
|
592
|
+
btn_down!(msg_send![&*dpad, right], eng, 15);
|
|
593
|
+
|
|
594
|
+
// Analog triggers -> axes 4/5.
|
|
595
|
+
let lt: Retained<AnyObject> = msg_send![extended, leftTrigger];
|
|
596
|
+
let rt: Retained<AnyObject> = msg_send![extended, rightTrigger];
|
|
597
|
+
let ltv: f32 = msg_send![&*lt, value];
|
|
598
|
+
let rtv: f32 = msg_send![&*rt, value];
|
|
599
|
+
eng.input.set_gamepad_axis(4, ltv);
|
|
600
|
+
eng.input.set_gamepad_axis(5, rtv);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
#[no_mangle]
|
|
605
|
+
pub extern "C" fn bloom_begin_drawing() {
|
|
606
|
+
// Poll events
|
|
607
|
+
let mtm = unsafe { MainThreadMarker::new_unchecked() };
|
|
608
|
+
let app = NSApplication::sharedApplication(mtm);
|
|
609
|
+
|
|
610
|
+
loop {
|
|
611
|
+
let event = unsafe {
|
|
612
|
+
app.nextEventMatchingMask_untilDate_inMode_dequeue(
|
|
613
|
+
NSEventMask::Any,
|
|
614
|
+
Some(&NSDate::distantPast()),
|
|
615
|
+
NSDefaultRunLoopMode,
|
|
616
|
+
true,
|
|
617
|
+
)
|
|
618
|
+
};
|
|
619
|
+
match event {
|
|
620
|
+
Some(event) => {
|
|
621
|
+
let event_type = event.r#type();
|
|
622
|
+
match event_type {
|
|
623
|
+
NSEventType::KeyDown => {
|
|
624
|
+
let keycode = event.keyCode();
|
|
625
|
+
let bloom_key = map_keycode(keycode);
|
|
626
|
+
if bloom_key > 0 {
|
|
627
|
+
engine().input.set_key_down(bloom_key);
|
|
628
|
+
}
|
|
629
|
+
// Extract typed characters for text input (E3b).
|
|
630
|
+
let chars_obj = event.characters();
|
|
631
|
+
if let Some(chars) = chars_obj {
|
|
632
|
+
let s = chars.to_string();
|
|
633
|
+
for c in s.chars() {
|
|
634
|
+
let cp = c as u32;
|
|
635
|
+
// Filter out control characters (keep printable + backspace).
|
|
636
|
+
if cp >= 32 || cp == 8 || cp == 13 || cp == 9 {
|
|
637
|
+
engine().input.push_char(cp);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
NSEventType::KeyUp => {
|
|
643
|
+
let keycode = event.keyCode();
|
|
644
|
+
let bloom_key = map_keycode(keycode);
|
|
645
|
+
if bloom_key > 0 {
|
|
646
|
+
engine().input.set_key_up(bloom_key);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
NSEventType::MouseMoved | NSEventType::LeftMouseDragged | NSEventType::RightMouseDragged => {
|
|
650
|
+
if engine().input.cursor_disabled {
|
|
651
|
+
// In disabled-cursor mode, use raw deltas from NSEvent
|
|
652
|
+
let dx: f64 = unsafe { msg_send![&*event, deltaX] };
|
|
653
|
+
let dy: f64 = unsafe { msg_send![&*event, deltaY] };
|
|
654
|
+
engine().input.accumulate_mouse_delta(dx, dy);
|
|
655
|
+
} else if let Some(window) = unsafe { &WINDOW } {
|
|
656
|
+
let loc = event.locationInWindow();
|
|
657
|
+
let frame = window.contentView().map(|v| v.frame()).unwrap_or(NSRect::ZERO);
|
|
658
|
+
engine().input.set_mouse_position(loc.x, frame.size.height - loc.y);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
NSEventType::LeftMouseDown => {
|
|
662
|
+
engine().input.set_mouse_button_down(0);
|
|
663
|
+
}
|
|
664
|
+
NSEventType::LeftMouseUp => {
|
|
665
|
+
engine().input.set_mouse_button_up(0);
|
|
666
|
+
}
|
|
667
|
+
NSEventType::RightMouseDown => {
|
|
668
|
+
engine().input.set_mouse_button_down(1);
|
|
669
|
+
}
|
|
670
|
+
NSEventType::RightMouseUp => {
|
|
671
|
+
engine().input.set_mouse_button_up(1);
|
|
672
|
+
}
|
|
673
|
+
NSEventType::ScrollWheel => {
|
|
674
|
+
// NSEvent's scrollingDeltaY is positive when scrolling up
|
|
675
|
+
// (away from the user). Normalize to "positive = zoom in"
|
|
676
|
+
// by flipping the sign — matches editor orbit convention.
|
|
677
|
+
let dy: f64 = unsafe { msg_send![&*event, scrollingDeltaY] };
|
|
678
|
+
engine().input.accumulate_mouse_wheel(dy);
|
|
679
|
+
}
|
|
680
|
+
_ => {}
|
|
681
|
+
}
|
|
682
|
+
app.sendEvent(&event);
|
|
683
|
+
}
|
|
684
|
+
None => break,
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Check if window was closed. In headless mode the window is
|
|
689
|
+
// intentionally hidden — skip the isVisible check so --capture
|
|
690
|
+
// can actually run to completion without instant-exit.
|
|
691
|
+
let is_headless = unsafe { HEADLESS };
|
|
692
|
+
if !is_headless && unsafe { WINDOW.as_ref().map(|w| !w.isVisible()).unwrap_or(true) } {
|
|
693
|
+
engine().should_close = true;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Handle window resize — track physical (backing) size for the
|
|
697
|
+
// swapchain while keeping the logical (points) size for user code.
|
|
698
|
+
if let Some(window) = unsafe { &WINDOW } {
|
|
699
|
+
if let Some(content_view) = window.contentView() {
|
|
700
|
+
let frame = content_view.frame();
|
|
701
|
+
let logical_w = frame.size.width as u32;
|
|
702
|
+
let logical_h = frame.size.height as u32;
|
|
703
|
+
let scale: f64 = unsafe { msg_send![&*window, backingScaleFactor] };
|
|
704
|
+
let scale = if scale > 0.0 { scale } else { 1.0 };
|
|
705
|
+
let physical_w = ((frame.size.width * scale) as u32).max(1);
|
|
706
|
+
let physical_h = ((frame.size.height * scale) as u32).max(1);
|
|
707
|
+
let eng = engine();
|
|
708
|
+
if logical_w > 0 && logical_h > 0
|
|
709
|
+
&& (physical_w != eng.renderer.physical_width()
|
|
710
|
+
|| physical_h != eng.renderer.physical_height()
|
|
711
|
+
|| logical_w != eng.renderer.width()
|
|
712
|
+
|| logical_h != eng.renderer.height())
|
|
713
|
+
{
|
|
714
|
+
eng.renderer.resize(physical_w, physical_h, logical_w, logical_h);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Apply cursor shape (Q2). resizeLeftRightCursor /
|
|
720
|
+
// resizeUpDownCursor are deprecated in newer AppKit but still
|
|
721
|
+
// function; the suggested replacements need a richer per-frame-
|
|
722
|
+
// axis API we don't have. Keep + suppress the deprecation
|
|
723
|
+
// warning until we land that.
|
|
724
|
+
#[allow(deprecated)]
|
|
725
|
+
match engine().input.cursor_shape {
|
|
726
|
+
1 => objc2_app_kit::NSCursor::pointingHandCursor().set(),
|
|
727
|
+
2 => objc2_app_kit::NSCursor::openHandCursor().set(),
|
|
728
|
+
3 => objc2_app_kit::NSCursor::IBeamCursor().set(),
|
|
729
|
+
4 => objc2_app_kit::NSCursor::resizeLeftRightCursor().set(),
|
|
730
|
+
5 => objc2_app_kit::NSCursor::resizeUpDownCursor().set(),
|
|
731
|
+
6 => objc2_app_kit::NSCursor::crosshairCursor().set(),
|
|
732
|
+
_ => {},
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Poll a connected game controller (no-op if none) before begin_frame
|
|
736
|
+
// snapshots input for the edge detectors.
|
|
737
|
+
poll_game_controllers();
|
|
738
|
+
|
|
739
|
+
engine().begin_frame();
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
#[no_mangle]
|
|
743
|
+
pub extern "C" fn bloom_end_drawing() {
|
|
744
|
+
// Pump geisterhand BEFORE end_frame.
|
|
745
|
+
// Screenshot function re-renders inline with captured VP + vertices.
|
|
746
|
+
extern "C" { fn perry_geisterhand_pump(); }
|
|
747
|
+
unsafe { perry_geisterhand_pump(); }
|
|
748
|
+
|
|
749
|
+
engine().end_frame();
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// ============================================================
|
|
753
|
+
// Input - Keyboard
|
|
754
|
+
// ============================================================
|
|
755
|
+
|
|
756
|
+
// ============================================================
|
|
757
|
+
// Input - Mouse
|
|
758
|
+
// ============================================================
|
|
759
|
+
|
|
760
|
+
// ============================================================
|
|
761
|
+
// Input - Gamepad
|
|
762
|
+
// ============================================================
|
|
763
|
+
|
|
764
|
+
// ============================================================
|
|
765
|
+
// Input - Touch
|
|
766
|
+
// ============================================================
|
|
767
|
+
|
|
768
|
+
// ============================================================
|
|
769
|
+
// Shapes
|
|
770
|
+
// ============================================================
|
|
771
|
+
|
|
772
|
+
// ============================================================
|
|
773
|
+
// Text
|
|
774
|
+
// ============================================================
|
|
775
|
+
|
|
776
|
+
// ============================================================
|
|
777
|
+
// Textures
|
|
778
|
+
// ============================================================
|
|
779
|
+
|
|
780
|
+
// ============================================================
|
|
781
|
+
// Camera 2D
|
|
782
|
+
// ============================================================
|
|
783
|
+
|
|
784
|
+
// ============================================================
|
|
785
|
+
// Camera 3D and 3D drawing
|
|
786
|
+
// ============================================================
|
|
787
|
+
|
|
788
|
+
// ============================================================
|
|
789
|
+
// Joint test
|
|
790
|
+
// ============================================================
|
|
791
|
+
|
|
792
|
+
// ============================================================
|
|
793
|
+
// Lighting
|
|
794
|
+
// ============================================================
|
|
795
|
+
|
|
796
|
+
// --- EN-005: procedural sky ---
|
|
797
|
+
// Toggle procedural-atmosphere rendering and steer the sun. The
|
|
798
|
+
// renderer owns the on/off flag + LUT state; setting the sun marks
|
|
799
|
+
// the sky-view LUT dirty so it re-bakes before the next frame.
|
|
800
|
+
|
|
801
|
+
// --- Post-FX knobs (heuristic visual layer; default-off) ---
|
|
802
|
+
|
|
803
|
+
// ============================================================
|
|
804
|
+
// Render quality toggles (individual + preset)
|
|
805
|
+
// ============================================================
|
|
806
|
+
|
|
807
|
+
// ============================================================
|
|
808
|
+
// Profiler — CPU phase timings (always available) + GPU timestamps
|
|
809
|
+
// (when the adapter supports TIMESTAMP_QUERY). Disabled by default.
|
|
810
|
+
// ============================================================
|
|
811
|
+
|
|
812
|
+
// ============================================================
|
|
813
|
+
// Models
|
|
814
|
+
// ============================================================
|
|
815
|
+
|
|
816
|
+
// ============================================================
|
|
817
|
+
// Phase 1c — material system FFI
|
|
818
|
+
// ============================================================
|
|
819
|
+
|
|
820
|
+
// ============================================================
|
|
821
|
+
// Audio
|
|
822
|
+
// ============================================================
|
|
823
|
+
|
|
824
|
+
#[no_mangle]
|
|
825
|
+
pub extern "C" fn bloom_init_audio() {
|
|
826
|
+
unsafe {
|
|
827
|
+
// Hand the render half to the audio thread before the callback
|
|
828
|
+
// can fire. Idempotent: a second init keeps the existing renderer.
|
|
829
|
+
if AUDIO_RENDERER.is_none() {
|
|
830
|
+
AUDIO_RENDERER = engine().audio.take_renderer();
|
|
831
|
+
}
|
|
832
|
+
let desc = AudioComponentDescription {
|
|
833
|
+
component_type: K_AUDIO_UNIT_TYPE_OUTPUT,
|
|
834
|
+
component_sub_type: K_AUDIO_UNIT_SUB_TYPE_DEFAULT_OUTPUT,
|
|
835
|
+
component_manufacturer: K_AUDIO_UNIT_MANUFACTURER_APPLE,
|
|
836
|
+
component_flags: 0,
|
|
837
|
+
component_flags_mask: 0,
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
let component = AudioComponentFindNext(std::ptr::null_mut(), &desc);
|
|
841
|
+
if component.is_null() {
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
let mut unit: AudioUnit = std::ptr::null_mut();
|
|
846
|
+
if AudioComponentInstanceNew(component, &mut unit) != 0 {
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Set stream format: 44100 Hz, stereo, float32
|
|
851
|
+
let stream_desc = AudioStreamBasicDescription {
|
|
852
|
+
sample_rate: 44100.0,
|
|
853
|
+
format_id: K_AUDIO_FORMAT_LINEAR_PCM,
|
|
854
|
+
format_flags: K_AUDIO_FORMAT_FLAG_IS_FLOAT | K_AUDIO_FORMAT_FLAG_IS_PACKED,
|
|
855
|
+
bytes_per_packet: 8,
|
|
856
|
+
frames_per_packet: 1,
|
|
857
|
+
bytes_per_frame: 8,
|
|
858
|
+
channels_per_frame: 2,
|
|
859
|
+
bits_per_channel: 32,
|
|
860
|
+
reserved: 0,
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
AudioUnitSetProperty(
|
|
864
|
+
unit,
|
|
865
|
+
K_AUDIO_UNIT_PROPERTY_STREAM_FORMAT,
|
|
866
|
+
K_AUDIO_UNIT_SCOPE_INPUT,
|
|
867
|
+
0,
|
|
868
|
+
&stream_desc as *const _ as *const std::ffi::c_void,
|
|
869
|
+
std::mem::size_of::<AudioStreamBasicDescription>() as u32,
|
|
870
|
+
);
|
|
871
|
+
|
|
872
|
+
// Set render callback
|
|
873
|
+
let callback_struct = AURenderCallbackStruct {
|
|
874
|
+
input_proc: audio_render_callback,
|
|
875
|
+
input_proc_ref_con: std::ptr::null_mut(),
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
AudioUnitSetProperty(
|
|
879
|
+
unit,
|
|
880
|
+
K_AUDIO_UNIT_PROPERTY_SET_RENDER_CALLBACK,
|
|
881
|
+
K_AUDIO_UNIT_SCOPE_INPUT,
|
|
882
|
+
0,
|
|
883
|
+
&callback_struct as *const _ as *const std::ffi::c_void,
|
|
884
|
+
std::mem::size_of::<AURenderCallbackStruct>() as u32,
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
AudioUnitInitialize(unit);
|
|
888
|
+
AudioOutputUnitStart(unit);
|
|
889
|
+
|
|
890
|
+
AUDIO_UNIT = Some(AudioUnitInstance { unit });
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
#[no_mangle]
|
|
895
|
+
pub extern "C" fn bloom_close_audio() {
|
|
896
|
+
unsafe {
|
|
897
|
+
if let Some(au) = AUDIO_UNIT.take() {
|
|
898
|
+
AudioOutputUnitStop(au.unit);
|
|
899
|
+
AudioComponentInstanceDispose(au.unit);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// ============================================================
|
|
905
|
+
// Music
|
|
906
|
+
// ============================================================
|
|
907
|
+
|
|
908
|
+
// ============================================================
|
|
909
|
+
// Utility
|
|
910
|
+
// ============================================================
|
|
911
|
+
|
|
912
|
+
#[no_mangle]
|
|
913
|
+
pub extern "C" fn bloom_toggle_fullscreen() {
|
|
914
|
+
unsafe {
|
|
915
|
+
if let Some(window) = &WINDOW {
|
|
916
|
+
let _: () = msg_send![window, toggleFullScreen: std::ptr::null::<std::ffi::c_void>()];
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
#[no_mangle]
|
|
922
|
+
pub extern "C" fn bloom_set_window_title(title_ptr: *const u8) {
|
|
923
|
+
let title = str_from_header(title_ptr);
|
|
924
|
+
unsafe {
|
|
925
|
+
if let Some(window) = &WINDOW {
|
|
926
|
+
let ns_title = NSString::from_str(title);
|
|
927
|
+
window.setTitle(&ns_title);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
#[no_mangle]
|
|
933
|
+
pub extern "C" fn bloom_set_window_icon(path_ptr: *const u8) {
|
|
934
|
+
let path = str_from_header(path_ptr);
|
|
935
|
+
unsafe {
|
|
936
|
+
let ns_path = NSString::from_str(path);
|
|
937
|
+
let image_cls = objc2::runtime::AnyClass::get(c"NSImage").unwrap();
|
|
938
|
+
let image: *mut objc2::runtime::AnyObject =
|
|
939
|
+
msg_send![image_cls, alloc];
|
|
940
|
+
if image.is_null() { return; }
|
|
941
|
+
let image: *mut objc2::runtime::AnyObject =
|
|
942
|
+
msg_send![image, initWithContentsOfFile: &*ns_path];
|
|
943
|
+
if image.is_null() { return; }
|
|
944
|
+
let app = NSApplication::sharedApplication(MainThreadMarker::new_unchecked());
|
|
945
|
+
let _: () = msg_send![&*app, setApplicationIconImage: image];
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
extern "C" {
|
|
950
|
+
fn CGDisplayHideCursor(display: u32) -> i32;
|
|
951
|
+
fn CGDisplayShowCursor(display: u32) -> i32;
|
|
952
|
+
fn CGAssociateMouseAndMouseCursorPosition(connected: u8) -> i32;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
#[no_mangle]
|
|
956
|
+
pub extern "C" fn bloom_disable_cursor() {
|
|
957
|
+
let input = &mut engine().input;
|
|
958
|
+
input.cursor_disabled = true;
|
|
959
|
+
input.clear_mouse_delta();
|
|
960
|
+
unsafe {
|
|
961
|
+
CGDisplayHideCursor(0);
|
|
962
|
+
CGAssociateMouseAndMouseCursorPosition(0); // dissociate = relative mode
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
#[no_mangle]
|
|
967
|
+
pub extern "C" fn bloom_enable_cursor() {
|
|
968
|
+
engine().input.cursor_disabled = false;
|
|
969
|
+
unsafe {
|
|
970
|
+
CGAssociateMouseAndMouseCursorPosition(1);
|
|
971
|
+
CGDisplayShowCursor(0);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// E4: Clipboard
|
|
976
|
+
#[no_mangle]
|
|
977
|
+
pub extern "C" fn bloom_set_clipboard_text(text_ptr: *const u8) {
|
|
978
|
+
let text = str_from_header(text_ptr);
|
|
979
|
+
if let Ok(mut clipboard) = arboard::Clipboard::new() {
|
|
980
|
+
let _ = clipboard.set_text(text.to_string());
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
#[no_mangle]
|
|
985
|
+
pub extern "C" fn bloom_get_clipboard_text() -> *const u8 {
|
|
986
|
+
match arboard::Clipboard::new() {
|
|
987
|
+
Ok(mut clipboard) => match clipboard.get_text() {
|
|
988
|
+
Ok(text) => alloc_perry_string(&text),
|
|
989
|
+
Err(_) => alloc_perry_string(""),
|
|
990
|
+
},
|
|
991
|
+
Err(_) => alloc_perry_string(""),
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// E5b: Native file dialogs (via rfd crate)
|
|
996
|
+
#[no_mangle]
|
|
997
|
+
pub extern "C" fn bloom_open_file_dialog(filter_ptr: *const u8, title_ptr: *const u8) -> *const u8 {
|
|
998
|
+
let filter = str_from_header(filter_ptr);
|
|
999
|
+
let title = str_from_header(title_ptr);
|
|
1000
|
+
let mut dialog = rfd::FileDialog::new().set_title(title);
|
|
1001
|
+
if !filter.is_empty() {
|
|
1002
|
+
dialog = dialog.add_filter("Files", &[filter]);
|
|
1003
|
+
}
|
|
1004
|
+
match dialog.pick_file() {
|
|
1005
|
+
Some(path) => alloc_perry_string(&path.to_string_lossy()),
|
|
1006
|
+
None => alloc_perry_string(""),
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
#[no_mangle]
|
|
1011
|
+
pub extern "C" fn bloom_save_file_dialog(default_name_ptr: *const u8, title_ptr: *const u8) -> *const u8 {
|
|
1012
|
+
let default_name = str_from_header(default_name_ptr);
|
|
1013
|
+
let title = str_from_header(title_ptr);
|
|
1014
|
+
let dialog = rfd::FileDialog::new()
|
|
1015
|
+
.set_title(title)
|
|
1016
|
+
.set_file_name(default_name);
|
|
1017
|
+
match dialog.save_file() {
|
|
1018
|
+
Some(path) => alloc_perry_string(&path.to_string_lossy()),
|
|
1019
|
+
None => alloc_perry_string(""),
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// ============================================================
|
|
1024
|
+
// Input injection + platform detection
|
|
1025
|
+
// ============================================================
|
|
1026
|
+
#[no_mangle]
|
|
1027
|
+
pub extern "C" fn bloom_get_platform() -> f64 { 1.0 }
|
|
1028
|
+
|
|
1029
|
+
/// Return the user's preferred OS language as a packed 2-letter code:
|
|
1030
|
+
/// `c0 * 256 + c1`, where c0/c1 are the ASCII bytes of the lowercased
|
|
1031
|
+
/// ISO-639 primary subtag (e.g. "en-US" -> "en" -> 101*256+110). The script
|
|
1032
|
+
/// subtag is dropped (zh-Hans/zh-Hant both pack as "zh"); callers map that to
|
|
1033
|
+
/// their supported variant. Falls back to "en" when no preference is set.
|
|
1034
|
+
#[no_mangle]
|
|
1035
|
+
pub extern "C" fn bloom_get_language() -> f64 {
|
|
1036
|
+
fn pack(code: &str) -> f64 {
|
|
1037
|
+
let lower = code.to_ascii_lowercase();
|
|
1038
|
+
let b = lower.as_bytes();
|
|
1039
|
+
if b.len() >= 2 { (b[0] as f64) * 256.0 + (b[1] as f64) } else { 101.0 * 256.0 + 110.0 }
|
|
1040
|
+
}
|
|
1041
|
+
let langs = objc2_foundation::NSLocale::preferredLanguages();
|
|
1042
|
+
match langs.firstObject() {
|
|
1043
|
+
Some(s) => pack(&s.to_string()),
|
|
1044
|
+
None => pack("en"),
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// ============================================================
|
|
1049
|
+
// Frame callbacks
|
|
1050
|
+
// ============================================================
|
|
1051
|
+
|
|
1052
|
+
// ============================================================
|
|
1053
|
+
// Multiple lights
|
|
1054
|
+
// ============================================================
|
|
1055
|
+
|
|
1056
|
+
// ============================================================
|
|
1057
|
+
// Scene graph (retained mode)
|
|
1058
|
+
// ============================================================
|
|
1059
|
+
|
|
1060
|
+
// ============================================================
|
|
1061
|
+
// Scene graph QoL — Q4/Q5/Q6/Q7
|
|
1062
|
+
// ============================================================
|
|
1063
|
+
|
|
1064
|
+
// ============================================================
|
|
1065
|
+
// Geometry generation
|
|
1066
|
+
// ============================================================
|
|
1067
|
+
|
|
1068
|
+
// ============================================================
|
|
1069
|
+
// Shadow mapping
|
|
1070
|
+
// ============================================================
|
|
1071
|
+
|
|
1072
|
+
// ============================================================
|
|
1073
|
+
// Post-processing
|
|
1074
|
+
// ============================================================
|
|
1075
|
+
|
|
1076
|
+
// ============================================================
|
|
1077
|
+
// 3D→2D Projection (for UI overlays positioned in 3D space)
|
|
1078
|
+
// ============================================================
|
|
1079
|
+
|
|
1080
|
+
/// Project a world-space 3D point to screen coordinates.
|
|
1081
|
+
/// Returns screen X. Call bloom_project_y for Y. Returns -9999 if behind camera.
|
|
1082
|
+
|
|
1083
|
+
// ============================================================
|
|
1084
|
+
// Thread-safe staging (for async asset loading via Perry threads)
|
|
1085
|
+
// ============================================================
|
|
1086
|
+
|
|
1087
|
+
// The blocking executor for wgpu's async adapter/device requests now
|
|
1088
|
+
// lives in `bloom_shared::attach` (used by both bloom_init_window and
|
|
1089
|
+
// bloom_attach_native via attach_engine).
|
|
1090
|
+
|
|
1091
|
+
// ============================================================
|
|
1092
|
+
// Geisterhand screenshot integration
|
|
1093
|
+
// ============================================================
|
|
1094
|
+
|
|
1095
|
+
/// Register Bloom's GPU-based screenshot capture with perry-geisterhand.
|
|
1096
|
+
/// This replaces perry-ui-macos's CGWindowListCreateImage approach with
|
|
1097
|
+
/// direct wgpu texture readback — works for Metal/Vulkan rendered content.
|
|
1098
|
+
fn bloom_register_geisterhand_screenshot() {
|
|
1099
|
+
// Try to register with geisterhand if it's linked (weak symbol)
|
|
1100
|
+
extern "C" {
|
|
1101
|
+
fn perry_geisterhand_register_screenshot_capture(
|
|
1102
|
+
f: extern "C" fn(*mut usize) -> *mut u8,
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
unsafe {
|
|
1106
|
+
perry_geisterhand_register_screenshot_capture(bloom_screenshot_capture);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
/// Capture the Bloom framebuffer as PNG.
|
|
1111
|
+
/// Called from geisterhand pump BEFORE end_frame in bloom_end_drawing.
|
|
1112
|
+
/// The vertices_3d/2d and VP matrix from the game loop are still populated.
|
|
1113
|
+
/// We render to a fresh surface texture with screenshot capture, producing
|
|
1114
|
+
/// the same visual output as the real frame.
|
|
1115
|
+
extern "C" fn bloom_screenshot_capture(out_len: *mut usize) -> *mut u8 {
|
|
1116
|
+
let eng = engine();
|
|
1117
|
+
|
|
1118
|
+
// Set capture flag and render inline
|
|
1119
|
+
eng.renderer.screenshot_requested = true;
|
|
1120
|
+
eng.scene.prepare(
|
|
1121
|
+
&eng.renderer.device,
|
|
1122
|
+
&eng.renderer.queue,
|
|
1123
|
+
&eng.renderer.vp_matrix(),
|
|
1124
|
+
&eng.renderer.prev_vp_matrix,
|
|
1125
|
+
eng.renderer.uniform_3d_layout(),
|
|
1126
|
+
// Screenshot capture renders everything the camera might see —
|
|
1127
|
+
// never occlusion-cull a one-shot capture.
|
|
1128
|
+
None,
|
|
1129
|
+
);
|
|
1130
|
+
eng.scene.prepare_materials(&eng.renderer);
|
|
1131
|
+
// Phase 1c: sync material PerFrame + PerView UBOs with the
|
|
1132
|
+
// current engine clock before the main HDR pass dispatches any
|
|
1133
|
+
// material draws that were submitted during this frame.
|
|
1134
|
+
{
|
|
1135
|
+
let t = eng.get_time() as f32;
|
|
1136
|
+
let dt = eng.delta_time as f32;
|
|
1137
|
+
eng.renderer.material_system_begin_frame(t, dt);
|
|
1138
|
+
}
|
|
1139
|
+
eng.renderer.end_frame_with_scene(&mut eng.scene, &mut eng.profiler);
|
|
1140
|
+
|
|
1141
|
+
match eng.renderer.screenshot_data.take() {
|
|
1142
|
+
Some((width, height, rgba)) => {
|
|
1143
|
+
// Encode RGBA pixels to PNG
|
|
1144
|
+
match encode_png(width, height, &rgba) {
|
|
1145
|
+
Some(png_data) => {
|
|
1146
|
+
let len = png_data.len();
|
|
1147
|
+
// Allocate with libc::malloc (caller will free with libc::free)
|
|
1148
|
+
let ptr = unsafe { libc::malloc(len) as *mut u8 };
|
|
1149
|
+
if ptr.is_null() {
|
|
1150
|
+
unsafe { *out_len = 0; }
|
|
1151
|
+
return std::ptr::null_mut();
|
|
1152
|
+
}
|
|
1153
|
+
unsafe {
|
|
1154
|
+
std::ptr::copy_nonoverlapping(png_data.as_ptr(), ptr, len);
|
|
1155
|
+
*out_len = len;
|
|
1156
|
+
}
|
|
1157
|
+
ptr
|
|
1158
|
+
}
|
|
1159
|
+
None => {
|
|
1160
|
+
unsafe { *out_len = 0; }
|
|
1161
|
+
std::ptr::null_mut()
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
None => {
|
|
1166
|
+
unsafe { *out_len = 0; }
|
|
1167
|
+
std::ptr::null_mut()
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/// Minimal PNG encoder (no external dependency).
|
|
1173
|
+
fn encode_png(width: u32, height: u32, rgba: &[u8]) -> Option<Vec<u8>> {
|
|
1174
|
+
use std::io::Write;
|
|
1175
|
+
|
|
1176
|
+
let mut png = Vec::new();
|
|
1177
|
+
// PNG signature
|
|
1178
|
+
png.write_all(&[137, 80, 78, 71, 13, 10, 26, 10]).ok()?;
|
|
1179
|
+
|
|
1180
|
+
// IHDR chunk
|
|
1181
|
+
let mut ihdr = Vec::new();
|
|
1182
|
+
ihdr.extend_from_slice(&width.to_be_bytes());
|
|
1183
|
+
ihdr.extend_from_slice(&height.to_be_bytes());
|
|
1184
|
+
ihdr.push(8); // bit depth
|
|
1185
|
+
ihdr.push(6); // color type: RGBA
|
|
1186
|
+
ihdr.push(0); // compression
|
|
1187
|
+
ihdr.push(0); // filter
|
|
1188
|
+
ihdr.push(0); // interlace
|
|
1189
|
+
write_png_chunk(&mut png, b"IHDR", &ihdr);
|
|
1190
|
+
|
|
1191
|
+
// IDAT chunk — raw pixel data with zlib
|
|
1192
|
+
// Build raw scanlines: each row starts with filter byte 0 (None)
|
|
1193
|
+
let row_bytes = (width * 4) as usize;
|
|
1194
|
+
let mut raw = Vec::with_capacity((row_bytes + 1) * height as usize);
|
|
1195
|
+
for y in 0..height as usize {
|
|
1196
|
+
raw.push(0); // filter: None
|
|
1197
|
+
let start = y * row_bytes;
|
|
1198
|
+
// Copy BGRA pixels, swapping B and R for PNG (which expects RGBA)
|
|
1199
|
+
for x in 0..width as usize {
|
|
1200
|
+
let idx = start + x * 4;
|
|
1201
|
+
// Metal Bgra8UnormSrgb: byte order is B, G, R, A
|
|
1202
|
+
raw.push(rgba[idx + 2]); // R (was at offset 2 in BGRA)
|
|
1203
|
+
raw.push(rgba[idx + 1]); // G (same position)
|
|
1204
|
+
raw.push(rgba[idx + 0]); // B (was at offset 0 in BGRA)
|
|
1205
|
+
raw.push(255); // A (force opaque — alpha from sRGB surface is unreliable)
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// Compress with deflate (store blocks, no actual compression for simplicity)
|
|
1210
|
+
let deflated = deflate_store(&raw);
|
|
1211
|
+
write_png_chunk(&mut png, b"IDAT", &deflated);
|
|
1212
|
+
|
|
1213
|
+
// IEND chunk
|
|
1214
|
+
write_png_chunk(&mut png, b"IEND", &[]);
|
|
1215
|
+
|
|
1216
|
+
Some(png)
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
fn write_png_chunk(out: &mut Vec<u8>, chunk_type: &[u8; 4], data: &[u8]) {
|
|
1220
|
+
let len = data.len() as u32;
|
|
1221
|
+
out.extend_from_slice(&len.to_be_bytes());
|
|
1222
|
+
out.extend_from_slice(chunk_type);
|
|
1223
|
+
out.extend_from_slice(data);
|
|
1224
|
+
// CRC32 over type + data
|
|
1225
|
+
let crc = crc32(&[chunk_type.as_slice(), data].concat());
|
|
1226
|
+
out.extend_from_slice(&crc.to_be_bytes());
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
fn crc32(data: &[u8]) -> u32 {
|
|
1230
|
+
let mut crc: u32 = 0xFFFFFFFF;
|
|
1231
|
+
for &byte in data {
|
|
1232
|
+
crc ^= byte as u32;
|
|
1233
|
+
for _ in 0..8 {
|
|
1234
|
+
if crc & 1 != 0 {
|
|
1235
|
+
crc = (crc >> 1) ^ 0xEDB88320;
|
|
1236
|
+
} else {
|
|
1237
|
+
crc >>= 1;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
!crc
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/// Minimal deflate: store blocks (no compression). Wraps in zlib format.
|
|
1245
|
+
fn deflate_store(data: &[u8]) -> Vec<u8> {
|
|
1246
|
+
let mut out = Vec::new();
|
|
1247
|
+
// Zlib header: CMF=0x78 (deflate, window=32K), FLG=0x01 (no dict, check bits)
|
|
1248
|
+
out.push(0x78);
|
|
1249
|
+
out.push(0x01);
|
|
1250
|
+
|
|
1251
|
+
// Split into 65535-byte store blocks
|
|
1252
|
+
let mut remaining = data.len();
|
|
1253
|
+
let mut offset = 0;
|
|
1254
|
+
while remaining > 0 {
|
|
1255
|
+
let block_size = remaining.min(65535);
|
|
1256
|
+
let is_last = remaining <= 65535;
|
|
1257
|
+
out.push(if is_last { 1 } else { 0 }); // BFINAL + BTYPE=00 (store)
|
|
1258
|
+
let len = block_size as u16;
|
|
1259
|
+
let nlen = !len;
|
|
1260
|
+
out.extend_from_slice(&len.to_le_bytes());
|
|
1261
|
+
out.extend_from_slice(&nlen.to_le_bytes());
|
|
1262
|
+
out.extend_from_slice(&data[offset..offset + block_size]);
|
|
1263
|
+
offset += block_size;
|
|
1264
|
+
remaining -= block_size;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// Adler-32 checksum
|
|
1268
|
+
let adler = adler32(data);
|
|
1269
|
+
out.extend_from_slice(&adler.to_be_bytes());
|
|
1270
|
+
out
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
fn adler32(data: &[u8]) -> u32 {
|
|
1274
|
+
let mut a: u32 = 1;
|
|
1275
|
+
let mut b: u32 = 0;
|
|
1276
|
+
for &byte in data {
|
|
1277
|
+
a = (a + byte as u32) % 65521;
|
|
1278
|
+
b = (b + a) % 65521;
|
|
1279
|
+
}
|
|
1280
|
+
(b << 16) | a
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// ============================================================
|
|
1284
|
+
// Scene picking (raycasting)
|
|
1285
|
+
// ============================================================
|
|
1286
|
+
|
|
1287
|
+
|
|
1288
|
+
// Q6: Multi-hit picking — returns all hits sorted by distance.
|
|
1289
|
+
|
|
1290
|
+
// ============================================================
|
|
1291
|
+
// Physics (Jolt 5.x) — FFI surface generated from shared macro
|
|
1292
|
+
// ============================================================
|
|
1293
|
+
|
|
1294
|
+
#[cfg(feature = "jolt")]
|
|
1295
|
+
#[inline]
|
|
1296
|
+
fn bloom_jolt_ffi_physics() -> &'static mut bloom_shared::physics_jolt::JoltPhysics {
|
|
1297
|
+
&mut engine().jolt
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
#[cfg(feature = "jolt")]
|
|
1301
|
+
bloom_shared::define_physics_ffi!();
|
|
1302
|
+
|