@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,156 @@
|
|
|
1
|
+
//! Fixed-capacity lock-free single-producer/single-consumer ring buffer.
|
|
2
|
+
//!
|
|
3
|
+
//! Purpose-built for the audio command queue: the main (FFI) thread is the
|
|
4
|
+
//! only producer, the platform audio thread the only consumer, and the
|
|
5
|
+
//! consumer must never block, allocate, or take a lock — an audio callback
|
|
6
|
+
//! that waits on a mutex held across a frame hitch produces an audible
|
|
7
|
+
//! glitch, and a poisoned lock would kill sound for the rest of the
|
|
8
|
+
//! session (both observed failure modes of the old shared-mutable mixer).
|
|
9
|
+
//!
|
|
10
|
+
//! Standard two-counter ring: `tail` is written only by the producer,
|
|
11
|
+
//! `head` only by the consumer; each side reads the other's counter with
|
|
12
|
+
//! Acquire and publishes its own with Release. One slot is sacrificed to
|
|
13
|
+
//! distinguish full from empty.
|
|
14
|
+
|
|
15
|
+
use std::cell::UnsafeCell;
|
|
16
|
+
use std::mem::MaybeUninit;
|
|
17
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
18
|
+
use std::sync::Arc;
|
|
19
|
+
|
|
20
|
+
struct Ring<T> {
|
|
21
|
+
buf: Box<[UnsafeCell<MaybeUninit<T>>]>,
|
|
22
|
+
/// next slot to read; owned by the consumer
|
|
23
|
+
head: AtomicUsize,
|
|
24
|
+
/// next slot to write; owned by the producer
|
|
25
|
+
tail: AtomicUsize,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The ring itself is shared between exactly two threads with disjoint
|
|
29
|
+
// roles; the unsafe impls are sound because Producer and Consumer are
|
|
30
|
+
// each !Clone and only touch their own counter + the slots their
|
|
31
|
+
// protocol entitles them to.
|
|
32
|
+
unsafe impl<T: Send> Send for Ring<T> {}
|
|
33
|
+
unsafe impl<T: Send> Sync for Ring<T> {}
|
|
34
|
+
|
|
35
|
+
impl<T> Drop for Ring<T> {
|
|
36
|
+
fn drop(&mut self) {
|
|
37
|
+
// By the time the last Arc drops we have exclusive access; drain
|
|
38
|
+
// any unconsumed items so their destructors (Arc<SoundData>!) run.
|
|
39
|
+
let mut head = *self.head.get_mut();
|
|
40
|
+
let tail = *self.tail.get_mut();
|
|
41
|
+
while head != tail {
|
|
42
|
+
unsafe { (*self.buf[head].get()).assume_init_drop() };
|
|
43
|
+
head = (head + 1) % self.buf.len();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pub struct Producer<T> {
|
|
49
|
+
ring: Arc<Ring<T>>,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
pub struct Consumer<T> {
|
|
53
|
+
ring: Arc<Ring<T>>,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
pub fn channel<T: Send>(capacity: usize) -> (Producer<T>, Consumer<T>) {
|
|
57
|
+
assert!(capacity >= 2);
|
|
58
|
+
let buf: Box<[UnsafeCell<MaybeUninit<T>>]> = (0..capacity)
|
|
59
|
+
.map(|_| UnsafeCell::new(MaybeUninit::uninit()))
|
|
60
|
+
.collect();
|
|
61
|
+
let ring = Arc::new(Ring {
|
|
62
|
+
buf,
|
|
63
|
+
head: AtomicUsize::new(0),
|
|
64
|
+
tail: AtomicUsize::new(0),
|
|
65
|
+
});
|
|
66
|
+
(Producer { ring: ring.clone() }, Consumer { ring })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
impl<T> Producer<T> {
|
|
70
|
+
/// Push an item; returns it back if the ring is full (the caller
|
|
71
|
+
/// decides whether dropping the command is acceptable).
|
|
72
|
+
pub fn push(&mut self, item: T) -> Result<(), T> {
|
|
73
|
+
let ring = &*self.ring;
|
|
74
|
+
let tail = ring.tail.load(Ordering::Relaxed);
|
|
75
|
+
let next = (tail + 1) % ring.buf.len();
|
|
76
|
+
if next == ring.head.load(Ordering::Acquire) {
|
|
77
|
+
return Err(item); // full
|
|
78
|
+
}
|
|
79
|
+
unsafe { (*ring.buf[tail].get()).write(item) };
|
|
80
|
+
ring.tail.store(next, Ordering::Release);
|
|
81
|
+
Ok(())
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
impl<T> Consumer<T> {
|
|
86
|
+
/// Pop the oldest item, if any. Wait-free; safe to call from a
|
|
87
|
+
/// real-time audio callback.
|
|
88
|
+
pub fn pop(&mut self) -> Option<T> {
|
|
89
|
+
let ring = &*self.ring;
|
|
90
|
+
let head = ring.head.load(Ordering::Relaxed);
|
|
91
|
+
if head == ring.tail.load(Ordering::Acquire) {
|
|
92
|
+
return None; // empty
|
|
93
|
+
}
|
|
94
|
+
let item = unsafe { (*ring.buf[head].get()).assume_init_read() };
|
|
95
|
+
ring.head.store((head + 1) % ring.buf.len(), Ordering::Release);
|
|
96
|
+
Some(item)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#[cfg(test)]
|
|
101
|
+
mod tests {
|
|
102
|
+
use super::*;
|
|
103
|
+
|
|
104
|
+
#[test]
|
|
105
|
+
fn push_pop_order() {
|
|
106
|
+
let (mut tx, mut rx) = channel::<u32>(4);
|
|
107
|
+
assert!(tx.push(1).is_ok());
|
|
108
|
+
assert!(tx.push(2).is_ok());
|
|
109
|
+
assert!(tx.push(3).is_ok()); // capacity 4 → 3 usable slots
|
|
110
|
+
assert!(tx.push(4).is_err()); // full
|
|
111
|
+
assert_eq!(rx.pop(), Some(1));
|
|
112
|
+
assert_eq!(rx.pop(), Some(2));
|
|
113
|
+
assert!(tx.push(4).is_ok());
|
|
114
|
+
assert_eq!(rx.pop(), Some(3));
|
|
115
|
+
assert_eq!(rx.pop(), Some(4));
|
|
116
|
+
assert_eq!(rx.pop(), None);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#[test]
|
|
120
|
+
fn cross_thread_stream() {
|
|
121
|
+
let (mut tx, mut rx) = channel::<u64>(64);
|
|
122
|
+
let producer = std::thread::spawn(move || {
|
|
123
|
+
for i in 0..100_000u64 {
|
|
124
|
+
loop {
|
|
125
|
+
match tx.push(i) {
|
|
126
|
+
Ok(()) => break,
|
|
127
|
+
Err(_) => std::hint::spin_loop(),
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
let mut expected = 0u64;
|
|
133
|
+
while expected < 100_000 {
|
|
134
|
+
if let Some(v) = rx.pop() {
|
|
135
|
+
assert_eq!(v, expected, "out-of-order or corrupted item");
|
|
136
|
+
expected += 1;
|
|
137
|
+
} else {
|
|
138
|
+
std::hint::spin_loop();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
producer.join().unwrap();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#[test]
|
|
145
|
+
fn drop_drains_unconsumed_items() {
|
|
146
|
+
let flag = Arc::new(());
|
|
147
|
+
let (mut tx, rx) = channel::<Arc<()>>(8);
|
|
148
|
+
for _ in 0..5 {
|
|
149
|
+
tx.push(flag.clone()).unwrap();
|
|
150
|
+
}
|
|
151
|
+
assert_eq!(Arc::strong_count(&flag), 6);
|
|
152
|
+
drop(tx);
|
|
153
|
+
drop(rx);
|
|
154
|
+
assert_eq!(Arc::strong_count(&flag), 1, "ring leaked queued items");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
//! Streamed music decode: a background worker decodes OGG/MP3 into PCM
|
|
2
|
+
//! chunks and feeds them over the lock-free SPSC ring; the audio thread's
|
|
3
|
+
//! MusicVoice consumes chunks instead of holding the whole decoded track.
|
|
4
|
+
//!
|
|
5
|
+
//! Memory math that motivates this: a 5-minute 44.1 kHz stereo track is
|
|
6
|
+
//! ~57 MB as f32 PCM but ~5 MB as OGG/MP3 bytes. Streaming keeps only the
|
|
7
|
+
//! compressed bytes (shared `Arc`) plus ~1.5 s of decoded ring buffer
|
|
8
|
+
//! resident. WAV stays full-decode (it IS PCM — nothing to win), and so
|
|
9
|
+
//! does wasm32 (no threads without COOP/COEP; the full-decode path is the
|
|
10
|
+
//! documented fallback there).
|
|
11
|
+
//!
|
|
12
|
+
//! Worker lifecycle: spawned per play_music on a streamed track, killed
|
|
13
|
+
//! by `stop` (AtomicBool) when the track is stopped/replaced/unloaded.
|
|
14
|
+
//! Looping restarts the decoder from byte 0 inside the worker, so a loop
|
|
15
|
+
//! seam never underruns the ring.
|
|
16
|
+
|
|
17
|
+
use super::spsc::Consumer;
|
|
18
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
19
|
+
use super::spsc::{self, Producer};
|
|
20
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
21
|
+
use std::sync::Arc;
|
|
22
|
+
|
|
23
|
+
/// Compressed source format of a streamed track.
|
|
24
|
+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
|
25
|
+
pub enum StreamKind {
|
|
26
|
+
Ogg,
|
|
27
|
+
#[cfg(feature = "mp3")]
|
|
28
|
+
Mp3,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// One message from the decode worker to the render-side voice.
|
|
32
|
+
pub enum StreamMsg {
|
|
33
|
+
/// Interleaved f32 PCM at the track's native sample rate/channels.
|
|
34
|
+
Chunk(Vec<f32>),
|
|
35
|
+
/// Non-looping track fully delivered.
|
|
36
|
+
End,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// ~0.37 s of stereo 44.1 kHz per chunk; the ring holds four — enough to
|
|
40
|
+
/// ride out >1 s of decoder starvation before an audible underrun.
|
|
41
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
42
|
+
const CHUNK_SAMPLES: usize = 32 * 1024;
|
|
43
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
44
|
+
const RING_CHUNKS: usize = 4;
|
|
45
|
+
|
|
46
|
+
/// Render-side handle to a running stream.
|
|
47
|
+
pub struct StreamConsumer {
|
|
48
|
+
pub rx: Consumer<StreamMsg>,
|
|
49
|
+
/// Set to stop the worker (dropped voice, stop_music, engine teardown).
|
|
50
|
+
pub stop: Arc<AtomicBool>,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
impl Drop for StreamConsumer {
|
|
54
|
+
fn drop(&mut self) {
|
|
55
|
+
self.stop.store(true, Ordering::Relaxed);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Probe an OGG/MP3 byte buffer for (sample_rate, channels) without
|
|
60
|
+
/// decoding the whole track. Returns None if the data doesn't parse.
|
|
61
|
+
pub fn probe(kind: StreamKind, bytes: &[u8]) -> Option<(u32, u16)> {
|
|
62
|
+
match kind {
|
|
63
|
+
StreamKind::Ogg => {
|
|
64
|
+
let reader =
|
|
65
|
+
lewton::inside_ogg::OggStreamReader::new(std::io::Cursor::new(bytes)).ok()?;
|
|
66
|
+
Some((
|
|
67
|
+
reader.ident_hdr.audio_sample_rate,
|
|
68
|
+
reader.ident_hdr.audio_channels as u16,
|
|
69
|
+
))
|
|
70
|
+
}
|
|
71
|
+
#[cfg(feature = "mp3")]
|
|
72
|
+
StreamKind::Mp3 => {
|
|
73
|
+
let mut decoder = minimp3::Decoder::new(std::io::Cursor::new(bytes));
|
|
74
|
+
let frame = decoder.next_frame().ok()?;
|
|
75
|
+
Some((frame.sample_rate as u32, frame.channels as u16))
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Spawn the decode worker for one playback of a streamed track.
|
|
81
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
82
|
+
pub fn start(kind: StreamKind, bytes: Arc<Vec<u8>>, looping: bool) -> StreamConsumer {
|
|
83
|
+
let (tx, rx) = spsc::channel(RING_CHUNKS + 1);
|
|
84
|
+
let stop = Arc::new(AtomicBool::new(false));
|
|
85
|
+
let stop_worker = stop.clone();
|
|
86
|
+
std::thread::Builder::new()
|
|
87
|
+
.name("bloom-music-decode".into())
|
|
88
|
+
.spawn(move || run_worker(kind, &bytes, looping, tx, &stop_worker))
|
|
89
|
+
.expect("spawn music decode worker");
|
|
90
|
+
StreamConsumer { rx, stop }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
94
|
+
fn run_worker(
|
|
95
|
+
kind: StreamKind,
|
|
96
|
+
bytes: &[u8],
|
|
97
|
+
looping: bool,
|
|
98
|
+
mut tx: Producer<StreamMsg>,
|
|
99
|
+
stop: &AtomicBool,
|
|
100
|
+
) {
|
|
101
|
+
// Push with backpressure: the ring fills ~1.5s ahead, then we sleep.
|
|
102
|
+
// 20ms poll keeps the worker essentially idle while the ring is full.
|
|
103
|
+
let mut push = |msg: StreamMsg| -> bool {
|
|
104
|
+
let mut m = msg;
|
|
105
|
+
loop {
|
|
106
|
+
if stop.load(Ordering::Relaxed) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
match tx.push(m) {
|
|
110
|
+
Ok(()) => return true,
|
|
111
|
+
Err(back) => {
|
|
112
|
+
m = back;
|
|
113
|
+
std::thread::sleep(std::time::Duration::from_millis(20));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
loop {
|
|
120
|
+
let completed = match kind {
|
|
121
|
+
StreamKind::Ogg => decode_ogg(bytes, stop, &mut push),
|
|
122
|
+
#[cfg(feature = "mp3")]
|
|
123
|
+
StreamKind::Mp3 => decode_mp3(bytes, stop, &mut push),
|
|
124
|
+
};
|
|
125
|
+
if !completed || !looping {
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
// looping: restart the decoder from the top
|
|
129
|
+
}
|
|
130
|
+
let _ = push(StreamMsg::End);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// Decode one full pass of the track, pushing chunks. Returns false if
|
|
134
|
+
/// stopped early (stop flag / consumer gone).
|
|
135
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
136
|
+
fn decode_ogg(bytes: &[u8], stop: &AtomicBool, push: &mut dyn FnMut(StreamMsg) -> bool) -> bool {
|
|
137
|
+
let Ok(mut reader) = lewton::inside_ogg::OggStreamReader::new(std::io::Cursor::new(bytes))
|
|
138
|
+
else {
|
|
139
|
+
return false;
|
|
140
|
+
};
|
|
141
|
+
let mut chunk: Vec<f32> = Vec::with_capacity(CHUNK_SAMPLES);
|
|
142
|
+
while let Ok(Some(packet)) = reader.read_dec_packet_itl() {
|
|
143
|
+
if stop.load(Ordering::Relaxed) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
for &s in &packet {
|
|
147
|
+
chunk.push(s as f32 / 32768.0);
|
|
148
|
+
}
|
|
149
|
+
if chunk.len() >= CHUNK_SAMPLES {
|
|
150
|
+
let full = std::mem::replace(&mut chunk, Vec::with_capacity(CHUNK_SAMPLES));
|
|
151
|
+
if !push(StreamMsg::Chunk(full)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if !chunk.is_empty() && !push(StreamMsg::Chunk(chunk)) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
true
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#[cfg(all(feature = "mp3", not(target_arch = "wasm32")))]
|
|
163
|
+
fn decode_mp3(bytes: &[u8], stop: &AtomicBool, push: &mut dyn FnMut(StreamMsg) -> bool) -> bool {
|
|
164
|
+
let mut decoder = minimp3::Decoder::new(std::io::Cursor::new(bytes));
|
|
165
|
+
let mut chunk: Vec<f32> = Vec::with_capacity(CHUNK_SAMPLES);
|
|
166
|
+
loop {
|
|
167
|
+
if stop.load(Ordering::Relaxed) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
match decoder.next_frame() {
|
|
171
|
+
Ok(frame) => {
|
|
172
|
+
for &s in &frame.data {
|
|
173
|
+
chunk.push(s as f32 / 32768.0);
|
|
174
|
+
}
|
|
175
|
+
if chunk.len() >= CHUNK_SAMPLES {
|
|
176
|
+
let full = std::mem::replace(&mut chunk, Vec::with_capacity(CHUNK_SAMPLES));
|
|
177
|
+
if !push(StreamMsg::Chunk(full)) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
Err(minimp3::Error::Eof) => break,
|
|
183
|
+
Err(_) => break,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if !chunk.is_empty() && !push(StreamMsg::Chunk(chunk)) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
true
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#[cfg(test)]
|
|
193
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
194
|
+
mod tests {
|
|
195
|
+
use super::*;
|
|
196
|
+
|
|
197
|
+
/// Tiny synthetic WAV→OGG isn't practical here; instead exercise the
|
|
198
|
+
/// worker plumbing with MP3 when available, and always exercise the
|
|
199
|
+
/// stop/backpressure logic with a hand-rolled pusher.
|
|
200
|
+
#[test]
|
|
201
|
+
fn stop_flag_terminates_backpressure_wait() {
|
|
202
|
+
let (tx, _rx) = spsc::channel::<StreamMsg>(2);
|
|
203
|
+
let stop = Arc::new(AtomicBool::new(false));
|
|
204
|
+
let stop2 = stop.clone();
|
|
205
|
+
let h = std::thread::spawn(move || {
|
|
206
|
+
let mut tx = tx;
|
|
207
|
+
// fill the ring, then keep pushing — blocks on backpressure
|
|
208
|
+
let mut pushed = 0;
|
|
209
|
+
loop {
|
|
210
|
+
match tx.push(StreamMsg::Chunk(vec![0.0; 4])) {
|
|
211
|
+
Ok(()) => pushed += 1,
|
|
212
|
+
Err(_) => {
|
|
213
|
+
if stop2.load(Ordering::Relaxed) {
|
|
214
|
+
return pushed;
|
|
215
|
+
}
|
|
216
|
+
std::thread::sleep(std::time::Duration::from_millis(5));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
std::thread::sleep(std::time::Duration::from_millis(30));
|
|
222
|
+
stop.store(true, Ordering::Relaxed);
|
|
223
|
+
let pushed = h.join().unwrap();
|
|
224
|
+
assert_eq!(pushed, 1, "ring of capacity 2 holds exactly 1 item");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
//! Custom WGSL shaders for Pascal Editor effects.
|
|
2
|
+
//!
|
|
3
|
+
//! These are hand-ported equivalents of the Three.js TSL (Texture Shading Language)
|
|
4
|
+
//! node materials used in the Pascal Editor:
|
|
5
|
+
//! - Wall cutaway dot pattern
|
|
6
|
+
//! - Zone gradient
|
|
7
|
+
//! - Item preview shimmer
|
|
8
|
+
//!
|
|
9
|
+
//! In the browser, these are defined as JavaScript function calls (TSL nodes) that
|
|
10
|
+
//! compile to WGSL at runtime. Since we compile natively, we write the WGSL directly.
|
|
11
|
+
|
|
12
|
+
/// Wall cutaway dot pattern — shows dots that fade with height when a wall
|
|
13
|
+
/// is in "cutaway" mode. Replaces the TSL `Fn(() => { ... fract ... step ... })`.
|
|
14
|
+
pub const WALL_CUTAWAY_SHADER: &str = "
|
|
15
|
+
struct CutawayParams {
|
|
16
|
+
dot_scale: vec4<f32>, // [scale, dot_size, fade_height, opacity]
|
|
17
|
+
base_color: vec4<f32>, // rgba
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
@group(0) @binding(0) var<uniform> params: CutawayParams;
|
|
21
|
+
|
|
22
|
+
struct VertexOutput {
|
|
23
|
+
@builtin(position) clip_position: vec4<f32>,
|
|
24
|
+
@location(0) local_pos: vec3<f32>,
|
|
25
|
+
@location(1) color: vec4<f32>,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
@fragment
|
|
29
|
+
fn fs_wall_cutaway(in: VertexOutput) -> @location(0) vec4<f32> {
|
|
30
|
+
let scale = params.dot_scale.x;
|
|
31
|
+
let dot_size = params.dot_scale.y;
|
|
32
|
+
let fade_height = params.dot_scale.z;
|
|
33
|
+
|
|
34
|
+
// Create repeating dot grid
|
|
35
|
+
let uv = vec2<f32>(in.local_pos.x, in.local_pos.y) / scale;
|
|
36
|
+
let grid_uv = fract(uv);
|
|
37
|
+
let dist = length(grid_uv - vec2<f32>(0.5));
|
|
38
|
+
let dots = step(dist, dot_size * 0.5);
|
|
39
|
+
|
|
40
|
+
// Vertical fade (transparent near bottom, opaque at top)
|
|
41
|
+
let y_fade = 1.0 - smoothstep(0.0, fade_height, in.local_pos.y);
|
|
42
|
+
|
|
43
|
+
let alpha = dots * y_fade * params.dot_scale.w;
|
|
44
|
+
if (alpha < 0.01) { discard; }
|
|
45
|
+
|
|
46
|
+
return vec4<f32>(params.base_color.rgb * in.color.rgb, alpha);
|
|
47
|
+
}
|
|
48
|
+
";
|
|
49
|
+
|
|
50
|
+
/// Zone gradient — vertical gradient for zone visualization walls.
|
|
51
|
+
/// Replaces the TSL `uv().y → mul(opacity)` with uniform.
|
|
52
|
+
pub const ZONE_GRADIENT_SHADER: &str = "
|
|
53
|
+
struct ZoneParams {
|
|
54
|
+
base_color: vec4<f32>, // rgba
|
|
55
|
+
opacity: vec4<f32>, // [max_opacity, 0, 0, 0]
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
@group(0) @binding(0) var<uniform> zone_params: ZoneParams;
|
|
59
|
+
|
|
60
|
+
struct VertexOutput {
|
|
61
|
+
@builtin(position) clip_position: vec4<f32>,
|
|
62
|
+
@location(0) uv: vec2<f32>,
|
|
63
|
+
@location(1) color: vec4<f32>,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
@fragment
|
|
67
|
+
fn fs_zone_gradient(in: VertexOutput) -> @location(0) vec4<f32> {
|
|
68
|
+
// Gradient: fully opaque at bottom, transparent at top
|
|
69
|
+
let gradient_t = in.uv.y;
|
|
70
|
+
let alpha = zone_params.opacity.x * (1.0 - gradient_t) * 0.6;
|
|
71
|
+
return vec4<f32>(zone_params.base_color.rgb, alpha);
|
|
72
|
+
}
|
|
73
|
+
";
|
|
74
|
+
|
|
75
|
+
/// Item preview shimmer — animated shimmer effect for item placement preview.
|
|
76
|
+
/// Replaces the TSL `positionLocal.y + time → fract → smoothstep`.
|
|
77
|
+
pub const PREVIEW_SHIMMER_SHADER: &str = "
|
|
78
|
+
struct ShimmerParams {
|
|
79
|
+
time: vec4<f32>, // [time, speed, 0, 0]
|
|
80
|
+
base_color: vec4<f32>, // rgba
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
@group(0) @binding(0) var<uniform> shimmer_params: ShimmerParams;
|
|
84
|
+
|
|
85
|
+
struct VertexOutput {
|
|
86
|
+
@builtin(position) clip_position: vec4<f32>,
|
|
87
|
+
@location(0) local_pos: vec3<f32>,
|
|
88
|
+
@location(1) color: vec4<f32>,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
@fragment
|
|
92
|
+
fn fs_preview_shimmer(in: VertexOutput) -> @location(0) vec4<f32> {
|
|
93
|
+
let time = shimmer_params.time.x;
|
|
94
|
+
let speed = shimmer_params.time.y;
|
|
95
|
+
|
|
96
|
+
// Scrolling shimmer band
|
|
97
|
+
let shimmer_y = fract((in.local_pos.y + time * speed) * 10.0);
|
|
98
|
+
let shimmer = smoothstep(0.0, 0.1, shimmer_y) * smoothstep(0.3, 0.2, shimmer_y);
|
|
99
|
+
|
|
100
|
+
let base = shimmer_params.base_color;
|
|
101
|
+
let brightness = 1.0 + shimmer * 0.3;
|
|
102
|
+
return vec4<f32>(base.rgb * brightness, base.a * 0.7);
|
|
103
|
+
}
|
|
104
|
+
";
|