@bloomengine/engine 0.3.1 → 0.3.2
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/README.md +33 -11
- package/native/linux/Cargo.lock +1571 -12
- package/native/linux/Cargo.toml +3 -0
- package/native/linux/src/lib.rs +745 -40
- package/native/macos/src/lib.rs +30 -118
- package/native/shared/build.rs +32 -4
- package/native/shared/src/postfx.rs +16 -10
- package/native/shared/src/renderer/formats.rs +1 -7
- package/native/shared/src/renderer/impulse_field.rs +2 -1
- package/native/shared/src/renderer/material_system.rs +15 -3
- package/native/shared/src/renderer/shaders.rs +7 -1
- package/native/shared/src/renderer/transient.rs +12 -12
- package/native/shared/src/string_header.rs +32 -0
- package/native/third_party/bloom_jolt/CMakeLists.txt +14 -5
- package/native/windows/Cargo.lock +1 -0
- package/native/windows/Cargo.toml +10 -1
- package/native/windows/src/lib.rs +226 -18
- package/package.json +8 -7
- package/src/core/colors.ts +34 -27
- package/src/core/index.ts +1 -2
package/native/macos/src/lib.rs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
use bloom_shared::engine::EngineState;
|
|
10
10
|
use bloom_shared::renderer::Renderer;
|
|
11
|
-
use bloom_shared::string_header::str_from_header;
|
|
11
|
+
use bloom_shared::string_header::{str_from_header, alloc_perry_string};
|
|
12
12
|
use bloom_shared::audio::{parse_wav, parse_ogg, parse_mp3};
|
|
13
13
|
|
|
14
14
|
use objc2::rc::Retained;
|
|
@@ -753,38 +753,32 @@ pub extern "C" fn bloom_is_mouse_button_released(btn: f64) -> f64 {
|
|
|
753
753
|
// ============================================================
|
|
754
754
|
|
|
755
755
|
#[no_mangle]
|
|
756
|
-
pub extern "C" fn bloom_is_gamepad_available(
|
|
757
|
-
let _ = gamepad;
|
|
756
|
+
pub extern "C" fn bloom_is_gamepad_available() -> f64 {
|
|
758
757
|
if engine().input.is_gamepad_available() { 1.0 } else { 0.0 }
|
|
759
758
|
}
|
|
760
759
|
|
|
761
760
|
#[no_mangle]
|
|
762
|
-
pub extern "C" fn bloom_get_gamepad_axis(
|
|
763
|
-
let _ = gamepad;
|
|
761
|
+
pub extern "C" fn bloom_get_gamepad_axis(axis: f64) -> f64 {
|
|
764
762
|
engine().input.get_gamepad_axis(axis as usize) as f64
|
|
765
763
|
}
|
|
766
764
|
|
|
767
765
|
#[no_mangle]
|
|
768
|
-
pub extern "C" fn bloom_is_gamepad_button_pressed(
|
|
769
|
-
|
|
770
|
-
if engine().input.is_gamepad_button_pressed(button as usize) { 1.0 } else { 0.0 }
|
|
766
|
+
pub extern "C" fn bloom_is_gamepad_button_pressed(btn: f64) -> f64 {
|
|
767
|
+
if engine().input.is_gamepad_button_pressed(btn as usize) { 1.0 } else { 0.0 }
|
|
771
768
|
}
|
|
772
769
|
|
|
773
770
|
#[no_mangle]
|
|
774
|
-
pub extern "C" fn bloom_is_gamepad_button_down(
|
|
775
|
-
|
|
776
|
-
if engine().input.is_gamepad_button_down(button as usize) { 1.0 } else { 0.0 }
|
|
771
|
+
pub extern "C" fn bloom_is_gamepad_button_down(btn: f64) -> f64 {
|
|
772
|
+
if engine().input.is_gamepad_button_down(btn as usize) { 1.0 } else { 0.0 }
|
|
777
773
|
}
|
|
778
774
|
|
|
779
775
|
#[no_mangle]
|
|
780
|
-
pub extern "C" fn bloom_is_gamepad_button_released(
|
|
781
|
-
|
|
782
|
-
if engine().input.is_gamepad_button_released(button as usize) { 1.0 } else { 0.0 }
|
|
776
|
+
pub extern "C" fn bloom_is_gamepad_button_released(btn: f64) -> f64 {
|
|
777
|
+
if engine().input.is_gamepad_button_released(btn as usize) { 1.0 } else { 0.0 }
|
|
783
778
|
}
|
|
784
779
|
|
|
785
780
|
#[no_mangle]
|
|
786
|
-
pub extern "C" fn bloom_get_gamepad_axis_count(
|
|
787
|
-
let _ = gamepad;
|
|
781
|
+
pub extern "C" fn bloom_get_gamepad_axis_count() -> f64 {
|
|
788
782
|
engine().input.get_gamepad_axis_count() as f64
|
|
789
783
|
}
|
|
790
784
|
|
|
@@ -793,13 +787,13 @@ pub extern "C" fn bloom_get_gamepad_axis_count(gamepad: f64) -> f64 {
|
|
|
793
787
|
// ============================================================
|
|
794
788
|
|
|
795
789
|
#[no_mangle]
|
|
796
|
-
pub extern "C" fn bloom_get_touch_x() -> f64 {
|
|
797
|
-
engine().input.get_touch_x(
|
|
790
|
+
pub extern "C" fn bloom_get_touch_x(index: f64) -> f64 {
|
|
791
|
+
engine().input.get_touch_x(index as usize)
|
|
798
792
|
}
|
|
799
793
|
|
|
800
794
|
#[no_mangle]
|
|
801
|
-
pub extern "C" fn bloom_get_touch_y() -> f64 {
|
|
802
|
-
engine().input.get_touch_y(
|
|
795
|
+
pub extern "C" fn bloom_get_touch_y(index: f64) -> f64 {
|
|
796
|
+
engine().input.get_touch_y(index as usize)
|
|
803
797
|
}
|
|
804
798
|
|
|
805
799
|
#[no_mangle]
|
|
@@ -1411,19 +1405,7 @@ pub extern "C" fn bloom_profiler_frame_history() -> *const u8 {
|
|
|
1411
1405
|
for (cpu, gpu) in &hist {
|
|
1412
1406
|
s.push_str(&format!("{:.2}|{:.2}\n", cpu, gpu));
|
|
1413
1407
|
}
|
|
1414
|
-
|
|
1415
|
-
let len = bytes.len();
|
|
1416
|
-
let total = 12 + len;
|
|
1417
|
-
let layout = std::alloc::Layout::from_size_align(total, 4).unwrap();
|
|
1418
|
-
unsafe {
|
|
1419
|
-
let ptr = std::alloc::alloc(layout);
|
|
1420
|
-
if ptr.is_null() { return std::ptr::null(); }
|
|
1421
|
-
*(ptr as *mut u32) = len as u32;
|
|
1422
|
-
*(ptr.add(4) as *mut u32) = len as u32;
|
|
1423
|
-
*(ptr.add(8) as *mut u32) = 1;
|
|
1424
|
-
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(12), len);
|
|
1425
|
-
ptr
|
|
1426
|
-
}
|
|
1408
|
+
alloc_perry_string(&s)
|
|
1427
1409
|
}
|
|
1428
1410
|
|
|
1429
1411
|
#[no_mangle]
|
|
@@ -1441,19 +1423,7 @@ pub extern "C" fn bloom_profiler_overlay_text() -> *const u8 {
|
|
|
1441
1423
|
}
|
|
1442
1424
|
s.push('\n');
|
|
1443
1425
|
}
|
|
1444
|
-
|
|
1445
|
-
let len = bytes.len();
|
|
1446
|
-
let total = 12 + len;
|
|
1447
|
-
let layout = std::alloc::Layout::from_size_align(total, 4).unwrap();
|
|
1448
|
-
unsafe {
|
|
1449
|
-
let ptr = std::alloc::alloc(layout);
|
|
1450
|
-
if ptr.is_null() { return std::ptr::null(); }
|
|
1451
|
-
*(ptr as *mut u32) = len as u32;
|
|
1452
|
-
*(ptr.add(4) as *mut u32) = len as u32;
|
|
1453
|
-
*(ptr.add(8) as *mut u32) = 1;
|
|
1454
|
-
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(12), len);
|
|
1455
|
-
ptr
|
|
1456
|
-
}
|
|
1426
|
+
alloc_perry_string(&s)
|
|
1457
1427
|
}
|
|
1458
1428
|
|
|
1459
1429
|
// ============================================================
|
|
@@ -2327,27 +2297,11 @@ pub extern "C" fn bloom_set_clipboard_text(text_ptr: *const u8) {
|
|
|
2327
2297
|
#[no_mangle]
|
|
2328
2298
|
pub extern "C" fn bloom_get_clipboard_text() -> *const u8 {
|
|
2329
2299
|
match arboard::Clipboard::new() {
|
|
2330
|
-
Ok(mut clipboard) => {
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
let total = 12 + len;
|
|
2336
|
-
let layout = std::alloc::Layout::from_size_align(total, 4).unwrap();
|
|
2337
|
-
unsafe {
|
|
2338
|
-
let ptr = std::alloc::alloc(layout);
|
|
2339
|
-
if ptr.is_null() { return std::ptr::null(); }
|
|
2340
|
-
*(ptr as *mut u32) = len as u32;
|
|
2341
|
-
*(ptr.add(4) as *mut u32) = len as u32;
|
|
2342
|
-
*(ptr.add(8) as *mut u32) = 1;
|
|
2343
|
-
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(12), len);
|
|
2344
|
-
ptr
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
|
-
Err(_) => std::ptr::null(),
|
|
2348
|
-
}
|
|
2349
|
-
}
|
|
2350
|
-
Err(_) => std::ptr::null(),
|
|
2300
|
+
Ok(mut clipboard) => match clipboard.get_text() {
|
|
2301
|
+
Ok(text) => alloc_perry_string(&text),
|
|
2302
|
+
Err(_) => alloc_perry_string(""),
|
|
2303
|
+
},
|
|
2304
|
+
Err(_) => alloc_perry_string(""),
|
|
2351
2305
|
}
|
|
2352
2306
|
}
|
|
2353
2307
|
|
|
@@ -2361,23 +2315,8 @@ pub extern "C" fn bloom_open_file_dialog(filter_ptr: *const u8, title_ptr: *cons
|
|
|
2361
2315
|
dialog = dialog.add_filter("Files", &[filter]);
|
|
2362
2316
|
}
|
|
2363
2317
|
match dialog.pick_file() {
|
|
2364
|
-
Some(path) =>
|
|
2365
|
-
|
|
2366
|
-
let bytes = s.as_bytes();
|
|
2367
|
-
let len = bytes.len();
|
|
2368
|
-
let total = 12 + len;
|
|
2369
|
-
let layout = std::alloc::Layout::from_size_align(total, 4).unwrap();
|
|
2370
|
-
unsafe {
|
|
2371
|
-
let ptr = std::alloc::alloc(layout);
|
|
2372
|
-
if ptr.is_null() { return std::ptr::null(); }
|
|
2373
|
-
*(ptr as *mut u32) = len as u32;
|
|
2374
|
-
*(ptr.add(4) as *mut u32) = len as u32;
|
|
2375
|
-
*(ptr.add(8) as *mut u32) = 1;
|
|
2376
|
-
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(12), len);
|
|
2377
|
-
ptr
|
|
2378
|
-
}
|
|
2379
|
-
}
|
|
2380
|
-
None => std::ptr::null(),
|
|
2318
|
+
Some(path) => alloc_perry_string(&path.to_string_lossy()),
|
|
2319
|
+
None => alloc_perry_string(""),
|
|
2381
2320
|
}
|
|
2382
2321
|
}
|
|
2383
2322
|
|
|
@@ -2389,23 +2328,8 @@ pub extern "C" fn bloom_save_file_dialog(default_name_ptr: *const u8, title_ptr:
|
|
|
2389
2328
|
.set_title(title)
|
|
2390
2329
|
.set_file_name(default_name);
|
|
2391
2330
|
match dialog.save_file() {
|
|
2392
|
-
Some(path) =>
|
|
2393
|
-
|
|
2394
|
-
let bytes = s.as_bytes();
|
|
2395
|
-
let len = bytes.len();
|
|
2396
|
-
let total = 12 + len;
|
|
2397
|
-
let layout = std::alloc::Layout::from_size_align(total, 4).unwrap();
|
|
2398
|
-
unsafe {
|
|
2399
|
-
let ptr = std::alloc::alloc(layout);
|
|
2400
|
-
if ptr.is_null() { return std::ptr::null(); }
|
|
2401
|
-
*(ptr as *mut u32) = len as u32;
|
|
2402
|
-
*(ptr.add(4) as *mut u32) = len as u32;
|
|
2403
|
-
*(ptr.add(8) as *mut u32) = 1;
|
|
2404
|
-
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(12), len);
|
|
2405
|
-
ptr
|
|
2406
|
-
}
|
|
2407
|
-
}
|
|
2408
|
-
None => std::ptr::null(),
|
|
2331
|
+
Some(path) => alloc_perry_string(&path.to_string_lossy()),
|
|
2332
|
+
None => alloc_perry_string(""),
|
|
2409
2333
|
}
|
|
2410
2334
|
}
|
|
2411
2335
|
|
|
@@ -2456,24 +2380,12 @@ pub extern "C" fn bloom_file_exists(path_ptr: *const u8) -> f64 {
|
|
|
2456
2380
|
#[no_mangle]
|
|
2457
2381
|
pub extern "C" fn bloom_read_file(path_ptr: *const u8) -> *const u8 {
|
|
2458
2382
|
let path = str_from_header(path_ptr);
|
|
2383
|
+
// Always return a valid Perry string. A null pointer would NaN-box into a
|
|
2384
|
+
// string-typed JS value pointing at address 0; `.length` / `.charCodeAt`
|
|
2385
|
+
// would then segfault. Callers detect "missing file" via `data.length === 0`.
|
|
2459
2386
|
match std::fs::read_to_string(path) {
|
|
2460
|
-
Ok(contents) =>
|
|
2461
|
-
|
|
2462
|
-
let bytes = contents.as_bytes();
|
|
2463
|
-
let len = bytes.len();
|
|
2464
|
-
let total = 12 + len; // 12 bytes header (3 × u32) + data
|
|
2465
|
-
let layout = std::alloc::Layout::from_size_align(total, 4).unwrap();
|
|
2466
|
-
unsafe {
|
|
2467
|
-
let ptr = std::alloc::alloc(layout);
|
|
2468
|
-
if ptr.is_null() { return std::ptr::null(); }
|
|
2469
|
-
*(ptr as *mut u32) = len as u32; // length
|
|
2470
|
-
*(ptr.add(4) as *mut u32) = len as u32; // capacity
|
|
2471
|
-
*(ptr.add(8) as *mut u32) = 1; // refcount (unique)
|
|
2472
|
-
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(12), len);
|
|
2473
|
-
ptr
|
|
2474
|
-
}
|
|
2475
|
-
}
|
|
2476
|
-
Err(_) => std::ptr::null(),
|
|
2387
|
+
Ok(contents) => alloc_perry_string(&contents),
|
|
2388
|
+
Err(_) => alloc_perry_string(""),
|
|
2477
2389
|
}
|
|
2478
2390
|
}
|
|
2479
2391
|
|
package/native/shared/build.rs
CHANGED
|
@@ -50,10 +50,38 @@ fn build_jolt() {
|
|
|
50
50
|
println!("cargo:rerun-if-changed={}", shim_dir.join("include/bloom_jolt.h").display());
|
|
51
51
|
println!("cargo:rerun-if-changed={}", shim_dir.join("src/bloom_jolt.cpp").display());
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
// Build into a stable, short path next to the shim itself rather than the
|
|
54
|
+
// per-build OUT_DIR. Two reasons:
|
|
55
|
+
// 1. Cargo wraps OUT_DIR with the `\\?\` long-path prefix on Windows
|
|
56
|
+
// whenever the absolute path approaches MAX_PATH; MSBuild and cl.exe
|
|
57
|
+
// both choke on `\\?\`-prefixed paths in subtle ways (echo'd build
|
|
58
|
+
// events fail with MSB3073, cl.exe rewrites the file to `\\testfile`
|
|
59
|
+
// and reports C1083).
|
|
60
|
+
// 2. The Jolt build is expensive and identical across every cargo target
|
|
61
|
+
// hash — caching it once per profile lets `cargo clean` not nuke a
|
|
62
|
+
// multi-minute compile.
|
|
63
|
+
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
|
64
|
+
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
|
|
65
|
+
let dst = shim_dir
|
|
66
|
+
.join("build")
|
|
67
|
+
.join(format!("{}-{}", target_os, target_arch));
|
|
68
|
+
|
|
69
|
+
let lib_ext = if target_os == "windows" { "lib" } else { "a" };
|
|
70
|
+
let lib_prefix = if target_os == "windows" { "" } else { "lib" };
|
|
71
|
+
let bloom_jolt_lib = dst
|
|
72
|
+
.join("lib")
|
|
73
|
+
.join(format!("{}bloom_jolt.{}", lib_prefix, lib_ext));
|
|
74
|
+
let jolt_lib = dst
|
|
75
|
+
.join("lib")
|
|
76
|
+
.join(format!("{}Jolt.{}", lib_prefix, lib_ext));
|
|
77
|
+
|
|
78
|
+
if !(bloom_jolt_lib.exists() && jolt_lib.exists()) {
|
|
79
|
+
let _ = cmake::Config::new(&shim_dir)
|
|
80
|
+
.out_dir(&dst)
|
|
81
|
+
.profile("Release")
|
|
82
|
+
.define("CMAKE_BUILD_TYPE", "Release")
|
|
83
|
+
.build();
|
|
84
|
+
}
|
|
57
85
|
|
|
58
86
|
println!("cargo:rustc-link-search=native={}", dst.join("lib").display());
|
|
59
87
|
println!("cargo:rustc-link-lib=static=bloom_jolt");
|
|
@@ -43,26 +43,30 @@ struct OutlineParams {
|
|
|
43
43
|
@group(0) @binding(2) var tex_sampler: sampler;
|
|
44
44
|
@group(0) @binding(3) var<uniform> params: OutlineParams;
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@location(0) uv: vec2<f32>,
|
|
49
|
-
};
|
|
46
|
+
// VertexOutput is defined in FULLSCREEN_VERT — this string is concatenated
|
|
47
|
+
// after it, so redefining here would be a WGSL parser error.
|
|
50
48
|
|
|
51
49
|
@fragment
|
|
52
50
|
fn fs_outline(in: VertexOutput) -> @location(0) vec4<f32> {
|
|
53
51
|
let color = textureSample(scene_color, tex_sampler, in.uv);
|
|
54
|
-
let center_id = textureSample(object_id_tex, tex_sampler, in.uv).r;
|
|
55
52
|
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
// Object IDs must not be filtered — interpolating IDs produces nonsense
|
|
54
|
+
// values at edges. Use textureLoad with integer coords; this also avoids
|
|
55
|
+
// requiring the FLOAT32_FILTERABLE wgpu feature (not advertised on Apple GPUs).
|
|
56
|
+
let dim = vec2<i32>(textureDimensions(object_id_tex));
|
|
57
|
+
let max_xy = dim - vec2<i32>(1, 1);
|
|
58
|
+
let center_pix = clamp(vec2<i32>(in.uv * vec2<f32>(dim)), vec2<i32>(0, 0), max_xy);
|
|
59
|
+
let center_id = textureLoad(object_id_tex, center_pix, 0).r;
|
|
60
|
+
|
|
61
|
+
let t = max(1, i32(round(params.thickness.x)));
|
|
58
62
|
|
|
59
63
|
// Sample neighbors for edge detection
|
|
60
64
|
var edge = 0.0;
|
|
61
65
|
for (var dy = -1; dy <= 1; dy++) {
|
|
62
66
|
for (var dx = -1; dx <= 1; dx++) {
|
|
63
67
|
if (dx == 0 && dy == 0) { continue; }
|
|
64
|
-
let
|
|
65
|
-
let neighbor_id =
|
|
68
|
+
let neighbor_pix = clamp(center_pix + vec2<i32>(dx, dy) * t, vec2<i32>(0, 0), max_xy);
|
|
69
|
+
let neighbor_id = textureLoad(object_id_tex, neighbor_pix, 0).r;
|
|
66
70
|
if (abs(neighbor_id - center_id) > 0.001) {
|
|
67
71
|
edge += 1.0;
|
|
68
72
|
}
|
|
@@ -169,7 +173,9 @@ impl PostFxPipeline {
|
|
|
169
173
|
label: Some("outline_bg_layout"),
|
|
170
174
|
entries: &[
|
|
171
175
|
bgl_texture(0, wgpu::TextureSampleType::Float { filterable: true }),
|
|
172
|
-
|
|
176
|
+
// R32Float — non-filterable on adapters without FLOAT32_FILTERABLE
|
|
177
|
+
// (e.g. Apple GPUs). Sampled via textureLoad in OUTLINE_FRAG.
|
|
178
|
+
bgl_texture(1, wgpu::TextureSampleType::Float { filterable: false }),
|
|
173
179
|
bgl_sampler(2),
|
|
174
180
|
bgl_uniform(3),
|
|
175
181
|
],
|
|
@@ -430,15 +430,9 @@ pub(super) fn create_mesh_sdf_texture(
|
|
|
430
430
|
sample_count: 1,
|
|
431
431
|
dimension: wgpu::TextureDimension::D3,
|
|
432
432
|
format: wgpu::TextureFormat::R32Float,
|
|
433
|
-
// COPY_DST is needed by ticket 022's disk cache so a cached
|
|
434
|
-
// SDF can be uploaded via queue.write_texture instead of
|
|
435
|
-
// re-baked. COPY_SRC is needed by the same ticket's readback
|
|
436
|
-
// path on cache miss. Both have zero runtime cost when the
|
|
437
|
-
// cache isn't used (just usage-flag bits at allocation).
|
|
438
433
|
usage: wgpu::TextureUsages::STORAGE_BINDING
|
|
439
434
|
| wgpu::TextureUsages::TEXTURE_BINDING
|
|
440
|
-
| wgpu::TextureUsages::COPY_SRC
|
|
441
|
-
| wgpu::TextureUsages::COPY_DST,
|
|
435
|
+
| wgpu::TextureUsages::COPY_SRC,
|
|
442
436
|
view_formats: &[],
|
|
443
437
|
});
|
|
444
438
|
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
|
@@ -150,7 +150,8 @@ impl ImpulseField {
|
|
|
150
150
|
dimension: wgpu::TextureDimension::D2,
|
|
151
151
|
format: wgpu::TextureFormat::R32Float,
|
|
152
152
|
usage: wgpu::TextureUsages::TEXTURE_BINDING
|
|
153
|
-
| wgpu::TextureUsages::STORAGE_BINDING
|
|
153
|
+
| wgpu::TextureUsages::STORAGE_BINDING
|
|
154
|
+
| wgpu::TextureUsages::COPY_SRC,
|
|
154
155
|
view_formats: &[],
|
|
155
156
|
});
|
|
156
157
|
let view = tex.create_view(&Default::default());
|
|
@@ -1775,11 +1775,21 @@ mod tests {
|
|
|
1775
1775
|
compatible_surface: None,
|
|
1776
1776
|
force_fallback_adapter: true,
|
|
1777
1777
|
})).ok()?;
|
|
1778
|
+
// The material ABI uses 5 bind groups (PerFrame, PerView,
|
|
1779
|
+
// PerMaterial, PerDraw, SceneInputs). downlevel_defaults caps
|
|
1780
|
+
// max_bind_groups at 4, which is fine on Metal (it silently
|
|
1781
|
+
// accepts more) but DX12 enforces it strictly — bump to 5 so
|
|
1782
|
+
// the user-material pipeline validates on every backend.
|
|
1783
|
+
// Also bump max_uniform_buffer_binding_size from 16KB to 64KB
|
|
1784
|
+
// for the JointMatrices UBO (1024 × mat4x4 = 64KB).
|
|
1785
|
+
let mut required_limits = wgpu::Limits::downlevel_defaults();
|
|
1786
|
+
required_limits.max_bind_groups = 5;
|
|
1787
|
+
required_limits.max_uniform_buffer_binding_size = 64 << 10;
|
|
1778
1788
|
let (device, queue) = pollster::block_on(adapter.request_device(
|
|
1779
1789
|
&wgpu::DeviceDescriptor {
|
|
1780
1790
|
label: Some("material-test-device"),
|
|
1781
1791
|
required_features: wgpu::Features::empty(),
|
|
1782
|
-
required_limits
|
|
1792
|
+
required_limits,
|
|
1783
1793
|
..Default::default()
|
|
1784
1794
|
},
|
|
1785
1795
|
)).ok()?;
|
|
@@ -1832,7 +1842,7 @@ fn fs_main(_in: VsOut) -> TranslucentOut {
|
|
|
1832
1842
|
/// at (-1,-1), (3,-1), (-1,3). The pipeline's MVP starts as
|
|
1833
1843
|
/// identity (we override it below) so the triangle covers the
|
|
1834
1844
|
/// whole viewport.
|
|
1835
|
-
fn make_fullscreen_tri(device: &wgpu::Device) -> (wgpu::Buffer, wgpu::Buffer, u32) {
|
|
1845
|
+
fn make_fullscreen_tri(device: &wgpu::Device, queue: &wgpu::Queue) -> (wgpu::Buffer, wgpu::Buffer, u32) {
|
|
1836
1846
|
let mut verts: [Vertex3D; 3] = [Vertex3D::default(); 3];
|
|
1837
1847
|
verts[0].position = [-1.0, -1.0, 0.5];
|
|
1838
1848
|
verts[1].position = [ 3.0, -1.0, 0.5];
|
|
@@ -1847,6 +1857,7 @@ fn fs_main(_in: VsOut) -> TranslucentOut {
|
|
|
1847
1857
|
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
|
1848
1858
|
mapped_at_creation: false,
|
|
1849
1859
|
});
|
|
1860
|
+
queue.write_buffer(&vb, 0, bytemuck::cast_slice(&verts));
|
|
1850
1861
|
let indices: [u32; 3] = [0, 1, 2];
|
|
1851
1862
|
let ib = device.create_buffer(&wgpu::BufferDescriptor {
|
|
1852
1863
|
label: Some("test_tri_ib"),
|
|
@@ -1854,6 +1865,7 @@ fn fs_main(_in: VsOut) -> TranslucentOut {
|
|
|
1854
1865
|
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
|
|
1855
1866
|
mapped_at_creation: false,
|
|
1856
1867
|
});
|
|
1868
|
+
queue.write_buffer(&ib, 0, bytemuck::cast_slice(&indices));
|
|
1857
1869
|
(vb, ib, 3)
|
|
1858
1870
|
}
|
|
1859
1871
|
|
|
@@ -1919,7 +1931,7 @@ fn fs_main(_in: VsOut) -> TranslucentOut {
|
|
|
1919
1931
|
[0.0, 0.0, 1.0, 0.0],
|
|
1920
1932
|
[0.0, 0.0, 0.0, 1.0],
|
|
1921
1933
|
];
|
|
1922
|
-
let (vb, ib, icount) = make_fullscreen_tri(&device);
|
|
1934
|
+
let (vb, ib, icount) = make_fullscreen_tri(&device, &queue);
|
|
1923
1935
|
|
|
1924
1936
|
sys.submit_draw(
|
|
1925
1937
|
&device, &queue, &joint_buf,
|
|
@@ -4337,6 +4337,12 @@ fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
|
|
|
4337
4337
|
var hit_uv = vec2<f32>(-1.0);
|
|
4338
4338
|
var hit_found = false;
|
|
4339
4339
|
var prev_t = 0.0;
|
|
4340
|
+
// FXC (the legacy HLSL compiler used by D3D11 + DX12 fallback in wgpu) refuses
|
|
4341
|
+
// to unroll a loop that contains an implicit-gradient texture sample when the
|
|
4342
|
+
// iteration count is uniform-driven, and refuses to *not* unroll because the
|
|
4343
|
+
// body has the gradient op — the only escape is to take the gradient out of
|
|
4344
|
+
// the loop. textureSampleLevel forces explicit LOD and removes the gradient
|
|
4345
|
+
// op, which is also what we want here (depth has no mips).
|
|
4340
4346
|
for (var i = 0u; i < n_steps; i = i + 1u) {
|
|
4341
4347
|
let ray_view = view_pos + r * t;
|
|
4342
4348
|
let ray_clip = u.proj * vec4<f32>(ray_view, 1.0);
|
|
@@ -4347,7 +4353,7 @@ fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
|
|
|
4347
4353
|
break;
|
|
4348
4354
|
}
|
|
4349
4355
|
let ray_uv = vec2<f32>(ray_ndc.x * 0.5 + 0.5, 1.0 - (ray_ndc.y * 0.5 + 0.5));
|
|
4350
|
-
let scene_depth =
|
|
4356
|
+
let scene_depth = textureSampleLevel(depth_tex, depth_samp, ray_uv, 0i);
|
|
4351
4357
|
|
|
4352
4358
|
if (ray_ndc.z >= scene_depth) {
|
|
4353
4359
|
let hit_view = view_pos_from_depth(ray_uv, scene_depth);
|
|
@@ -86,6 +86,9 @@ pub struct TransientId(pub u32);
|
|
|
86
86
|
|
|
87
87
|
/// Internal record per live allocation.
|
|
88
88
|
struct Slot {
|
|
89
|
+
/// Stable handle id — survives `Vec` reordering when invalidation
|
|
90
|
+
/// drops other slots, so `TransientId` lookups stay valid.
|
|
91
|
+
id: u32,
|
|
89
92
|
desc: TransientDesc,
|
|
90
93
|
/// Physical extent at allocation time — used for resize detection.
|
|
91
94
|
extent: (u32, u32),
|
|
@@ -173,13 +176,13 @@ impl TransientPool {
|
|
|
173
176
|
let target_extent = desc.size.extent(self.swap_size.0, self.swap_size.1);
|
|
174
177
|
|
|
175
178
|
// Look for an existing free slot with identical desc + extent.
|
|
176
|
-
for
|
|
179
|
+
for slot in self.slots.iter_mut() {
|
|
177
180
|
if !slot.in_use
|
|
178
181
|
&& slot.desc == desc
|
|
179
182
|
&& slot.extent == target_extent
|
|
180
183
|
{
|
|
181
184
|
slot.in_use = true;
|
|
182
|
-
return TransientId(
|
|
185
|
+
return TransientId(slot.id);
|
|
183
186
|
}
|
|
184
187
|
}
|
|
185
188
|
|
|
@@ -201,13 +204,10 @@ impl TransientPool {
|
|
|
201
204
|
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
|
202
205
|
let id = self.next_id;
|
|
203
206
|
self.next_id += 1;
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
assert_eq!(id, slot_index,
|
|
209
|
-
"TransientPool invariant: slot ids are contiguous until Phase 3b adds aliasing");
|
|
210
|
-
self.slots.push(Slot { desc, extent: target_extent, texture, view, in_use: true });
|
|
207
|
+
// Ids are stable — surviving slots keep theirs across resizes
|
|
208
|
+
// (which can drop swapchain-relative slots and shrink the Vec),
|
|
209
|
+
// so handle lookups stay valid even when the slot index shifts.
|
|
210
|
+
self.slots.push(Slot { id, desc, extent: target_extent, texture, view, in_use: true });
|
|
211
211
|
TransientId(id)
|
|
212
212
|
}
|
|
213
213
|
|
|
@@ -215,7 +215,7 @@ impl TransientPool {
|
|
|
215
215
|
/// reuse pool and can be handed back to a subsequent `acquire`
|
|
216
216
|
/// with a matching desc.
|
|
217
217
|
pub fn release(&mut self, id: TransientId) {
|
|
218
|
-
if let Some(slot) = self.slots.
|
|
218
|
+
if let Some(slot) = self.slots.iter_mut().find(|s| s.id == id.0) {
|
|
219
219
|
slot.in_use = false;
|
|
220
220
|
}
|
|
221
221
|
}
|
|
@@ -223,12 +223,12 @@ impl TransientPool {
|
|
|
223
223
|
/// Get the underlying texture for a transient. Borrowed for the
|
|
224
224
|
/// pool's lifetime — callers hold the borrow only while encoding.
|
|
225
225
|
pub fn texture(&self, id: TransientId) -> Option<&wgpu::Texture> {
|
|
226
|
-
self.slots.
|
|
226
|
+
self.slots.iter().find(|s| s.id == id.0).map(|s| &s.texture)
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
/// Get the default view for a transient.
|
|
230
230
|
pub fn view(&self, id: TransientId) -> Option<&wgpu::TextureView> {
|
|
231
|
-
self.slots.
|
|
231
|
+
self.slots.iter().find(|s| s.id == id.0).map(|s| &s.view)
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
/// Frame-end book-keeping. Currently does nothing because
|
|
@@ -33,3 +33,35 @@ pub fn str_from_header(ptr: *const u8) -> &'static str {
|
|
|
33
33
|
std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len))
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
/// Allocate a Perry 0.5.x heap string suitable for returning across the
|
|
38
|
+
/// FFI boundary (declared as `returns: "string"` in package.json). Layout
|
|
39
|
+
/// matches `StringHeader` above: 5×u32 header (utf16_len, byte_len,
|
|
40
|
+
/// capacity, refcount, flags) followed by UTF-8 data.
|
|
41
|
+
///
|
|
42
|
+
/// Older engine code allocated 12 bytes (Perry 0.4.x layout) and Perry's
|
|
43
|
+
/// 0.5.x runtime read 8 bytes into the payload — strings came back with a
|
|
44
|
+
/// garbage prefix and read past the end. Always go through this helper.
|
|
45
|
+
pub fn alloc_perry_string(s: &str) -> *const u8 {
|
|
46
|
+
let bytes = s.as_bytes();
|
|
47
|
+
let byte_len = bytes.len();
|
|
48
|
+
// ASCII fast path: utf16_len == byte_len when every byte is < 0x80.
|
|
49
|
+
let utf16_len = if bytes.iter().all(|&b| b < 0x80) {
|
|
50
|
+
byte_len
|
|
51
|
+
} else {
|
|
52
|
+
s.encode_utf16().count()
|
|
53
|
+
};
|
|
54
|
+
let total = std::mem::size_of::<StringHeader>() + byte_len;
|
|
55
|
+
let layout = std::alloc::Layout::from_size_align(total, 4).unwrap();
|
|
56
|
+
unsafe {
|
|
57
|
+
let ptr = std::alloc::alloc(layout);
|
|
58
|
+
if ptr.is_null() { return std::ptr::null(); }
|
|
59
|
+
*(ptr as *mut u32) = utf16_len as u32;
|
|
60
|
+
*(ptr.add(4) as *mut u32) = byte_len as u32;
|
|
61
|
+
*(ptr.add(8) as *mut u32) = byte_len as u32; // capacity
|
|
62
|
+
*(ptr.add(12) as *mut u32) = 1; // refcount=unique
|
|
63
|
+
*(ptr.add(16) as *mut u32) = 0; // flags
|
|
64
|
+
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(20), byte_len);
|
|
65
|
+
ptr
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -8,11 +8,18 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
|
|
8
8
|
|
|
9
9
|
# ---------------------------------------------------------------------------
|
|
10
10
|
# JoltPhysics build options.
|
|
11
|
-
# Matches the "Distribution" flavour: no asserts, no profile, no debug renderer
|
|
12
|
-
#
|
|
13
|
-
#
|
|
11
|
+
# Matches the "Distribution" flavour: no asserts, no profile, no debug renderer.
|
|
12
|
+
# We keep object stream off — we don't use Jolt's scene serialisation (Bloom
|
|
13
|
+
# handles save/load itself).
|
|
14
|
+
#
|
|
15
|
+
# IPO is OFF because Rust's link step on Linux/GCC routes through rust-lld,
|
|
16
|
+
# which doesn't load gcc's lto-wrapper plugin. With IPO on, libJolt.a's archive
|
|
17
|
+
# members are GIMPLE bitcode and rust-lld leaves their implementations
|
|
18
|
+
# unresolved (undefined-symbol errors on JPH::Free, BodyManager locks, vtables,
|
|
19
|
+
# etc — see issue #44). Jolt is heavily inlined via its precompiled header
|
|
20
|
+
# already, so the perf delta from skipping LTO on the static archive is small.
|
|
14
21
|
# ---------------------------------------------------------------------------
|
|
15
|
-
set(INTERPROCEDURAL_OPTIMIZATION
|
|
22
|
+
set(INTERPROCEDURAL_OPTIMIZATION OFF CACHE BOOL "" FORCE)
|
|
16
23
|
set(USE_STATIC_MSVC_RUNTIME_LIBRARY OFF CACHE BOOL "" FORCE)
|
|
17
24
|
set(TARGET_UNIT_TESTS OFF CACHE BOOL "" FORCE)
|
|
18
25
|
set(TARGET_HELLO_WORLD OFF CACHE BOOL "" FORCE)
|
|
@@ -21,8 +28,10 @@ set(TARGET_SAMPLES OFF CACHE BOOL "" FORCE)
|
|
|
21
28
|
set(TARGET_VIEWER OFF CACHE BOOL "" FORCE)
|
|
22
29
|
set(OBJECT_LAYER_BITS 16 CACHE STRING "" FORCE)
|
|
23
30
|
set(USE_ASSERTS OFF CACHE BOOL "" FORCE)
|
|
24
|
-
set(
|
|
31
|
+
set(PROFILER_IN_DEBUG_AND_RELEASE OFF CACHE BOOL "" FORCE)
|
|
32
|
+
set(PROFILER_IN_DISTRIBUTION OFF CACHE BOOL "" FORCE)
|
|
25
33
|
set(DEBUG_RENDERER_IN_DEBUG_AND_RELEASE OFF CACHE BOOL "" FORCE)
|
|
34
|
+
set(DEBUG_RENDERER_IN_DISTRIBUTION OFF CACHE BOOL "" FORCE)
|
|
26
35
|
set(CROSS_PLATFORM_DETERMINISTIC OFF CACHE BOOL "" FORCE)
|
|
27
36
|
set(FLOATING_POINT_EXCEPTIONS_ENABLED OFF CACHE BOOL "" FORCE)
|
|
28
37
|
set(DOUBLE_PRECISION OFF CACHE BOOL "" FORCE)
|
|
@@ -8,11 +8,20 @@ name = "bloom_windows"
|
|
|
8
8
|
crate-type = ["staticlib"]
|
|
9
9
|
|
|
10
10
|
[features]
|
|
11
|
-
|
|
11
|
+
# Jolt is opt-in on Windows because Perry's native-library build pipeline
|
|
12
|
+
# does not forward cargo `--features` to nativeLibrary crates, and bundling
|
|
13
|
+
# Jolt's static archives into bloom_windows.lib leaves the final lld-link
|
|
14
|
+
# step unable to resolve `JPH::*` symbols (the staticlib has no embedded
|
|
15
|
+
# search path and the C++ library lives off cargo's normal lib search).
|
|
16
|
+
# Games that need bloom/physics on Windows must opt in explicitly until the
|
|
17
|
+
# perry side gains a way to thread `cargo:rustc-link-search` through to the
|
|
18
|
+
# final link line.
|
|
19
|
+
default = []
|
|
12
20
|
jolt = ["bloom-shared/jolt"]
|
|
13
21
|
|
|
14
22
|
[dependencies]
|
|
15
23
|
bloom-shared = { path = "../shared", default-features = false, features = ["mp3"] }
|
|
24
|
+
image = { version = "0.25", default-features = false, features = ["hdr"] }
|
|
16
25
|
raw-window-handle = "0.6"
|
|
17
26
|
wgpu = "29"
|
|
18
27
|
|