@bloomengine/engine 0.3.1 → 0.3.3

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.
Files changed (65) hide show
  1. package/README.md +33 -11
  2. package/native/linux/Cargo.lock +1571 -12
  3. package/native/linux/Cargo.toml +3 -0
  4. package/native/linux/src/lib.rs +745 -40
  5. package/native/macos/src/lib.rs +30 -118
  6. package/native/shared/build.rs +32 -4
  7. package/native/shared/src/postfx.rs +16 -10
  8. package/native/shared/src/renderer/formats.rs +1 -7
  9. package/native/shared/src/renderer/impulse_field.rs +2 -1
  10. package/native/shared/src/renderer/material_system.rs +15 -3
  11. package/native/shared/src/renderer/shaders.rs +7 -1
  12. package/native/shared/src/renderer/transient.rs +12 -12
  13. package/native/shared/src/string_header.rs +32 -0
  14. package/native/third_party/JoltPhysics/Build/Android/PerformanceTest/build.gradle +51 -0
  15. package/native/third_party/JoltPhysics/Build/Android/PerformanceTest/src/main/AndroidManifest.xml +20 -0
  16. package/native/third_party/JoltPhysics/Build/Android/PerformanceTest/src/main/cpp/CMakeLists.txt +20 -0
  17. package/native/third_party/JoltPhysics/Build/Android/UnitTests/build.gradle +51 -0
  18. package/native/third_party/JoltPhysics/Build/Android/UnitTests/src/main/AndroidManifest.xml +20 -0
  19. package/native/third_party/JoltPhysics/Build/Android/UnitTests/src/main/cpp/CMakeLists.txt +20 -0
  20. package/native/third_party/JoltPhysics/Build/Android/build.gradle +17 -0
  21. package/native/third_party/JoltPhysics/Build/Android/gradle/wrapper/gradle-wrapper.jar +0 -0
  22. package/native/third_party/JoltPhysics/Build/Android/gradle/wrapper/gradle-wrapper.properties +5 -0
  23. package/native/third_party/JoltPhysics/Build/Android/gradle.properties +21 -0
  24. package/native/third_party/JoltPhysics/Build/Android/gradlew +185 -0
  25. package/native/third_party/JoltPhysics/Build/Android/gradlew.bat +89 -0
  26. package/native/third_party/JoltPhysics/Build/Android/settings.gradle +10 -0
  27. package/native/third_party/JoltPhysics/Build/CMakeLists.txt +449 -0
  28. package/native/third_party/JoltPhysics/Build/README.md +250 -0
  29. package/native/third_party/JoltPhysics/Build/cmake_linux_clang_gcc.sh +28 -0
  30. package/native/third_party/JoltPhysics/Build/cmake_linux_emscripten.sh +19 -0
  31. package/native/third_party/JoltPhysics/Build/cmake_linux_mingw.sh +19 -0
  32. package/native/third_party/JoltPhysics/Build/cmake_vs2022_cl.bat +3 -0
  33. package/native/third_party/JoltPhysics/Build/cmake_vs2022_cl_32bit.bat +3 -0
  34. package/native/third_party/JoltPhysics/Build/cmake_vs2022_cl_arm.bat +3 -0
  35. package/native/third_party/JoltPhysics/Build/cmake_vs2022_cl_arm_32bit.bat +4 -0
  36. package/native/third_party/JoltPhysics/Build/cmake_vs2022_cl_cross_platform_deterministic.bat +3 -0
  37. package/native/third_party/JoltPhysics/Build/cmake_vs2022_cl_double.bat +3 -0
  38. package/native/third_party/JoltPhysics/Build/cmake_vs2022_cl_no_object_stream.bat +3 -0
  39. package/native/third_party/JoltPhysics/Build/cmake_vs2022_clang.bat +10 -0
  40. package/native/third_party/JoltPhysics/Build/cmake_vs2022_clang_cross_platform_deterministic.bat +10 -0
  41. package/native/third_party/JoltPhysics/Build/cmake_vs2022_clang_double.bat +10 -0
  42. package/native/third_party/JoltPhysics/Build/cmake_vs2022_uwp.bat +5 -0
  43. package/native/third_party/JoltPhysics/Build/cmake_vs2022_uwp_arm.bat +5 -0
  44. package/native/third_party/JoltPhysics/Build/cmake_vs2026_cl.bat +3 -0
  45. package/native/third_party/JoltPhysics/Build/cmake_vs2026_cl_cross_platform_deterministic.bat +3 -0
  46. package/native/third_party/JoltPhysics/Build/cmake_vs2026_cl_double.bat +3 -0
  47. package/native/third_party/JoltPhysics/Build/cmake_vs2026_clang.bat +10 -0
  48. package/native/third_party/JoltPhysics/Build/cmake_vs2026_clang_cross_platform_deterministic.bat +10 -0
  49. package/native/third_party/JoltPhysics/Build/cmake_vs2026_clang_double.bat +10 -0
  50. package/native/third_party/JoltPhysics/Build/cmake_windows_mingw.sh +19 -0
  51. package/native/third_party/JoltPhysics/Build/cmake_xcode_ios.sh +4 -0
  52. package/native/third_party/JoltPhysics/Build/cmake_xcode_macos.sh +4 -0
  53. package/native/third_party/JoltPhysics/Build/iOS/JoltViewerInfo.plist +34 -0
  54. package/native/third_party/JoltPhysics/Build/iOS/SamplesInfo.plist +34 -0
  55. package/native/third_party/JoltPhysics/Build/iOS/UnitTestsInfo.plist +34 -0
  56. package/native/third_party/JoltPhysics/Build/macOS/icon.icns +0 -0
  57. package/native/third_party/JoltPhysics/Build/macos_install_vulkan_sdk.sh +13 -0
  58. package/native/third_party/JoltPhysics/Build/ubuntu24_install_vulkan_sdk.sh +4 -0
  59. package/native/third_party/bloom_jolt/CMakeLists.txt +14 -5
  60. package/native/windows/Cargo.lock +1 -0
  61. package/native/windows/Cargo.toml +10 -1
  62. package/native/windows/src/lib.rs +226 -18
  63. package/package.json +9 -7
  64. package/src/core/colors.ts +34 -27
  65. package/src/core/index.ts +1 -2
