@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,1124 @@
|
|
|
1
|
+
//! bloom-watchos: Canvas + (future) SceneKit backend for watchOS.
|
|
2
|
+
//!
|
|
3
|
+
//! The Swift shell (`BloomWatchApp.swift`, compiled by Perry via
|
|
4
|
+
//! `--features watchos-swift-app`) owns `@main`. During init it spawns the
|
|
5
|
+
//! game thread calling `_perry_user_main`. The root view is a SwiftUI
|
|
6
|
+
//! `Canvas` that drains this crate's draw list each frame. Digital Crown
|
|
7
|
+
//! rotation, taps, and layout dimensions come back in through the
|
|
8
|
+
//! `bloom_watchos_*` inbound functions.
|
|
9
|
+
|
|
10
|
+
#![allow(non_upper_case_globals)]
|
|
11
|
+
|
|
12
|
+
mod ffi_stubs;
|
|
13
|
+
mod ffi_stubs_manual;
|
|
14
|
+
mod draw_list;
|
|
15
|
+
mod textures;
|
|
16
|
+
mod audio;
|
|
17
|
+
mod scene;
|
|
18
|
+
mod models;
|
|
19
|
+
mod postfx;
|
|
20
|
+
|
|
21
|
+
/// Perry StringHeader layout — mirrors bloom-shared's copy. Inlined here
|
|
22
|
+
/// because we don't depend on bloom-shared (keeps the watchos crate
|
|
23
|
+
/// wgpu-free).
|
|
24
|
+
#[repr(C)]
|
|
25
|
+
struct StringHeader {
|
|
26
|
+
utf16_len: u32,
|
|
27
|
+
byte_len: u32,
|
|
28
|
+
capacity: u32,
|
|
29
|
+
refcount: u32,
|
|
30
|
+
// Perry 0.5.x's canonical StringHeader is 5×u32 = 20 bytes (data at +20).
|
|
31
|
+
// Omitting `flags` made this 16 bytes, so `perry_str` read every incoming
|
|
32
|
+
// string 4 bytes early — text rendered with a 4-null prefix + truncated
|
|
33
|
+
// tail ("BLOOM JUMP" → "BLOOM"), and file paths came back corrupted so
|
|
34
|
+
// levels never loaded.
|
|
35
|
+
flags: u32,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Decode a Perry-side string pointer (i64 on this ABI) into a borrowed &str.
|
|
39
|
+
/// Returns "" for null / too-small pointers to keep the FFI boundary robust.
|
|
40
|
+
fn perry_str<'a>(ptr: i64) -> &'a str {
|
|
41
|
+
if ptr == 0 { return ""; }
|
|
42
|
+
let p = ptr as *const u8;
|
|
43
|
+
if (p as usize) < 0x1000 { return ""; }
|
|
44
|
+
unsafe {
|
|
45
|
+
let header = p as *const StringHeader;
|
|
46
|
+
let len = (*header).byte_len as usize;
|
|
47
|
+
let data = p.add(std::mem::size_of::<StringHeader>());
|
|
48
|
+
std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len))
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Allocate a Perry-side heap string (StringHeader + UTF-8 payload) and return
|
|
53
|
+
/// its pointer as the i64 the FFI boundary expects. Mirrors `perry_str`'s
|
|
54
|
+
/// layout so a string we hand back is read identically to one Perry passed us.
|
|
55
|
+
/// Returning this (never 0) for a `returns:"string"` function is mandatory:
|
|
56
|
+
/// Perry's inline `.length` dereferences the pointer, so a null segfaults the
|
|
57
|
+
/// caller.
|
|
58
|
+
fn alloc_perry_string(s: &str) -> i64 {
|
|
59
|
+
let bytes = s.as_bytes();
|
|
60
|
+
let byte_len = bytes.len();
|
|
61
|
+
let utf16_len = if bytes.iter().all(|&b| b < 0x80) {
|
|
62
|
+
byte_len
|
|
63
|
+
} else {
|
|
64
|
+
s.encode_utf16().count()
|
|
65
|
+
};
|
|
66
|
+
// A value RETURNED across the FFI boundary is consumed by Perry's internal
|
|
67
|
+
// string machinery, whose canonical StringHeader is 5×u32 = 20 bytes
|
|
68
|
+
// (utf16_len, byte_len, capacity, refcount, flags) with data at +20.
|
|
69
|
+
// (The local 16-byte `StringHeader`/`perry_str` above describe the *incoming*
|
|
70
|
+
// arg representation, which omits `flags`; don't conflate the two.)
|
|
71
|
+
const HEADER_SIZE: usize = 20;
|
|
72
|
+
let total = HEADER_SIZE + byte_len;
|
|
73
|
+
unsafe {
|
|
74
|
+
let layout = std::alloc::Layout::from_size_align(total, 4).unwrap();
|
|
75
|
+
let ptr = std::alloc::alloc(layout);
|
|
76
|
+
if ptr.is_null() {
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
*(ptr.add(0) as *mut u32) = utf16_len as u32;
|
|
80
|
+
*(ptr.add(4) as *mut u32) = byte_len as u32;
|
|
81
|
+
*(ptr.add(8) as *mut u32) = byte_len as u32; // capacity
|
|
82
|
+
*(ptr.add(12) as *mut u32) = 1; // refcount = unique
|
|
83
|
+
*(ptr.add(16) as *mut u32) = 0; // flags
|
|
84
|
+
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(HEADER_SIZE), byte_len);
|
|
85
|
+
ptr as i64
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
use std::ffi::{c_char, c_void};
|
|
90
|
+
use std::sync::atomic::{AtomicI64, AtomicU64, AtomicUsize, Ordering};
|
|
91
|
+
use std::sync::OnceLock;
|
|
92
|
+
use std::time::Instant;
|
|
93
|
+
|
|
94
|
+
use draw_list::{kind, DrawCmd};
|
|
95
|
+
|
|
96
|
+
// ============================================================
|
|
97
|
+
// Minimal engine state (input + timing). Rendering state lives
|
|
98
|
+
// in the draw_list and textures modules.
|
|
99
|
+
// ============================================================
|
|
100
|
+
|
|
101
|
+
const MAX_TOUCH: usize = 4;
|
|
102
|
+
|
|
103
|
+
struct WatchState {
|
|
104
|
+
crown_bits: AtomicU64, // f64 bits
|
|
105
|
+
language_code: AtomicU64, // packed 2-letter code (c0*256+c1), set by Swift at launch
|
|
106
|
+
touch_x: [AtomicU64; MAX_TOUCH],
|
|
107
|
+
touch_y: [AtomicU64; MAX_TOUCH],
|
|
108
|
+
touch_active: [AtomicU64; MAX_TOUCH],
|
|
109
|
+
screen_w: AtomicU64, // f64 bits
|
|
110
|
+
screen_h: AtomicU64, // f64 bits
|
|
111
|
+
target_fps: AtomicI64,
|
|
112
|
+
start: OnceLock<Instant>,
|
|
113
|
+
last_frame_ns: AtomicU64,
|
|
114
|
+
frame_count: AtomicUsize,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fn state() -> &'static WatchState {
|
|
118
|
+
static S: OnceLock<WatchState> = OnceLock::new();
|
|
119
|
+
S.get_or_init(|| WatchState {
|
|
120
|
+
crown_bits: AtomicU64::new(0),
|
|
121
|
+
language_code: AtomicU64::new(25966), // "en" until Swift reports the locale
|
|
122
|
+
touch_x: std::array::from_fn(|_| AtomicU64::new(0)),
|
|
123
|
+
touch_y: std::array::from_fn(|_| AtomicU64::new(0)),
|
|
124
|
+
touch_active: std::array::from_fn(|_| AtomicU64::new(0)),
|
|
125
|
+
screen_w: AtomicU64::new(198f64.to_bits()),
|
|
126
|
+
screen_h: AtomicU64::new(242f64.to_bits()),
|
|
127
|
+
target_fps: AtomicI64::new(30),
|
|
128
|
+
start: OnceLock::new(),
|
|
129
|
+
last_frame_ns: AtomicU64::new(0),
|
|
130
|
+
frame_count: AtomicUsize::new(0),
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fn now_nanos() -> u64 {
|
|
135
|
+
let s = state();
|
|
136
|
+
let start = s.start.get_or_init(Instant::now);
|
|
137
|
+
start.elapsed().as_nanos() as u64
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fn add_crown(delta: f64) {
|
|
141
|
+
let s = state();
|
|
142
|
+
loop {
|
|
143
|
+
let cur = s.crown_bits.load(Ordering::Acquire);
|
|
144
|
+
let new = (f64::from_bits(cur) + delta).to_bits();
|
|
145
|
+
if s.crown_bits
|
|
146
|
+
.compare_exchange_weak(cur, new, Ordering::AcqRel, Ordering::Acquire)
|
|
147
|
+
.is_ok()
|
|
148
|
+
{
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fn consume_crown() -> f64 {
|
|
155
|
+
let s = state();
|
|
156
|
+
let bits = s.crown_bits.swap(0, Ordering::AcqRel);
|
|
157
|
+
f64::from_bits(bits)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ============================================================
|
|
161
|
+
// Swift → Rust inbound hooks. Called from BloomWatchApp.swift.
|
|
162
|
+
// ============================================================
|
|
163
|
+
|
|
164
|
+
#[no_mangle]
|
|
165
|
+
pub extern "C" fn bloom_watchos_crown_delta(delta: f64) { add_crown(delta); }
|
|
166
|
+
|
|
167
|
+
/// Swift reports the user's preferred language at launch as a packed
|
|
168
|
+
/// 2-letter ISO-639 primary subtag (c0*256 + c1, lowercased), matching
|
|
169
|
+
/// the bloom_get_language contract on the other platforms.
|
|
170
|
+
#[no_mangle]
|
|
171
|
+
pub extern "C" fn bloom_watchos_set_language(code: f64) {
|
|
172
|
+
let c = code as u64;
|
|
173
|
+
if c > 0 {
|
|
174
|
+
state().language_code.store(c, Ordering::Release);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#[no_mangle]
|
|
179
|
+
pub extern "C" fn bloom_watchos_touch(index: i64, x: f64, y: f64, active: i64) {
|
|
180
|
+
let s = state();
|
|
181
|
+
let i = index as usize;
|
|
182
|
+
if i < MAX_TOUCH {
|
|
183
|
+
s.touch_x[i].store(x.to_bits(), Ordering::Release);
|
|
184
|
+
s.touch_y[i].store(y.to_bits(), Ordering::Release);
|
|
185
|
+
s.touch_active[i].store(active as u64, Ordering::Release);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#[no_mangle]
|
|
190
|
+
pub extern "C" fn bloom_watchos_set_screen(w: f64, h: f64) {
|
|
191
|
+
let s = state();
|
|
192
|
+
s.screen_w.store(w.to_bits(), Ordering::Release);
|
|
193
|
+
s.screen_h.store(h.to_bits(), Ordering::Release);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#[no_mangle]
|
|
197
|
+
pub extern "C" fn bloom_watchos_set_bundle_path(path: *const c_char) {
|
|
198
|
+
if path.is_null() { return; }
|
|
199
|
+
let s = unsafe { std::ffi::CStr::from_ptr(path) };
|
|
200
|
+
if let Ok(str) = s.to_str() {
|
|
201
|
+
textures::set_bundle_path(str);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Draw-list snapshot accessors for Swift Canvas.
|
|
206
|
+
|
|
207
|
+
#[no_mangle]
|
|
208
|
+
pub extern "C" fn bloom_watchos_frame_count() -> u64 { draw_list::frame() }
|
|
209
|
+
|
|
210
|
+
#[no_mangle]
|
|
211
|
+
pub extern "C" fn bloom_watchos_copy_draw_list(dst: *mut DrawCmd, max: i64) -> i64 {
|
|
212
|
+
draw_list::copy_into(dst, max)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/// Returns [r,g,b,a] of the current clear color, 0-255 per channel, into a
|
|
216
|
+
/// Swift-supplied 4-element f64 buffer.
|
|
217
|
+
#[no_mangle]
|
|
218
|
+
pub extern "C" fn bloom_watchos_clear_color(out: *mut f64) {
|
|
219
|
+
if out.is_null() { return; }
|
|
220
|
+
let c = draw_list::clear_rgba();
|
|
221
|
+
unsafe { std::ptr::copy_nonoverlapping(c.as_ptr(), out, 4); }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#[no_mangle]
|
|
225
|
+
pub extern "C" fn bloom_watchos_texture_path(handle: u32) -> *const c_char {
|
|
226
|
+
textures::path_ptr(handle)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// Copy camera state [px,py,pz, tx,ty,tz, ux,uy,uz, fovy, proj] into a
|
|
230
|
+
/// Swift-supplied 11-element f64 buffer. Called by the Swift SceneView.
|
|
231
|
+
#[no_mangle]
|
|
232
|
+
pub extern "C" fn bloom_watchos_camera_state(out: *mut f64) {
|
|
233
|
+
draw_list::camera_snapshot(out);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/// 1 if the game has ever opened a 3D mode section — hints to the Swift
|
|
237
|
+
/// shell that it should host a SceneView layer underneath the Canvas.
|
|
238
|
+
#[no_mangle]
|
|
239
|
+
pub extern "C" fn bloom_watchos_has_3d() -> f64 {
|
|
240
|
+
if draw_list::has_3d() { 1.0 } else { 0.0 }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ============================================================
|
|
244
|
+
// Perry game-loop handshake.
|
|
245
|
+
// ============================================================
|
|
246
|
+
//
|
|
247
|
+
// With --features watchos-swift-app, Perry doesn't generate its own main() —
|
|
248
|
+
// BloomWatchApp.swift is @main. Perry's watchos runtime still provides the
|
|
249
|
+
// `perry_main_init` C symbol which does global TS init; the Swift App calls
|
|
250
|
+
// it before spawning the game thread. We don't need to register any ObjC
|
|
251
|
+
// classes from this crate.
|
|
252
|
+
|
|
253
|
+
#[no_mangle]
|
|
254
|
+
pub extern "C" fn perry_register_native_classes() {}
|
|
255
|
+
|
|
256
|
+
#[no_mangle]
|
|
257
|
+
pub extern "C" fn perry_scene_will_connect(_scene: *const c_void) {}
|
|
258
|
+
|
|
259
|
+
// ============================================================
|
|
260
|
+
// Platform / timing / input overrides
|
|
261
|
+
// ============================================================
|
|
262
|
+
|
|
263
|
+
#[no_mangle]
|
|
264
|
+
pub extern "C" fn bloom_get_platform() -> f64 { 8.0 }
|
|
265
|
+
|
|
266
|
+
/// Preferred OS language packed as `c0*256+c1`. TODO: real per-OS detection; returns "en" for now.
|
|
267
|
+
#[no_mangle]
|
|
268
|
+
pub extern "C" fn bloom_get_language() -> f64 {
|
|
269
|
+
state().language_code.load(Ordering::Acquire) as f64
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
#[no_mangle]
|
|
273
|
+
pub extern "C" fn bloom_get_crown_rotation() -> f64 { consume_crown() }
|
|
274
|
+
|
|
275
|
+
#[no_mangle]
|
|
276
|
+
pub extern "C" fn bloom_get_screen_width() -> f64 {
|
|
277
|
+
f64::from_bits(state().screen_w.load(Ordering::Acquire))
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
#[no_mangle]
|
|
281
|
+
pub extern "C" fn bloom_get_screen_height() -> f64 {
|
|
282
|
+
f64::from_bits(state().screen_h.load(Ordering::Acquire))
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#[no_mangle]
|
|
286
|
+
pub extern "C" fn bloom_get_touch_x(index: f64) -> f64 {
|
|
287
|
+
let s = state();
|
|
288
|
+
let i = index as usize;
|
|
289
|
+
if i < MAX_TOUCH { f64::from_bits(s.touch_x[i].load(Ordering::Acquire)) } else { 0.0 }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
#[no_mangle]
|
|
293
|
+
pub extern "C" fn bloom_get_touch_y(index: f64) -> f64 {
|
|
294
|
+
let s = state();
|
|
295
|
+
let i = index as usize;
|
|
296
|
+
if i < MAX_TOUCH { f64::from_bits(s.touch_y[i].load(Ordering::Acquire)) } else { 0.0 }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
#[no_mangle]
|
|
300
|
+
pub extern "C" fn bloom_get_touch_count() -> f64 {
|
|
301
|
+
let s = state();
|
|
302
|
+
let mut n = 0.0;
|
|
303
|
+
for i in 0..MAX_TOUCH {
|
|
304
|
+
if s.touch_active[i].load(Ordering::Acquire) != 0 { n += 1.0; }
|
|
305
|
+
}
|
|
306
|
+
n
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#[no_mangle]
|
|
310
|
+
pub extern "C" fn bloom_is_touch_active(index: f64) -> f64 {
|
|
311
|
+
let s = state();
|
|
312
|
+
let i = index as usize;
|
|
313
|
+
if i < MAX_TOUCH && s.touch_active[i].load(Ordering::Acquire) != 0 { 1.0 } else { 0.0 }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
#[no_mangle]
|
|
317
|
+
pub extern "C" fn bloom_get_max_touch_points() -> f64 {
|
|
318
|
+
MAX_TOUCH as f64
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#[no_mangle]
|
|
322
|
+
pub extern "C" fn bloom_get_delta_time() -> f64 {
|
|
323
|
+
let s = state();
|
|
324
|
+
let now = now_nanos();
|
|
325
|
+
let last = s.last_frame_ns.swap(now, Ordering::AcqRel);
|
|
326
|
+
if last == 0 { 1.0 / 30.0 } else { (now - last) as f64 / 1_000_000_000.0 }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
#[no_mangle]
|
|
330
|
+
pub extern "C" fn bloom_get_time() -> f64 {
|
|
331
|
+
now_nanos() as f64 / 1_000_000_000.0
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
#[no_mangle]
|
|
335
|
+
pub extern "C" fn bloom_get_fps() -> f64 {
|
|
336
|
+
state().target_fps.load(Ordering::Acquire) as f64
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
#[no_mangle]
|
|
340
|
+
pub extern "C" fn bloom_set_target_fps(fps: f64) {
|
|
341
|
+
state().target_fps.store(fps as i64, Ordering::Release);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// No-op on watchOS (no deferred 3D pipeline); kept to satisfy the FFI
|
|
345
|
+
// surface so cross-platform game code can call setDirect2DMode() freely.
|
|
346
|
+
#[no_mangle]
|
|
347
|
+
pub extern "C" fn bloom_set_direct_2d_mode(_on: f64) {}
|
|
348
|
+
|
|
349
|
+
#[no_mangle]
|
|
350
|
+
pub extern "C" fn bloom_init_window(_w: f64, _h: f64, _title: i64, _fullscreen: f64) {}
|
|
351
|
+
|
|
352
|
+
#[no_mangle]
|
|
353
|
+
pub extern "C" fn bloom_close_window() {}
|
|
354
|
+
|
|
355
|
+
#[no_mangle]
|
|
356
|
+
pub extern "C" fn bloom_window_should_close() -> f64 { 0.0 }
|
|
357
|
+
|
|
358
|
+
// ============================================================
|
|
359
|
+
// Drawing lifecycle
|
|
360
|
+
// ============================================================
|
|
361
|
+
|
|
362
|
+
#[no_mangle]
|
|
363
|
+
pub extern "C" fn bloom_begin_drawing() {
|
|
364
|
+
// Pace the game thread to target fps. Canvas refresh is driven by the
|
|
365
|
+
// frame counter change on the Swift side, so this sleep also controls
|
|
366
|
+
// apparent frame rate in the view.
|
|
367
|
+
let fps = state().target_fps.load(Ordering::Acquire);
|
|
368
|
+
if fps > 0 {
|
|
369
|
+
let frame_ns = 1_000_000_000u64 / fps as u64;
|
|
370
|
+
std::thread::sleep(std::time::Duration::from_nanos(frame_ns));
|
|
371
|
+
}
|
|
372
|
+
draw_list::begin();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
#[no_mangle]
|
|
376
|
+
pub extern "C" fn bloom_end_drawing() {
|
|
377
|
+
draw_list::end();
|
|
378
|
+
state().frame_count.fetch_add(1, Ordering::Relaxed);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
#[no_mangle]
|
|
382
|
+
pub extern "C" fn bloom_clear_background(r: f64, g: f64, b: f64, a: f64) {
|
|
383
|
+
draw_list::set_clear(r, g, b, a);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
#[no_mangle]
|
|
387
|
+
pub extern "C" fn bloom_run_game(_callback: f64) {}
|
|
388
|
+
|
|
389
|
+
#[no_mangle]
|
|
390
|
+
pub extern "C" fn bloom_is_any_input_pressed() -> f64 {
|
|
391
|
+
let s = state();
|
|
392
|
+
for i in 0..MAX_TOUCH {
|
|
393
|
+
if s.touch_active[i].load(Ordering::Acquire) != 0 { return 1.0; }
|
|
394
|
+
}
|
|
395
|
+
if f64::from_bits(s.crown_bits.load(Ordering::Acquire)).abs() > 0.0 { return 1.0; }
|
|
396
|
+
0.0
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#[no_mangle] pub extern "C" fn bloom_is_key_pressed(_k: f64) -> f64 { 0.0 }
|
|
400
|
+
#[no_mangle] pub extern "C" fn bloom_is_key_down(_k: f64) -> f64 { 0.0 }
|
|
401
|
+
#[no_mangle] pub extern "C" fn bloom_is_key_released(_k: f64) -> f64 { 0.0 }
|
|
402
|
+
|
|
403
|
+
#[no_mangle] pub extern "C" fn bloom_inject_key_down(_k: f64) {}
|
|
404
|
+
#[no_mangle] pub extern "C" fn bloom_inject_key_up(_k: f64) {}
|
|
405
|
+
#[no_mangle] pub extern "C" fn bloom_inject_gamepad_axis(_a: f64, _v: f64) {}
|
|
406
|
+
#[no_mangle] pub extern "C" fn bloom_inject_gamepad_button_down(_b: f64) {}
|
|
407
|
+
#[no_mangle] pub extern "C" fn bloom_inject_gamepad_button_up(_b: f64) {}
|
|
408
|
+
|
|
409
|
+
#[no_mangle] pub extern "C" fn bloom_is_gamepad_available() -> f64 { 0.0 }
|
|
410
|
+
#[no_mangle] pub extern "C" fn bloom_get_gamepad_axis(_a: f64) -> f64 { 0.0 }
|
|
411
|
+
#[no_mangle] pub extern "C" fn bloom_is_gamepad_button_pressed(_b: f64) -> f64 { 0.0 }
|
|
412
|
+
#[no_mangle] pub extern "C" fn bloom_is_gamepad_button_down(_b: f64) -> f64 { 0.0 }
|
|
413
|
+
#[no_mangle] pub extern "C" fn bloom_is_gamepad_button_released(_b: f64) -> f64 { 0.0 }
|
|
414
|
+
#[no_mangle] pub extern "C" fn bloom_get_gamepad_axis_count() -> f64 { 0.0 }
|
|
415
|
+
|
|
416
|
+
// ============================================================
|
|
417
|
+
// 2D shape commands → draw list
|
|
418
|
+
// ============================================================
|
|
419
|
+
|
|
420
|
+
#[no_mangle]
|
|
421
|
+
pub extern "C" fn bloom_draw_rect(x: f64, y: f64, w: f64, h: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
422
|
+
let mut c = DrawCmd::zero();
|
|
423
|
+
c.kind = kind::RECT;
|
|
424
|
+
c.x = x; c.y = y; c.w = w; c.h = h;
|
|
425
|
+
c.r = r; c.g = g; c.b = b; c.a = a;
|
|
426
|
+
draw_list::push(c);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
#[no_mangle]
|
|
430
|
+
pub extern "C" fn bloom_draw_rect_lines(x: f64, y: f64, w: f64, h: f64, thickness: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
431
|
+
let mut c = DrawCmd::zero();
|
|
432
|
+
c.kind = kind::RECT_LINES;
|
|
433
|
+
c.x = x; c.y = y; c.w = w; c.h = h;
|
|
434
|
+
c.thickness = thickness;
|
|
435
|
+
c.r = r; c.g = g; c.b = b; c.a = a;
|
|
436
|
+
draw_list::push(c);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
#[no_mangle]
|
|
440
|
+
pub extern "C" fn bloom_draw_circle(cx: f64, cy: f64, rad: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
441
|
+
let mut c = DrawCmd::zero();
|
|
442
|
+
c.kind = kind::CIRCLE;
|
|
443
|
+
c.x = cx; c.y = cy; c.w = rad; c.h = rad;
|
|
444
|
+
c.r = r; c.g = g; c.b = b; c.a = a;
|
|
445
|
+
draw_list::push(c);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// 2D camera: emit marker commands carrying the camera so the Swift Canvas can
|
|
449
|
+
// apply the matching affine transform to every world-space draw until end.
|
|
450
|
+
// world→screen is (world - target) * zoom + offset. Without this, gameplay
|
|
451
|
+
// tiles/sprites (drawn between begin/end) render at raw world coords off-screen.
|
|
452
|
+
#[no_mangle]
|
|
453
|
+
pub extern "C" fn bloom_begin_mode_2d(ox: f64, oy: f64, tx: f64, ty: f64, _rot: f64, zoom: f64) {
|
|
454
|
+
let mut c = DrawCmd::zero();
|
|
455
|
+
c.kind = kind::BEGIN_2D;
|
|
456
|
+
c.x = ox; c.y = oy; // screen offset
|
|
457
|
+
c.w = tx; c.h = ty; // world target
|
|
458
|
+
c.size = zoom; // zoom
|
|
459
|
+
draw_list::push(c);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
#[no_mangle]
|
|
463
|
+
pub extern "C" fn bloom_end_mode_2d() {
|
|
464
|
+
let mut c = DrawCmd::zero();
|
|
465
|
+
c.kind = kind::END_2D;
|
|
466
|
+
draw_list::push(c);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
#[no_mangle]
|
|
470
|
+
pub extern "C" fn bloom_draw_circle_lines(cx: f64, cy: f64, rad: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
471
|
+
// ABI is (cx, cy, radius, r, g, b, a) — no thickness param (matches
|
|
472
|
+
// package.json + src/shapes). A phantom 4th `thickness` param here
|
|
473
|
+
// used to shift every color one slot right on watchOS.
|
|
474
|
+
let mut c = DrawCmd::zero();
|
|
475
|
+
c.kind = kind::CIRCLE_LINES;
|
|
476
|
+
c.x = cx; c.y = cy; c.w = rad; c.h = rad;
|
|
477
|
+
c.thickness = 1.0;
|
|
478
|
+
c.r = r; c.g = g; c.b = b; c.a = a;
|
|
479
|
+
draw_list::push(c);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
#[no_mangle]
|
|
483
|
+
pub extern "C" fn bloom_draw_line(x1: f64, y1: f64, x2: f64, y2: f64, thickness: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
484
|
+
let mut c = DrawCmd::zero();
|
|
485
|
+
c.kind = kind::LINE;
|
|
486
|
+
c.x = x1; c.y = y1;
|
|
487
|
+
c.src_x = x2; c.src_y = y2;
|
|
488
|
+
c.thickness = thickness;
|
|
489
|
+
c.r = r; c.g = g; c.b = b; c.a = a;
|
|
490
|
+
draw_list::push(c);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
#[no_mangle]
|
|
494
|
+
pub extern "C" fn bloom_draw_triangle(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
495
|
+
let mut c = DrawCmd::zero();
|
|
496
|
+
c.kind = kind::TRIANGLE;
|
|
497
|
+
c.x = x1; c.y = y1;
|
|
498
|
+
c.src_x = x2; c.src_y = y2;
|
|
499
|
+
c.src_w = x3; c.src_h = y3;
|
|
500
|
+
c.r = r; c.g = g; c.b = b; c.a = a;
|
|
501
|
+
draw_list::push(c);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
#[no_mangle]
|
|
505
|
+
pub extern "C" fn bloom_draw_poly(_cx: f64, _cy: f64, _sides: f64, _radius: f64, _rot: f64, _r: f64, _g: f64, _b: f64, _a: f64) {
|
|
506
|
+
// Polygon fill — defer; Canvas handles via context.fill(Path.addArc).
|
|
507
|
+
// Will land with the rest of the Canvas geometry work.
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ============================================================
|
|
511
|
+
// Texture commands
|
|
512
|
+
// ============================================================
|
|
513
|
+
|
|
514
|
+
#[no_mangle]
|
|
515
|
+
pub extern "C" fn bloom_load_texture(path: i64) -> f64 {
|
|
516
|
+
textures::load(perry_str(path)) as f64
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
#[no_mangle]
|
|
520
|
+
pub extern "C" fn bloom_unload_texture(_handle: f64) {
|
|
521
|
+
// Retain by handle — unload is a no-op on watch (reloading isn't common
|
|
522
|
+
// in Jump-class games; we keep cached CGImages on the Swift side for
|
|
523
|
+
// the life of the process).
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
#[no_mangle]
|
|
527
|
+
pub extern "C" fn bloom_get_texture_width(handle: f64) -> f64 {
|
|
528
|
+
textures::width(handle as u32) as f64
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
#[no_mangle]
|
|
532
|
+
pub extern "C" fn bloom_get_texture_height(handle: f64) -> f64 {
|
|
533
|
+
textures::height(handle as u32) as f64
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
#[no_mangle]
|
|
537
|
+
pub extern "C" fn bloom_draw_texture(handle: f64, x: f64, y: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
538
|
+
let h = handle as u32;
|
|
539
|
+
if h == 0 { return; }
|
|
540
|
+
let mut c = DrawCmd::zero();
|
|
541
|
+
c.kind = kind::TEXTURE;
|
|
542
|
+
c.tex = h;
|
|
543
|
+
c.x = x; c.y = y;
|
|
544
|
+
c.w = textures::width(h) as f64;
|
|
545
|
+
c.h = textures::height(h) as f64;
|
|
546
|
+
c.r = r; c.g = g; c.b = b; c.a = a;
|
|
547
|
+
draw_list::push(c);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
#[no_mangle]
|
|
551
|
+
pub extern "C" fn bloom_draw_texture_rec(handle: f64,
|
|
552
|
+
src_x: f64, src_y: f64, src_w: f64, src_h: f64,
|
|
553
|
+
dst_x: f64, dst_y: f64,
|
|
554
|
+
r: f64, g: f64, b: f64, a: f64,
|
|
555
|
+
) {
|
|
556
|
+
let h = handle as u32;
|
|
557
|
+
if h == 0 { return; }
|
|
558
|
+
let mut c = DrawCmd::zero();
|
|
559
|
+
c.kind = kind::TEXTURE_REC;
|
|
560
|
+
c.tex = h;
|
|
561
|
+
c.x = dst_x; c.y = dst_y;
|
|
562
|
+
c.w = src_w; c.h = src_h;
|
|
563
|
+
c.src_x = src_x; c.src_y = src_y; c.src_w = src_w; c.src_h = src_h;
|
|
564
|
+
c.r = r; c.g = g; c.b = b; c.a = a;
|
|
565
|
+
draw_list::push(c);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
#[no_mangle]
|
|
569
|
+
pub extern "C" fn bloom_draw_texture_pro(handle: f64,
|
|
570
|
+
src_x: f64, src_y: f64, src_w: f64, src_h: f64,
|
|
571
|
+
dst_x: f64, dst_y: f64, dst_w: f64, dst_h: f64,
|
|
572
|
+
ox: f64, oy: f64, rot: f64,
|
|
573
|
+
r: f64, g: f64, b: f64, a: f64,
|
|
574
|
+
) {
|
|
575
|
+
let h = handle as u32;
|
|
576
|
+
if h == 0 { return; }
|
|
577
|
+
let mut c = DrawCmd::zero();
|
|
578
|
+
c.kind = kind::TEXTURE_PRO;
|
|
579
|
+
c.tex = h;
|
|
580
|
+
c.x = dst_x; c.y = dst_y; c.w = dst_w; c.h = dst_h;
|
|
581
|
+
c.src_x = src_x; c.src_y = src_y; c.src_w = src_w; c.src_h = src_h;
|
|
582
|
+
c.ox = ox; c.oy = oy; c.rot = rot;
|
|
583
|
+
c.r = r; c.g = g; c.b = b; c.a = a;
|
|
584
|
+
draw_list::push(c);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
#[no_mangle]
|
|
588
|
+
pub extern "C" fn bloom_set_texture_filter(_handle: f64, _mode: f64) {}
|
|
589
|
+
|
|
590
|
+
// ============================================================
|
|
591
|
+
// Text
|
|
592
|
+
// ============================================================
|
|
593
|
+
|
|
594
|
+
#[no_mangle]
|
|
595
|
+
pub extern "C" fn bloom_draw_text(text: i64, x: f64, y: f64, size: f64,
|
|
596
|
+
r: f64, g: f64, b: f64, a: f64,
|
|
597
|
+
) {
|
|
598
|
+
push_text(perry_str(text), x, y, size, r, g, b, a);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
#[no_mangle]
|
|
602
|
+
pub extern "C" fn bloom_draw_text_ex(_font: f64, text: i64,
|
|
603
|
+
x: f64, y: f64, size: f64, _spacing: f64,
|
|
604
|
+
r: f64, g: f64, b: f64, a: f64,
|
|
605
|
+
) {
|
|
606
|
+
push_text(perry_str(text), x, y, size, r, g, b, a);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
fn push_text(s: &str, x: f64, y: f64, size: f64,
|
|
610
|
+
r: f64, g: f64, b: f64, a: f64,
|
|
611
|
+
) {
|
|
612
|
+
if s.is_empty() { return; }
|
|
613
|
+
let mut c = DrawCmd::zero();
|
|
614
|
+
c.kind = kind::TEXT;
|
|
615
|
+
c.x = x; c.y = y;
|
|
616
|
+
c.size = size;
|
|
617
|
+
c.r = r; c.g = g; c.b = b; c.a = a;
|
|
618
|
+
c.set_text(s);
|
|
619
|
+
draw_list::push(c);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/// Crude text width estimate — fontSize * 0.55 * char_count. Good enough for
|
|
623
|
+
/// HUD layout; real glyph metrics would require calling back into Swift
|
|
624
|
+
/// (CoreText) or bundling a font parser.
|
|
625
|
+
#[no_mangle]
|
|
626
|
+
pub extern "C" fn bloom_measure_text(text: i64, size: f64) -> f64 {
|
|
627
|
+
perry_str(text).chars().count() as f64 * size * 0.55
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
#[no_mangle]
|
|
631
|
+
pub extern "C" fn bloom_measure_text_ex(_font: f64, text: i64, size: f64, _spacing: f64) -> f64 {
|
|
632
|
+
bloom_measure_text(text, size)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
#[no_mangle]
|
|
636
|
+
pub extern "C" fn bloom_load_font(_path: i64, _size: f64) -> f64 {
|
|
637
|
+
1.0 // All text uses the system font on watch; single sentinel handle.
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
#[no_mangle]
|
|
641
|
+
pub extern "C" fn bloom_unload_font(_handle: f64) {}
|
|
642
|
+
|
|
643
|
+
// ============================================================
|
|
644
|
+
// File I/O — minimal std::fs-backed implementations
|
|
645
|
+
// ============================================================
|
|
646
|
+
|
|
647
|
+
#[no_mangle]
|
|
648
|
+
pub extern "C" fn bloom_file_exists(path: i64) -> f64 {
|
|
649
|
+
let p = perry_str(path);
|
|
650
|
+
if p.is_empty() { return 0.0; }
|
|
651
|
+
// Resolve bundle-relative paths the same way textures does.
|
|
652
|
+
let full = if p.starts_with('/') { p.to_string() } else { textures::resolve_bundle_path(p) };
|
|
653
|
+
if std::path::Path::new(&full).exists() { 1.0 } else { 0.0 }
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
#[no_mangle]
|
|
657
|
+
pub extern "C" fn bloom_read_file(path: i64) -> i64 {
|
|
658
|
+
let p = perry_str(path);
|
|
659
|
+
if p.is_empty() {
|
|
660
|
+
return alloc_perry_string("");
|
|
661
|
+
}
|
|
662
|
+
// Resolve bundle-relative paths the same way bloom_file_exists / textures do.
|
|
663
|
+
let full = if p.starts_with('/') {
|
|
664
|
+
p.to_string()
|
|
665
|
+
} else {
|
|
666
|
+
textures::resolve_bundle_path(p)
|
|
667
|
+
};
|
|
668
|
+
// Always hand back a valid StringHeader — empty on a miss, never null.
|
|
669
|
+
match std::fs::read_to_string(&full) {
|
|
670
|
+
Ok(contents) => alloc_perry_string(&contents),
|
|
671
|
+
Err(_) => alloc_perry_string(""),
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
#[no_mangle]
|
|
676
|
+
pub extern "C" fn bloom_write_file(_path: i64, _data: i64) -> f64 {
|
|
677
|
+
0.0 // watchOS app sandbox — defer real write support.
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// ============================================================
|
|
681
|
+
// 3D immediate-mode primitives
|
|
682
|
+
// ============================================================
|
|
683
|
+
|
|
684
|
+
#[no_mangle]
|
|
685
|
+
pub extern "C" fn bloom_begin_mode_3d(
|
|
686
|
+
px: f64, py: f64, pz: f64,
|
|
687
|
+
tx: f64, ty: f64, tz: f64,
|
|
688
|
+
ux: f64, uy: f64, uz: f64,
|
|
689
|
+
fovy: f64, proj: f64,
|
|
690
|
+
) {
|
|
691
|
+
draw_list::set_camera(px, py, pz, tx, ty, tz, ux, uy, uz, fovy, proj);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
#[no_mangle]
|
|
695
|
+
pub extern "C" fn bloom_end_mode_3d() {
|
|
696
|
+
// No-op: 3D commands are already in the list; the camera stays set until
|
|
697
|
+
// the next begin_mode_3d. Swift's SceneView draws 3D cmds under the
|
|
698
|
+
// Canvas layer each frame.
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
#[no_mangle]
|
|
702
|
+
pub extern "C" fn bloom_draw_cube(x: f64, y: f64, z: f64, w: f64, h: f64, d: f64,
|
|
703
|
+
r: f64, g: f64, b: f64, a: f64,
|
|
704
|
+
) {
|
|
705
|
+
let mut c = DrawCmd::zero();
|
|
706
|
+
c.kind = kind::CUBE;
|
|
707
|
+
c.x = x; c.y = y; c.src_x = z; // src_x doubles as z on 3D cmds
|
|
708
|
+
c.w = w; c.h = h; c.size = d;
|
|
709
|
+
c.r = r; c.g = g; c.b = b; c.a = a;
|
|
710
|
+
draw_list::push(c);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
#[no_mangle]
|
|
714
|
+
pub extern "C" fn bloom_draw_cube_wires(x: f64, y: f64, z: f64, w: f64, h: f64, d: f64,
|
|
715
|
+
r: f64, g: f64, b: f64, a: f64,
|
|
716
|
+
) {
|
|
717
|
+
let mut c = DrawCmd::zero();
|
|
718
|
+
c.kind = kind::CUBE_WIRES;
|
|
719
|
+
c.x = x; c.y = y; c.src_x = z; // src_x doubles as z on 3D cmds
|
|
720
|
+
c.w = w; c.h = h; c.size = d;
|
|
721
|
+
c.r = r; c.g = g; c.b = b; c.a = a;
|
|
722
|
+
draw_list::push(c);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
#[no_mangle]
|
|
726
|
+
pub extern "C" fn bloom_draw_sphere(x: f64, y: f64, z: f64, radius: f64,
|
|
727
|
+
r: f64, g: f64, b: f64, a: f64,
|
|
728
|
+
) {
|
|
729
|
+
let mut c = DrawCmd::zero();
|
|
730
|
+
c.kind = kind::SPHERE;
|
|
731
|
+
c.x = x; c.y = y; c.src_x = z; // src_x doubles as z on 3D cmds
|
|
732
|
+
c.w = radius;
|
|
733
|
+
c.r = r; c.g = g; c.b = b; c.a = a;
|
|
734
|
+
draw_list::push(c);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
#[no_mangle]
|
|
738
|
+
pub extern "C" fn bloom_draw_sphere_wires(x: f64, y: f64, z: f64, radius: f64,
|
|
739
|
+
r: f64, g: f64, b: f64, a: f64,
|
|
740
|
+
) {
|
|
741
|
+
let mut c = DrawCmd::zero();
|
|
742
|
+
c.kind = kind::SPHERE_WIRES;
|
|
743
|
+
c.x = x; c.y = y; c.src_x = z; // src_x doubles as z on 3D cmds
|
|
744
|
+
c.w = radius;
|
|
745
|
+
c.r = r; c.g = g; c.b = b; c.a = a;
|
|
746
|
+
draw_list::push(c);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
#[no_mangle]
|
|
750
|
+
pub extern "C" fn bloom_draw_cylinder(x: f64, y: f64, z: f64,
|
|
751
|
+
radius_top: f64, _radius_bottom: f64, height: f64,
|
|
752
|
+
r: f64, g: f64, b: f64, a: f64,
|
|
753
|
+
) {
|
|
754
|
+
// ABI is 10 params (no slices) — that's bloom_draw_cylinder_ex. The
|
|
755
|
+
// phantom `slices` param shifted every color one slot right.
|
|
756
|
+
let mut c = DrawCmd::zero();
|
|
757
|
+
c.kind = kind::CYLINDER;
|
|
758
|
+
c.x = x; c.y = y; c.src_x = z; // src_x doubles as z on 3D cmds
|
|
759
|
+
c.w = radius_top; c.h = height;
|
|
760
|
+
c.r = r; c.g = g; c.b = b; c.a = a;
|
|
761
|
+
draw_list::push(c);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
#[no_mangle]
|
|
765
|
+
pub extern "C" fn bloom_draw_plane(x: f64, y: f64, z: f64, w: f64, h: f64,
|
|
766
|
+
r: f64, g: f64, b: f64, a: f64,
|
|
767
|
+
) {
|
|
768
|
+
let mut c = DrawCmd::zero();
|
|
769
|
+
c.kind = kind::PLANE;
|
|
770
|
+
c.x = x; c.y = y; c.src_x = z; // src_x doubles as z on 3D cmds
|
|
771
|
+
c.w = w; c.h = h;
|
|
772
|
+
c.r = r; c.g = g; c.b = b; c.a = a;
|
|
773
|
+
draw_list::push(c);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// ============================================================
|
|
777
|
+
// Audio (forwards to Swift BloomAudioManager)
|
|
778
|
+
// ============================================================
|
|
779
|
+
|
|
780
|
+
#[no_mangle] pub extern "C" fn bloom_init_audio() { audio::init_audio(); }
|
|
781
|
+
#[no_mangle] pub extern "C" fn bloom_close_audio() { audio::close_audio(); }
|
|
782
|
+
|
|
783
|
+
#[no_mangle]
|
|
784
|
+
pub extern "C" fn bloom_load_sound(path: i64) -> f64 {
|
|
785
|
+
audio::load_sound(perry_str(path)) as f64
|
|
786
|
+
}
|
|
787
|
+
#[no_mangle] pub extern "C" fn bloom_play_sound(handle: f64) { audio::play_sound(handle as u32); }
|
|
788
|
+
#[no_mangle] pub extern "C" fn bloom_stop_sound(handle: f64) { audio::stop_sound(handle as u32); }
|
|
789
|
+
#[no_mangle] pub extern "C" fn bloom_set_sound_volume(handle: f64, v: f64) {
|
|
790
|
+
audio::set_sound_volume(handle as u32, v as f32);
|
|
791
|
+
}
|
|
792
|
+
#[no_mangle] pub extern "C" fn bloom_set_master_volume(v: f64) {
|
|
793
|
+
audio::set_master_volume(v as f32);
|
|
794
|
+
}
|
|
795
|
+
#[no_mangle] pub extern "C" fn bloom_play_sound_3d(handle: f64, _x: f64, _y: f64, _z: f64) {
|
|
796
|
+
// No spatial audio on watch — play at full 2D volume.
|
|
797
|
+
audio::play_sound(handle as u32);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
#[no_mangle]
|
|
801
|
+
pub extern "C" fn bloom_load_music(path: i64) -> f64 {
|
|
802
|
+
audio::load_music(perry_str(path)) as f64
|
|
803
|
+
}
|
|
804
|
+
#[no_mangle] pub extern "C" fn bloom_play_music(handle: f64) { audio::play_music(handle as u32); }
|
|
805
|
+
#[no_mangle] pub extern "C" fn bloom_stop_music(handle: f64) { audio::stop_music(handle as u32); }
|
|
806
|
+
#[no_mangle] pub extern "C" fn bloom_set_music_volume(handle: f64, v: f64) {
|
|
807
|
+
audio::set_music_volume(handle as u32, v as f32);
|
|
808
|
+
}
|
|
809
|
+
#[no_mangle] pub extern "C" fn bloom_is_music_playing(handle: f64) -> f64 {
|
|
810
|
+
if audio::is_music_playing(handle as u32) { 1.0 } else { 0.0 }
|
|
811
|
+
}
|
|
812
|
+
#[no_mangle] pub extern "C" fn bloom_update_music_stream(_handle: f64) {
|
|
813
|
+
// AVAudioPlayer owns stream pumping internally — no per-frame poke needed.
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// ============================================================
|
|
817
|
+
// Retained scene graph (bloom_scene_*) — synced to SCNNodes in Swift
|
|
818
|
+
// ============================================================
|
|
819
|
+
|
|
820
|
+
#[no_mangle] pub extern "C" fn bloom_scene_create_node() -> f64 { scene::create() as f64 }
|
|
821
|
+
#[no_mangle] pub extern "C" fn bloom_scene_destroy_node(h: f64) { scene::destroy(h as u32); }
|
|
822
|
+
#[no_mangle] pub extern "C" fn bloom_scene_set_visible(h: f64, v: f64) {
|
|
823
|
+
scene::set_visible(h as u32, v > 0.5);
|
|
824
|
+
}
|
|
825
|
+
#[no_mangle] pub extern "C" fn bloom_scene_set_cast_shadow(_h: f64, _v: f64) {
|
|
826
|
+
// SceneKit manages shadow casting via per-node + light config; deferred.
|
|
827
|
+
}
|
|
828
|
+
#[no_mangle] pub extern "C" fn bloom_scene_set_receive_shadow(_h: f64, _v: f64) {}
|
|
829
|
+
#[no_mangle] pub extern "C" fn bloom_scene_set_parent(h: f64, parent: f64) {
|
|
830
|
+
scene::set_parent(h as u32, parent as u32);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/// Transform is a raw pointer to 16 column-major f64s (128 bytes). No
|
|
834
|
+
/// StringHeader wrapping — bloom's TS side passes a `number[]` which Perry
|
|
835
|
+
/// forwards as a pointer to the Float64 data.
|
|
836
|
+
#[no_mangle]
|
|
837
|
+
pub extern "C" fn bloom_scene_set_transform(handle: f64, matrix_ptr: i64) {
|
|
838
|
+
if matrix_ptr == 0 { return; }
|
|
839
|
+
let src = unsafe { std::slice::from_raw_parts(matrix_ptr as *const f64, 16) };
|
|
840
|
+
let mut arr = [0.0f32; 16];
|
|
841
|
+
for i in 0..16 { arr[i] = src[i] as f32; }
|
|
842
|
+
scene::set_transform(handle as u32, arr);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/// Geometry: vert_ptr points to `vertex_count * 12` f64s (12 floats per
|
|
846
|
+
/// vertex: xyz, nx ny nz, rgba, uv). idx_ptr points to `index_count` f64s
|
|
847
|
+
/// (each integer index boxed as f64 — matches bloom's TS `number[]` calls).
|
|
848
|
+
#[no_mangle]
|
|
849
|
+
pub extern "C" fn bloom_scene_update_geometry(
|
|
850
|
+
handle: f64, verts_ptr: i64, vertex_count: f64,
|
|
851
|
+
idx_ptr: i64, index_count: f64,
|
|
852
|
+
) {
|
|
853
|
+
if verts_ptr == 0 || idx_ptr == 0 { return; }
|
|
854
|
+
let vcount = vertex_count as usize;
|
|
855
|
+
let icount = index_count as usize;
|
|
856
|
+
let verts = unsafe { std::slice::from_raw_parts(verts_ptr as *const f64, vcount * 12) };
|
|
857
|
+
let idx_f64 = unsafe { std::slice::from_raw_parts(idx_ptr as *const f64, icount) };
|
|
858
|
+
scene::update_geometry_f64(handle as u32, verts, idx_f64);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
#[no_mangle]
|
|
862
|
+
pub extern "C" fn bloom_scene_set_material_color(h: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
863
|
+
scene::set_color(h as u32, [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a as f32 / 255.0]);
|
|
864
|
+
}
|
|
865
|
+
#[no_mangle]
|
|
866
|
+
pub extern "C" fn bloom_scene_set_material_pbr(h: f64, roughness: f64, metalness: f64) {
|
|
867
|
+
scene::set_pbr(h as u32, roughness as f32, metalness as f32);
|
|
868
|
+
}
|
|
869
|
+
#[no_mangle]
|
|
870
|
+
pub extern "C" fn bloom_scene_set_material_texture(h: f64, tex: f64) {
|
|
871
|
+
scene::set_texture(h as u32, tex as u32);
|
|
872
|
+
}
|
|
873
|
+
#[no_mangle] pub extern "C" fn bloom_scene_node_count() -> f64 { scene::node_count() as f64 }
|
|
874
|
+
|
|
875
|
+
#[no_mangle]
|
|
876
|
+
pub extern "C" fn bloom_scene_get_transform(handle: f64, out: f64) -> f64 {
|
|
877
|
+
// `out` is a pointer to 16 f32s the caller has allocated. Write them.
|
|
878
|
+
if out == 0.0 { return 0.0; }
|
|
879
|
+
let t = scene::get_transform(handle as u32);
|
|
880
|
+
unsafe {
|
|
881
|
+
std::ptr::copy_nonoverlapping(t.as_ptr(), out as usize as *mut f32, 16);
|
|
882
|
+
}
|
|
883
|
+
1.0
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// ============================================================
|
|
887
|
+
// Models — glTF (.glb) loading + scene attachment
|
|
888
|
+
// ============================================================
|
|
889
|
+
|
|
890
|
+
#[no_mangle]
|
|
891
|
+
pub extern "C" fn bloom_load_model(path: i64) -> f64 {
|
|
892
|
+
let p = perry_str(path);
|
|
893
|
+
if p.is_empty() { return 0.0; }
|
|
894
|
+
let full = if p.starts_with('/') { p.to_string() } else { textures::resolve_bundle_path(p) };
|
|
895
|
+
models::load(&full) as f64
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
#[no_mangle]
|
|
899
|
+
pub extern "C" fn bloom_gen_mesh_cube(w: f64, h: f64, d: f64) -> f64 {
|
|
900
|
+
models::gen_cube_mesh(w as f32, h as f32, d as f32) as f64
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/// Attach a loaded model to a scene node. If the model has a glTF scene
|
|
904
|
+
/// hierarchy, the full subtree is spawned as bloom scene-node descendants of
|
|
905
|
+
/// `node_handle` with each glTF node's TRS transform baked in. For
|
|
906
|
+
/// single-node models this collapses to the pre-#15 behavior (the target
|
|
907
|
+
/// node gets the one mesh's first primitive, sibling primitives become its
|
|
908
|
+
/// children).
|
|
909
|
+
///
|
|
910
|
+
/// The `mesh_idx` parameter is only honored when the model has no scene
|
|
911
|
+
/// hierarchy — it picks which mesh to instantiate into `node_handle`.
|
|
912
|
+
#[no_mangle]
|
|
913
|
+
pub extern "C" fn bloom_scene_attach_model(node_handle: f64, model_handle: f64, mesh_idx: f64) {
|
|
914
|
+
let Some(model) = models::get(model_handle as u32) else { return; };
|
|
915
|
+
let root = node_handle as u32;
|
|
916
|
+
|
|
917
|
+
// If the model has a scene graph with non-trivial hierarchy (>1 node or a
|
|
918
|
+
// transform on the root), walk it. Otherwise fall back to single-mesh
|
|
919
|
+
// mode so mesh_idx still picks which mesh to attach.
|
|
920
|
+
let single_mesh = model.nodes.len() <= 1
|
|
921
|
+
&& model.scene_roots.len() <= 1
|
|
922
|
+
&& model.nodes.first().map(|n| n.matrix.is_none()
|
|
923
|
+
&& n.translation == [0.0; 3]
|
|
924
|
+
&& n.rotation == [0.0, 0.0, 0.0, 1.0]
|
|
925
|
+
&& n.scale == [1.0; 3]).unwrap_or(true);
|
|
926
|
+
|
|
927
|
+
if single_mesh {
|
|
928
|
+
let mesh_i = mesh_idx as usize;
|
|
929
|
+
let Some(mesh) = model.meshes.get(mesh_i) else { return; };
|
|
930
|
+
attach_mesh(root, mesh);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Scene-hierarchy path: one bloom scene node per glTF node. The attach
|
|
935
|
+
// target `root` itself doesn't gain geometry — it's the hierarchy's
|
|
936
|
+
// anchor. Children materialize via scene::create + scene::set_parent.
|
|
937
|
+
// We build a gltf_node_idx → bloom_handle map during the walk, then
|
|
938
|
+
// resolve skin joint references once everyone's in place.
|
|
939
|
+
let mut gltf_to_bloom: std::collections::HashMap<usize, u32> = std::collections::HashMap::new();
|
|
940
|
+
let mut skinned_nodes: Vec<(u32, usize)> = Vec::new(); // (bloom_handle, gltf_node_idx)
|
|
941
|
+
for scene_root in &model.scene_roots {
|
|
942
|
+
spawn_node_subtree(&model, *scene_root, root, &mut gltf_to_bloom, &mut skinned_nodes);
|
|
943
|
+
}
|
|
944
|
+
// Second pass: attach skin data to each skinned mesh-node. Joints are
|
|
945
|
+
// resolved to the bloom handles we just created.
|
|
946
|
+
for (bloom_handle, gltf_idx) in skinned_nodes {
|
|
947
|
+
let gltf_node = &model.nodes[gltf_idx];
|
|
948
|
+
let Some(skin_idx) = gltf_node.skin else { continue };
|
|
949
|
+
let Some(skin) = model.skins.get(skin_idx) else { continue };
|
|
950
|
+
let Some(mesh_idx) = gltf_node.mesh else { continue };
|
|
951
|
+
let Some(mesh) = model.meshes.get(mesh_idx) else { continue };
|
|
952
|
+
let Some(prim) = mesh.primitives.first() else { continue };
|
|
953
|
+
|
|
954
|
+
let joint_handles: Vec<u32> = skin.joints.iter()
|
|
955
|
+
.map(|&gi| *gltf_to_bloom.get(&gi).unwrap_or(&0))
|
|
956
|
+
.collect();
|
|
957
|
+
scene::set_skin(bloom_handle,
|
|
958
|
+
joint_handles,
|
|
959
|
+
skin.inverse_bind_matrices.clone(),
|
|
960
|
+
prim.joint_indices.clone(),
|
|
961
|
+
prim.weights.clone(),
|
|
962
|
+
);
|
|
963
|
+
|
|
964
|
+
// Animation tracks — resolve channel targets to bloom handles.
|
|
965
|
+
// Auto-select animation 0 for now (typically the idle/survey clip).
|
|
966
|
+
// Follow-up: bloom_update_model_animation to switch between them.
|
|
967
|
+
if let Some(anim) = model.animations.first() {
|
|
968
|
+
let mut tracks: Vec<scene::AnimTrack> = Vec::new();
|
|
969
|
+
for ch in &anim.channels {
|
|
970
|
+
let Some(&bone_h) = gltf_to_bloom.get(&ch.target_node) else { continue };
|
|
971
|
+
if bone_h == 0 { continue; }
|
|
972
|
+
tracks.push(scene::AnimTrack {
|
|
973
|
+
bone_handle: bone_h,
|
|
974
|
+
path: ch.path as u32,
|
|
975
|
+
times: ch.times.clone(),
|
|
976
|
+
values: ch.values.clone(),
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
if !tracks.is_empty() {
|
|
980
|
+
scene::set_anim_tracks(bloom_handle, tracks);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
fn attach_mesh(handle: u32, mesh: &models::Mesh) {
|
|
987
|
+
if mesh.primitives.is_empty() { return; }
|
|
988
|
+
apply_primitive(handle, &mesh.primitives[0]);
|
|
989
|
+
for prim in mesh.primitives.iter().skip(1) {
|
|
990
|
+
let child = scene::create();
|
|
991
|
+
scene::set_parent(child, handle);
|
|
992
|
+
apply_primitive(child, prim);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
fn spawn_node_subtree(
|
|
997
|
+
model: &models::Model, node_idx: usize, parent: u32,
|
|
998
|
+
gltf_to_bloom: &mut std::collections::HashMap<usize, u32>,
|
|
999
|
+
skinned_nodes: &mut Vec<(u32, usize)>,
|
|
1000
|
+
) {
|
|
1001
|
+
let Some(gltf_node) = model.nodes.get(node_idx) else { return; };
|
|
1002
|
+
let bloom_node = scene::create();
|
|
1003
|
+
gltf_to_bloom.insert(node_idx, bloom_node);
|
|
1004
|
+
scene::set_parent(bloom_node, parent);
|
|
1005
|
+
scene::set_transform(bloom_node, gltf_node.local_transform());
|
|
1006
|
+
if let Some(mi) = gltf_node.mesh {
|
|
1007
|
+
if let Some(mesh) = model.meshes.get(mi) {
|
|
1008
|
+
attach_mesh(bloom_node, mesh);
|
|
1009
|
+
if gltf_node.skin.is_some() {
|
|
1010
|
+
skinned_nodes.push((bloom_node, node_idx));
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
for &child_idx in &gltf_node.children {
|
|
1015
|
+
spawn_node_subtree(model, child_idx, bloom_node, gltf_to_bloom, skinned_nodes);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
fn apply_primitive(handle: u32, prim: &models::Primitive) {
|
|
1020
|
+
scene::set_geometry(handle,
|
|
1021
|
+
prim.positions.clone(), prim.normals.clone(),
|
|
1022
|
+
prim.uvs.clone(), prim.indices.clone());
|
|
1023
|
+
scene::set_color(handle, prim.color);
|
|
1024
|
+
scene::set_pbr(handle, prim.roughness, prim.metallic);
|
|
1025
|
+
scene::set_pbr_textures(handle,
|
|
1026
|
+
prim.tex_base_color, prim.tex_normal,
|
|
1027
|
+
prim.tex_metallic_roughness, prim.tex_emissive, prim.tex_occlusion);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// ============================================================
|
|
1031
|
+
// Post-processing — stored for Swift to translate into SwiftUI modifiers
|
|
1032
|
+
// ============================================================
|
|
1033
|
+
|
|
1034
|
+
#[no_mangle] pub extern "C" fn bloom_enable_postfx() { postfx::set_enabled(true); }
|
|
1035
|
+
#[no_mangle] pub extern "C" fn bloom_disable_postfx() { postfx::set_enabled(false); }
|
|
1036
|
+
#[no_mangle] pub extern "C" fn bloom_set_vignette(strength: f64, softness: f64) {
|
|
1037
|
+
postfx::set_vignette(strength, softness);
|
|
1038
|
+
}
|
|
1039
|
+
#[no_mangle] pub extern "C" fn bloom_set_chromatic_aberration(strength: f64) {
|
|
1040
|
+
postfx::set_chromatic_aberration(strength);
|
|
1041
|
+
}
|
|
1042
|
+
#[no_mangle] pub extern "C" fn bloom_set_film_grain(strength: f64) {
|
|
1043
|
+
postfx::set_film_grain(strength);
|
|
1044
|
+
}
|
|
1045
|
+
#[no_mangle] pub extern "C" fn bloom_set_manual_exposure(v: f64) { postfx::set_exposure(v); }
|
|
1046
|
+
#[no_mangle] pub extern "C" fn bloom_set_auto_exposure(on: f64) {
|
|
1047
|
+
postfx::set_auto_exposure(on > 0.5);
|
|
1048
|
+
}
|
|
1049
|
+
#[no_mangle] pub extern "C" fn bloom_set_sun_shafts(
|
|
1050
|
+
strength: f64, decay: f64, r: f64, g: f64, b: f64,
|
|
1051
|
+
) {
|
|
1052
|
+
postfx::set_sun_shafts(strength, decay, r, g, b);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
#[no_mangle]
|
|
1056
|
+
pub extern "C" fn bloom_watchos_postfx_state(out: *mut postfx::PostFxState) {
|
|
1057
|
+
postfx::snapshot(out);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
#[no_mangle] pub extern "C" fn bloom_add_directional_light(
|
|
1061
|
+
dx: f64, dy: f64, dz: f64, r: f64, g: f64, b: f64, intensity: f64,
|
|
1062
|
+
) {
|
|
1063
|
+
scene::add_directional_light(dx as f32, dy as f32, dz as f32,
|
|
1064
|
+
r as f32, g as f32, b as f32, intensity as f32);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
#[no_mangle] pub extern "C" fn bloom_add_point_light(
|
|
1068
|
+
x: f64, y: f64, z: f64, range: f64, r: f64, g: f64, b: f64, intensity: f64,
|
|
1069
|
+
) {
|
|
1070
|
+
scene::add_point_light(x as f32, y as f32, z as f32, range as f32,
|
|
1071
|
+
r as f32, g as f32, b as f32, intensity as f32);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// ============================================================
|
|
1075
|
+
// Scene snapshot accessors for Swift
|
|
1076
|
+
// ============================================================
|
|
1077
|
+
|
|
1078
|
+
#[no_mangle]
|
|
1079
|
+
pub extern "C" fn bloom_watchos_scene_drain_dirty(dst: *mut scene::SceneNodeInfo, max: i64) -> i64 {
|
|
1080
|
+
scene::drain_dirty(dst, max)
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
#[no_mangle]
|
|
1084
|
+
pub extern "C" fn bloom_watchos_scene_drain_destroyed(dst: *mut u32, max: i64) -> i64 {
|
|
1085
|
+
scene::drain_destroyed(dst, max)
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
#[no_mangle]
|
|
1089
|
+
pub extern "C" fn bloom_watchos_scene_copy_lights(dst: *mut scene::Light, max: i64) -> i64 {
|
|
1090
|
+
scene::copy_lights(dst, max)
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
#[no_mangle]
|
|
1094
|
+
pub extern "C" fn bloom_watchos_scene_geometry(handle: u32, out: *mut scene::GeometryPtrs) {
|
|
1095
|
+
if out.is_null() { return; }
|
|
1096
|
+
unsafe { *out = scene::geometry_ptrs(handle); }
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
#[no_mangle]
|
|
1100
|
+
pub extern "C" fn bloom_watchos_scene_skin(handle: u32, out: *mut scene::SkinPtrs) {
|
|
1101
|
+
if out.is_null() { return; }
|
|
1102
|
+
unsafe { *out = scene::skin_ptrs(handle); }
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
#[no_mangle]
|
|
1106
|
+
pub extern "C" fn bloom_watchos_scene_anim_track_count(handle: u32) -> i64 {
|
|
1107
|
+
scene::anim_track_count(handle)
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
#[no_mangle]
|
|
1111
|
+
pub extern "C" fn bloom_watchos_scene_anim_track_info(handle: u32, idx: i64, out: *mut scene::AnimTrackInfo) {
|
|
1112
|
+
scene::anim_track_info(handle, idx, out);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
#[no_mangle]
|
|
1116
|
+
pub extern "C" fn bloom_draw_grid(slices: f64, spacing: f64) {
|
|
1117
|
+
let mut c = DrawCmd::zero();
|
|
1118
|
+
c.kind = kind::GRID;
|
|
1119
|
+
c.w = slices;
|
|
1120
|
+
c.h = spacing;
|
|
1121
|
+
// Grid color — light gray, matches bloom's default.
|
|
1122
|
+
c.r = 200.0; c.g = 200.0; c.b = 200.0; c.a = 255.0;
|
|
1123
|
+
draw_list::push(c);
|
|
1124
|
+
}
|