From afb3610a3ae8998c415bb63b53e9c56089507883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 12 Jun 2026 09:49:31 +0200 Subject: [PATCH 1/3] feat(en-014): feature-gate models3d + image-extras; watchOS stub generator updates - bloom-shared: new models3d (gltf/image_dds) and image-extras (jpeg/bmp/ tga/hdr) cargo features, default-on; pure-2D games opt out via Perry's per-package feature forwarding - macos: forward the new features; HDR decode gated behind image-extras - watchOS: gen_stubs.js manual-override support (ffi_stubs_manual.rs), draw-list + Swift host updates - mesh SDF textures allocate COPY_DST for ticket 022 disk cache - gitignore: perry cache, local jolt cmake tree, sweep outputs --- .gitignore | 9 ++ native/macos/Cargo.toml | 21 ++- native/macos/src/lib.rs | 192 ++++++++++++++++++++++++++ native/shared/Cargo.toml | 17 ++- native/shared/src/engine.rs | 3 + native/shared/src/lib.rs | 2 + native/shared/src/renderer/formats.rs | 8 +- native/shared/src/renderer/mod.rs | 1 + native/shared/src/staging.rs | 5 + 9 files changed, 247 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index f2f9383..2aacb18 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,15 @@ npm/jolt-prebuilt/lib/ # Claude Code harness-local state .claude/scheduled_tasks.lock +# Perry compiler cache +.perry-cache/ + +# CMake build tree from local bloom_jolt builds (CI produces the canonical archives) +native/third_party/bloom_jolt/build/ + +# Perf sweep outputs +examples/intel-sponza/sweep.csv + # Local research / external-engine reference clones. Not part of the # Bloom source tree — each dir is a full Unity / Unreal / Three.js / # Babylon.js project set up locally for side-by-side perf comparison diff --git a/native/macos/Cargo.toml b/native/macos/Cargo.toml index b576cb0..490bfe1 100644 --- a/native/macos/Cargo.toml +++ b/native/macos/Cargo.toml @@ -8,12 +8,21 @@ name = "bloom_macos" crate-type = ["staticlib"] [features] -# Jolt physics is enabled by default because Perry's native-library build -# pipeline does not forward cargo feature flags to nativeLibrary crates — -# any game importing `bloom/physics` would otherwise fail to link. The -# tradeoff is ~60s extra cold-build time for games that don't use physics. -default = ["jolt"] +# Everything defaults ON so existing games are unaffected. Pure-2D games +# opt out per-project via Perry's `[native-library.""]` feature +# forwarding in perry.toml (perry >= 0.5.1126), e.g.: +# [native-library."@bloomengine/engine"] +# default-features = false +# which drops Jolt, glTF/DDS model loading, and the non-PNG image codecs +# from the game binary. +default = ["jolt", "models3d", "image-extras"] jolt = ["bloom-shared/jolt"] +# EN-014 — glTF/DDS model loading + ModelManager (and this crate's +# bloom_load_model/draw_model/... FFI surface). +models3d = ["bloom-shared/models3d"] +# EN-014 — image codecs beyond PNG; also gates this crate's own HDR +# env-map decode (bloom_set_env_clear_from_hdr). +image-extras = ["bloom-shared/image-extras", "image/hdr"] # Match perry-runtime's panic strategy so the final perry-driven link # doesn't see two copies of rust_eh_personality (and friends) from two @@ -25,7 +34,7 @@ panic = "abort" [dependencies] bloom-shared = { path = "../shared", default-features = false, features = ["mp3"] } -image = { version = "0.25", default-features = false, features = ["hdr"] } +image = { version = "0.25", default-features = false } objc2 = "0.6" objc2-foundation = { version = "0.3", features = ["NSDate", "NSRunLoop", "NSString", "NSThread", "NSLocale", "NSArray"] } objc2-app-kit = { version = "0.3", features = ["NSWindow", "NSView", "NSEvent", "NSApplication", "NSScreen", "NSGraphicsContext", "NSRunningApplication", "NSResponder"] } diff --git a/native/macos/src/lib.rs b/native/macos/src/lib.rs index 140bfe0..7546018 100644 --- a/native/macos/src/lib.rs +++ b/native/macos/src/lib.rs @@ -635,6 +635,7 @@ pub extern "C" fn bloom_clear_background(r: f64, g: f64, b: f64, a: f64) { engine().renderer.set_clear_color(r, g, b, a); } +#[cfg(feature = "image-extras")] /// Load an HDR equirectangular environment map and upload it to the /// GPU. Subsequent frames sample it per-background-pixel via a sky /// pass, so the background matches a path-traced reference instead of @@ -1430,6 +1431,7 @@ pub extern "C" fn bloom_profiler_overlay_text() -> *const u8 { // Models // ============================================================ +#[cfg(feature = "models3d")] #[no_mangle] pub extern "C" fn bloom_load_model(path_ptr: *const u8) -> f64 { let path = str_from_header(path_ptr); @@ -1453,6 +1455,7 @@ pub extern "C" fn bloom_load_model(path_ptr: *const u8) -> f64 { } } +#[cfg(feature = "models3d")] #[no_mangle] pub extern "C" fn bloom_draw_model(handle: f64, x: f64, y: f64, z: f64, scale: f64, r: f64, g: f64, b: f64, a: f64) { let eng = engine(); @@ -1472,6 +1475,7 @@ pub extern "C" fn bloom_draw_model(handle: f64, x: f64, y: f64, z: f64, scale: f } } +#[cfg(feature = "models3d")] #[no_mangle] pub extern "C" fn bloom_draw_model_rotated( handle: f64, x: f64, y: f64, z: f64, @@ -1503,16 +1507,19 @@ pub extern "C" fn bloom_draw_model_rotated( } } +#[cfg(feature = "models3d")] #[no_mangle] pub extern "C" fn bloom_unload_model(handle: f64) { engine().models.unload_model(handle); } +#[cfg(feature = "models3d")] #[no_mangle] pub extern "C" fn bloom_gen_mesh_cube(w: f64, h: f64, d: f64) -> f64 { engine().models.gen_mesh_cube(w as f32, h as f32, d as f32) } +#[cfg(feature = "models3d")] #[no_mangle] pub extern "C" fn bloom_gen_mesh_heightmap(image_handle: f64, size_x: f64, size_y: f64, size_z: f64) -> f64 { let eng = engine(); @@ -1653,6 +1660,7 @@ pub extern "C" fn bloom_create_instance_buffer( engine().renderer.create_instance_buffer(&raw_f32, count) as f64 } +#[cfg(feature = "models3d")] /// EN-001 — submit an instanced material draw. The mesh at /// (mesh_handle, mesh_idx) is drawn `instance_count` times via a /// single `draw_indexed` with the instance buffer bound at vertex @@ -1912,6 +1920,7 @@ pub extern "C" fn bloom_clear_all_post_passes() { engine().renderer.clear_all_post_passes(); } +#[cfg(feature = "models3d")] #[no_mangle] pub extern "C" fn bloom_draw_material( material: f64, @@ -1935,6 +1944,7 @@ pub extern "C" fn bloom_draw_material( ); } +#[cfg(feature = "models3d")] #[no_mangle] pub extern "C" fn bloom_create_mesh(vertex_ptr: *const f64, vertex_count: f64, index_ptr: *const f64, index_count: f64) -> f64 { // Perry's TS `number[]` is f64-laid-out in memory; Perry passes a @@ -1959,6 +1969,7 @@ pub extern "C" fn bloom_create_mesh(vertex_ptr: *const f64, vertex_count: f64, i engine().models.create_mesh(&vertex_data, &index_data) } +#[cfg(feature = "models3d")] #[no_mangle] pub extern "C" fn bloom_load_model_animation(path_ptr: *const u8) -> f64 { let path = str_from_header(path_ptr); @@ -1968,6 +1979,7 @@ pub extern "C" fn bloom_load_model_animation(path_ptr: *const u8) -> f64 { } } +#[cfg(feature = "models3d")] #[no_mangle] pub extern "C" fn bloom_update_model_animation(handle: f64, anim_index: f64, time: f64, scale: f64, px: f64, py: f64, pz: f64, rot_y: f64) { // Take a single Y-axis angle (radians) instead of sin/cos, so the @@ -1988,6 +2000,7 @@ pub extern "C" fn bloom_update_model_animation(handle: f64, anim_index: f64, tim } } +#[cfg(feature = "models3d")] #[no_mangle] pub extern "C" fn bloom_get_model_mesh_count(handle: f64) -> f64 { match engine().models.get(handle) { @@ -1996,6 +2009,7 @@ pub extern "C" fn bloom_get_model_mesh_count(handle: f64) -> f64 { } } +#[cfg(feature = "models3d")] #[no_mangle] pub extern "C" fn bloom_get_model_material_count(handle: f64) -> f64 { match engine().models.get(handle) { @@ -2336,26 +2350,32 @@ pub extern "C" fn bloom_save_file_dialog(default_name_ptr: *const u8, title_ptr: // Model bounds accessors. Return the axis-aligned bounding box of a loaded // model in model-local coordinates. Editors use these to size gizmos, auto- // frame the camera on selection, and snap placed entities onto terrain. +#[cfg(feature = "models3d")] #[no_mangle] pub extern "C" fn bloom_get_model_bounds_min_x(model_handle: f64) -> f64 { engine().models.get_bounds(model_handle).0[0] as f64 } +#[cfg(feature = "models3d")] #[no_mangle] pub extern "C" fn bloom_get_model_bounds_min_y(model_handle: f64) -> f64 { engine().models.get_bounds(model_handle).0[1] as f64 } +#[cfg(feature = "models3d")] #[no_mangle] pub extern "C" fn bloom_get_model_bounds_min_z(model_handle: f64) -> f64 { engine().models.get_bounds(model_handle).0[2] as f64 } +#[cfg(feature = "models3d")] #[no_mangle] pub extern "C" fn bloom_get_model_bounds_max_x(model_handle: f64) -> f64 { engine().models.get_bounds(model_handle).1[0] as f64 } +#[cfg(feature = "models3d")] #[no_mangle] pub extern "C" fn bloom_get_model_bounds_max_y(model_handle: f64) -> f64 { engine().models.get_bounds(model_handle).1[1] as f64 } +#[cfg(feature = "models3d")] #[no_mangle] pub extern "C" fn bloom_get_model_bounds_max_z(model_handle: f64) -> f64 { engine().models.get_bounds(model_handle).1[2] as f64 @@ -2626,6 +2646,7 @@ pub extern "C" fn bloom_scene_set_material_water(handle: f64, wave_amp: f64, wav } // Q9: Generate a ribbon mesh along a Catmull-Rom spline. +#[cfg(feature = "models3d")] #[no_mangle] pub extern "C" fn bloom_gen_mesh_spline_ribbon(points_ptr: *const u8, point_count: f64, widths_ptr: *const u8, width_count: f64) -> f64 { let n = point_count as usize; @@ -2899,6 +2920,7 @@ pub extern "C" fn bloom_project_screen_y() -> f64 { unsafe { LAST_PROJECT.1 } } +#[cfg(feature = "models3d")] /// Attach a loaded GLTF model's mesh geometry to a scene node. /// Copies the vertex/index data from the model into the scene node. #[no_mangle] @@ -2959,6 +2981,7 @@ pub extern "C" fn bloom_stage_texture(path_ptr: *const u8) -> f64 { } } +#[cfg(feature = "models3d")] #[no_mangle] pub extern "C" fn bloom_stage_model(path_ptr: *const u8) -> f64 { let path = str_from_header(path_ptr); @@ -3005,6 +3028,7 @@ pub extern "C" fn bloom_commit_texture(staging_handle: f64) -> f64 { }) } +#[cfg(feature = "models3d")] #[no_mangle] pub extern "C" fn bloom_commit_model(staging_handle: f64) -> f64 { let staged = match bloom_shared::staging::take_model(staging_handle) { @@ -3373,3 +3397,171 @@ fn bloom_jolt_ffi_physics() -> &'static mut bloom_shared::physics_jolt::JoltPhys #[cfg(feature = "jolt")] bloom_shared::define_physics_ffi!(); + +// ─── EN-014 feature-off stubs ─────────────────────────────────────── +// The Perry TS glue for an imported engine module references every FFI +// symbol that module declares, whether or not the game calls it. When a +// feature is compiled out, keep the symbols as warn-once no-ops so the +// link succeeds and a stray call is diagnosable instead of UB. +#[cfg(not(feature = "image-extras"))] +#[no_mangle] +pub extern "C" fn bloom_set_env_clear_from_hdr(_path_ptr: *const u8) { + feature_off_warn_once("bloom_set_env_clear_from_hdr", "image-extras"); +} +#[cfg(not(feature = "models3d"))] +#[no_mangle] +pub extern "C" fn bloom_load_model(_path_ptr: *const u8) -> f64 { + feature_off_warn_once("bloom_load_model", "models3d"); + 0.0 +} +#[cfg(not(feature = "models3d"))] +#[no_mangle] +pub extern "C" fn bloom_draw_model(_handle: f64, _x: f64, _y: f64, _z: f64, _scale: f64, _r: f64, _g: f64, _b: f64, _a: f64) { + feature_off_warn_once("bloom_draw_model", "models3d"); +} +#[cfg(not(feature = "models3d"))] +#[no_mangle] +pub extern "C" fn bloom_draw_model_rotated( + handle: f64, _x: f64, _y: f64, _z: f64, + _scale: f64, _rot_y: f64, + _color_packed_argb: f64, +) { + feature_off_warn_once("bloom_draw_model_rotated", "models3d"); +} +#[cfg(not(feature = "models3d"))] +#[no_mangle] +pub extern "C" fn bloom_unload_model(_handle: f64) { + feature_off_warn_once("bloom_unload_model", "models3d"); +} +#[cfg(not(feature = "models3d"))] +#[no_mangle] +pub extern "C" fn bloom_gen_mesh_cube(_w: f64, _h: f64, _d: f64) -> f64 { + feature_off_warn_once("bloom_gen_mesh_cube", "models3d"); + 0.0 +} +#[cfg(not(feature = "models3d"))] +#[no_mangle] +pub extern "C" fn bloom_gen_mesh_heightmap(_image_handle: f64, _size_x: f64, _size_y: f64, _size_z: f64) -> f64 { + feature_off_warn_once("bloom_gen_mesh_heightmap", "models3d"); + 0.0 +} +#[cfg(not(feature = "models3d"))] +#[no_mangle] +pub extern "C" fn bloom_draw_material( + material: f64, + _mesh_handle: f64, + _mesh_idx: f64, + _x: f64, _y: f64, _z: f64, _scale: f64, + _r: f64, _g: f64, _b: f64, _a: f64, +) { + feature_off_warn_once("bloom_draw_material", "models3d"); +} +#[cfg(not(feature = "models3d"))] +#[no_mangle] +pub extern "C" fn bloom_create_mesh(_vertex_ptr: *const f64, _vertex_count: f64, _index_ptr: *const f64, _index_count: f64) -> f64 { + feature_off_warn_once("bloom_create_mesh", "models3d"); + 0.0 +} +#[cfg(not(feature = "models3d"))] +#[no_mangle] +pub extern "C" fn bloom_load_model_animation(_path_ptr: *const u8) -> f64 { + feature_off_warn_once("bloom_load_model_animation", "models3d"); + 0.0 +} +#[cfg(not(feature = "models3d"))] +#[no_mangle] +pub extern "C" fn bloom_update_model_animation(_handle: f64, _anim_index: f64, _time: f64, _scale: f64, _px: f64, _py: f64, _pz: f64, _rot_y: f64) { + feature_off_warn_once("bloom_update_model_animation", "models3d"); +} +#[cfg(not(feature = "models3d"))] +#[no_mangle] +pub extern "C" fn bloom_get_model_mesh_count(_handle: f64) -> f64 { + feature_off_warn_once("bloom_get_model_mesh_count", "models3d"); + 0.0 +} +#[cfg(not(feature = "models3d"))] +#[no_mangle] +pub extern "C" fn bloom_get_model_material_count(_handle: f64) -> f64 { + feature_off_warn_once("bloom_get_model_material_count", "models3d"); + 0.0 +} +#[cfg(not(feature = "models3d"))] +#[no_mangle] +pub extern "C" fn bloom_get_model_bounds_min_x(_model_handle: f64) -> f64 { + feature_off_warn_once("bloom_get_model_bounds_min_x", "models3d"); + 0.0 +} +#[cfg(not(feature = "models3d"))] +#[no_mangle] +pub extern "C" fn bloom_get_model_bounds_min_y(_model_handle: f64) -> f64 { + feature_off_warn_once("bloom_get_model_bounds_min_y", "models3d"); + 0.0 +} +#[cfg(not(feature = "models3d"))] +#[no_mangle] +pub extern "C" fn bloom_get_model_bounds_min_z(_model_handle: f64) -> f64 { + feature_off_warn_once("bloom_get_model_bounds_min_z", "models3d"); + 0.0 +} +#[cfg(not(feature = "models3d"))] +#[no_mangle] +pub extern "C" fn bloom_get_model_bounds_max_x(_model_handle: f64) -> f64 { + feature_off_warn_once("bloom_get_model_bounds_max_x", "models3d"); + 0.0 +} +#[cfg(not(feature = "models3d"))] +#[no_mangle] +pub extern "C" fn bloom_get_model_bounds_max_y(_model_handle: f64) -> f64 { + feature_off_warn_once("bloom_get_model_bounds_max_y", "models3d"); + 0.0 +} +#[cfg(not(feature = "models3d"))] +#[no_mangle] +pub extern "C" fn bloom_get_model_bounds_max_z(_model_handle: f64) -> f64 { + feature_off_warn_once("bloom_get_model_bounds_max_z", "models3d"); + 0.0 +} +#[cfg(not(feature = "models3d"))] +#[no_mangle] +pub extern "C" fn bloom_gen_mesh_spline_ribbon(_points_ptr: *const u8, _point_count: f64, _widths_ptr: *const u8, _width_count: f64) -> f64 { + feature_off_warn_once("bloom_gen_mesh_spline_ribbon", "models3d"); + 0.0 +} +#[cfg(not(feature = "models3d"))] +#[no_mangle] +pub extern "C" fn bloom_scene_attach_model(_node_handle: f64, _model_handle: f64, _mesh_index: f64) { + feature_off_warn_once("bloom_scene_attach_model", "models3d"); +} +#[cfg(not(feature = "models3d"))] +#[no_mangle] +pub extern "C" fn bloom_stage_model(_path_ptr: *const u8) -> f64 { + feature_off_warn_once("bloom_stage_model", "models3d"); + 0.0 +} +#[cfg(not(feature = "models3d"))] +#[no_mangle] +pub extern "C" fn bloom_commit_model(_staging_handle: f64) -> f64 { + feature_off_warn_once("bloom_commit_model", "models3d"); + 0.0 +} + +#[cfg(any(not(feature = "models3d"), not(feature = "image-extras")))] +fn feature_off_warn_once(name: &str, feature: &str) { + use std::sync::Mutex; + static WARNED: Mutex> = Mutex::new(Vec::new()); + let mut warned = WARNED.lock().unwrap(); + if !warned.iter().any(|n| *n == name) { + // names come from string literals above — leak-free 'static via Box::leak is + // overkill; just store a leaked copy once per distinct symbol. + warned.push(Box::leak(name.to_string().into_boxed_str())); + eprintln!("bloom: {name}() ignored — engine built without the `{feature}` feature"); + } +} +#[cfg(not(feature = "models3d"))] +#[no_mangle] +pub extern "C" fn bloom_submit_material_draw_instanced( + _material: f64, _mesh_handle: f64, _mesh_idx: f64, + _instance_buffer: f64, _instance_count: f64, +) { + feature_off_warn_once("bloom_submit_material_draw_instanced", "models3d"); +} diff --git a/native/shared/Cargo.toml b/native/shared/Cargo.toml index 9f2b1ac..d016b4d 100644 --- a/native/shared/Cargo.toml +++ b/native/shared/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [features] -default = ["mp3", "jolt", "hot-reload"] +default = ["mp3", "jolt", "hot-reload", "models3d", "image-extras"] mp3 = ["dep:minimp3"] jolt = [] # EN-008 — gate the `notify` filesystem watcher behind a compile-time @@ -13,6 +13,15 @@ jolt = [] # the dep tree entirely. hot-reload = ["dep:notify"] web = ["dep:web-time"] +# EN-014 — 3D model loading (glTF + DDS) and the ModelManager. Pure-2D +# games disable this via Perry's `[native-library.""]` feature +# forwarding to drop gltf/gltf_json/image_dds from the binary entirely. +# Default-on so existing 3D games are unaffected. +models3d = ["dep:gltf", "dep:image_dds"] +# EN-014 — image codecs beyond PNG. `image::load_from_memory` is +# format-agnostic, so disabling this only changes which file formats +# decode at runtime; PNG always works. +image-extras = ["image/jpeg", "image/bmp", "image/tga", "image/hdr"] [build-dependencies] cmake = "0.1" @@ -24,9 +33,9 @@ fontdue = "0.9" libc = "0.2" bytemuck = { version = "1", features = ["derive"] } half = "2" -image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tga", "hdr"] } -image_dds = { version = "0.7", default-features = false, features = ["ddsfile", "image"] } -gltf = { version = "1", features = ["KHR_materials_pbrSpecularGlossiness", "KHR_materials_transmission"] } +image = { version = "0.25", default-features = false, features = ["png"] } +image_dds = { version = "0.7", default-features = false, features = ["ddsfile", "image"], optional = true } +gltf = { version = "1", features = ["KHR_materials_pbrSpecularGlossiness", "KHR_materials_transmission"], optional = true } lewton = "0.10" minimp3 = { version = "0.5", optional = true } earcutr = "0.4" diff --git a/native/shared/src/engine.rs b/native/shared/src/engine.rs index 1f2dd69..9bf8921 100644 --- a/native/shared/src/engine.rs +++ b/native/shared/src/engine.rs @@ -3,6 +3,7 @@ use crate::input::InputState; use crate::renderer::Renderer; use crate::text_renderer::TextRenderer; use crate::textures::TextureManager; +#[cfg(feature = "models3d")] use crate::models::ModelManager; use crate::scene::SceneGraph; use crate::frame_callbacks::FrameCallbackSystem; @@ -23,6 +24,7 @@ pub struct EngineState { pub input: InputState, pub audio: AudioMixer, pub textures: TextureManager, + #[cfg(feature = "models3d")] pub models: ModelManager, pub scene: SceneGraph, pub frame_callbacks: FrameCallbackSystem, @@ -69,6 +71,7 @@ impl EngineState { input: InputState::new(), audio: AudioMixer::new(), textures: TextureManager::new(), + #[cfg(feature = "models3d")] models: ModelManager::new(), scene, frame_callbacks: FrameCallbackSystem::new(), diff --git a/native/shared/src/lib.rs b/native/shared/src/lib.rs index 940c8c8..92f7125 100644 --- a/native/shared/src/lib.rs +++ b/native/shared/src/lib.rs @@ -10,6 +10,7 @@ pub mod renderer; pub mod text_renderer; pub mod audio; pub mod textures; +#[cfg(feature = "models3d")] pub mod models; pub mod scene; pub mod frame_callbacks; @@ -38,6 +39,7 @@ pub use audio::{AudioMixer, SoundData, parse_wav, parse_ogg}; #[cfg(feature = "mp3")] pub use audio::parse_mp3; pub use textures::TextureManager; +#[cfg(feature = "models3d")] pub use models::ModelManager; pub use scene::SceneGraph; pub use frame_callbacks::FrameCallbackSystem; diff --git a/native/shared/src/renderer/formats.rs b/native/shared/src/renderer/formats.rs index 3c3e61a..5c0cc8d 100644 --- a/native/shared/src/renderer/formats.rs +++ b/native/shared/src/renderer/formats.rs @@ -430,9 +430,15 @@ pub(super) fn create_mesh_sdf_texture( sample_count: 1, dimension: wgpu::TextureDimension::D3, format: wgpu::TextureFormat::R32Float, + // COPY_DST is needed by ticket 022's disk cache so a cached + // SDF can be uploaded via queue.write_texture instead of + // re-baked. COPY_SRC is needed by the same ticket's readback + // path on cache miss. Both have zero runtime cost when the + // cache isn't used (just usage-flag bits at allocation). usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING - | wgpu::TextureUsages::COPY_SRC, + | wgpu::TextureUsages::COPY_SRC + | wgpu::TextureUsages::COPY_DST, view_formats: &[], }); let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); diff --git a/native/shared/src/renderer/mod.rs b/native/shared/src/renderer/mod.rs index d0ae9de..dae168b 100644 --- a/native/shared/src/renderer/mod.rs +++ b/native/shared/src/renderer/mod.rs @@ -12286,6 +12286,7 @@ impl Renderer { /// Returns true if the model was cached successfully (static model). /// Returns false if the model is skinned (uncacheable). + #[cfg(feature = "models3d")] pub fn cache_model_if_static(&mut self, handle_bits: u64, meshes: &[crate::models::MeshData]) -> bool { if let Some(entry) = self.model_gpu_cache.get(&handle_bits) { return entry.is_some(); diff --git a/native/shared/src/staging.rs b/native/shared/src/staging.rs index b14d35e..a10f7e1 100644 --- a/native/shared/src/staging.rs +++ b/native/shared/src/staging.rs @@ -1,4 +1,5 @@ use std::sync::{Mutex, OnceLock}; +#[cfg(feature = "models3d")] use crate::models::ModelData; use crate::audio::SoundData; @@ -8,6 +9,7 @@ pub struct StagedTexture { pub height: u32, } +#[cfg(feature = "models3d")] pub struct StagedModel { pub model: ModelData, pub textures: Vec, @@ -21,6 +23,7 @@ fn texture_store() -> &'static Mutex>> { INSTANCE.get_or_init(|| Mutex::new(Vec::new())) } +#[cfg(feature = "models3d")] fn model_store() -> &'static Mutex>> { static INSTANCE: OnceLock>>> = OnceLock::new(); INSTANCE.get_or_init(|| Mutex::new(Vec::new())) @@ -73,10 +76,12 @@ pub fn take_texture(handle: f64) -> Option { take_from(texture_store(), handle) } +#[cfg(feature = "models3d")] pub fn stage_model(model: StagedModel) -> f64 { stage_into(model_store(), model) } +#[cfg(feature = "models3d")] pub fn take_model(handle: f64) -> Option { take_from(model_store(), handle) } From 824e2c46191008ee17f8305e7a0e509c1876bda2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 12 Jun 2026 09:58:30 +0200 Subject: [PATCH 2/3] fix(web): forward models3d + image-extras features to bloom-shared The wasm build compiles bloom-shared with --no-default-features, so the new EN-014 gates removed EngineState.models out from under bloom-web's unconditional model FFI (24 build errors in CI). Web games do use 3D models, so the web crate now declares the same default-on feature set as native/macos; pure-2D web games opt out via Perry feature forwarding once the web FFI surface is gated (tracked for the FFI consolidation work). --- native/web/Cargo.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/native/web/Cargo.toml b/native/web/Cargo.toml index 95dfa55..7fa2b16 100644 --- a/native/web/Cargo.toml +++ b/native/web/Cargo.toml @@ -7,8 +7,13 @@ edition = "2021" crate-type = ["cdylib"] [features] -default = ["jolt"] +# Mirror native/macos: everything defaults ON so existing web games are +# unaffected; pure-2D games opt out via Perry's per-package feature +# forwarding (see native/shared/Cargo.toml for the EN-014 rationale). +default = ["jolt", "models3d", "image-extras"] jolt = ["bloom-shared/jolt"] +models3d = ["bloom-shared/models3d"] +image-extras = ["bloom-shared/image-extras"] [dependencies] bloom-shared = { path = "../shared", default-features = false, features = ["web"] } From a50572c85b5e7482ea6f1de9c8d418dabadd2e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 12 Jun 2026 10:02:45 +0200 Subject: [PATCH 3/3] fix: forward models3d + image-extras from all platform crates Same break class as the web fix one commit back: every platform crate compiles bloom-shared with default-features = false, so the EN-014 gates removed EngineState.models from all of them while their model FFI stayed unconditional (build-linux CI failure; windows/android/ ios/tvos would fail identically, they're just not in CI). Mirror the macos feature block everywhere. --- native/android/Cargo.toml | 6 +++++- native/ios/Cargo.toml | 6 +++++- native/linux/Cargo.toml | 6 +++++- native/tvos/Cargo.toml | 6 +++++- native/windows/Cargo.toml | 6 +++++- 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/native/android/Cargo.toml b/native/android/Cargo.toml index c2bdb5d..3283597 100644 --- a/native/android/Cargo.toml +++ b/native/android/Cargo.toml @@ -8,8 +8,12 @@ name = "bloom_android" crate-type = ["staticlib", "cdylib"] [features] -default = ["jolt"] +default = ["jolt", "models3d", "image-extras"] jolt = ["bloom-shared/jolt"] +# EN-014 — see native/shared/Cargo.toml. Default-on so existing games are +# unaffected; pure-2D games opt out via Perry feature forwarding. +models3d = ["bloom-shared/models3d"] +image-extras = ["bloom-shared/image-extras"] [dependencies] bloom-shared = { path = "../shared", default-features = false, features = ["mp3"] } diff --git a/native/ios/Cargo.toml b/native/ios/Cargo.toml index 77a51a9..0463ab1 100644 --- a/native/ios/Cargo.toml +++ b/native/ios/Cargo.toml @@ -8,8 +8,12 @@ name = "bloom_ios" crate-type = ["staticlib"] [features] -default = ["jolt"] +default = ["jolt", "models3d", "image-extras"] jolt = ["bloom-shared/jolt"] +# EN-014 — see native/shared/Cargo.toml. Default-on so existing games are +# unaffected; pure-2D games opt out via Perry feature forwarding. +models3d = ["bloom-shared/models3d"] +image-extras = ["bloom-shared/image-extras"] # Match perry-runtime's panic strategy so the final perry-driven link # doesn't see two copies of rust_eh_personality (and friends) from two diff --git a/native/linux/Cargo.toml b/native/linux/Cargo.toml index 779ad99..473194d 100644 --- a/native/linux/Cargo.toml +++ b/native/linux/Cargo.toml @@ -8,8 +8,12 @@ name = "bloom_linux" crate-type = ["staticlib"] [features] -default = ["jolt"] +default = ["jolt", "models3d", "image-extras"] jolt = ["bloom-shared/jolt"] +# EN-014 — see native/shared/Cargo.toml. Default-on so existing games are +# unaffected; pure-2D games opt out via Perry feature forwarding. +models3d = ["bloom-shared/models3d"] +image-extras = ["bloom-shared/image-extras"] [dependencies] bloom-shared = { path = "../shared", default-features = false, features = ["mp3"] } diff --git a/native/tvos/Cargo.toml b/native/tvos/Cargo.toml index fd6f496..9b3d5c2 100644 --- a/native/tvos/Cargo.toml +++ b/native/tvos/Cargo.toml @@ -8,8 +8,12 @@ name = "bloom_tvos" crate-type = ["staticlib"] [features] -default = ["jolt"] +default = ["jolt", "models3d", "image-extras"] jolt = ["bloom-shared/jolt"] +# EN-014 — see native/shared/Cargo.toml. Default-on so existing games are +# unaffected; pure-2D games opt out via Perry feature forwarding. +models3d = ["bloom-shared/models3d"] +image-extras = ["bloom-shared/image-extras"] # Match perry-runtime's panic strategy so the final perry-driven link # doesn't see two copies of rust_eh_personality (and friends) from two diff --git a/native/windows/Cargo.toml b/native/windows/Cargo.toml index 580362e..4599fad 100644 --- a/native/windows/Cargo.toml +++ b/native/windows/Cargo.toml @@ -16,8 +16,12 @@ crate-type = ["staticlib"] # Games that need bloom/physics on Windows must opt in explicitly until the # perry side gains a way to thread `cargo:rustc-link-search` through to the # final link line. -default = [] +default = ["models3d", "image-extras"] jolt = ["bloom-shared/jolt"] +# EN-014 — see native/shared/Cargo.toml. Default-on so existing games are +# unaffected; pure-2D games opt out via Perry feature forwarding. +models3d = ["bloom-shared/models3d"] +image-extras = ["bloom-shared/image-extras"] [dependencies] bloom-shared = { path = "../shared", default-features = false, features = ["mp3"] }