@@ -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(gamepad: f64) -> f64 {
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(gamepad: f64, axis: f64) -> f64 {
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(gamepad: f64, button: f64) -> f64 {
769
- let _ = gamepad;
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(gamepad: f64, button: f64) -> f64 {
775
- let _ = gamepad;
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(gamepad: f64, button: f64) -> f64 {
781
- let _ = gamepad;
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(gamepad: f64) -> f64 {
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(0)
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(0)
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
- let bytes = s.as_bytes();
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
- let bytes = s.as_bytes();
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
- match clipboard.get_text() {
2332
- Ok(text) => {
2333
- let bytes = text.as_bytes();
2334
- let len = bytes.len();
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
- let s = path.to_string_lossy();
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
- let s = path.to_string_lossy();
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
- // Return Perry-format string: StringHeader (length u32 + capacity u32 + refcount u32) followed by UTF-8 data
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
 
@@ -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
- let dst = cmake::Config::new(&shim_dir)
54
- .profile("Release")
55
- .define("CMAKE_BUILD_TYPE", "Release")
56
- .build();
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
- struct VertexOutput {
47
- @builtin(position) position: vec4<f32>,
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
- let pixel = vec2<f32>(1.0 / params.screen_size.x, 1.0 / params.screen_size.y);
57
- let t = params.thickness.x;
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 offset = vec2<f32>(f32(dx), f32(dy)) * pixel * t;
65
- let neighbor_id = textureSample(object_id_tex, tex_sampler, in.uv + offset).r;
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
- bgl_texture(1, wgpu::TextureSampleType::Float { filterable: true }),
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: wgpu::Limits::downlevel_defaults(),
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 = textureSample(depth_tex, depth_samp, ray_uv);
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 (i, slot) in self.slots.iter_mut().enumerate() {
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(i as u32);
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
- // Slot index equals the id in this simple implementation
205
- // once we add alias-based reuse the mapping becomes indirect,
206
- // but for now that's overkill.
207
- let slot_index = self.slots.len() as u32;
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.get_mut(id.0 as usize) {
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.get(id.0 as usize).map(|s| &s.texture)
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.get(id.0 as usize).map(|s| &s.view)
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
+ }
@@ -0,0 +1,51 @@
1
+ plugins {
2
+ id 'com.android.application'
3
+ }
4
+
5
+ android {
6
+ compileSdk 33
7
+ ndkVersion "26.1.10909125"
8
+
9
+ defaultConfig {
10
+ applicationId "com.joltphysics.performancetest"
11
+ minSdk 21
12
+ targetSdk 33
13
+ versionCode 1
14
+ versionName "1.0"
15
+ ndk.abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64', 'x86'
16
+
17
+ externalNativeBuild {
18
+ cmake {
19
+ cppFlags '-std=c++17 -Wall -Werror -ffp-model=precise -ffp-contract=off -DJPH_PROFILE_ENABLED -DJPH_DEBUG_RENDERER'
20
+ arguments '-DANDROID_TOOLCHAIN=clang', '-DANDROID_STL=c++_static', '-DCROSS_PLATFORM_DETERMINISTIC=ON'
21
+ }
22
+ }
23
+ signingConfig signingConfigs.debug
24
+ }
25
+
26
+ buildTypes {
27
+ release {
28
+ minifyEnabled false
29
+ }
30
+ }
31
+
32
+ compileOptions {
33
+ sourceCompatibility JavaVersion.VERSION_1_8
34
+ targetCompatibility JavaVersion.VERSION_1_8
35
+ }
36
+
37
+ externalNativeBuild {
38
+ cmake {
39
+ path file('src/main/cpp/CMakeLists.txt')
40
+ version '3.22.1'
41
+ }
42
+ }
43
+
44
+ buildFeatures {
45
+ viewBinding true
46
+ }
47
+ namespace 'com.joltphysics.performancetest'
48
+ }
49
+
50
+ dependencies {
51
+ }
@@ -0,0 +1,20 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
3
+
4
+ <application
5
+ android:allowBackup="true"
6
+ android:label="Jolt Physics Performance Test"
7
+ android:supportsRtl="false"
8
+ android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
9
+ <activity
10
+ android:name="android.app.NativeActivity"
11
+ android:exported="true">
12
+ <meta-data android:name="android.app.lib_name" android:value="PerformanceTest"/>
13
+ <intent-filter>
14
+ <action android:name="android.intent.action.MAIN"/>
15
+ <category android:name="android.intent.category.LAUNCHER"/>
16
+ </intent-filter>
17
+ </activity>
18
+ </application>
19
+
20
+ </manifest>
@@ -0,0 +1,20 @@
1
+ cmake_minimum_required(VERSION 3.10.2)
2
+
3
+ project("JoltPhysicsPerformanceTest")
4
+
5
+ # Make sure we include the app glue sources
6
+ set(APP_GLUE_DIR ${ANDROID_NDK}/sources/android/native_app_glue)
7
+ include_directories(${APP_GLUE_DIR})
8
+
9
+ # Set repository root
10
+ set(PHYSICS_REPO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../")
11
+
12
+ # Make targets
13
+ include(${PHYSICS_REPO_ROOT}/Jolt/Jolt.cmake)
14
+ include(${PHYSICS_REPO_ROOT}/PerformanceTest/PerformanceTest.cmake)
15
+
16
+ # Link shared native library
17
+ set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -u ANativeActivity_onCreate")
18
+ add_library(PerformanceTest SHARED ${PERFORMANCE_TEST_SRC_FILES} ${APP_GLUE_DIR}/android_native_app_glue.c)
19
+ target_include_directories(PerformanceTest PUBLIC Jolt ${JOLT_PHYSICS_ROOT} ${PERFORMANCE_TEST_ROOT})
20
+ target_link_libraries(PerformanceTest Jolt android log)