@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.
Files changed (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +231 -0
  3. package/native/android/Cargo.lock +1848 -0
  4. package/native/android/Cargo.toml +24 -0
  5. package/native/android/src/lib.rs +702 -0
  6. package/native/ios/Cargo.lock +1690 -0
  7. package/native/ios/Cargo.toml +32 -0
  8. package/native/ios/src/lib.rs +1267 -0
  9. package/native/linux/Cargo.lock +3279 -0
  10. package/native/linux/Cargo.toml +29 -0
  11. package/native/linux/src/lib.rs +1331 -0
  12. package/native/macos/Cargo.lock +3310 -0
  13. package/native/macos/Cargo.toml +46 -0
  14. package/native/macos/src/lib.rs +1302 -0
  15. package/native/shared/Cargo.lock +1899 -0
  16. package/native/shared/Cargo.toml +62 -0
  17. package/native/shared/assets/default_font.ttf +0 -0
  18. package/native/shared/build.rs +270 -0
  19. package/native/shared/shaders/common/clouds.wgsl +122 -0
  20. package/native/shared/shaders/common/fog.wgsl +16 -0
  21. package/native/shared/shaders/common/foliage_wind.wgsl +98 -0
  22. package/native/shared/shaders/common/imposter.wgsl +112 -0
  23. package/native/shared/shaders/common/pbr.wgsl +186 -0
  24. package/native/shared/shaders/common/shadows.wgsl +186 -0
  25. package/native/shared/shaders/common/sky.wgsl +8 -0
  26. package/native/shared/shaders/common/tonemap.wgsl +25 -0
  27. package/native/shared/shaders/impulse_field.wgsl +57 -0
  28. package/native/shared/shaders/material_abi.wgsl +383 -0
  29. package/native/shared/shaders/materials/test_minimal.wgsl +42 -0
  30. package/native/shared/src/anim_mixer.rs +61 -0
  31. package/native/shared/src/attach.rs +263 -0
  32. package/native/shared/src/audio/decode.rs +123 -0
  33. package/native/shared/src/audio/mod.rs +863 -0
  34. package/native/shared/src/audio/render.rs +892 -0
  35. package/native/shared/src/audio/spsc.rs +156 -0
  36. package/native/shared/src/audio/stream.rs +226 -0
  37. package/native/shared/src/custom_shaders.rs +104 -0
  38. package/native/shared/src/decals.rs +245 -0
  39. package/native/shared/src/drs.rs +211 -0
  40. package/native/shared/src/engine.rs +261 -0
  41. package/native/shared/src/ffi.rs +116 -0
  42. package/native/shared/src/ffi_core/assets.rs +388 -0
  43. package/native/shared/src/ffi_core/audio_ffi.rs +184 -0
  44. package/native/shared/src/ffi_core/draw.rs +334 -0
  45. package/native/shared/src/ffi_core/game_loop.rs +577 -0
  46. package/native/shared/src/ffi_core/input.rs +234 -0
  47. package/native/shared/src/ffi_core/mod.rs +127 -0
  48. package/native/shared/src/ffi_core/models.rs +1154 -0
  49. package/native/shared/src/ffi_core/ragdoll_ffi.rs +261 -0
  50. package/native/shared/src/ffi_core/scene.rs +626 -0
  51. package/native/shared/src/ffi_core/vfx.rs +212 -0
  52. package/native/shared/src/ffi_core/visual.rs +691 -0
  53. package/native/shared/src/frame_callbacks.rs +122 -0
  54. package/native/shared/src/geometry.rs +236 -0
  55. package/native/shared/src/handles.rs +182 -0
  56. package/native/shared/src/input.rs +448 -0
  57. package/native/shared/src/jolt_sys.rs +822 -0
  58. package/native/shared/src/lib.rs +55 -0
  59. package/native/shared/src/models.rs +1093 -0
  60. package/native/shared/src/models_gltf.rs +1280 -0
  61. package/native/shared/src/particles.rs +391 -0
  62. package/native/shared/src/physics_jolt.rs +1908 -0
  63. package/native/shared/src/picking.rs +298 -0
  64. package/native/shared/src/postfx.rs +345 -0
  65. package/native/shared/src/profiler.rs +492 -0
  66. package/native/shared/src/ragdoll.rs +474 -0
  67. package/native/shared/src/renderer/atmosphere_lut.rs +573 -0
  68. package/native/shared/src/renderer/brdf_lut.rs +154 -0
  69. package/native/shared/src/renderer/draw2d.rs +143 -0
  70. package/native/shared/src/renderer/formats.rs +822 -0
  71. package/native/shared/src/renderer/froxel.rs +421 -0
  72. package/native/shared/src/renderer/gi_bake.rs +653 -0
  73. package/native/shared/src/renderer/graph.rs +462 -0
  74. package/native/shared/src/renderer/hiz.rs +269 -0
  75. package/native/shared/src/renderer/hot_reload.rs +390 -0
  76. package/native/shared/src/renderer/impulse_field.rs +456 -0
  77. package/native/shared/src/renderer/lighting.rs +154 -0
  78. package/native/shared/src/renderer/material_instancing.rs +171 -0
  79. package/native/shared/src/renderer/material_pipeline.rs +700 -0
  80. package/native/shared/src/renderer/material_system.rs +1996 -0
  81. package/native/shared/src/renderer/material_system_tests.rs +601 -0
  82. package/native/shared/src/renderer/material_system_wasm.rs +41 -0
  83. package/native/shared/src/renderer/mod.rs +12556 -0
  84. package/native/shared/src/renderer/model_draw.rs +641 -0
  85. package/native/shared/src/renderer/occlusion.rs +429 -0
  86. package/native/shared/src/renderer/planar_pass.rs +593 -0
  87. package/native/shared/src/renderer/planar_reflection.rs +499 -0
  88. package/native/shared/src/renderer/post_pass.rs +249 -0
  89. package/native/shared/src/renderer/postfx_chain.rs +728 -0
  90. package/native/shared/src/renderer/pt_pass.rs +577 -0
  91. package/native/shared/src/renderer/scene_pass.rs +607 -0
  92. package/native/shared/src/renderer/shader_include.rs +205 -0
  93. package/native/shared/src/renderer/shader_library.rs +135 -0
  94. package/native/shared/src/renderer/shaders/ao.rs +570 -0
  95. package/native/shared/src/renderer/shaders/core.rs +1243 -0
  96. package/native/shared/src/renderer/shaders/env.rs +907 -0
  97. package/native/shared/src/renderer/shaders/gi.rs +810 -0
  98. package/native/shared/src/renderer/shaders/mod.rs +19 -0
  99. package/native/shared/src/renderer/shaders/post.rs +1558 -0
  100. package/native/shared/src/renderer/shaders/pt.rs +1859 -0
  101. package/native/shared/src/renderer/shaders/ssgi.rs +1586 -0
  102. package/native/shared/src/renderer/shadow_pass.rs +731 -0
  103. package/native/shared/src/renderer/ssgi_pass.rs +392 -0
  104. package/native/shared/src/renderer/ssr_pass.rs +188 -0
  105. package/native/shared/src/renderer/texture_store.rs +473 -0
  106. package/native/shared/src/renderer/transient.rs +591 -0
  107. package/native/shared/src/renderer/types.rs +941 -0
  108. package/native/shared/src/renderer/util.rs +152 -0
  109. package/native/shared/src/scene.rs +1362 -0
  110. package/native/shared/src/sdf_cache.rs +274 -0
  111. package/native/shared/src/shadows.rs +1036 -0
  112. package/native/shared/src/staging.rs +102 -0
  113. package/native/shared/src/string_header.rs +266 -0
  114. package/native/shared/src/text_renderer.rs +502 -0
  115. package/native/shared/src/textures.rs +197 -0
  116. package/native/tvos/Cargo.lock +1693 -0
  117. package/native/tvos/Cargo.toml +36 -0
  118. package/native/tvos/metal-patched/Cargo.toml +178 -0
  119. package/native/tvos/metal-patched/LICENSE-APACHE +201 -0
  120. package/native/tvos/metal-patched/LICENSE-MIT +25 -0
  121. package/native/tvos/metal-patched/src/acceleration_structure.rs +667 -0
  122. package/native/tvos/metal-patched/src/acceleration_structure_pass.rs +108 -0
  123. package/native/tvos/metal-patched/src/argument.rs +366 -0
  124. package/native/tvos/metal-patched/src/blitpass.rs +102 -0
  125. package/native/tvos/metal-patched/src/buffer.rs +71 -0
  126. package/native/tvos/metal-patched/src/capturedescriptor.rs +76 -0
  127. package/native/tvos/metal-patched/src/capturemanager.rs +113 -0
  128. package/native/tvos/metal-patched/src/commandbuffer.rs +192 -0
  129. package/native/tvos/metal-patched/src/commandqueue.rs +44 -0
  130. package/native/tvos/metal-patched/src/computepass.rs +107 -0
  131. package/native/tvos/metal-patched/src/constants.rs +152 -0
  132. package/native/tvos/metal-patched/src/counters.rs +119 -0
  133. package/native/tvos/metal-patched/src/depthstencil.rs +190 -0
  134. package/native/tvos/metal-patched/src/device.rs +2134 -0
  135. package/native/tvos/metal-patched/src/drawable.rs +39 -0
  136. package/native/tvos/metal-patched/src/encoder.rs +2041 -0
  137. package/native/tvos/metal-patched/src/heap.rs +281 -0
  138. package/native/tvos/metal-patched/src/indirect_encoder.rs +344 -0
  139. package/native/tvos/metal-patched/src/lib.rs +657 -0
  140. package/native/tvos/metal-patched/src/library.rs +902 -0
  141. package/native/tvos/metal-patched/src/mps.rs +575 -0
  142. package/native/tvos/metal-patched/src/pipeline/compute.rs +475 -0
  143. package/native/tvos/metal-patched/src/pipeline/mod.rs +71 -0
  144. package/native/tvos/metal-patched/src/pipeline/render.rs +762 -0
  145. package/native/tvos/metal-patched/src/renderpass.rs +443 -0
  146. package/native/tvos/metal-patched/src/resource.rs +182 -0
  147. package/native/tvos/metal-patched/src/sampler.rs +165 -0
  148. package/native/tvos/metal-patched/src/sync.rs +178 -0
  149. package/native/tvos/metal-patched/src/texture.rs +352 -0
  150. package/native/tvos/metal-patched/src/types.rs +90 -0
  151. package/native/tvos/metal-patched/src/vertexdescriptor.rs +250 -0
  152. package/native/tvos/src/audio_backend.rs +197 -0
  153. package/native/tvos/src/lib.rs +1891 -0
  154. package/native/visionos/Cargo.lock +1693 -0
  155. package/native/visionos/Cargo.toml +40 -0
  156. package/native/visionos/src/audio_backend.rs +197 -0
  157. package/native/visionos/src/lib.rs +1887 -0
  158. package/native/watchos/Cargo.lock +16 -0
  159. package/native/watchos/Cargo.toml +19 -0
  160. package/native/watchos/shaders/bloom_postfx.metal +99 -0
  161. package/native/watchos/src/BloomWatchApp.swift +1267 -0
  162. package/native/watchos/src/BloomWatchAudio.swift +179 -0
  163. package/native/watchos/src/audio.rs +55 -0
  164. package/native/watchos/src/draw_list.rs +229 -0
  165. package/native/watchos/src/ffi_stubs.rs +915 -0
  166. package/native/watchos/src/ffi_stubs_manual.rs +35 -0
  167. package/native/watchos/src/lib.rs +1124 -0
  168. package/native/watchos/src/models.rs +746 -0
  169. package/native/watchos/src/postfx.rs +95 -0
  170. package/native/watchos/src/scene.rs +534 -0
  171. package/native/watchos/src/textures.rs +184 -0
  172. package/native/web/Cargo.lock +1657 -0
  173. package/native/web/Cargo.toml +43 -0
  174. package/native/web/bloom_glue.js +695 -0
  175. package/native/web/build.sh +131 -0
  176. package/native/web/index.html +35 -0
  177. package/native/web/jolt_bridge.js +1519 -0
  178. package/native/web/src/input_ffi.rs +286 -0
  179. package/native/web/src/lib.rs +1796 -0
  180. package/native/web/src/material_ffi.rs +710 -0
  181. package/native/web/src/parity_ffi.rs +343 -0
  182. package/native/web/src/physics_ffi.rs +643 -0
  183. package/native/web/src/ragdoll_ffi.rs +250 -0
  184. package/native/web/src/render_settings.rs +98 -0
  185. package/native/windows/Cargo.lock +1815 -0
  186. package/native/windows/Cargo.toml +68 -0
  187. package/native/windows/src/lib.rs +1486 -0
  188. package/package.json +4279 -0
  189. package/src/audio/index.ts +315 -0
  190. package/src/core/colors.ts +63 -0
  191. package/src/core/index.ts +1206 -0
  192. package/src/core/keys.ts +63 -0
  193. package/src/core/types.ts +104 -0
  194. package/src/index.ts +171 -0
  195. package/src/math/index.ts +516 -0
  196. package/src/mobile/index.ts +294 -0
  197. package/src/models/index.ts +1258 -0
  198. package/src/physics/index.ts +1134 -0
  199. package/src/scene/index.ts +698 -0
  200. package/src/shapes/index.ts +120 -0
  201. package/src/text/index.ts +48 -0
  202. package/src/textures/index.ts +187 -0
  203. package/src/vfx/index.ts +191 -0
  204. package/src/world/index.ts +24 -0
  205. package/src/world/loader.ts +423 -0
  206. package/src/world/prefab.ts +217 -0
  207. package/src/world/render.ts +172 -0
  208. package/src/world/saver.ts +108 -0
  209. package/src/world/serialize.ts +301 -0
  210. package/src/world/terrain.ts +355 -0
  211. package/src/world/types.ts +160 -0
  212. package/src/world/validate.ts +319 -0
  213. package/src/world/version.ts +114 -0
@@ -0,0 +1,102 @@
1
+ use std::sync::{Mutex, OnceLock};
2
+ #[cfg(feature = "models3d")]
3
+ use crate::models::ModelData;
4
+ use crate::audio::SoundData;
5
+
6
+ pub struct StagedTexture {
7
+ pub data: Vec<u8>,
8
+ pub width: u32,
9
+ pub height: u32,
10
+ /// Normal maps need `register_texture_kind`'s linear-space + LEADR mip
11
+ /// path at commit time; registering them like albedo (sRGB) visibly
12
+ /// flattens the shading. Set by `load_gltf_staged` from the material's
13
+ /// `normal_texture` references, mirroring `load_gltf_with_textures`.
14
+ pub is_normal: bool,
15
+ }
16
+
17
+ #[cfg(feature = "models3d")]
18
+ pub struct StagedModel {
19
+ pub model: ModelData,
20
+ pub textures: Vec<StagedTexture>,
21
+ }
22
+
23
+ // Thread-safe staging stores using Mutex<Vec<Option<T>>>.
24
+ // The lock is only held during insert/remove (microseconds), not during decode.
25
+
26
+ fn texture_store() -> &'static Mutex<Vec<Option<StagedTexture>>> {
27
+ static INSTANCE: OnceLock<Mutex<Vec<Option<StagedTexture>>>> = OnceLock::new();
28
+ INSTANCE.get_or_init(|| Mutex::new(Vec::new()))
29
+ }
30
+
31
+ #[cfg(feature = "models3d")]
32
+ fn model_store() -> &'static Mutex<Vec<Option<StagedModel>>> {
33
+ static INSTANCE: OnceLock<Mutex<Vec<Option<StagedModel>>>> = OnceLock::new();
34
+ INSTANCE.get_or_init(|| Mutex::new(Vec::new()))
35
+ }
36
+
37
+ fn sound_store() -> &'static Mutex<Vec<Option<SoundData>>> {
38
+ static INSTANCE: OnceLock<Mutex<Vec<Option<SoundData>>>> = OnceLock::new();
39
+ INSTANCE.get_or_init(|| Mutex::new(Vec::new()))
40
+ }
41
+
42
+ fn stage_into<T>(store: &Mutex<Vec<Option<T>>>, item: T) -> f64 {
43
+ let mut vec = store.lock().unwrap();
44
+ // Reuse freed slots
45
+ for (i, slot) in vec.iter_mut().enumerate() {
46
+ if slot.is_none() {
47
+ *slot = Some(item);
48
+ return (i + 1) as f64;
49
+ }
50
+ }
51
+ vec.push(Some(item));
52
+ vec.len() as f64
53
+ }
54
+
55
+ fn take_from<T>(store: &Mutex<Vec<Option<T>>>, handle: f64) -> Option<T> {
56
+ let idx = handle as usize;
57
+ if idx == 0 { return None; }
58
+ let mut vec = store.lock().unwrap();
59
+ if idx > vec.len() { return None; }
60
+ vec[idx - 1].take()
61
+ }
62
+
63
+ // Public API
64
+
65
+ /// Decode image bytes (PNG/JPEG/etc) and stage the result. Thread-safe.
66
+ pub fn decode_and_stage_texture(file_data: &[u8]) -> f64 {
67
+ let img = match image::load_from_memory(file_data) {
68
+ Ok(img) => img.to_rgba8(),
69
+ Err(_) => return 0.0,
70
+ };
71
+ let width = img.width();
72
+ let height = img.height();
73
+ // Standalone staged textures are albedo-class; nothing routes a normal
74
+ // map through this path (models carry theirs inside StagedModel).
75
+ stage_texture(StagedTexture { data: img.into_raw(), width, height, is_normal: false })
76
+ }
77
+
78
+ pub fn stage_texture(tex: StagedTexture) -> f64 {
79
+ stage_into(texture_store(), tex)
80
+ }
81
+
82
+ pub fn take_texture(handle: f64) -> Option<StagedTexture> {
83
+ take_from(texture_store(), handle)
84
+ }
85
+
86
+ #[cfg(feature = "models3d")]
87
+ pub fn stage_model(model: StagedModel) -> f64 {
88
+ stage_into(model_store(), model)
89
+ }
90
+
91
+ #[cfg(feature = "models3d")]
92
+ pub fn take_model(handle: f64) -> Option<StagedModel> {
93
+ take_from(model_store(), handle)
94
+ }
95
+
96
+ pub fn stage_sound(sound: SoundData) -> f64 {
97
+ stage_into(sound_store(), sound)
98
+ }
99
+
100
+ pub fn take_sound(handle: f64) -> Option<SoundData> {
101
+ take_from(sound_store(), handle)
102
+ }
@@ -0,0 +1,266 @@
1
+ //! Perry string ABI: the header layout strings carry across the FFI.
2
+ //!
3
+ //! This mirrors `perry_runtime::string::StringHeader` (see
4
+ //! `perry/crates/perry-runtime/src/string/mod.rs`) — defined locally so the
5
+ //! engine doesn't pull the whole perry-runtime crate in as a dependency.
6
+ //!
7
+ //! ## Upgrade protocol
8
+ //!
9
+ //! Perry owns this layout and has changed it before (0.5.18 added `flags`;
10
+ //! engines built against the old 16-byte header read a 4-byte garbage
11
+ //! prefix on every string). There is no version symbol exported by the
12
+ //! Perry runtime to handshake against, so the defenses are:
13
+ //!
14
+ //! 1. Compile-time size/offset assertions below — any local edit that
15
+ //! diverges from the documented layout fails the build.
16
+ //! 2. [`header_looks_valid`] — invariant checks on every incoming
17
+ //! header. A Perry-side layout change makes these fire on the first
18
+ //! string the engine receives (typically the window title in
19
+ //! `bloom_init_window`), turning silent corruption into a loud
20
+ //! log-once diagnostic.
21
+ //! 3. Checked UTF-8 conversion — a wrong `byte_len` can no longer cause
22
+ //! undefined behavior, only an empty string + diagnostic.
23
+ //!
24
+ //! When bumping Perry across a runtime-ABI change: update the struct,
25
+ //! the assertions, and the doc reference above in the same commit.
26
+
27
+ /// Header for heap-allocated Perry strings. UTF-8 payload follows
28
+ /// immediately after the header.
29
+ #[repr(C)]
30
+ pub struct StringHeader {
31
+ /// Length in UTF-16 code units (JS `.length` semantics). At offset 0
32
+ /// for Perry's inline codegen.
33
+ pub utf16_len: u32,
34
+ /// Length in UTF-8 bytes.
35
+ pub byte_len: u32,
36
+ /// Capacity in bytes (allocated space for data).
37
+ pub capacity: u32,
38
+ /// Reference hint: 0=shared, 1=unique (in-place append OK).
39
+ pub refcount: u32,
40
+ /// Bit flags (STRING_FLAG_HAS_LONE_SURROGATES = 1). Added in Perry
41
+ /// 0.5.18.
42
+ pub flags: u32,
43
+ }
44
+
45
+ // Layout is an ABI contract — fail the build if the struct drifts from the
46
+ // documented Perry layout.
47
+ const _: () = {
48
+ assert!(std::mem::size_of::<StringHeader>() == 20);
49
+ assert!(std::mem::offset_of!(StringHeader, utf16_len) == 0);
50
+ assert!(std::mem::offset_of!(StringHeader, byte_len) == 4);
51
+ assert!(std::mem::offset_of!(StringHeader, capacity) == 8);
52
+ assert!(std::mem::offset_of!(StringHeader, refcount) == 12);
53
+ assert!(std::mem::offset_of!(StringHeader, flags) == 16);
54
+ };
55
+
56
+ /// All flag bits Perry currently defines.
57
+ const KNOWN_FLAGS: u32 = 1; // STRING_FLAG_HAS_LONE_SURROGATES
58
+
59
+ /// Sanity-check a header against invariants that hold for every string the
60
+ /// current Perry runtime produces. A failed check means either a corrupt
61
+ /// pointer or — the case this exists for — a Perry-side layout change
62
+ /// shifting which u32 lands in which field.
63
+ fn header_looks_valid(h: &StringHeader) -> bool {
64
+ h.byte_len <= h.capacity
65
+ && h.capacity < (1 << 31)
66
+ // utf16 length is never larger than the utf8 byte length
67
+ && h.utf16_len <= h.byte_len
68
+ && (h.flags & !KNOWN_FLAGS) == 0
69
+ }
70
+
71
+ fn abi_mismatch_warn_once(what: &str) {
72
+ use std::sync::atomic::{AtomicBool, Ordering};
73
+ static WARNED: AtomicBool = AtomicBool::new(false);
74
+ if !WARNED.swap(true, Ordering::Relaxed) {
75
+ crate::ffi::log_error(&format!(
76
+ "bloom: incoming Perry string failed ABI validation ({what}). \
77
+ This usually means the Perry runtime's StringHeader layout \
78
+ changed — see native/shared/src/string_header.rs for the \
79
+ upgrade protocol. Returning empty strings instead of reading \
80
+ garbage; further occurrences are suppressed."
81
+ ));
82
+ }
83
+ }
84
+
85
+ /// Extract a `&str` from a `*const StringHeader` pointer (Perry string
86
+ /// format).
87
+ ///
88
+ /// The returned slice borrows Perry-owned memory that is only guaranteed
89
+ /// to live for the duration of the FFI call — copy it (`to_string`) before
90
+ /// stashing it anywhere. The `'static` lifetime is a legacy artifact of
91
+ /// the FFI signatures, not a promise.
92
+ ///
93
+ /// Never causes undefined behavior: null/garbage pointers, implausible
94
+ /// headers, and invalid UTF-8 all yield `""` plus a one-time diagnostic.
95
+ /// Like [`str_from_header`], but says whether it FAILED rather than papering over it
96
+ /// with an empty string.
97
+ ///
98
+ /// The distinction is not academic. `bloom_write_file` used `str_from_header`, got
99
+ /// `""` back when a string failed ABI validation, wrote a ZERO-BYTE FILE, and
100
+ /// returned SUCCESS. The editor's save path therefore destroyed every world it
101
+ /// saved and reported that it had saved it. An empty string and a failed string are
102
+ /// not the same thing, and any FFI that *persists* its input has to know which it
103
+ /// is holding.
104
+ pub fn try_str_from_header(ptr: *const u8) -> Option<&'static str> {
105
+ if ptr.is_null() || (ptr as usize) < 0x1000 {
106
+ return Some("");
107
+ }
108
+ unsafe {
109
+ let header = &*(ptr as *const StringHeader);
110
+ if !header_looks_valid(header) {
111
+ abi_mismatch_warn_once("header invariants violated");
112
+ return None;
113
+ }
114
+ let len = header.byte_len as usize;
115
+ let data = ptr.add(std::mem::size_of::<StringHeader>());
116
+ match std::str::from_utf8(std::slice::from_raw_parts(data, len)) {
117
+ Ok(s) => Some(s),
118
+ Err(_) => {
119
+ abi_mismatch_warn_once("payload is not UTF-8");
120
+ None
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ pub fn str_from_header(ptr: *const u8) -> &'static str {
127
+ if ptr.is_null() || (ptr as usize) < 0x1000 {
128
+ return "";
129
+ }
130
+ unsafe {
131
+ let header = &*(ptr as *const StringHeader);
132
+ if !header_looks_valid(header) {
133
+ abi_mismatch_warn_once("header invariants violated");
134
+ return "";
135
+ }
136
+ let len = header.byte_len as usize;
137
+ let data = ptr.add(std::mem::size_of::<StringHeader>());
138
+ match std::str::from_utf8(std::slice::from_raw_parts(data, len)) {
139
+ Ok(s) => s,
140
+ Err(_) => {
141
+ abi_mismatch_warn_once("payload is not UTF-8");
142
+ ""
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ /// Allocate a Perry heap string suitable for returning across the FFI
149
+ /// boundary (declared as `returns: "string"` in package.json).
150
+ ///
151
+ /// Older engine code allocated the 12-byte Perry 0.4.x header by hand and
152
+ /// Perry's 0.5.x runtime read 8 bytes into the payload. Always go through
153
+ /// this helper — the layout comes from the `StringHeader` type, which the
154
+ /// compile-time assertions above pin to the documented ABI.
155
+ /// EN-020 — Perry's string scanners (`split`, `indexOf`, …) step
156
+ /// word-at-a-time and may read up to a word past `byte_len`. With an
157
+ /// exactly-sized allocation that lands flush against an unmapped page,
158
+ /// that overread is an access violation (observed 3/3 in the shooter as
159
+ /// `perry_fn_…getProfilerOverlay` / `…getProfilerFrameHistory`, faulting
160
+ /// reads at page ends; historical EN-020 signature `main.exe+0xe8e5`,
161
+ /// read at `0x…FFF8`). Every string this function returns is scanned by
162
+ /// Perry, so keep a zeroed tail pad behind the payload — `capacity`
163
+ /// still reports `byte_len`, so Perry never writes into the pad.
164
+ const TAIL_PAD: usize = 16;
165
+
166
+ pub fn alloc_perry_string(s: &str) -> *const u8 {
167
+ let bytes = s.as_bytes();
168
+ let byte_len = bytes.len();
169
+ // ASCII fast path: utf16_len == byte_len when every byte is < 0x80.
170
+ let utf16_len = if bytes.iter().all(|&b| b < 0x80) {
171
+ byte_len
172
+ } else {
173
+ s.encode_utf16().count()
174
+ };
175
+ let total = std::mem::size_of::<StringHeader>() + byte_len + TAIL_PAD;
176
+ let layout = std::alloc::Layout::from_size_align(total, 4).unwrap();
177
+ unsafe {
178
+ let ptr = std::alloc::alloc(layout);
179
+ if ptr.is_null() {
180
+ return std::ptr::null();
181
+ }
182
+ (ptr as *mut StringHeader).write(StringHeader {
183
+ utf16_len: utf16_len as u32,
184
+ byte_len: byte_len as u32,
185
+ capacity: byte_len as u32,
186
+ refcount: 1, // unique
187
+ flags: 0,
188
+ });
189
+ std::ptr::copy_nonoverlapping(
190
+ bytes.as_ptr(),
191
+ ptr.add(std::mem::size_of::<StringHeader>()),
192
+ byte_len,
193
+ );
194
+ std::ptr::write_bytes(
195
+ ptr.add(std::mem::size_of::<StringHeader>() + byte_len),
196
+ 0,
197
+ TAIL_PAD,
198
+ );
199
+ ptr
200
+ }
201
+ }
202
+
203
+ #[cfg(test)]
204
+ mod tests {
205
+ use super::*;
206
+
207
+ #[test]
208
+ fn round_trip_ascii() {
209
+ let p = alloc_perry_string("hello bloom");
210
+ assert_eq!(str_from_header(p), "hello bloom");
211
+ }
212
+
213
+ #[test]
214
+ fn round_trip_multibyte() {
215
+ let p = alloc_perry_string("héllo 🌸");
216
+ assert_eq!(str_from_header(p), "héllo 🌸");
217
+ // utf16_len: 'héllo ' = 6 units, emoji = 2 (surrogate pair)
218
+ let h = unsafe { &*(p as *const StringHeader) };
219
+ assert_eq!(h.utf16_len, 8);
220
+ assert_eq!(h.byte_len, "héllo 🌸".len() as u32);
221
+ }
222
+
223
+ #[test]
224
+ fn rejects_null_and_low_pointers() {
225
+ assert_eq!(str_from_header(std::ptr::null()), "");
226
+ assert_eq!(str_from_header(0x10 as *const u8), "");
227
+ }
228
+
229
+ #[test]
230
+ fn rejects_implausible_header() {
231
+ // byte_len > capacity — the signature of a shifted layout.
232
+ let bogus = StringHeader {
233
+ utf16_len: 7,
234
+ byte_len: 100,
235
+ capacity: 8,
236
+ refcount: 1,
237
+ flags: 0,
238
+ };
239
+ let mut buf = vec![0u8; std::mem::size_of::<StringHeader>() + 8];
240
+ unsafe {
241
+ (buf.as_mut_ptr() as *mut StringHeader).write(bogus);
242
+ }
243
+ assert_eq!(str_from_header(buf.as_ptr()), "");
244
+ }
245
+
246
+ #[test]
247
+ fn tail_pad_present_and_zeroed() {
248
+ // EN-020 regression guard: a word-stepping scanner may read past
249
+ // byte_len; the pad must exist and read as NULs.
250
+ let p = alloc_perry_string("abc");
251
+ let payload_end = std::mem::size_of::<StringHeader>() + 3;
252
+ for i in 0..TAIL_PAD {
253
+ assert_eq!(unsafe { *p.add(payload_end + i) }, 0, "pad byte {i} not zero");
254
+ }
255
+ }
256
+
257
+ #[test]
258
+ fn rejects_invalid_utf8() {
259
+ let p = alloc_perry_string("abcd") as *mut u8;
260
+ unsafe {
261
+ // stomp the payload with a bare continuation byte
262
+ *p.add(std::mem::size_of::<StringHeader>()) = 0xFF;
263
+ }
264
+ assert_eq!(str_from_header(p), "");
265
+ }
266
+ }