@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,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
|
+
}
|