diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e69e0d5..28fcab9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -71,6 +71,23 @@ jobs: working-directory: native/shared run: cargo test --release + # --------------------------------------------------------------------------- + # FFI parity: every function in package.json's perry.nativeLibrary manifest + # must be exported with matching arity by every platform crate (hand-written + # or via define_core_ffi!/define_physics_ffi!). This is the check whose + # absence let Android ship 60 functions behind (#59) and Windows ship the + # whole scene-graph surface as silent no-op stubs. Cheap (pure text parse), + # runs everywhere, gates merges. + # --------------------------------------------------------------------------- + ffi-parity: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: validate-ffi + run: node tools/validate-ffi.js + - name: file line limit (2000, ratcheting baseline) + run: node tools/check-file-lines.js + # --------------------------------------------------------------------------- # Lint: rustfmt + clippy. Advisory while the codebase is still in flux. # Flip `continue-on-error: false` once the lint baseline is clean. diff --git a/.gitignore b/.gitignore index 2aacb18..ff9505c 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,6 @@ tools/unreal_reference/ tools/dump_dds/target/ tools/dump_dds/Cargo.lock examples/intel-sponza/assets/outdoor.hdr + +# Golden-test failure artifacts (written next to the golden on mismatch) +*.actual.png diff --git a/docs/migration-0.5.md b/docs/migration-0.5.md new file mode 100644 index 0000000..b26e1b5 --- /dev/null +++ b/docs/migration-0.5.md @@ -0,0 +1,66 @@ +# Migrating to Bloom 0.5 + +0.5 makes the API consistent in three places where conventions silently +diverged. Each change is breaking on purpose — the old inconsistencies +caused invisible bugs (colors that rendered white, rotations that were +60× too fast). All engine examples are already migrated and serve as +references. + +## Surface colors are 0–255 everywhere + +`setSceneNodeColor`, `setOutlineColor`, and the color part of +`setSceneNodeWaterMaterial` previously took 0–1 floats — the only places +in the API that did. They now take 0–255 like every `draw*` call and the +`Colors` presets. + +```ts +// before // after +setSceneNodeColor(node, 0.75, 0.75, 0.7); setSceneNodeColor(node, 191, 191, 179); +setSceneNodeColor(node, c.r/255, c.g/255, …) setSceneNodeColor(node, c.r, c.g, c.b, c.a); // Colors presets now just work +``` + +Symptom of unmigrated code: scene nodes render almost black (values +divided twice). + +**Unchanged:** light colors (`addDirectionalLight`, `addPointLight`) +stay 0–1 floats with a separate intensity — that's the radiometric +convention (Unity and Unreal do the same), and light color × intensity +can meaningfully exceed 1.0. The `*.world.json` format also keeps 0–1 +tints (serialized data is versioned separately); the loader converts. + +## Angles are degrees everywhere + +`drawModelRotated`'s `rotY` was radians; `Camera2D.rotation` was degrees. +Everything user-facing is now degrees (the raylib convention). + +```ts +// before // after +drawModelRotated(m, p, 1.0, Math.PI / 2, t); drawModelRotated(m, p, 1.0, 90, t); +``` + +Symptom of unmigrated code: models spin ~57× faster than intended. + +**Unchanged:** physics angular velocity stays radians/sec (SI, matches +Jolt), and quaternions are quaternions. + +## `Texture.handle` (was `Texture.id`) + +`Texture` was the only resource type whose handle field was named `id`; +`Sound`, `Music`, `Font`, and `Model` all use `handle`. + +```ts +// before // after +myAtlas.id myAtlas.handle +``` + +## Also in 0.5 (non-breaking) + +- `physics.step(world, dt)` is now fixed-timestep with an accumulator + and returns the interpolation alpha; `physics.stepVariable` is the + old exact-dt behavior. See docs/physics.md "Stepping". +- Stale handles (use-after-free/destroy) now fail lookups instead of + aliasing whatever object reused the slot. +- `*Raw` function variants are documented `@internal` — they exist only + as a compiler workaround and will be removed. +- Coordinate system is now documented at the top of the physics and + scene modules: right-handed, Y-up, meters, SI units. diff --git a/docs/physics.md b/docs/physics.md index 54a1b6e..aa7fc3c 100644 --- a/docs/physics.md +++ b/docs/physics.md @@ -107,12 +107,50 @@ const ball = physics.createBody(world, ballShape, { // 4. Call optimizeBroadphase once after initial body setup. physics.optimizeBroadphase(world); -// 5. In your game loop: -physics.step(world, 1 / 60); +// 5. In your game loop — pass the real frame delta; step() runs the +// simulation at a fixed rate internally (see "Stepping" below): +physics.step(world, deltaTime); const pos = physics.getBodyPosition(ball); // ... read positions and render sprites / meshes at those transforms ``` +## Stepping + +`physics.step(world, deltaTime)` is **fixed-timestep**: it accumulates the +wall-clock delta and advances the solver in whole steps of 1/60 s +(configurable via `setFixedTimestep(world, hz, maxSteps?)`). Variable-size +solver steps feed frame hitches straight into the constraint solver — +tunneling and joint explosions on any slow frame — so the accumulator is +the default, with two protections baked in: + +- A single frame's contribution is clamped to 0.25 s (debugger pauses and + OS hitches produce one slowed-down frame, not minutes of catch-up). +- At most `maxSteps` (default 4) fixed steps run per frame; surplus + backlog is dropped. The simulation slows down instead of spiraling. + +`step` returns the **interpolation alpha**: how far the carried remainder +sits between the last two physics states. Two ways to use it: + +```typescript +// Easiest: let the engine blend body transforms for rendering. +physics.setInterpolation(world, true); +runGame((dt) => { + physics.step(world, dt); + const p = physics.getBodyPosition(ball); // already smoothed +}); + +// Manual: interpolate game-side state with the same alpha. +const alpha = physics.step(world, dt); // or physics.getStepAlpha(world) +``` + +With interpolation on, `getBodyPosition`/`getBodyRotation` return the +blended state; physics queries (raycasts, overlaps, contacts) always see +the raw simulation state. + +Exact-dt stepping is still available as `physics.stepVariable(world, dt)` +for code that drives its own accumulator — it trades stability for +control. + ## Character controllers (Tier 2) For player movement, use `CharacterVirtual` — Jolt's kinematic controller with diff --git a/examples/pbr-spheres/main.ts b/examples/pbr-spheres/main.ts index ecd8355..ce6eb4b 100644 --- a/examples/pbr-spheres/main.ts +++ b/examples/pbr-spheres/main.ts @@ -195,7 +195,7 @@ for (let row = 0; row < GRID_N; row = row + 1) { const node = createSceneNode(); attachModelToNode(node, sphereHandle, 0); - setSceneNodeColor(node, BASE_R, BASE_G, BASE_B); + setSceneNodeColor(node, BASE_R * 255, BASE_G * 255, BASE_B * 255); setSceneNodePbr(node, roughness, metallic); setSceneNodeCastShadow(node, false); setSceneNodeReceiveShadow(node, false); diff --git a/examples/renderer-test/main.ts b/examples/renderer-test/main.ts index 911f0a3..43b9519 100644 --- a/examples/renderer-test/main.ts +++ b/examples/renderer-test/main.ts @@ -231,7 +231,7 @@ function placeSphere( roughness: number, metalness: number, ): number { const node = placeNode(sphereHandle, 0, px, py, pz, scale, scale, scale); - setSceneNodeColor(node, cr, cg, cb); + setSceneNodeColor(node, cr * 255, cg * 255, cb * 255); setSceneNodePbr(node, roughness, metalness); return node; } @@ -243,7 +243,7 @@ function placeCube( roughness: number, metalness: number, ): number { const node = placeNode(cubeHandle, 0, px, py, pz, sx, sy, sz); - setSceneNodeColor(node, cr, cg, cb); + setSceneNodeColor(node, cr * 255, cg * 255, cb * 255); setSceneNodePbr(node, roughness, metalness); // Thin horizontal slabs (floors) should receive but not cast // shadows — otherwise they fill the shadow map with their own @@ -374,7 +374,7 @@ function setupWater(): void { updateSceneNodeGeometry(waterNode, wv, wi); const wm = mat4Translate(mat4Identity(), { x: 0, y: 0.2, z: cz }); setSceneNodeTransform(waterNode, wm); - setSceneNodeWaterMaterial(waterNode, 0.15, 1.5, 0.1, 0.3, 0.5, 0.6); + setSceneNodeWaterMaterial(waterNode, 0.15, 1.5, 26, 77, 128, 153); setSceneNodeReceiveShadow(waterNode, true); // Rocks / objects sticking out of water @@ -457,7 +457,7 @@ function setupThinGeometry(): void { const x = cx - 5.5 + i * 1.0; const node = createSceneNode(); attachModelToNode(node, cubeHandle, 0); - setSceneNodeColor(node, 0.6, 0.6, 0.62); + setSceneNodeColor(node, 153, 153, 158); setSceneNodePbr(node, 0.3, 1.0); setSceneNodeCastShadow(node, true); setSceneNodeReceiveShadow(node, true); diff --git a/examples/scene-graph/interactive.ts b/examples/scene-graph/interactive.ts index 937eeb2..02cfeaa 100644 --- a/examples/scene-graph/interactive.ts +++ b/examples/scene-graph/interactive.ts @@ -52,7 +52,7 @@ let selectedWallId: string | null = null; const floorHandle = createSceneNode(); const floorPolygon = [-10, -10, 10, -10, 10, 10, -10, 10]; extrudePolygon(floorHandle, floorPolygon, 0.02); -setSceneNodeColor(floorHandle, 0.85, 0.85, 0.82, 1.0); +setSceneNodeColor(floorHandle, 217, 217, 209, 255); setSceneNodePbr(floorHandle, 0.7, 0.0); // Handle → wall ID lookup (for picking) @@ -112,9 +112,9 @@ function wallSystem(dt: number): void { // Color based on selection if (wall.id === selectedWallId) { - setSceneNodeColor(wall.handle, 0.3, 0.6, 1.0, 1.0); + setSceneNodeColor(wall.handle, 77, 153, 255, 255); } else { - setSceneNodeColor(wall.handle, 0.95, 0.95, 0.92, 1.0); + setSceneNodeColor(wall.handle, 242, 242, 235, 255); } setSceneNodePbr(wall.handle, 0.8, 0.0); diff --git a/examples/scene-graph/main.ts b/examples/scene-graph/main.ts index ce05d0c..2b6f2f4 100644 --- a/examples/scene-graph/main.ts +++ b/examples/scene-graph/main.ts @@ -149,10 +149,10 @@ const floorIdx: number[] = [0, 1, 2, 0, 2, 3]; updateSceneNodeGeometry(floor, floorVerts, floorIdx); // Set materials -setSceneNodeColor(wall1, 0.95, 0.95, 0.92, 1.0); -setSceneNodeColor(wall2, 0.92, 0.92, 0.88, 1.0); -setSceneNodeColor(wall3, 0.90, 0.90, 0.86, 1.0); -setSceneNodeColor(floor, 0.7, 0.7, 0.65, 1.0); +setSceneNodeColor(wall1, 242, 242, 235, 255); +setSceneNodeColor(wall2, 235, 235, 224, 255); +setSceneNodeColor(wall3, 230, 230, 219, 255); +setSceneNodeColor(floor, 179, 179, 166, 255); // Set PBR properties setSceneNodePbr(wall1, 0.8, 0.0); diff --git a/examples/scene-graph/room.ts b/examples/scene-graph/room.ts index 6abba02..e91811b 100644 --- a/examples/scene-graph/room.ts +++ b/examples/scene-graph/room.ts @@ -151,7 +151,7 @@ function slabSystem(dt: number): void { extrudePolygon(handle, flat, slab.elevation); // Gray floor material - setSceneNodeColor(handle, 0.75, 0.75, 0.70, 1.0); + setSceneNodeColor(handle, 191, 191, 179, 255); setSceneNodePbr(handle, 0.6, 0.0); clearDirty(id); @@ -203,7 +203,7 @@ function wallSystem(dt: number): void { } // White wall material - setSceneNodeColor(handle, 0.95, 0.95, 0.92, 1.0); + setSceneNodeColor(handle, 242, 242, 235, 255); setSceneNodePbr(handle, 0.8, 0.0); clearDirty(id); diff --git a/examples/scene-graph/shadows.ts b/examples/scene-graph/shadows.ts index 3ffd678..16c3026 100644 --- a/examples/scene-graph/shadows.ts +++ b/examples/scene-graph/shadows.ts @@ -51,7 +51,7 @@ setDirectionalLight(0.5, 1.0, 0.3, 255, 240, 220, 0.7); const floor = createSceneNode(); const floorPoly = [-5, -5, 5, -5, 5, 5, -5, 5]; extrudePolygon(floor, floorPoly, 0.05); -setSceneNodeColor(floor, 0.8, 0.78, 0.72, 1.0); +setSceneNodeColor(floor, 204, 199, 184, 255); setSceneNodePbr(floor, 0.7, 0.0); // Walls @@ -69,7 +69,7 @@ function makeWall(sx: number, sz: number, ex: number, ez: number): void { sx - nx, sz - nz, ]; extrudePolygon(node, poly, 3.0); - setSceneNodeColor(node, 0.95, 0.93, 0.88, 1.0); + setSceneNodeColor(node, 242, 237, 224, 255); setSceneNodePbr(node, 0.85, 0.0); } @@ -88,7 +88,7 @@ function makeBox(cx: number, cy: number, cz: number, w: number, h: number, d: nu // Offset Y via transform const t = mat4Translate(mat4Identity(), 0, cy, 0); setSceneNodeTransform(node, t); - setSceneNodeColor(node, r, g, b, 1.0); + setSceneNodeColor(node, r * 255, g * 255, b * 255, 255); setSceneNodePbr(node, 0.6, 0.0); } diff --git a/native/android/src/lib.rs b/native/android/src/lib.rs index 540f5e2..48f3524 100644 --- a/native/android/src/lib.rs +++ b/native/android/src/lib.rs @@ -14,6 +14,16 @@ static mut ASSET_BASE_PATH: Option = None; fn engine() -> &'static mut EngineState { unsafe { ENGINE.get_mut().expect("Engine not initialized") } } +/// Asset-path hook for define_core_ffi! — routes through this platform's +/// resolve_path (relative paths don't resolve from the app working dir here). +fn bloom_resolve_asset_path(path: &str) -> std::borrow::Cow<'_, str> { + std::borrow::Cow::Owned(resolve_path(path)) +} + +// The full shared (non-physics) FFI surface. See bloom_shared::ffi_core +// docs for the contract; tools/validate-ffi.js checks parity in CI. +bloom_shared::define_core_ffi!(); + /// Resolve relative asset paths to the app's base asset directory. /// On Android, relative paths like "assets/models/tree.glb" won't resolve @@ -176,6 +186,11 @@ pub extern "C" fn bloom_init_window(width: f64, height: f64, title_ptr: *const u if supported.contains(wgpu::Features::TIMESTAMP_QUERY) { required_features |= wgpu::Features::TIMESTAMP_QUERY; } + // Cooked BC7 textures (bloom-cook) upload compressed when the + // adapter has BC support; without it they CPU-decode at load. + if supported.contains(wgpu::Features::TEXTURE_COMPRESSION_BC) { + required_features |= wgpu::Features::TEXTURE_COMPRESSION_BC; + } if !force_sw_gi && supported.contains(rt_mask) { required_features |= rt_mask; } @@ -327,146 +342,14 @@ pub extern "C" fn bloom_end_drawing() { // Audio (Oboe / AAudio) // ============================================================ -#[no_mangle] -pub extern "C" fn bloom_clear_background(r: f64, g: f64, b: f64, a: f64) { - engine().renderer.set_clear_color(r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_set_target_fps(fps: f64) { engine().target_fps = fps; } - -#[no_mangle] -pub extern "C" fn bloom_set_direct_2d_mode(on: f64) { engine().direct_2d_mode = on > 0.5; } - -#[no_mangle] -pub extern "C" fn bloom_get_delta_time() -> f64 { engine().delta_time } - -#[no_mangle] -pub extern "C" fn bloom_get_fps() -> f64 { engine().get_fps() } - -#[no_mangle] -pub extern "C" fn bloom_get_screen_width() -> f64 { engine().screen_width() } - -#[no_mangle] -pub extern "C" fn bloom_get_screen_height() -> f64 { engine().screen_height() } - -#[no_mangle] -pub extern "C" fn bloom_is_key_pressed(key: f64) -> f64 { - if engine().input.is_key_pressed(key as usize) { 1.0 } else { 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_is_key_down(key: f64) -> f64 { - if engine().input.is_key_down(key as usize) { 1.0 } else { 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_is_key_released(key: f64) -> f64 { - if engine().input.is_key_released(key as usize) { 1.0 } else { 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_get_mouse_x() -> f64 { engine().input.mouse_x } - -#[no_mangle] -pub extern "C" fn bloom_get_mouse_y() -> f64 { engine().input.mouse_y } - -#[no_mangle] -pub extern "C" fn bloom_is_mouse_button_pressed(btn: f64) -> f64 { - if engine().input.is_mouse_button_pressed(btn as usize) { 1.0 } else { 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_is_mouse_button_down(btn: f64) -> f64 { - if engine().input.is_mouse_button_down(btn as usize) { 1.0 } else { 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_is_mouse_button_released(btn: f64) -> f64 { - if engine().input.is_mouse_button_released(btn as usize) { 1.0 } else { 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_draw_line(x1: f64, y1: f64, x2: f64, y2: f64, thickness: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_line(x1, y1, x2, y2, thickness, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_rect(x: f64, y: f64, w: f64, h: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_rect(x, y, w, h, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_rect_lines(x: f64, y: f64, w: f64, h: f64, thickness: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_rect_lines(x, y, w, h, thickness, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_circle(cx: f64, cy: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_circle(cx, cy, radius, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_circle_lines(cx: f64, cy: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_circle_lines(cx, cy, radius, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_triangle(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_triangle(x1, y1, x2, y2, x3, y3, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_poly(cx: f64, cy: f64, sides: f64, radius: f64, rotation: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_poly(cx, cy, sides, radius, rotation, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_text(text_ptr: *const u8, x: f64, y: f64, size: f64, r: f64, g: f64, b: f64, a: f64) { - let text = str_from_header(text_ptr); - let eng = engine(); - let mut text_renderer = std::mem::replace(&mut eng.text, bloom_shared::text_renderer::TextRenderer::empty()); - text_renderer.draw_text(&mut eng.renderer, text, x, y, size as u32, r, g, b, a); - eng.text = text_renderer; -} - -#[no_mangle] -pub extern "C" fn bloom_measure_text(text_ptr: *const u8, size: f64) -> f64 { - let text = str_from_header(text_ptr); - engine().text.measure_text(text, size as u32) -} - -#[no_mangle] -pub extern "C" fn bloom_load_font(path_ptr: *const u8, size: f64) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(resolve_path(path)) { Ok(data) => engine().text.load_font(&data) as f64, Err(_) => 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_unload_font(font_handle: f64) { - engine().text.unload_font(font_handle as usize); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_text_ex(font_handle: f64, text_ptr: *const u8, x: f64, y: f64, size: f64, spacing: f64, r: f64, g: f64, b: f64, a: f64) { - let text = str_from_header(text_ptr); - let eng = engine(); - let mut text_renderer = std::mem::replace(&mut eng.text, bloom_shared::text_renderer::TextRenderer::empty()); - text_renderer.draw_text_ex(&mut eng.renderer, font_handle as usize, text, x, y, size as u32, spacing as f32, r, g, b, a); - eng.text = text_renderer; -} - -#[no_mangle] -pub extern "C" fn bloom_measure_text_ex(font_handle: f64, text_ptr: *const u8, size: f64, spacing: f64) -> f64 { - let text = str_from_header(text_ptr); - engine().text.measure_text_ex(font_handle as usize, text, size as u32, spacing as f32) -} - #[no_mangle] pub extern "C" fn bloom_init_audio() { AUDIO_RUNNING.store(true, Ordering::SeqCst); - std::thread::spawn(|| { - android_audio_thread(); + // Move the render half into the audio thread; the engine keeps only + // the command-producing control half. + let renderer = engine().audio.take_renderer(); + std::thread::spawn(move || { + android_audio_thread(renderer); }); } @@ -476,11 +359,16 @@ pub extern "C" fn bloom_close_audio() { std::thread::sleep(std::time::Duration::from_millis(50)); } -fn android_audio_thread() { +fn android_audio_thread(renderer: Option) { // Use oboe (AAudio/OpenSL ES wrapper) for audio output use oboe::*; - struct BloomAudioCallback; + // The render half is owned by the oboe callback — AAudio invokes + // on_audio_ready from its own realtime thread, and the callback never + // touches shared engine state (see audio/mod.rs contract). + struct BloomAudioCallback { + renderer: Option, + } impl AudioOutputCallback for BloomAudioCallback { type FrameType = (f32, Stereo); @@ -491,8 +379,8 @@ fn android_audio_thread() { let ptr = frames.as_mut_ptr() as *mut f32; let interleaved = unsafe { std::slice::from_raw_parts_mut(ptr, len) }; for s in interleaved.iter_mut() { *s = 0.0; } - unsafe { - ENGINE.get_mut().map(|eng| { eng.audio.mix_output(interleaved); }); + if let Some(r) = self.renderer.as_mut() { + r.mix(interleaved); } if AUDIO_RUNNING.load(Ordering::SeqCst) { DataCallbackResult::Continue @@ -508,7 +396,7 @@ fn android_audio_thread() { .set_format::() .set_channel_count::() .set_sample_rate(44100) - .set_callback(BloomAudioCallback) + .set_callback(BloomAudioCallback { renderer }) .open_stream(); match stream { @@ -524,952 +412,122 @@ fn android_audio_thread() { } } -#[no_mangle] -pub extern "C" fn bloom_load_sound(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(resolve_path(path)) { - Ok(data) => { - if let Some(s) = parse_wav(&data) { engine().audio.load_sound(s) } - else if let Some(s) = parse_ogg(&data) { engine().audio.load_sound(s) } - else if let Some(s) = parse_mp3(&data) { engine().audio.load_sound(s) } - else { 0.0 } - } - Err(_) => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_play_sound(handle: f64) { engine().audio.play_sound(handle); } -#[no_mangle] -pub extern "C" fn bloom_stop_sound(handle: f64) { engine().audio.stop_sound(handle); } -#[no_mangle] -pub extern "C" fn bloom_set_sound_volume(handle: f64, volume: f64) { engine().audio.set_sound_volume(handle, volume as f32); } -#[no_mangle] -pub extern "C" fn bloom_set_master_volume(volume: f64) { engine().audio.master_volume = volume as f32; } - -#[no_mangle] -pub extern "C" fn bloom_play_sound_3d(handle: f64, x: f64, y: f64, z: f64) { - engine().audio.play_sound_3d(handle, x as f32, y as f32, z as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_listener_position(x: f64, y: f64, z: f64, fx: f64, fy: f64, fz: f64) { - engine().audio.set_listener_position(x as f32, y as f32, z as f32, fx as f32, fy as f32, fz as f32); -} - // --- Texture FFI --- -#[no_mangle] -pub extern "C" fn bloom_load_texture(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(resolve_path(path)) { - Ok(data) => { - let eng = engine(); - let renderer_ptr = &mut eng.renderer as *mut bloom_shared::renderer::Renderer; - eng.textures.load_texture(unsafe { &mut *renderer_ptr }, &data) - } - Err(_) => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_unload_texture(handle: f64) { - let eng = engine(); - let renderer_ptr = &mut eng.renderer as *mut bloom_shared::renderer::Renderer; - eng.textures.unload_texture(handle, unsafe { &mut *renderer_ptr }); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_texture(handle: f64, x: f64, y: f64, r: f64, g: f64, b: f64, a: f64) { - let eng = engine(); - if let Some(tex) = eng.textures.get(handle) { - let idx = tex.bind_group_idx; - eng.renderer.draw_texture(idx, x, y, r, g, b, a); - } -} +// --- Camera FFI --- -#[no_mangle] -pub extern "C" fn bloom_draw_texture_rec(handle: f64, src_x: f64, src_y: f64, src_w: f64, src_h: f64, dst_x: f64, dst_y: f64, r: f64, g: f64, b: f64, a: f64) { - let eng = engine(); - if let Some(tex) = eng.textures.get(handle) { - let idx = tex.bind_group_idx; - eng.renderer.draw_texture_rec(idx, src_x, src_y, src_w, src_h, dst_x, dst_y, r, g, b, a); - } -} +// --- 3D Drawing FFI --- -#[no_mangle] -pub extern "C" fn bloom_draw_texture_pro(handle: f64, src_x: f64, src_y: f64, src_w: f64, src_h: f64, dst_x: f64, dst_y: f64, dst_w: f64, dst_h: f64, origin_x: f64, origin_y: f64, rotation: f64, r: f64, g: f64, b: f64, a: f64) { - let eng = engine(); - if let Some(tex) = eng.textures.get(handle) { - let idx = tex.bind_group_idx; - eng.renderer.draw_texture_pro(idx, src_x, src_y, src_w, src_h, dst_x, dst_y, dst_w, dst_h, origin_x, origin_y, rotation, r, g, b, a); - } -} +// --- Model FFI --- -#[no_mangle] -pub extern "C" fn bloom_get_texture_width(handle: f64) -> f64 { - engine().textures.get(handle).map(|t| t.width as f64).unwrap_or(0.0) -} +// ============================================================ +// Phase 1c — material system FFI +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_get_texture_height(handle: f64) -> f64 { - engine().textures.get(handle).map(|t| t.height as f64).unwrap_or(0.0) -} +// --- Music FFI --- -#[no_mangle] -pub extern "C" fn bloom_gen_texture_mipmaps(_handle: f64) { - // No-op: wgpu handles mipmaps internally -} +// --- Gamepad FFI --- -#[no_mangle] -pub extern "C" fn bloom_set_texture_filter(handle: f64, mode: f64) { - let eng = engine(); - if let Some(tex) = eng.textures.get(handle) { - let bind_group_idx = tex.bind_group_idx; - eng.renderer.set_texture_filter(bind_group_idx, mode > 0.5); - } -} +// --- Skeletal Animation Debug --- -#[no_mangle] -pub extern "C" fn bloom_load_image(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(resolve_path(path)) { Ok(data) => engine().textures.load_image(&data), Err(_) => 0.0 } -} +// --- Lighting --- -#[no_mangle] -pub extern "C" fn bloom_image_resize(handle: f64, w: f64, h: f64) { - engine().textures.image_resize(handle, w as u32, h as u32); -} +// --- Utility FFI --- #[no_mangle] -pub extern "C" fn bloom_image_crop(handle: f64, x: f64, y: f64, w: f64, h: f64) { - engine().textures.image_crop(handle, x as u32, y as u32, w as u32, h as u32); -} - +pub extern "C" fn bloom_toggle_fullscreen() {} #[no_mangle] -pub extern "C" fn bloom_image_flip_h(handle: f64) { - engine().textures.image_flip_h(handle); -} - +pub extern "C" fn bloom_set_window_title(title_ptr: *const u8) { let _ = str_from_header(title_ptr); } #[no_mangle] -pub extern "C" fn bloom_image_flip_v(handle: f64) { - engine().textures.image_flip_v(handle); -} +pub extern "C" fn bloom_set_window_icon(path_ptr: *const u8) { let _ = str_from_header(path_ptr); } #[no_mangle] -pub extern "C" fn bloom_load_texture_from_image(handle: f64) -> f64 { - let eng = engine(); - let renderer_ptr = &mut eng.renderer as *mut bloom_shared::renderer::Renderer; - eng.textures.load_texture_from_image(handle, unsafe { &mut *renderer_ptr }) +pub extern "C" fn bloom_disable_cursor() { + engine().input.cursor_disabled = true; } -// --- Camera FFI --- - #[no_mangle] -pub extern "C" fn bloom_begin_mode_2d(offset_x: f64, offset_y: f64, target_x: f64, target_y: f64, rotation: f64, zoom: f64) { - engine().renderer.begin_mode_2d(offset_x as f32, offset_y as f32, target_x as f32, target_y as f32, rotation as f32, zoom as f32); +pub extern "C" fn bloom_enable_cursor() { + engine().input.cursor_disabled = false; } -#[no_mangle] -pub extern "C" fn bloom_end_mode_2d() { engine().renderer.end_mode_2d(); } +// E4: Clipboard (stub on this platform) #[no_mangle] -pub extern "C" fn bloom_begin_mode_3d(pos_x: f64, pos_y: f64, pos_z: f64, target_x: f64, target_y: f64, target_z: f64, up_x: f64, up_y: f64, up_z: f64, fovy: f64, projection: f64) { - engine().renderer.begin_mode_3d(pos_x as f32, pos_y as f32, pos_z as f32, target_x as f32, target_y as f32, target_z as f32, up_x as f32, up_y as f32, up_z as f32, fovy as f32, projection as f32); -} +pub extern "C" fn bloom_set_clipboard_text(_text_ptr: *const u8) {} #[no_mangle] -pub extern "C" fn bloom_end_mode_3d() { engine().renderer.end_mode_3d(); } - -// --- 3D Drawing FFI --- +pub extern "C" fn bloom_get_clipboard_text() -> *const u8 { std::ptr::null() } +// E5b: File dialogs (stub on this platform) #[no_mangle] -pub extern "C" fn bloom_draw_cube(x: f64, y: f64, z: f64, w: f64, h: f64, d: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_cube(x, y, z, w, h, d, r, g, b, a); -} -#[no_mangle] -pub extern "C" fn bloom_draw_cube_wires(x: f64, y: f64, z: f64, w: f64, h: f64, d: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_cube_wires(x, y, z, w, h, d, r, g, b, a); -} -#[no_mangle] -pub extern "C" fn bloom_draw_sphere(x: f64, y: f64, z: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_sphere(x, y, z, radius, r, g, b, a); -} -#[no_mangle] -pub extern "C" fn bloom_draw_sphere_wires(x: f64, y: f64, z: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_sphere_wires(x, y, z, radius, r, g, b, a); -} -#[no_mangle] -pub extern "C" fn bloom_draw_cylinder(x: f64, y: f64, z: f64, rt: f64, rb: f64, h: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_cylinder(x, y, z, rt, rb, h, r, g, b, a); -} -#[no_mangle] -pub extern "C" fn bloom_draw_plane(x: f64, y: f64, z: f64, w: f64, d: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_plane(x, y, z, w, d, r, g, b, a); -} +pub extern "C" fn bloom_open_file_dialog(_filter_ptr: *const u8, _title_ptr: *const u8) -> *const u8 { std::ptr::null() } #[no_mangle] -pub extern "C" fn bloom_draw_grid(slices: f64, spacing: f64) { - engine().renderer.draw_grid(slices as i32, spacing); -} +pub extern "C" fn bloom_save_file_dialog(_default_name_ptr: *const u8, _title_ptr: *const u8) -> *const u8 { std::ptr::null() } #[no_mangle] -pub extern "C" fn bloom_draw_ray(ox: f64, oy: f64, oz: f64, dx: f64, dy: f64, dz: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_ray(ox, oy, oz, dx, dy, dz, r, g, b, a); -} - -// --- Model FFI --- +pub extern "C" fn bloom_get_platform() -> f64 { 5.0 } +/// Preferred OS language packed as `c0*256+c1` (ISO-639 primary subtag), read +/// from the device locale system property via the NDK (no JNI). Tries the +/// user-set locale first, then the factory defaults. Falls back to "en". #[no_mangle] -pub extern "C" fn bloom_load_model(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(resolve_path(path)) { - Ok(data) => { - let eng = engine(); - let renderer_ptr = &mut eng.renderer as *mut crate::Renderer; - eng.models.load_model_with_textures(&data, unsafe { &mut *renderer_ptr }) - } - Err(_) => 0.0, - } -} -#[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(); - if let Some(model) = eng.models.get(handle) { - let tint = [(r / 255.0) as f32, (g / 255.0) as f32, (b / 255.0) as f32, (a / 255.0) as f32]; - let position = [x as f32, y as f32, z as f32]; - let handle_bits = handle.to_bits(); - if eng.renderer.cache_model_if_static(handle_bits, &model.meshes) { - eng.renderer.draw_model_cached(handle_bits, position, scale as f32, tint); +pub extern "C" fn bloom_get_language() -> f64 { + fn parse(buf: &[u8], n: i32) -> Option { + if n < 2 { return None; } + let lc = |b: u8| if b.is_ascii_uppercase() { b + 32 } else { b }; + let (c0, c1) = (lc(buf[0]), lc(buf[1])); + if c0.is_ascii_alphabetic() && c1.is_ascii_alphabetic() { + Some((c0 as f64) * 256.0 + (c1 as f64)) } else { - for mesh in &model.meshes { - let tex_idx = mesh.texture_idx.unwrap_or(0); - eng.renderer.draw_model_mesh_tinted(&mesh.vertices, &mesh.indices, position, scale as f32, tint, tex_idx); - } + None } } -} -#[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, -) { - let bits = color_packed_argb as u32; - let a = ((bits >> 24) & 0xff) as f32 / 255.0; - let r = ((bits >> 16) & 0xff) as f32 / 255.0; - let g = ((bits >> 8) & 0xff) as f32 / 255.0; - let b = ( bits & 0xff) as f32 / 255.0; - let eng = engine(); - if let Some(model) = eng.models.get(handle) { - let position = [x as f32, y as f32, z as f32]; - let scale = scale as f32; - let tint = [r, g, b, a]; - for mesh in &model.meshes { - let tex_idx = mesh.texture_idx.unwrap_or(0); - eng.renderer.draw_model_mesh_tinted_rotated( - &mesh.vertices, &mesh.indices, position, scale, tint, tex_idx, rot_y as f32, - ); - } + let props: [&[u8]; 3] = [ + b"persist.sys.locale\0", + b"ro.product.locale\0", + b"ro.product.locale.language\0", + ]; + for prop in props { + let mut buf = [0u8; 92]; // PROP_VALUE_MAX + let n = unsafe { + libc::__system_property_get( + prop.as_ptr() as *const libc::c_char, + buf.as_mut_ptr() as *mut libc::c_char, + ) + }; + if let Some(v) = parse(&buf, n) { return v; } } + 25966.0 } -#[no_mangle] -pub extern "C" fn bloom_unload_model(handle: f64) { engine().models.unload_model(handle); } -#[no_mangle] -pub extern "C" fn bloom_get_model_mesh_count(handle: f64) -> f64 { - match engine().models.get(handle) { - Some(model) => model.meshes.len() as f64, - None => 0.0, - } -} +// ============================================================ +// JNI Bridge for Bloom game applications +// ============================================================ +// +// These functions bridge the Android Java/Kotlin layer to the +// Bloom engine. Any Bloom game on Android should use the +// com.bloomengine.game.BloomGameBridge Kotlin class. -#[no_mangle] -pub extern "C" fn bloom_get_model_material_count(handle: f64) -> f64 { - match engine().models.get(handle) { - Some(model) => model.meshes.len() as f64, - None => 0.0, - } +extern "C" { + fn ANativeWindow_fromSurface(env: *mut libc::c_void, surface: *mut libc::c_void) -> *mut libc::c_void; + fn mallopt(param: i32, value: i32) -> i32; + fn __android_log_print(prio: i32, tag: *const u8, fmt: *const u8, ...) -> i32; + fn main() -> i32; } +/// JNI_OnLoad: called when System.loadLibrary() loads this .so. +/// Disables MTE heap tagging (required for Perry NaN-boxing) and +/// reads the asset base path from BLOOM_ASSET_PATH env var. #[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) -} +pub extern "C" fn JNI_OnLoad(_vm: *mut libc::c_void, _reserved: *mut libc::c_void) -> i32 { + unsafe { + // Disable MTE heap tagging for Perry NaN-boxing compatibility. + // Perry uses 48-bit pointers; Android's scudo allocator may tag + // the top byte, corrupting NaN-boxed pointer values. + mallopt(-204, 0); -#[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(); - if let Some(img) = eng.textures.images.get(image_handle) { - let data = img.data.clone(); - let w = img.width; - let h = img.height; - eng.models.gen_mesh_heightmap(&data, w, h, size_x as f32, size_y as f32, size_z as f32) - } else { - 0.0 - } -} - -#[no_mangle] -pub extern "C" fn bloom_load_shader(source_ptr: *const u8) -> f64 { - let source = str_from_header(source_ptr); - engine().renderer.load_custom_shader(source) as f64 -} - -#[no_mangle] -pub extern "C" fn bloom_create_mesh(vertex_ptr: *const f32, vertex_count: f64, index_ptr: *const u32, index_count: f64) -> f64 { - if vertex_ptr.is_null() || index_ptr.is_null() { return 0.0; } - let vcount = vertex_count as usize; - let icount = index_count as usize; - let vertex_data = unsafe { std::slice::from_raw_parts(vertex_ptr, vcount * 12) }; // 12 floats per vertex - let index_data = unsafe { std::slice::from_raw_parts(index_ptr, icount) }; - engine().models.create_mesh(vertex_data, index_data) -} - -// ============================================================ -// Phase 1c — material system FFI -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_set_material_params( - handle: f64, - params_ptr: *const f64, - param_count: f64, -) { - let count = param_count as usize; - if count > 64 { - eprintln!("[material] set_material_params: param_count {} > 64 (256-byte UBO cap)", count); - return; - } - let mut bytes = vec![0u8; count * 4]; - if !params_ptr.is_null() && count > 0 { - let slots = unsafe { std::slice::from_raw_parts(params_ptr, count) }; - for (i, &v) in slots.iter().enumerate() { - let f = v as f32; - bytes[i*4..i*4+4].copy_from_slice(&f.to_le_bytes()); - } - } - let eng = engine(); - if let Err(e) = eng.renderer.material_system.set_user_params( - &eng.renderer.device, &eng.renderer.queue, - handle as u32, &bytes, - ) { - eprintln!("[material] set_material_params failed: {}", e); - } -} - -#[no_mangle] -pub extern "C" fn bloom_compile_material(source_ptr: *const u8) -> f64 { - let source = str_from_header(source_ptr); - match engine().renderer.compile_material(source) { - Ok(handle) => handle as f64, - Err(e) => { - eprintln!("[material] compile failed: {:?}", e); - 0.0 - } - } -} - -#[no_mangle] -pub extern "C" fn bloom_compile_material_refractive(source_ptr: *const u8) -> f64 { - use bloom_shared::renderer::material_pipeline::{FragmentProfile, Bucket}; - let source = str_from_header(source_ptr); - match engine().renderer.compile_material_with_options( - source, FragmentProfile::Translucent, Bucket::Refractive, true, false, - ) { - Ok(handle) => handle as f64, - Err(e) => { eprintln!("[refractive] compile failed: {:?}", e); 0.0 } - } -} - -#[no_mangle] -pub extern "C" fn bloom_compile_material_transparent(source_ptr: *const u8) -> f64 { - use bloom_shared::renderer::material_pipeline::{FragmentProfile, Bucket}; - let source = str_from_header(source_ptr); - match engine().renderer.compile_material_with_options( - source, FragmentProfile::Translucent, Bucket::Transparent, false, false, - ) { - Ok(handle) => handle as f64, - Err(e) => { eprintln!("[material] compile failed: {:?}", e); 0.0 } - } -} - -#[no_mangle] -pub extern "C" fn bloom_compile_material_additive(source_ptr: *const u8) -> f64 { - use bloom_shared::renderer::material_pipeline::{FragmentProfile, Bucket}; - let source = str_from_header(source_ptr); - match engine().renderer.compile_material_with_options( - source, FragmentProfile::Translucent, Bucket::Additive, false, false, - ) { - Ok(handle) => handle as f64, - Err(e) => { eprintln!("[material] compile failed: {:?}", e); 0.0 } - } -} - -#[no_mangle] -pub extern "C" fn bloom_compile_material_cutout(source_ptr: *const u8) -> f64 { - use bloom_shared::renderer::material_pipeline::{FragmentProfile, Bucket}; - let source = str_from_header(source_ptr); - match engine().renderer.compile_material_with_options( - source, FragmentProfile::Opaque, Bucket::Cutout, false, false, - ) { - Ok(handle) => handle as f64, - Err(e) => { eprintln!("[material] compile failed: {:?}", e); 0.0 } - } -} - -#[no_mangle] -pub extern "C" fn bloom_compile_material_instanced(source_ptr: *const u8) -> f64 { - let source = str_from_header(source_ptr); - match engine().renderer.compile_material_instanced(source) { - Ok(handle) => handle as f64, - Err(e) => { eprintln!("[material] instanced compile failed: {:?}", e); 0.0 } - } -} - -#[no_mangle] -pub extern "C" fn bloom_create_instance_buffer( - data_ptr: *const f64, instance_count: f64, -) -> f64 { - if data_ptr.is_null() || instance_count <= 0.0 { return 0.0; } - let count = instance_count as u32; - let slot_count = (count as usize) * 9; - let raw_f64 = unsafe { std::slice::from_raw_parts(data_ptr, slot_count) }; - let raw_f32: Vec = raw_f64.iter().map(|&v| v as f32).collect(); - engine().renderer.create_instance_buffer(&raw_f32, count) as f64 -} - -#[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, -) { - let eng = engine(); - let handle_bits = mesh_handle.to_bits(); - if let Some(model) = eng.models.get(mesh_handle) { - eng.renderer.cache_model_if_static(handle_bits, &model.meshes); - } - eng.renderer.submit_material_draw_instanced( - material as u32, - handle_bits, - mesh_idx as usize, - instance_buffer as u32, - instance_count as u32, - ); -} - -#[no_mangle] -pub extern "C" fn bloom_destroy_instance_buffer(handle: f64) { - engine().renderer.destroy_instance_buffer(handle as u32); -} - -/// EN-011 — create a planar reflection probe. See macOS lib.rs for the -/// full doc comment; this entry-point exists on every native platform -/// so games can target the same FFI surface across iOS/tvOS/Windows/ -/// Linux/Android. -#[no_mangle] -pub extern "C" fn bloom_create_planar_reflection( - plane_y: f64, nx: f64, ny: f64, nz: f64, resolution: f64, -) -> f64 { - engine().renderer.create_planar_reflection( - plane_y as f32, - [nx as f32, ny as f32, nz as f32], - resolution as u32, - ) as f64 -} - -/// EN-011 — link a material to a planar reflection probe. `probe = 0` -/// reverts the binding to the engine's default 1×1 black texture. -#[no_mangle] -pub extern "C" fn bloom_set_material_reflection_probe( - material: f64, probe: f64, -) { - engine().renderer.set_material_reflection_probe(material as u32, probe as u32); -} - -/// EN-014 — create a texture array from concatenated RGBA8 byte data. -/// See macOS lib.rs for the full doc comment; this entry-point exists -/// on every native platform so a TS game targets the same FFI across -/// iOS / tvOS / Windows / Linux / Android. -#[no_mangle] -pub extern "C" fn bloom_create_texture_array( - data_ptr: *const u8, - data_len: f64, - width: f64, - height: f64, - layer_count: f64, -) -> f64 { - // EN-014 V2 — V1 forwards to _ex with default sRGB / no mips. - bloom_create_texture_array_ex(data_ptr, data_len, width, height, layer_count, 0.0, 1.0) -} - -/// EN-014 V2 — explicit format + mip control. See macOS lib.rs for docs. -#[no_mangle] -pub extern "C" fn bloom_create_texture_array_ex( - data_ptr: *const u8, - data_len: f64, - width: f64, - height: f64, - layer_count: f64, - format: f64, - mip_levels: f64, -) -> f64 { - if data_ptr.is_null() || data_len <= 0.0 { return 0.0; } - let w = width as u32; - let h = height as u32; - if w == 0 || h == 0 { return 0.0; } - let layers_count = (layer_count as u32) - .min(bloom_shared::renderer::material_system::MAX_TEXTURE_ARRAY_LAYERS); - if layers_count == 0 { return 0.0; } - let layer_size = (w as usize) * (h as usize) * 4; - let total_bytes = (data_len as usize) - .min(layers_count as usize * layer_size); - let bytes = unsafe { std::slice::from_raw_parts(data_ptr, total_bytes) }; - let mut layers: Vec<(&[u8], u32, u32)> = Vec::with_capacity(layers_count as usize); - for i in 0..(layers_count as usize) { - let start = i * layer_size; - let end = start + layer_size; - if end > bytes.len() { break; } - layers.push((&bytes[start..end], w, h)); - } - engine().renderer.create_texture_array_ex(&layers, format as u32, mip_levels as u32) as f64 -} - -/// EN-014 — link a texture-array handle to a material at one of three -/// slots: 0 = albedo (binding 14), 1 = normal (binding 15), -/// 2 = MR (binding 16). Pass `array = 0` to revert to the stub. -#[no_mangle] -pub extern "C" fn bloom_set_material_texture_array( - material: f64, slot: f64, array: f64, -) { - engine().renderer.set_material_texture_array( - material as u32, slot as u32, array as u32, - ); -} - -/// EN-012 — set the shading model for a material (0=default lit, -/// 1=foliage, 2=subsurface V2 stub). -#[no_mangle] -pub extern "C" fn bloom_set_material_shading_model( - material: f64, model: f64, -) { - engine().renderer.set_material_shading_model(material as u32, model as u32); -} - -/// EN-012 — set the foliage shading parameters for a material. -/// Only takes effect when shading_model == 1 (foliage). -#[no_mangle] -pub extern "C" fn bloom_set_material_foliage( - material: f64, - trans_r: f64, trans_g: f64, trans_b: f64, - trans_amount: f64, wrap_factor: f64, -) { - engine().renderer.set_material_foliage( - material as u32, - [trans_r as f32, trans_g as f32, trans_b as f32], - trans_amount as f32, wrap_factor as f32, - ); -} - -#[no_mangle] -pub extern "C" fn bloom_compile_material_from_file( - path_ptr: *const u8, - bucket_kind: f64, -) -> f64 { - use bloom_shared::renderer::material_pipeline::{FragmentProfile, Bucket}; - let path = str_from_header(path_ptr); - let (profile, bucket, reads_scene) = match bucket_kind as u32 { - 0 => (FragmentProfile::Opaque, Bucket::Opaque, false), - 1 => (FragmentProfile::Translucent, Bucket::Transparent, false), - 2 => (FragmentProfile::Translucent, Bucket::Refractive, true), - 3 => (FragmentProfile::Translucent, Bucket::Additive, false), - 4 => (FragmentProfile::Opaque, Bucket::Cutout, false), - _ => { - eprintln!("[material] from_file: unknown bucket_kind {bucket_kind}"); - return 0.0; - } - }; - match engine().renderer.compile_material_from_file( - std::path::Path::new(path), profile, bucket, reads_scene, - ) { - Ok(handle) => handle as f64, - Err(e) => { eprintln!("[material] from_file failed: {e}"); 0.0 } - } -} - -/// EN-017 — compile + install a fullscreen post-pass material. -/// See `bloom-macos::bloom_set_post_pass` for the full ABI. -#[no_mangle] -pub extern "C" fn bloom_set_post_pass(source_ptr: *const u8) -> f64 { - let source = str_from_header(source_ptr); - match engine().renderer.set_post_pass(source) { - Ok(()) => 1.0, - Err(e) => { eprintln!("[post_pass] compile failed: {:?}", e); 0.0 } - } -} - -/// EN-017 — uninstall the active post-pass. -#[no_mangle] -pub extern "C" fn bloom_clear_post_pass() { - engine().renderer.clear_post_pass(); -} - -/// EN-017 V2 — append a post-pass to the stack. -/// See `bloom-macos::bloom_add_post_pass` for the full ABI. -#[no_mangle] -pub extern "C" fn bloom_add_post_pass(source_ptr: *const u8) -> f64 { - let source = str_from_header(source_ptr); - match engine().renderer.add_post_pass(source) { - Ok(h) => h as f64, - Err(e) => { eprintln!("[post_pass] compile failed: {:?}", e); 0.0 } - } -} - -/// EN-017 V2 — wipe the entire post-pass stack. -#[no_mangle] -pub extern "C" fn bloom_clear_all_post_passes() { - engine().renderer.clear_all_post_passes(); -} - -#[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, -) { - let eng = engine(); - let handle_bits = mesh_handle.to_bits(); - if let Some(model) = eng.models.get(mesh_handle) { - eng.renderer.cache_model_if_static(handle_bits, &model.meshes); - } - eng.renderer.submit_material_draw( - material as u32, - handle_bits, - mesh_idx as usize, - [x as f32, y as f32, z as f32], - scale as f32, - [(r / 255.0) as f32, (g / 255.0) as f32, (b / 255.0) as f32, (a / 255.0) as f32], - ); -} - -#[no_mangle] -pub extern "C" fn bloom_load_model_animation(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(resolve_path(path)) { - Ok(data) => engine().models.load_model_animation(&data), - Err(_) => 0.0, - } -} - -#[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_sin: f64, rot_cos: f64) { - let eng = engine(); - eng.models.update_model_animation(handle, anim_index as usize, time as f32); - if let Some(anim) = eng.models.get_animation(handle) { - if !anim.joint_matrices.is_empty() { - eng.renderer.set_joint_matrices_scaled(&anim.joint_matrices, scale as f32, [px as f32, py as f32, pz as f32], rot_sin as f32, rot_cos as f32); - } - } -} - -// --- Music FFI --- - -#[no_mangle] -pub extern "C" fn bloom_load_music(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(resolve_path(path)) { - Ok(data) => { - if let Some(s) = parse_ogg(&data) { engine().audio.load_music(s) } - else if let Some(s) = parse_wav(&data) { engine().audio.load_music(s) } - else if let Some(s) = parse_mp3(&data) { engine().audio.load_music(s) } - else { 0.0 } - } - Err(_) => 0.0, - } -} -#[no_mangle] -pub extern "C" fn bloom_play_music(handle: f64) { engine().audio.play_music(handle); } -#[no_mangle] -pub extern "C" fn bloom_stop_music(handle: f64) { engine().audio.stop_music(handle); } -#[no_mangle] -pub extern "C" fn bloom_update_music_stream(handle: f64) { engine().audio.update_music_stream(handle); } -#[no_mangle] -pub extern "C" fn bloom_set_music_volume(handle: f64, volume: f64) { engine().audio.set_music_volume(handle, volume as f32); } -#[no_mangle] -pub extern "C" fn bloom_is_music_playing(handle: f64) -> f64 { if engine().audio.is_music_playing(handle) { 1.0 } else { 0.0 } } - -// --- Gamepad FFI --- - -#[no_mangle] -pub extern "C" fn bloom_is_gamepad_available() -> f64 { if engine().input.is_gamepad_available() { 1.0 } else { 0.0 } } -#[no_mangle] -pub extern "C" fn bloom_get_gamepad_axis(axis: f64) -> f64 { engine().input.get_gamepad_axis(axis as usize) as f64 } -#[no_mangle] -pub extern "C" fn bloom_is_gamepad_button_pressed(btn: f64) -> f64 { if engine().input.is_gamepad_button_pressed(btn as usize) { 1.0 } else { 0.0 } } -#[no_mangle] -pub extern "C" fn bloom_is_gamepad_button_down(btn: f64) -> f64 { if engine().input.is_gamepad_button_down(btn as usize) { 1.0 } else { 0.0 } } -#[no_mangle] -pub extern "C" fn bloom_is_gamepad_button_released(btn: f64) -> f64 { if engine().input.is_gamepad_button_released(btn as usize) { 1.0 } else { 0.0 } } -#[no_mangle] -pub extern "C" fn bloom_get_gamepad_axis_count() -> f64 { engine().input.get_gamepad_axis_count() as f64 } - -// --- Skeletal Animation Debug --- - -#[no_mangle] -pub extern "C" fn bloom_set_joint_test(_joint: f64, _angle: f64) { - // No-op for now — skeletal animation testing -} - -// --- Lighting --- - -#[no_mangle] -pub extern "C" fn bloom_set_ambient_light(r: f64, g: f64, b: f64, intensity: f64) { - engine().renderer.set_ambient_light(r, g, b, intensity); -} - -#[no_mangle] -pub extern "C" fn bloom_set_directional_light(dx: f64, dy: f64, dz: f64, r: f64, g: f64, b: f64, intensity: f64) { - engine().renderer.set_directional_light(dx, dy, dz, r, g, b, intensity); -} - -#[no_mangle] -pub extern "C" fn bloom_set_procedural_sky(enabled: f64, rayleigh_density: f64, mie_density: f64, ground_albedo: f64) { - engine().renderer.set_procedural_sky( - enabled != 0.0, - rayleigh_density as f32, - mie_density as f32, - ground_albedo as f32, - ); -} - -#[no_mangle] -pub extern "C" fn bloom_set_sun_direction(dx: f64, dy: f64, dz: f64, intensity: f64) { - engine().renderer.set_sun_direction(dx as f32, dy as f32, dz as f32, intensity as f32); -} - -// --- Utility FFI --- - -#[no_mangle] -pub extern "C" fn bloom_toggle_fullscreen() {} -#[no_mangle] -pub extern "C" fn bloom_set_window_title(title_ptr: *const u8) { let _ = str_from_header(title_ptr); } -#[no_mangle] -pub extern "C" fn bloom_set_window_icon(path_ptr: *const u8) { let _ = str_from_header(path_ptr); } - -#[no_mangle] -pub extern "C" fn bloom_disable_cursor() { - engine().input.cursor_disabled = true; -} - -#[no_mangle] -pub extern "C" fn bloom_enable_cursor() { - engine().input.cursor_disabled = false; -} - -#[no_mangle] -pub extern "C" fn bloom_get_mouse_delta_x() -> f64 { - engine().input.mouse_delta_x -} - -#[no_mangle] -pub extern "C" fn bloom_get_mouse_delta_y() -> f64 { - engine().input.mouse_delta_y -} - -// Accumulated scroll wheel delta since the last call. Reading consumes the -// value (returns 0 on the next call until the user scrolls again). Used by -// the editor's orbit camera and any scrollable UI panel. -#[no_mangle] -pub extern "C" fn bloom_get_mouse_wheel() -> f64 { - engine().input.consume_mouse_wheel() -} - -#[no_mangle] -pub extern "C" fn bloom_get_char_pressed() -> f64 { - engine().input.pop_char() as f64 -} - -// Q2: Cursor shape -#[no_mangle] -pub extern "C" fn bloom_set_cursor_shape(shape: f64) { - engine().input.cursor_shape = shape as u32; -} - -// E4: Clipboard (stub on this platform) -#[no_mangle] -pub extern "C" fn bloom_set_clipboard_text(_text_ptr: *const u8) {} -#[no_mangle] -pub extern "C" fn bloom_get_clipboard_text() -> *const u8 { std::ptr::null() } - -// E5b: File dialogs (stub on this platform) -#[no_mangle] -pub extern "C" fn bloom_open_file_dialog(_filter_ptr: *const u8, _title_ptr: *const u8) -> *const u8 { std::ptr::null() } -#[no_mangle] -pub extern "C" fn bloom_save_file_dialog(_default_name_ptr: *const u8, _title_ptr: *const u8) -> *const u8 { std::ptr::null() } - -// 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. -#[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 -} -#[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 -} -#[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 -} -#[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 -} -#[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 -} -#[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 -} - -#[no_mangle] -pub extern "C" fn bloom_write_file(path_ptr: *const u8, data_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - let data = str_from_header(data_ptr); - match std::fs::write(path, data.as_bytes()) { - Ok(_) => 1.0, - Err(_) => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_file_exists(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - let resolved = resolve_path(path); - if std::path::Path::new(&resolved).exists() { 1.0 } else { 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_read_file(path_ptr: *const u8) -> *const u8 { - let path = str_from_header(path_ptr); - // Always return a valid Perry string. A null pointer would NaN-box into a - // string-typed value pointing at address 0; subsequent `.length` / - // `.charCodeAt` reads dereference the bogus StringHeader and segfault. - // Callers detect "missing file" via `data.length === 0` (e.g. the jump - // game's discoverLevels probe across level1..level10 / custom_*). - // Parity with native/linux — Android previously returned null on Err, - // crashing discoverLevels at the first non-existent level file. - match std::fs::read_to_string(resolve_path(path)) { - Ok(contents) => alloc_perry_string(&contents), - Err(_) => alloc_perry_string(""), - } -} - -#[no_mangle] -pub extern "C" fn bloom_get_touch_x(index: f64) -> f64 { engine().input.get_touch_x(index as usize) } -#[no_mangle] -pub extern "C" fn bloom_get_touch_y(index: f64) -> f64 { engine().input.get_touch_y(index as usize) } -#[no_mangle] -pub extern "C" fn bloom_get_touch_count() -> f64 { engine().input.get_touch_count() as f64 } -#[no_mangle] -pub extern "C" fn bloom_get_time() -> f64 { engine().get_time() } - -// Input injection + platform detection -#[no_mangle] -pub extern "C" fn bloom_inject_key_down(key: f64) { - engine().input.set_key_down(key as usize); -} -#[no_mangle] -pub extern "C" fn bloom_inject_key_up(key: f64) { - engine().input.set_key_up(key as usize); -} -#[no_mangle] -pub extern "C" fn bloom_inject_gamepad_axis(axis: f64, value: f64) { - engine().input.set_gamepad_axis(axis as usize, value as f32); -} -#[no_mangle] -pub extern "C" fn bloom_inject_gamepad_button_down(button: f64) { - engine().input.set_gamepad_button_down(button as usize); -} -#[no_mangle] -pub extern "C" fn bloom_inject_gamepad_button_up(button: f64) { - engine().input.set_gamepad_button_up(button as usize); -} -#[no_mangle] -pub extern "C" fn bloom_get_platform() -> f64 { 5.0 } - -/// Preferred OS language packed as `c0*256+c1` (ISO-639 primary subtag), read -/// from the device locale system property via the NDK (no JNI). Tries the -/// user-set locale first, then the factory defaults. Falls back to "en". -#[no_mangle] -pub extern "C" fn bloom_get_language() -> f64 { - fn parse(buf: &[u8], n: i32) -> Option { - if n < 2 { return None; } - let lc = |b: u8| if b.is_ascii_uppercase() { b + 32 } else { b }; - let (c0, c1) = (lc(buf[0]), lc(buf[1])); - if c0.is_ascii_alphabetic() && c1.is_ascii_alphabetic() { - Some((c0 as f64) * 256.0 + (c1 as f64)) - } else { - None - } - } - let props: [&[u8]; 3] = [ - b"persist.sys.locale\0", - b"ro.product.locale\0", - b"ro.product.locale.language\0", - ]; - for prop in props { - let mut buf = [0u8; 92]; // PROP_VALUE_MAX - let n = unsafe { - libc::__system_property_get( - prop.as_ptr() as *const libc::c_char, - buf.as_mut_ptr() as *mut libc::c_char, - ) - }; - if let Some(v) = parse(&buf, n) { return v; } - } - 25966.0 -} -#[no_mangle] -pub extern "C" fn bloom_is_any_input_pressed() -> f64 { - if engine().input.is_any_input_pressed() { 1.0 } else { 0.0 } -} -#[no_mangle] -pub extern "C" fn bloom_get_crown_rotation() -> f64 { - engine().input.consume_crown_rotation() -} - -// ============================================================ -// JNI Bridge for Bloom game applications -// ============================================================ -// -// These functions bridge the Android Java/Kotlin layer to the -// Bloom engine. Any Bloom game on Android should use the -// com.bloomengine.game.BloomGameBridge Kotlin class. - -extern "C" { - fn ANativeWindow_fromSurface(env: *mut libc::c_void, surface: *mut libc::c_void) -> *mut libc::c_void; - fn mallopt(param: i32, value: i32) -> i32; - fn __android_log_print(prio: i32, tag: *const u8, fmt: *const u8, ...) -> i32; - fn main() -> i32; -} - -/// JNI_OnLoad: called when System.loadLibrary() loads this .so. -/// Disables MTE heap tagging (required for Perry NaN-boxing) and -/// reads the asset base path from BLOOM_ASSET_PATH env var. -#[no_mangle] -pub extern "C" fn JNI_OnLoad(_vm: *mut libc::c_void, _reserved: *mut libc::c_void) -> i32 { - unsafe { - // Disable MTE heap tagging for Perry NaN-boxing compatibility. - // Perry uses 48-bit pointers; Android's scudo allocator may tag - // the top byte, corrupting NaN-boxed pointer values. - mallopt(-204, 0); - - __android_log_print( - 3, b"BloomEngine\0".as_ptr(), - b"JNI_OnLoad: MTE disabled\0".as_ptr(), - ); + __android_log_print( + 3, b"BloomEngine\0".as_ptr(), + b"JNI_OnLoad: MTE disabled\0".as_ptr(), + ); } // Read asset base path from environment (set by Activity before loadLibrary) @@ -1483,801 +541,92 @@ pub extern "C" fn JNI_OnLoad(_vm: *mut libc::c_void, _reserved: *mut libc::c_voi } } - 0x00010006 // JNI_VERSION_1_6 -} - -/// Pass the Android Surface to the engine so it can create a wgpu rendering surface. -/// Called from BloomGameBridge.nativeSetSurface(surface). -#[no_mangle] -pub unsafe extern "C" fn Java_com_bloomengine_game_BloomGameBridge_nativeSetSurface( - env: *mut libc::c_void, - _class: *mut libc::c_void, - surface: *mut libc::c_void, -) { - let window = ANativeWindow_fromSurface(env, surface); - __android_log_print( - 3, b"BloomEngine\0".as_ptr(), - b"nativeSetSurface: ANativeWindow acquired\0".as_ptr(), - ); - bloom_android_set_native_window(window); -} - -/// Run the compiled game's main() function on the game thread. -/// Called from BloomGameBridge.nativeMain(). -#[no_mangle] -pub unsafe extern "C" fn Java_com_bloomengine_game_BloomGameBridge_nativeMain( - _env: *mut libc::c_void, - _class: *mut libc::c_void, -) { - __android_log_print( - 3, b"BloomEngine\0".as_ptr(), - b"nativeMain: calling main()\0".as_ptr(), - ); - main(); - __android_log_print( - 3, b"BloomEngine\0".as_ptr(), - b"nativeMain: main() returned\0".as_ptr(), - ); -} - -/// Forward touch events from the Android UI thread to the engine's input system. -/// Called from BloomGameBridge.nativeOnTouch(action, x, y, pointerIndex). -#[no_mangle] -pub extern "C" fn Java_com_bloomengine_game_BloomGameBridge_nativeOnTouch( - _env: *mut libc::c_void, - _class: *mut libc::c_void, - action: i32, - x: f64, - y: f64, - pointer_index: i32, -) { - bloom_android_on_touch(action, x, y, pointer_index); -} - -/// Signal the engine to close when the Activity is destroyed. -/// Called from BloomGameBridge.nativeOnDestroy(). -#[no_mangle] -pub extern "C" fn Java_com_bloomengine_game_BloomGameBridge_nativeOnDestroy( - _env: *mut libc::c_void, - _class: *mut libc::c_void, -) { - unsafe { - if let Some(eng) = ENGINE.get_mut() { - eng.should_close = true; - } - } -} - -// ============================================================ -// Thread-safe staging (for async asset loading via Perry threads) -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_stage_texture(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(resolve_path(path)) { - Ok(data) => bloom_shared::staging::decode_and_stage_texture(&data), - Err(_) => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_stage_model(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - let data = match std::fs::read(resolve_path(path)) { - Ok(d) => d, - Err(_) => return 0.0, - }; - match bloom_shared::models::load_gltf_staged(&data) { - Some(staged) => bloom_shared::staging::stage_model(staged), - None => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_stage_sound(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - let data = match std::fs::read(resolve_path(path)) { - Ok(d) => d, - Err(_) => return 0.0, - }; - let sound_data = if path.ends_with(".ogg") || path.ends_with(".OGG") { - parse_ogg(&data) - } else if path.ends_with(".mp3") || path.ends_with(".MP3") { - parse_mp3(&data) - } else { - parse_wav(&data) - }; - match sound_data { - Some(sd) => bloom_shared::staging::stage_sound(sd), - None => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_commit_texture(staging_handle: f64) -> f64 { - let staged = match bloom_shared::staging::take_texture(staging_handle) { - Some(s) => s, - None => return 0.0, - }; - let eng = engine(); - let bind_group_idx = eng.renderer.register_texture(staged.width, staged.height, &staged.data); - eng.textures.textures.alloc(bloom_shared::textures::TextureData { - bind_group_idx, width: staged.width, height: staged.height, - }) -} - -#[no_mangle] -pub extern "C" fn bloom_commit_model(staging_handle: f64) -> f64 { - let staged = match bloom_shared::staging::take_model(staging_handle) { - Some(s) => s, - None => return 0.0, - }; - let eng = engine(); - let mut tex_map: Vec = Vec::with_capacity(staged.textures.len()); - for tex in &staged.textures { - tex_map.push(eng.renderer.register_texture(tex.width, tex.height, &tex.data)); - } - let mut model = staged.model; - for mesh in &mut model.meshes { - if let Some(ref mut idx) = mesh.texture_idx { - let staged_idx = *idx as usize; - if staged_idx > 0 && staged_idx <= tex_map.len() { - *idx = tex_map[staged_idx - 1]; - } else { - mesh.texture_idx = None; - } - } - } - eng.models.models.alloc(model) -} - -#[no_mangle] -pub extern "C" fn bloom_commit_sound(staging_handle: f64) -> f64 { - match bloom_shared::staging::take_sound(staging_handle) { - Some(sd) => engine().audio.load_sound(sd), - None => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_commit_music(staging_handle: f64) -> f64 { - match bloom_shared::staging::take_sound(staging_handle) { - Some(sd) => engine().audio.load_music(sd), - None => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_run_game(_callback: extern "C" fn(f64)) { - // No-op on native. The TypeScript runGame() helper provides the while loop. -} - - - -// Q6: Multi-hit picking -static mut LAST_PICK_ALL: Vec = Vec::new(); - -#[no_mangle] -pub extern "C" fn bloom_scene_pick_all(screen_x: f64, screen_y: f64, max_results: f64) -> f64 { - let eng = engine(); - let inv_vp = eng.renderer.inverse_vp_matrix(); - let cam_pos = eng.renderer.camera_pos(); - let w = eng.renderer.width() as f32; - let h = eng.renderer.height() as f32; - let (origin, direction) = bloom_shared::picking::screen_to_ray( - screen_x as f32, screen_y as f32, w, h, &inv_vp, &cam_pos, - ); - let results = bloom_shared::picking::raycast_scene_all(&eng.scene, &origin, &direction, max_results as usize); - let count = results.len(); - unsafe { LAST_PICK_ALL = results; } - count as f64 -} -#[no_mangle] -pub extern "C" fn bloom_pick_all_handle(index: f64) -> f64 { - let i = index as usize; - unsafe { LAST_PICK_ALL.get(i).map(|r| r.handle).unwrap_or(0.0) } -} -#[no_mangle] -pub extern "C" fn bloom_pick_all_distance(index: f64) -> f64 { - let i = index as usize; - unsafe { LAST_PICK_ALL.get(i).map(|r| r.distance as f64).unwrap_or(0.0) } -} -// ============================================================ - -#[no_mangle] pub extern "C" fn bloom_take_screenshot(_path_ptr: *const u8) {} -#[no_mangle] pub extern "C" fn bloom_set_env_clear_from_hdr(_path_ptr: *const u8) {} -#[no_mangle] pub extern "C" fn bloom_set_fog(_r: f64, _g: f64, _b: f64, _density: f64, _height_ref: f64, _height_falloff: f64) {} -#[no_mangle] pub extern "C" fn bloom_set_chromatic_aberration(_strength: f64) {} -#[no_mangle] pub extern "C" fn bloom_set_vignette(_strength: f64, _softness: f64) {} -#[no_mangle] pub extern "C" fn bloom_set_film_grain(_strength: f64) {} -#[no_mangle] pub extern "C" fn bloom_set_sun_shafts(_strength: f64, _decay: f64, _r: f64, _g: f64, _b: f64) {} -#[no_mangle] pub extern "C" fn bloom_set_auto_exposure(_on: f64) {} -#[no_mangle] pub extern "C" fn bloom_set_taa_enabled(_on: f64) {} -#[no_mangle] pub extern "C" fn bloom_set_render_scale(_scale: f64) {} -#[no_mangle] pub extern "C" fn bloom_get_render_scale() -> f64 { 1.0 } -#[no_mangle] pub extern "C" fn bloom_set_upscale_mode(_mode: f64) {} -#[no_mangle] pub extern "C" fn bloom_set_cas_strength(_strength: f64) {} -#[no_mangle] pub extern "C" fn bloom_get_physical_width() -> f64 { 0.0 } -#[no_mangle] pub extern "C" fn bloom_get_physical_height() -> f64 { 0.0 } -#[no_mangle] pub extern "C" fn bloom_set_auto_resolution(_target_hz: f64, _enabled: f64) {} -#[no_mangle] pub extern "C" fn bloom_set_manual_exposure(_value: f64) {} -#[no_mangle] pub extern "C" fn bloom_set_env_intensity(_intensity: f64) {} -#[no_mangle] pub extern "C" fn bloom_set_ssgi_enabled(_enabled: f64) {} -#[no_mangle] pub extern "C" fn bloom_set_ssgi_intensity(_intensity: f64) {} -#[no_mangle] pub extern "C" fn bloom_set_ssgi_radius(_radius: f64) {} -#[no_mangle] pub extern "C" fn bloom_set_dof(_enabled: f64, _focus_distance: f64, _aperture: f64) {} -// Ticket 011: real quality / profiler implementations. Prior build had -// no-op stubs — TS games calling setQualityPreset / setProfilerEnabled etc. -// linked fine but did nothing at runtime on Android. -#[no_mangle] pub extern "C" fn bloom_set_quality_preset(preset: f64) { - engine().renderer.apply_quality_preset(preset as u32); -} -#[no_mangle] pub extern "C" fn bloom_set_shadows_enabled(on: f64) { - engine().renderer.set_shadows_enabled(on != 0.0); -} -#[no_mangle] pub extern "C" fn bloom_set_shadows_always_fresh(on: f64) { - engine().renderer.set_shadows_always_fresh(on != 0.0); -} -#[no_mangle] pub extern "C" fn bloom_set_bloom_enabled(on: f64) { - engine().renderer.set_bloom_enabled(on != 0.0); -} -#[no_mangle] pub extern "C" fn bloom_set_early_z_enabled(_on: f64) {} -#[no_mangle] pub extern "C" fn bloom_set_ssao_enabled(on: f64) { - engine().renderer.set_ssao_enabled(on != 0.0); -} -#[no_mangle] pub extern "C" fn bloom_set_ssao_intensity(value: f64) { - engine().renderer.set_ssao_strength(value as f32); -} -#[no_mangle] pub extern "C" fn bloom_set_ssao_radius(world_radius: f64) { - engine().renderer.set_ssao_radius(world_radius as f32); -} -#[no_mangle] pub extern "C" fn bloom_set_wind(dir_x: f64, dir_z: f64, amplitude: f64, frequency: f64) { - engine().renderer.set_wind(dir_x as f32, dir_z as f32, amplitude as f32, frequency as f32); -} -#[no_mangle] pub extern "C" fn bloom_set_ssr_enabled(on: f64) { - engine().renderer.set_ssr_enabled(on != 0.0); -} -#[no_mangle] pub extern "C" fn bloom_set_motion_blur_enabled(on: f64) { - engine().renderer.set_motion_blur_enabled(on != 0.0); -} -#[no_mangle] pub extern "C" fn bloom_set_sss_enabled(on: f64) { - engine().renderer.set_sss_enabled(on != 0.0); -} -#[no_mangle] pub extern "C" fn bloom_set_profiler_enabled(on: f64) { - engine().profiler.set_enabled(on != 0.0); -} -#[no_mangle] pub extern "C" fn bloom_get_profiler_frame_cpu_us() -> f64 { - engine().profiler.avg_frame_cpu_us() -} -#[no_mangle] pub extern "C" fn bloom_get_profiler_frame_gpu_us() -> f64 { - engine().profiler.avg_frame_gpu_us() -} -#[no_mangle] pub extern "C" fn bloom_print_profiler_summary() { - // Android has no stdout — log the summary via android_log so - // `adb logcat` picks it up alongside the rest of the engine log. - // %s + ptr variant so `%` characters in the summary (none today, - // but cheap safety) aren't interpreted as format specifiers. - let summary = engine().profiler.summary(); - if let Ok(c) = std::ffi::CString::new(summary) { - unsafe { - __android_log_print( - 4, - b"BloomEngine\0".as_ptr(), - b"%s\0".as_ptr(), - c.as_ptr(), - ); - } - } -} - -// ============================================================ -// Physics (Jolt 5.x) — FFI surface generated from shared macro -// ============================================================ - -#[cfg(feature = "jolt")] -#[inline] -fn bloom_jolt_ffi_physics() -> &'static mut bloom_shared::physics_jolt::JoltPhysics { - &mut engine().jolt + 0x00010006 // JNI_VERSION_1_6 } -#[cfg(feature = "jolt")] -bloom_shared::define_physics_ffi!(); - -// === Android FFI parity: ported from native/linux/src/lib.rs (shared renderer/scene) === -// Backing statics for the ported pick/project FFI (mirror native/linux). -static mut LAST_PROJECT: (f64, f64) = (0.0, 0.0); -static mut LAST_PICK: Option = None; - +/// Pass the Android Surface to the engine so it can create a wgpu rendering surface. +/// Called from BloomGameBridge.nativeSetSurface(surface). #[no_mangle] -pub extern "C" fn bloom_add_directional_light( - dx: f64, dy: f64, dz: f64, - r: f64, g: f64, b: f64, - intensity: f64, +pub unsafe extern "C" fn Java_com_bloomengine_game_BloomGameBridge_nativeSetSurface( + env: *mut libc::c_void, + _class: *mut libc::c_void, + surface: *mut libc::c_void, ) { - engine().renderer.add_directional_light( - dx as f32, dy as f32, dz as f32, - r as f32, g as f32, b as f32, - intensity as f32, + let window = ANativeWindow_fromSurface(env, surface); + __android_log_print( + 3, b"BloomEngine\0".as_ptr(), + b"nativeSetSurface: ANativeWindow acquired\0".as_ptr(), ); + bloom_android_set_native_window(window); } +/// Run the compiled game's main() function on the game thread. +/// Called from BloomGameBridge.nativeMain(). #[no_mangle] -pub extern "C" fn bloom_add_point_light( - x: f64, y: f64, z: f64, range: f64, - r: f64, g: f64, b: f64, - intensity: f64, +pub unsafe extern "C" fn Java_com_bloomengine_game_BloomGameBridge_nativeMain( + _env: *mut libc::c_void, + _class: *mut libc::c_void, ) { - engine().renderer.add_point_light( - x as f32, y as f32, z as f32, range as f32, - r as f32, g as f32, b as f32, - intensity as f32, + __android_log_print( + 3, b"BloomEngine\0".as_ptr(), + b"nativeMain: calling main()\0".as_ptr(), ); -} - -#[no_mangle] -pub extern "C" fn bloom_begin_texture_mode(_handle: f64) { - // Stub: no-op until GPU render-to-texture is wired. -} - -#[no_mangle] -pub extern "C" fn bloom_disable_postfx() { - engine().postfx = None; -} - -#[no_mangle] -pub extern "C" fn bloom_disable_shadows() { - engine().renderer.shadow_map.disable(); -} - -#[no_mangle] -pub extern "C" fn bloom_dump_shadow_map(path_ptr: *const u8) { - let path = str_from_header(path_ptr).to_string(); - engine().renderer.dump_shadow_map(&path); -} - -#[no_mangle] -pub extern "C" fn bloom_enable_postfx() { - let eng = engine(); - let w = eng.renderer.width(); - let h = eng.renderer.height(); - let fmt = eng.renderer.surface_format(); - eng.postfx = Some(bloom_shared::postfx::PostFxPipeline::new( - &eng.renderer.device, w, h, fmt, - )); -} - -#[no_mangle] -pub extern "C" fn bloom_enable_shadows() { - engine().renderer.shadow_map.enable(); -} - -#[no_mangle] -pub extern "C" fn bloom_end_texture_mode() { - // Stub: no-op. -} - -// Q9: Generate a ribbon mesh along a Catmull-Rom spline. -#[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; - let wn = width_count as usize; - let points = unsafe { std::slice::from_raw_parts(points_ptr as *const f32, n * 3) }; - let widths = unsafe { std::slice::from_raw_parts(widths_ptr as *const f32, wn) }; - engine().models.gen_mesh_spline_ribbon(points, widths) -} - -#[no_mangle] -pub extern "C" fn bloom_get_render_texture_texture(handle: f64) -> f64 { - engine().textures.get_render_texture_texture(handle) -} - -// Q1: Render texture FFI (stub — GPU implementation deferred). -#[no_mangle] -pub extern "C" fn bloom_load_render_texture(width: f64, height: f64) -> f64 { - engine().textures.load_render_texture(width as u32, height as u32) -} - -#[no_mangle] -pub extern "C" fn bloom_pick_hit_distance() -> f64 { - unsafe { LAST_PICK.as_ref().map(|r| r.distance as f64).unwrap_or(0.0) } -} - -#[no_mangle] -pub extern "C" fn bloom_pick_hit_handle() -> f64 { - unsafe { LAST_PICK.as_ref().map(|r| r.handle).unwrap_or(0.0) } -} - -#[no_mangle] -pub extern "C" fn bloom_pick_hit_normal_x() -> f64 { - unsafe { LAST_PICK.as_ref().map(|r| r.normal[0] as f64).unwrap_or(0.0) } -} - -#[no_mangle] -pub extern "C" fn bloom_pick_hit_normal_y() -> f64 { - unsafe { LAST_PICK.as_ref().map(|r| r.normal[1] as f64).unwrap_or(0.0) } -} - -#[no_mangle] -pub extern "C" fn bloom_pick_hit_normal_z() -> f64 { - unsafe { LAST_PICK.as_ref().map(|r| r.normal[2] as f64).unwrap_or(0.0) } -} - -#[no_mangle] -pub extern "C" fn bloom_pick_hit_x() -> f64 { - unsafe { LAST_PICK.as_ref().map(|r| r.point[0] as f64).unwrap_or(0.0) } -} - -#[no_mangle] -pub extern "C" fn bloom_pick_hit_y() -> f64 { - unsafe { LAST_PICK.as_ref().map(|r| r.point[1] as f64).unwrap_or(0.0) } -} - -#[no_mangle] -pub extern "C" fn bloom_pick_hit_z() -> f64 { - unsafe { LAST_PICK.as_ref().map(|r| r.point[2] as f64).unwrap_or(0.0) } -} - -#[no_mangle] -pub extern "C" fn bloom_postfx_set_hovered(handle: f64) { - if let Some(pfx) = &mut engine().postfx { - pfx.set_hovered(handle); - } -} - -#[no_mangle] -pub extern "C" fn bloom_postfx_set_outline_color(r: f64, g: f64, b: f64, a: f64) { - if let Some(pfx) = &mut engine().postfx { - pfx.outline_params.color_selected = [r as f32, g as f32, b as f32, a as f32]; - } -} - -#[no_mangle] -pub extern "C" fn bloom_postfx_set_outline_thickness(thickness: f64) { - if let Some(pfx) = &mut engine().postfx { - pfx.outline_params.thickness[0] = thickness as f32; - } -} - -#[no_mangle] -pub extern "C" fn bloom_postfx_set_selected(handle: f64) { - if let Some(pfx) = &mut engine().postfx { - if handle == 0.0 { - pfx.set_selected(Vec::new()); - } else { - pfx.set_selected(vec![handle]); - } - } -} - -#[no_mangle] -pub extern "C" fn bloom_profiler_frame_history() -> *const u8 { - let hist = engine().profiler.frame_history(); - let mut s = String::with_capacity(hist.len() * 24); - for (cpu, gpu) in &hist { - s.push_str(&format!("{:.2}|{:.2}\n", cpu, gpu)); - } - alloc_perry_string(&s) -} - -#[no_mangle] -pub extern "C" fn bloom_profiler_overlay_text() -> *const u8 { - let snap = engine().profiler.snapshot(); - let mut s = String::with_capacity(snap.len() * 48); - for (label, cpu, gpu) in &snap { - s.push_str(label); - s.push('|'); - s.push_str(&format!("{:.2}", cpu)); - s.push('|'); - match gpu { - Some(g) => s.push_str(&format!("{:.2}", g)), - None => s.push_str("-1"), - } - s.push('\n'); - } - alloc_perry_string(&s) -} - -#[no_mangle] -pub extern "C" fn bloom_project_screen_y() -> f64 { - unsafe { LAST_PROJECT.1 } -} - -#[no_mangle] -pub extern "C" fn bloom_project_to_screen(wx: f64, wy: f64, wz: f64) -> f64 { - let eng = engine(); - let vp = eng.renderer.vp_matrix(); - let w = eng.renderer.width() as f32; - let h = eng.renderer.height() as f32; - - let x = wx as f32; - let y = wy as f32; - let z = wz as f32; - let clip_x = vp[0][0]*x + vp[1][0]*y + vp[2][0]*z + vp[3][0]; - let clip_y = vp[0][1]*x + vp[1][1]*y + vp[2][1]*z + vp[3][1]; - let clip_w = vp[0][3]*x + vp[1][3]*y + vp[2][3]*z + vp[3][3]; - - if clip_w <= 0.0 { - unsafe { LAST_PROJECT = (-9999.0, -9999.0); } - return -9999.0; - } - - let ndc_x = clip_x / clip_w; - let ndc_y = clip_y / clip_w; - let screen_x = ((ndc_x + 1.0) * 0.5 * w) as f64; - let screen_y = ((1.0 - ndc_y) * 0.5 * h) as f64; - - unsafe { LAST_PROJECT = (screen_x, screen_y); } - screen_x -} - -#[no_mangle] -pub extern "C" fn bloom_register_frame_callback(priority: f64, callback: extern "C" fn(f64)) -> f64 { - engine().frame_callbacks.register(priority as i32, callback) as f64 -} - -#[no_mangle] -pub extern "C" fn bloom_scene_attach_model(node_handle: f64, model_handle: f64, mesh_index: f64) { - let eng = engine(); - let mi = mesh_index as usize; - - let model_data = match eng.models.models.get(model_handle) { - Some(md) => md, - None => return, - }; - if mi >= model_data.meshes.len() { return; } - let mesh = &model_data.meshes[mi]; - - let vertices = mesh.vertices.clone(); - let indices = mesh.indices.clone(); - let base_color_tex = mesh.texture_idx; - let normal_tex = mesh.normal_texture_idx; - let mr_tex = mesh.metallic_roughness_texture_idx; - let emissive_tex = mesh.emissive_texture_idx; - let emissive_factor = mesh.emissive_factor; - eng.scene.update_geometry(node_handle, vertices, indices); - - if let Some(tex_idx) = base_color_tex { - eng.scene.set_material_texture(node_handle, tex_idx); - } - if let Some(tex_idx) = normal_tex { - eng.scene.set_material_normal_texture(node_handle, tex_idx); - } - if let Some(tex_idx) = mr_tex { - eng.scene.set_material_metallic_roughness_texture(node_handle, tex_idx); - } - if let Some(tex_idx) = emissive_tex { - eng.scene.set_material_emissive_texture(node_handle, tex_idx); - } - eng.scene.set_material_emissive_factor( - node_handle, - emissive_factor[0], - emissive_factor[1], - emissive_factor[2], + main(); + __android_log_print( + 3, b"BloomEngine\0".as_ptr(), + b"nativeMain: main() returned\0".as_ptr(), ); } +/// Forward touch events from the Android UI thread to the engine's input system. +/// Called from BloomGameBridge.nativeOnTouch(action, x, y, pointerIndex). #[no_mangle] -pub extern "C" fn bloom_scene_create_node() -> f64 { - engine().scene.create_node() -} - -#[no_mangle] -pub extern "C" fn bloom_scene_destroy_node(handle: f64) { - engine().scene.destroy_node(handle); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_extrude_polygon( - handle: f64, - polygon_ptr: *const f64, - polygon_count: f64, - depth: f64, +pub extern "C" fn Java_com_bloomengine_game_BloomGameBridge_nativeOnTouch( + _env: *mut libc::c_void, + _class: *mut libc::c_void, + action: i32, + x: f64, + y: f64, + pointer_index: i32, ) { - if polygon_ptr.is_null() { return; } - let n = polygon_count as usize; - let polygon = unsafe { std::slice::from_raw_parts(polygon_ptr, n * 2) }; - - let geo = bloom_shared::geometry::extrude_polygon(polygon, &[], depth); - engine().scene.update_geometry(handle, geo.vertices, geo.indices); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_get_bounds_max_x(handle: f64) -> f64 { engine().scene.get_bounds(handle).1[0] as f64 } - -#[no_mangle] -pub extern "C" fn bloom_scene_get_bounds_max_y(handle: f64) -> f64 { engine().scene.get_bounds(handle).1[1] as f64 } - -#[no_mangle] -pub extern "C" fn bloom_scene_get_bounds_max_z(handle: f64) -> f64 { engine().scene.get_bounds(handle).1[2] as f64 } - -#[no_mangle] -pub extern "C" fn bloom_scene_get_bounds_min_x(handle: f64) -> f64 { engine().scene.get_bounds(handle).0[0] as f64 } - -#[no_mangle] -pub extern "C" fn bloom_scene_get_bounds_min_y(handle: f64) -> f64 { engine().scene.get_bounds(handle).0[1] as f64 } - -#[no_mangle] -pub extern "C" fn bloom_scene_get_bounds_min_z(handle: f64) -> f64 { engine().scene.get_bounds(handle).0[2] as f64 } - -// Scene graph QoL — Q4/Q5/Q6/Q7 -#[no_mangle] -pub extern "C" fn bloom_scene_get_transform(handle: f64, index: f64) -> f64 { - let mat = engine().scene.get_transform(handle); - let i = index as usize; - let col = i / 4; - let row = i % 4; - if col < 4 && row < 4 { mat[col][row] as f64 } else { 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_scene_get_user_data(handle: f64) -> f64 { engine().scene.get_user_data(handle) as f64 } - -#[no_mangle] -pub extern "C" fn bloom_scene_node_count() -> f64 { - engine().scene.node_count() as f64 -} - -#[no_mangle] -pub extern "C" fn bloom_scene_node_index_count(handle: f64) -> f64 { - match engine().scene.nodes.get(handle) { - Some(node) => node.indices.len() as f64, - None => -1.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_scene_node_vertex_count(handle: f64) -> f64 { - match engine().scene.nodes.get(handle) { - Some(node) => node.vertices.len() as f64, - None => -1.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_scene_pick(screen_x: f64, screen_y: f64) -> f64 { - let eng = engine(); - let inv_vp = eng.renderer.inverse_vp_matrix(); - let cam_pos = eng.renderer.camera_pos(); - let w = eng.renderer.width() as f32; - let h = eng.renderer.height() as f32; - - let (origin, direction) = bloom_shared::picking::screen_to_ray( - screen_x as f32, screen_y as f32, - w, h, &inv_vp, &cam_pos, - ); - - let result = bloom_shared::picking::raycast_scene(&eng.scene, &origin, &direction); - let hit = result.hit; - unsafe { LAST_PICK = Some(result); } - if hit { 1.0 } else { 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_scene_set_cast_shadow(handle: f64, cast: f64) { - engine().scene.set_cast_shadow(handle, cast != 0.0); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_set_material_color(handle: f64, r: f64, g: f64, b: f64, a: f64) { - engine().scene.set_material_color(handle, r as f32, g as f32, b as f32, a as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_set_material_pbr(handle: f64, roughness: f64, metalness: f64) { - engine().scene.set_material_pbr(handle, roughness as f32, metalness as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_set_material_texture(handle: f64, texture_idx: f64) { - engine().scene.set_material_texture(handle, texture_idx as u32); -} - -// Q8: Set a water material on a scene node (translucent tint, low roughness). -#[no_mangle] -pub extern "C" fn bloom_scene_set_material_water(handle: f64, wave_amp: f64, wave_speed: f64, r: f64, g: f64, b: f64, a: f64) { - engine().scene.set_material_water(handle, wave_amp as f32, wave_speed as f32, r as f32, g as f32, b as f32, a as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_set_parent(handle: f64, parent: f64) { - engine().scene.set_parent(handle, parent); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_set_receive_shadow(handle: f64, receive: f64) { - engine().scene.set_receive_shadow(handle, receive != 0.0); + bloom_android_on_touch(action, x, y, pointer_index); } +/// Signal the engine to close when the Activity is destroyed. +/// Called from BloomGameBridge.nativeOnDestroy(). #[no_mangle] -pub extern "C" fn bloom_scene_set_transform(handle: f64, mat_ptr: *const f64) { - if mat_ptr.is_null() { return; } - let slice = unsafe { std::slice::from_raw_parts(mat_ptr, 16) }; - let mut mat = [[0.0f32; 4]; 4]; - for col in 0..4 { - for row in 0..4 { - mat[col][row] = slice[col * 4 + row] as f32; +pub extern "C" fn Java_com_bloomengine_game_BloomGameBridge_nativeOnDestroy( + _env: *mut libc::c_void, + _class: *mut libc::c_void, +) { + unsafe { + if let Some(eng) = ENGINE.get_mut() { + eng.should_close = true; } } - engine().scene.set_transform(handle, mat); } -#[no_mangle] -pub extern "C" fn bloom_scene_set_user_data(handle: f64, data: f64) { engine().scene.set_user_data(handle, data as i64); } - -#[no_mangle] -pub extern "C" fn bloom_scene_set_visible(handle: f64, visible: f64) { - engine().scene.set_visible(handle, visible != 0.0); -} +// ============================================================ +// Thread-safe staging (for async asset loading via Perry threads) +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_scene_subtract_box( - handle: f64, - min_x: f64, min_y: f64, min_z: f64, - max_x: f64, max_y: f64, max_z: f64, -) { - let eng = engine(); - if let Some(node) = eng.scene.nodes.get(handle) { - let current = bloom_shared::geometry::GeometryData { - vertices: node.vertices.clone(), - indices: node.indices.clone(), - }; - let result = bloom_shared::geometry::subtract_box( - ¤t, - [min_x as f32, min_y as f32, min_z as f32], - [max_x as f32, max_y as f32, max_z as f32], - ); - eng.scene.update_geometry(handle, result.vertices, result.indices); - } -} -#[no_mangle] -pub extern "C" fn bloom_scene_update_geometry( - handle: f64, - vert_ptr: *const f64, - vert_count: f64, - idx_ptr: *const f64, - idx_count: f64, -) { - if vert_ptr.is_null() || idx_ptr.is_null() { return; } - let nv = vert_count as usize; - let ni = idx_count as usize; - - let vert_floats = unsafe { std::slice::from_raw_parts(vert_ptr, nv * 12) }; - let idx_floats = unsafe { std::slice::from_raw_parts(idx_ptr, ni) }; - - let mut vertices = Vec::with_capacity(nv); - for i in 0..nv { - let base = i * 12; - vertices.push(bloom_shared::renderer::Vertex3D { - position: [vert_floats[base] as f32, vert_floats[base+1] as f32, vert_floats[base+2] as f32], - normal: [vert_floats[base+3] as f32, vert_floats[base+4] as f32, vert_floats[base+5] as f32], - color: [vert_floats[base+6] as f32, vert_floats[base+7] as f32, vert_floats[base+8] as f32, vert_floats[base+9] as f32], - uv: [vert_floats[base+10] as f32, vert_floats[base+11] as f32], - joints: [0.0; 4], - weights: [0.0; 4], - tangent: [0.0; 4], - }); - } - let indices: Vec = idx_floats.iter().map(|&v| v as u32).collect(); +// Q6: Multi-hit picking +// ============================================================ - engine().scene.update_geometry(handle, vertices, indices); -} +// ============================================================ +// Physics (Jolt 5.x) — FFI surface generated from shared macro +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_splat_impulse(x: f64, z: f64, radius: f64, strength: f64) { - engine().renderer.impulse_field.submit_splat( - x as f32, z as f32, radius as f32, strength as f32, - ); +#[cfg(feature = "jolt")] +#[inline] +fn bloom_jolt_ffi_physics() -> &'static mut bloom_shared::physics_jolt::JoltPhysics { + &mut engine().jolt } -#[no_mangle] -pub extern "C" fn bloom_unload_render_texture(handle: f64) { - engine().textures.unload_render_texture(handle); -} +#[cfg(feature = "jolt")] +bloom_shared::define_physics_ffi!(); -#[no_mangle] -pub extern "C" fn bloom_unregister_frame_callback(id: f64) { - engine().frame_callbacks.unregister(id as u64); -} +// === Android FFI parity: ported from native/linux/src/lib.rs (shared renderer/scene) === +// Backing statics for the ported pick/project FFI (mirror native/linux). diff --git a/native/ios/src/lib.rs b/native/ios/src/lib.rs index b906390..ce2c55c 100644 --- a/native/ios/src/lib.rs +++ b/native/ios/src/lib.rs @@ -1,7 +1,15 @@ +// `static mut` is intentional throughout this FFI surface — Perry calls +// us from a single OS thread (the UIKit main run-loop), so the engine +// singleton + view/window scratch state never race. The 2024 lint +// flagging `&UI_VIEW`-style accesses is a real concern in +// multi-threaded code, but inapplicable here. Suppress at the crate +// root to avoid a dozen noise lines in every build. Mirrors +// native/macos/src/lib.rs. +#![allow(static_mut_refs)] + use bloom_shared::engine::EngineState; use bloom_shared::renderer::Renderer; -use bloom_shared::string_header::{str_from_header, alloc_perry_string}; -use bloom_shared::audio::{parse_wav, parse_ogg, parse_mp3, SoundData}; +use bloom_shared::string_header::str_from_header; use objc2::encode::{Encode, Encoding, RefEncode}; use objc2::rc::{Allocated, Retained}; @@ -41,6 +49,16 @@ fn resolve_path(path: &str) -> String { fn engine() -> &'static mut EngineState { unsafe { ENGINE.get_mut().expect("Engine not initialized") } } +/// Asset-path hook for define_core_ffi! — routes through this platform's +/// resolve_path (relative paths don't resolve from the app working dir here). +fn bloom_resolve_asset_path(path: &str) -> std::borrow::Cow<'_, str> { + std::borrow::Cow::Owned(resolve_path(path)) +} + +// The full shared (non-physics) FFI surface. See bloom_shared::ffi_core +// docs for the contract; tools/validate-ffi.js checks parity in CI. +bloom_shared::define_core_ffi!(); + // ============================================================ // CG types with objc2 Encode @@ -334,6 +352,11 @@ unsafe extern "C" fn scene_will_connect( if supported.contains(wgpu::Features::TIMESTAMP_QUERY) { required_features |= wgpu::Features::TIMESTAMP_QUERY; } + // Cooked BC7 textures (bloom-cook) upload compressed when the + // adapter has BC support; without it they CPU-decode at load. + if supported.contains(wgpu::Features::TEXTURE_COMPRESSION_BC) { + required_features |= wgpu::Features::TEXTURE_COMPRESSION_BC; + } if !force_sw_gi && supported.contains(rt_mask) { required_features |= rt_mask; } @@ -529,6 +552,11 @@ pub unsafe extern "C" fn perry_scene_will_connect(scene: *const c_void) { if supported.contains(wgpu::Features::TIMESTAMP_QUERY) { required_features |= wgpu::Features::TIMESTAMP_QUERY; } + // Cooked BC7 textures (bloom-cook) upload compressed when the + // adapter has BC support; without it they CPU-decode at load. + if supported.contains(wgpu::Features::TEXTURE_COMPRESSION_BC) { + required_features |= wgpu::Features::TEXTURE_COMPRESSION_BC; + } if !force_sw_gi && supported.contains(rt_mask) { required_features |= rt_mask; } @@ -748,2171 +776,331 @@ pub extern "C" fn bloom_end_drawing() { engine().end_frame(); } -#[no_mangle] -pub extern "C" fn bloom_clear_background(r: f64, g: f64, b: f64, a: f64) { - engine().renderer.set_clear_color(r, g, b, a); -} +// ============================================================ +// Keyboard input +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_set_target_fps(fps: f64) { engine().target_fps = fps; } +// ============================================================ +// Mouse input +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_set_direct_2d_mode(on: f64) { engine().direct_2d_mode = on > 0.5; } +// ============================================================ +// Shape drawing +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_get_delta_time() -> f64 { engine().delta_time } +// ============================================================ +// Text +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_get_fps() -> f64 { engine().get_fps() } +// ============================================================ +// Textures +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_get_screen_width() -> f64 { engine().screen_width() } +// ============================================================ +// Camera 2D +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_get_screen_height() -> f64 { engine().screen_height() } +// ============================================================ +// 3D Camera and Drawing +// ============================================================ // ============================================================ -// Keyboard input +// Joint test (skeletal animation debug) // ============================================================ -#[no_mangle] -pub extern "C" fn bloom_is_key_pressed(key: f64) -> f64 { - if engine().input.is_key_pressed(key as usize) { 1.0 } else { 0.0 } -} +// ============================================================ +// Lighting +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_is_key_down(key: f64) -> f64 { - if engine().input.is_key_down(key as usize) { 1.0 } else { 0.0 } -} +// ============================================================ +// Models +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_is_key_released(key: f64) -> f64 { - if engine().input.is_key_released(key as usize) { 1.0 } else { 0.0 } -} +// ============================================================ +// Phase 1c — material system FFI +// ============================================================ // ============================================================ -// Mouse input +// CoreAudio (iOS) — RemoteIO Audio Unit // ============================================================ -#[no_mangle] -pub extern "C" fn bloom_get_mouse_x() -> f64 { engine().input.mouse_x } +type AudioUnit = *mut c_void; +type OSStatus = i32; +type AudioUnitPropertyID = u32; +type AudioUnitScope = u32; +type AudioUnitElement = u32; -#[no_mangle] -pub extern "C" fn bloom_get_mouse_y() -> f64 { engine().input.mouse_y } +#[repr(C)] +#[derive(Clone, Copy)] +struct AudioComponentDescription { + component_type: u32, + component_sub_type: u32, + component_manufacturer: u32, + component_flags: u32, + component_flags_mask: u32, +} -#[no_mangle] -pub extern "C" fn bloom_is_mouse_button_pressed(btn: f64) -> f64 { - if engine().input.is_mouse_button_pressed(btn as usize) { 1.0 } else { 0.0 } +#[repr(C)] +#[derive(Clone, Copy)] +struct AudioStreamBasicDescription { + sample_rate: f64, + format_id: u32, + format_flags: u32, + bytes_per_packet: u32, + frames_per_packet: u32, + bytes_per_frame: u32, + channels_per_frame: u32, + bits_per_channel: u32, + reserved: u32, } -#[no_mangle] -pub extern "C" fn bloom_is_mouse_button_down(btn: f64) -> f64 { - if engine().input.is_mouse_button_down(btn as usize) { 1.0 } else { 0.0 } +#[repr(C)] +struct AudioBufferList { + number_buffers: u32, + buffers: [AudioBufferData; 1], } -#[no_mangle] -pub extern "C" fn bloom_is_mouse_button_released(btn: f64) -> f64 { - if engine().input.is_mouse_button_released(btn as usize) { 1.0 } else { 0.0 } +#[repr(C)] +struct AudioBufferData { + number_channels: u32, + data_byte_size: u32, + data: *mut c_void, } -// ============================================================ -// Shape drawing -// ============================================================ +type AURenderCallback = unsafe extern "C" fn( + in_ref_con: *mut c_void, + io_action_flags: *mut u32, + in_time_stamp: *const c_void, + in_bus_number: u32, + in_number_frames: u32, + io_data: *mut AudioBufferList, +) -> OSStatus; -#[no_mangle] -pub extern "C" fn bloom_draw_line(x1: f64, y1: f64, x2: f64, y2: f64, thickness: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_line(x1, y1, x2, y2, thickness, r, g, b, a); +#[repr(C)] +struct AURenderCallbackStruct { + input_proc: AURenderCallback, + input_proc_ref_con: *mut c_void, } -#[no_mangle] -pub extern "C" fn bloom_draw_rect(x: f64, y: f64, w: f64, h: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_rect(x, y, w, h, r, g, b, a); -} +type AudioComponent = *mut c_void; -#[no_mangle] -pub extern "C" fn bloom_draw_rect_lines(x: f64, y: f64, w: f64, h: f64, thickness: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_rect_lines(x, y, w, h, thickness, r, g, b, a); +#[link(name = "AudioToolbox", kind = "framework")] +extern "C" { + fn AudioComponentFindNext(component: AudioComponent, desc: *const AudioComponentDescription) -> AudioComponent; + fn AudioComponentInstanceNew(component: AudioComponent, out: *mut AudioUnit) -> OSStatus; + fn AudioUnitSetProperty( + unit: AudioUnit, + property_id: AudioUnitPropertyID, + scope: AudioUnitScope, + element: AudioUnitElement, + data: *const c_void, + data_size: u32, + ) -> OSStatus; + fn AudioUnitInitialize(unit: AudioUnit) -> OSStatus; + fn AudioOutputUnitStart(unit: AudioUnit) -> OSStatus; + fn AudioOutputUnitStop(unit: AudioUnit) -> OSStatus; + fn AudioComponentInstanceDispose(unit: AudioUnit) -> OSStatus; } -#[no_mangle] -pub extern "C" fn bloom_draw_circle(cx: f64, cy: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_circle(cx, cy, radius, r, g, b, a); -} +const K_AUDIO_UNIT_TYPE_OUTPUT: u32 = u32::from_be_bytes(*b"auou"); +const K_AUDIO_UNIT_SUB_TYPE_REMOTE_IO: u32 = u32::from_be_bytes(*b"rioc"); +const K_AUDIO_UNIT_MANUFACTURER_APPLE: u32 = u32::from_be_bytes(*b"appl"); -#[no_mangle] -pub extern "C" fn bloom_draw_circle_lines(cx: f64, cy: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_circle_lines(cx, cy, radius, r, g, b, a); +const K_AUDIO_UNIT_PROPERTY_STREAM_FORMAT: AudioUnitPropertyID = 8; +const K_AUDIO_UNIT_PROPERTY_SET_RENDER_CALLBACK: AudioUnitPropertyID = 23; +const K_AUDIO_UNIT_SCOPE_INPUT: AudioUnitScope = 1; + +const K_AUDIO_FORMAT_LINEAR_PCM: u32 = u32::from_be_bytes(*b"lpcm"); +const K_AUDIO_FORMAT_FLAG_IS_FLOAT: u32 = 1; +const K_AUDIO_FORMAT_FLAG_IS_PACKED: u32 = 8; + +struct AudioUnitInstance { unit: AudioUnit } +unsafe impl Send for AudioUnitInstance {} +unsafe impl Sync for AudioUnitInstance {} + +static mut AUDIO_UNIT: Option = None; +// Render half of the audio system — owned by the CoreAudio render thread +// after bloom_init_audio hands it off via AudioMixer::take_renderer. +// See native/shared/src/audio/mod.rs for the threading contract. +static mut AUDIO_RENDERER: Option = None; + +unsafe extern "C" fn audio_render_callback( + _in_ref_con: *mut c_void, + _io_action_flags: *mut u32, + _in_time_stamp: *const c_void, + _in_bus_number: u32, + in_number_frames: u32, + io_data: *mut AudioBufferList, +) -> OSStatus { + let buffer_list = &mut *io_data; + let buffer = &mut buffer_list.buffers[0]; + let num_samples = in_number_frames as usize * 2; + let output = std::slice::from_raw_parts_mut(buffer.data as *mut f32, num_samples); + match AUDIO_RENDERER.as_mut() { + Some(r) => r.mix(output), + None => output.iter_mut().for_each(|s| *s = 0.0), + } + 0 } #[no_mangle] -pub extern "C" fn bloom_draw_triangle(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_triangle(x1, y1, x2, y2, x3, y3, r, g, b, a); +pub extern "C" fn bloom_init_audio() { + unsafe { + // Hand the render half to the audio thread before the callback + // can fire. Idempotent: a second init keeps the existing renderer. + if AUDIO_RENDERER.is_none() { + AUDIO_RENDERER = engine().audio.take_renderer(); + } + let desc = AudioComponentDescription { + component_type: K_AUDIO_UNIT_TYPE_OUTPUT, + component_sub_type: K_AUDIO_UNIT_SUB_TYPE_REMOTE_IO, + component_manufacturer: K_AUDIO_UNIT_MANUFACTURER_APPLE, + component_flags: 0, + component_flags_mask: 0, + }; + + let component = AudioComponentFindNext(std::ptr::null_mut(), &desc); + if component.is_null() { return; } + + let mut unit: AudioUnit = std::ptr::null_mut(); + if AudioComponentInstanceNew(component, &mut unit) != 0 { return; } + + let stream_desc = AudioStreamBasicDescription { + sample_rate: 44100.0, + format_id: K_AUDIO_FORMAT_LINEAR_PCM, + format_flags: K_AUDIO_FORMAT_FLAG_IS_FLOAT | K_AUDIO_FORMAT_FLAG_IS_PACKED, + bytes_per_packet: 8, + frames_per_packet: 1, + bytes_per_frame: 8, + channels_per_frame: 2, + bits_per_channel: 32, + reserved: 0, + }; + + AudioUnitSetProperty( + unit, K_AUDIO_UNIT_PROPERTY_STREAM_FORMAT, K_AUDIO_UNIT_SCOPE_INPUT, 0, + &stream_desc as *const _ as *const c_void, + std::mem::size_of::() as u32, + ); + + let callback_struct = AURenderCallbackStruct { + input_proc: audio_render_callback, + input_proc_ref_con: std::ptr::null_mut(), + }; + + AudioUnitSetProperty( + unit, K_AUDIO_UNIT_PROPERTY_SET_RENDER_CALLBACK, K_AUDIO_UNIT_SCOPE_INPUT, 0, + &callback_struct as *const _ as *const c_void, + std::mem::size_of::() as u32, + ); + + AudioUnitInitialize(unit); + AudioOutputUnitStart(unit); + AUDIO_UNIT = Some(AudioUnitInstance { unit }); + } } #[no_mangle] -pub extern "C" fn bloom_draw_poly(cx: f64, cy: f64, sides: f64, radius: f64, rotation: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_poly(cx, cy, sides, radius, rotation, r, g, b, a); +pub extern "C" fn bloom_close_audio() { + unsafe { + if let Some(au) = AUDIO_UNIT.take() { + AudioOutputUnitStop(au.unit); + AudioComponentInstanceDispose(au.unit); + } + } } // ============================================================ -// Text +// Music // ============================================================ -#[no_mangle] -pub extern "C" fn bloom_draw_text(text_ptr: *const u8, x: f64, y: f64, size: f64, r: f64, g: f64, b: f64, a: f64) { - let text = str_from_header(text_ptr); - let eng = engine(); - let mut text_renderer = std::mem::replace(&mut eng.text, bloom_shared::text_renderer::TextRenderer::empty()); - text_renderer.draw_text(&mut eng.renderer, text, x, y, size as u32, r, g, b, a); - eng.text = text_renderer; -} +// ============================================================ +// Gamepad input +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_measure_text(text_ptr: *const u8, size: f64) -> f64 { - let text = str_from_header(text_ptr); - engine().text.measure_text(text, size as u32) -} +// ============================================================ +// Touch input +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_load_font(path_ptr: *const u8, _size: f64) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(resolve_path(path)) { Ok(data) => engine().text.load_font(&data) as f64, Err(_) => 0.0 } -} +// ============================================================ +// Utility +// ============================================================ #[no_mangle] -pub extern "C" fn bloom_unload_font(font_handle: f64) { - engine().text.unload_font(font_handle as usize); -} +pub extern "C" fn bloom_toggle_fullscreen() {} #[no_mangle] -pub extern "C" fn bloom_draw_text_ex(font_handle: f64, text_ptr: *const u8, x: f64, y: f64, size: f64, spacing: f64, r: f64, g: f64, b: f64, a: f64) { - let text = str_from_header(text_ptr); - let eng = engine(); - let mut text_renderer = std::mem::replace(&mut eng.text, bloom_shared::text_renderer::TextRenderer::empty()); - text_renderer.draw_text_ex(&mut eng.renderer, font_handle as usize, text, x, y, size as u32, spacing as f32, r, g, b, a); - eng.text = text_renderer; -} +pub extern "C" fn bloom_set_window_title(_title_ptr: *const u8) {} #[no_mangle] -pub extern "C" fn bloom_measure_text_ex(font_handle: f64, text_ptr: *const u8, size: f64, spacing: f64) -> f64 { - let text = str_from_header(text_ptr); - engine().text.measure_text_ex(font_handle as usize, text, size as u32, spacing as f32) -} - -// ============================================================ -// Textures -// ============================================================ +pub extern "C" fn bloom_set_window_icon(_path_ptr: *const u8) {} #[no_mangle] -pub extern "C" fn bloom_load_texture(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(resolve_path(path)) { - Ok(data) => { - let eng = engine(); - let renderer_ptr = &mut eng.renderer as *mut Renderer; - eng.textures.load_texture(unsafe { &mut *renderer_ptr }, &data) - } - Err(_) => 0.0, - } +pub extern "C" fn bloom_disable_cursor() { + engine().input.cursor_disabled = true; } #[no_mangle] -pub extern "C" fn bloom_unload_texture(handle: f64) { - let eng = engine(); - let renderer_ptr = &mut eng.renderer as *mut Renderer; - eng.textures.unload_texture(handle, unsafe { &mut *renderer_ptr }); +pub extern "C" fn bloom_enable_cursor() { + engine().input.cursor_disabled = false; } +// E4: Clipboard (stub on this platform) #[no_mangle] -pub extern "C" fn bloom_draw_texture(handle: f64, x: f64, y: f64, r: f64, g: f64, b: f64, a: f64) { - let eng = engine(); - if let Some(tex) = eng.textures.get(handle) { - let idx = tex.bind_group_idx; - eng.renderer.draw_texture(idx, x, y, r, g, b, a); - } -} - -#[no_mangle] -pub extern "C" fn bloom_draw_texture_pro( - handle: f64, - src_x: f64, src_y: f64, src_w: f64, src_h: f64, - dst_x: f64, dst_y: f64, dst_w: f64, dst_h: f64, - origin_x: f64, origin_y: f64, rotation: f64, - r: f64, g: f64, b: f64, a: f64, -) { - let eng = engine(); - if let Some(tex) = eng.textures.get(handle) { - let idx = tex.bind_group_idx; - eng.renderer.draw_texture_pro(idx, src_x, src_y, src_w, src_h, dst_x, dst_y, dst_w, dst_h, origin_x, origin_y, rotation, r, g, b, a); - } -} - -#[no_mangle] -pub extern "C" fn bloom_draw_texture_rec( - handle: f64, - src_x: f64, src_y: f64, src_w: f64, src_h: f64, - dst_x: f64, dst_y: f64, - r: f64, g: f64, b: f64, a: f64, -) { - let eng = engine(); - if let Some(tex) = eng.textures.get(handle) { - let idx = tex.bind_group_idx; - eng.renderer.draw_texture_rec(idx, src_x, src_y, src_w, src_h, dst_x, dst_y, r, g, b, a); - } -} - -#[no_mangle] -pub extern "C" fn bloom_get_texture_width(handle: f64) -> f64 { - engine().textures.get(handle).map(|t| t.width as f64).unwrap_or(0.0) -} - -#[no_mangle] -pub extern "C" fn bloom_get_texture_height(handle: f64) -> f64 { - engine().textures.get(handle).map(|t| t.height as f64).unwrap_or(0.0) -} - -#[no_mangle] -pub extern "C" fn bloom_gen_texture_mipmaps(_handle: f64) {} - -#[no_mangle] -pub extern "C" fn bloom_set_texture_filter(handle: f64, mode: f64) { - let eng = engine(); - if let Some(tex) = eng.textures.get(handle) { - let bind_group_idx = tex.bind_group_idx; - eng.renderer.set_texture_filter(bind_group_idx, mode > 0.5); - } -} - -#[no_mangle] -pub extern "C" fn bloom_load_image(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(resolve_path(path)) { - Ok(data) => engine().textures.load_image(&data), - Err(_) => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_image_resize(handle: f64, w: f64, h: f64) { - engine().textures.image_resize(handle, w as u32, h as u32); -} - -#[no_mangle] -pub extern "C" fn bloom_image_crop(handle: f64, x: f64, y: f64, w: f64, h: f64) { - engine().textures.image_crop(handle, x as u32, y as u32, w as u32, h as u32); -} - +pub extern "C" fn bloom_set_clipboard_text(_text_ptr: *const u8) {} #[no_mangle] -pub extern "C" fn bloom_image_flip_h(handle: f64) { - engine().textures.image_flip_h(handle); -} +pub extern "C" fn bloom_get_clipboard_text() -> *const u8 { std::ptr::null() } +// E5b: File dialogs (stub on this platform) #[no_mangle] -pub extern "C" fn bloom_image_flip_v(handle: f64) { - engine().textures.image_flip_v(handle); -} - +pub extern "C" fn bloom_open_file_dialog(_filter_ptr: *const u8, _title_ptr: *const u8) -> *const u8 { std::ptr::null() } #[no_mangle] -pub extern "C" fn bloom_load_texture_from_image(handle: f64) -> f64 { - let eng = engine(); - let renderer_ptr = &mut eng.renderer as *mut Renderer; - eng.textures.load_texture_from_image(handle, unsafe { &mut *renderer_ptr }) -} +pub extern "C" fn bloom_save_file_dialog(_default_name_ptr: *const u8, _title_ptr: *const u8) -> *const u8 { std::ptr::null() } // ============================================================ -// Camera 2D +// Input injection + platform detection // ============================================================ - #[no_mangle] -pub extern "C" fn bloom_begin_mode_2d(offset_x: f64, offset_y: f64, target_x: f64, target_y: f64, rotation: f64, zoom: f64) { - engine().renderer.begin_mode_2d( - offset_x as f32, offset_y as f32, - target_x as f32, target_y as f32, - rotation as f32, zoom as f32, - ); -} +pub extern "C" fn bloom_get_platform() -> f64 { 2.0 } +/// Preferred OS language packed as `c0*256+c1` (ISO-639 primary subtag). See macos lib for format. #[no_mangle] -pub extern "C" fn bloom_end_mode_2d() { - engine().renderer.end_mode_2d(); +pub extern "C" fn bloom_get_language() -> f64 { + fn pack(code: &str) -> f64 { let l = code.to_ascii_lowercase(); let b = l.as_bytes(); if b.len() >= 2 { (b[0] as f64) * 256.0 + (b[1] as f64) } else { 25966.0 } } + let langs = objc2_foundation::NSLocale::preferredLanguages(); + match langs.firstObject() { Some(s) => pack(&s.to_string()), None => 25966.0 } } // ============================================================ -// 3D Camera and Drawing +// Thread-safe staging (for async asset loading via Perry threads) // ============================================================ -#[no_mangle] -pub extern "C" fn bloom_begin_mode_3d( - pos_x: f64, pos_y: f64, pos_z: f64, - target_x: f64, target_y: f64, target_z: f64, - up_x: f64, up_y: f64, up_z: f64, - fovy: f64, projection: f64, -) { - engine().renderer.begin_mode_3d( - pos_x as f32, pos_y as f32, pos_z as f32, - target_x as f32, target_y as f32, target_z as f32, - up_x as f32, up_y as f32, up_z as f32, - fovy as f32, projection as f32, - ); -} - -#[no_mangle] -pub extern "C" fn bloom_end_mode_3d() { - engine().renderer.end_mode_3d(); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_cube(x: f64, y: f64, z: f64, w: f64, h: f64, d: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_cube(x, y, z, w, h, d, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_cube_wires(x: f64, y: f64, z: f64, w: f64, h: f64, d: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_cube_wires(x, y, z, w, h, d, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_sphere(cx: f64, cy: f64, cz: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_sphere(cx, cy, cz, radius, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_sphere_wires(cx: f64, cy: f64, cz: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_sphere_wires(cx, cy, cz, radius, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_cylinder(x: f64, y: f64, z: f64, radius_top: f64, radius_bottom: f64, height: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_cylinder(x, y, z, radius_top, radius_bottom, height, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_plane(cx: f64, cy: f64, cz: f64, w: f64, d: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_plane(cx, cy, cz, w, d, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_grid(slices: f64, spacing: f64) { - engine().renderer.draw_grid(slices as i32, spacing); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_ray(origin_x: f64, origin_y: f64, origin_z: f64, dir_x: f64, dir_y: f64, dir_z: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_ray(origin_x, origin_y, origin_z, dir_x, dir_y, dir_z, r, g, b, a); -} +// Q6: Multi-hit picking // ============================================================ -// Joint test (skeletal animation debug) -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_set_joint_test(_joint: f64, _angle: f64) { - // No-op for now — skeletal animation testing -} // ============================================================ -// Lighting +// Render quality toggles (individual + preset) — ticket 011 +// Mirror of the macOS FFI surface added in commit 95da6af; previously +// macOS-only, now exposed on every native platform so non-macOS builds +// don't fail at runtime (missing symbol) when the TS API invokes them. // ============================================================ -#[no_mangle] -pub extern "C" fn bloom_set_ambient_light(r: f64, g: f64, b: f64, intensity: f64) { - engine().renderer.set_ambient_light(r, g, b, intensity); -} - -#[no_mangle] -pub extern "C" fn bloom_set_directional_light(dx: f64, dy: f64, dz: f64, r: f64, g: f64, b: f64, intensity: f64) { - engine().renderer.set_directional_light(dx, dy, dz, r, g, b, intensity); -} - -#[no_mangle] -pub extern "C" fn bloom_set_procedural_sky(enabled: f64, rayleigh_density: f64, mie_density: f64, ground_albedo: f64) { - engine().renderer.set_procedural_sky( - enabled != 0.0, - rayleigh_density as f32, - mie_density as f32, - ground_albedo as f32, - ); -} - -#[no_mangle] -pub extern "C" fn bloom_set_sun_direction(dx: f64, dy: f64, dz: f64, intensity: f64) { - engine().renderer.set_sun_direction(dx as f32, dy as f32, dz as f32, intensity as f32); -} - // ============================================================ -// Models +// Profiler — CPU phase timings (always available) + GPU timestamps +// (when the adapter supports TIMESTAMP_QUERY). Disabled by default. // ============================================================ -#[no_mangle] -pub extern "C" fn bloom_load_model(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(resolve_path(path)) { - Ok(data) => { - let eng = engine(); - let renderer_ptr = &mut eng.renderer as *mut crate::Renderer; - eng.models.load_model_with_textures(&data, unsafe { &mut *renderer_ptr }) - } - Err(_) => 0.0, - } -} - -#[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(); - if let Some(model) = eng.models.get(handle) { - let tint = [(r / 255.0) as f32, (g / 255.0) as f32, (b / 255.0) as f32, (a / 255.0) as f32]; - let position = [x as f32, y as f32, z as f32]; - let handle_bits = handle.to_bits(); - if eng.renderer.cache_model_if_static(handle_bits, &model.meshes) { - eng.renderer.draw_model_cached(handle_bits, position, scale as f32, tint); - } else { - for mesh in &model.meshes { - let tex_idx = mesh.texture_idx.unwrap_or(0); - eng.renderer.draw_model_mesh_tinted(&mesh.vertices, &mesh.indices, position, scale as f32, tint, tex_idx); - } - } - } -} - -#[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, -) { - let bits = color_packed_argb as u32; - let a = ((bits >> 24) & 0xff) as f32 / 255.0; - let r = ((bits >> 16) & 0xff) as f32 / 255.0; - let g = ((bits >> 8) & 0xff) as f32 / 255.0; - let b = ( bits & 0xff) as f32 / 255.0; - let eng = engine(); - if let Some(model) = eng.models.get(handle) { - let position = [x as f32, y as f32, z as f32]; - let scale = scale as f32; - let tint = [r, g, b, a]; - for mesh in &model.meshes { - let tex_idx = mesh.texture_idx.unwrap_or(0); - eng.renderer.draw_model_mesh_tinted_rotated( - &mesh.vertices, &mesh.indices, position, scale, tint, tex_idx, rot_y as f32, - ); - } - } -} - -#[no_mangle] -pub extern "C" fn bloom_unload_model(handle: f64) { - engine().models.unload_model(handle); -} - -#[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) -} - -#[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(); - if let Some(img) = eng.textures.images.get(image_handle) { - let data = img.data.clone(); - let w = img.width; - let h = img.height; - eng.models.gen_mesh_heightmap(&data, w, h, size_x as f32, size_y as f32, size_z as f32) - } else { - 0.0 - } -} - -#[no_mangle] -pub extern "C" fn bloom_load_shader(source_ptr: *const u8) -> f64 { - let source = str_from_header(source_ptr); - engine().renderer.load_custom_shader(source) as f64 -} - -#[no_mangle] -pub extern "C" fn bloom_create_mesh(vertex_ptr: *const f32, vertex_count: f64, index_ptr: *const u32, index_count: f64) -> f64 { - if vertex_ptr.is_null() || index_ptr.is_null() { return 0.0; } - let vcount = vertex_count as usize; - let icount = index_count as usize; - let vertex_data = unsafe { std::slice::from_raw_parts(vertex_ptr, vcount * 12) }; - let index_data = unsafe { std::slice::from_raw_parts(index_ptr, icount) }; - engine().models.create_mesh(vertex_data, index_data) -} - // ============================================================ -// Phase 1c — material system FFI +// Physics (Jolt 5.x) — FFI surface generated from shared macro // ============================================================ -#[no_mangle] -pub extern "C" fn bloom_set_material_params( - handle: f64, - params_ptr: *const f64, - param_count: f64, -) { - let count = param_count as usize; - if count > 64 { - eprintln!("[material] set_material_params: param_count {} > 64 (256-byte UBO cap)", count); - return; - } - let mut bytes = vec![0u8; count * 4]; - if !params_ptr.is_null() && count > 0 { - let slots = unsafe { std::slice::from_raw_parts(params_ptr, count) }; - for (i, &v) in slots.iter().enumerate() { - let f = v as f32; - bytes[i*4..i*4+4].copy_from_slice(&f.to_le_bytes()); - } - } - let eng = engine(); - if let Err(e) = eng.renderer.material_system.set_user_params( - &eng.renderer.device, &eng.renderer.queue, - handle as u32, &bytes, - ) { - eprintln!("[material] set_material_params failed: {}", e); - } -} - -#[no_mangle] -pub extern "C" fn bloom_compile_material(source_ptr: *const u8) -> f64 { - let source = str_from_header(source_ptr); - match engine().renderer.compile_material(source) { - Ok(handle) => handle as f64, - Err(e) => { - eprintln!("[material] compile failed: {:?}", e); - 0.0 - } - } -} - -#[no_mangle] -pub extern "C" fn bloom_compile_material_refractive(source_ptr: *const u8) -> f64 { - use bloom_shared::renderer::material_pipeline::{FragmentProfile, Bucket}; - let source = str_from_header(source_ptr); - match engine().renderer.compile_material_with_options( - source, FragmentProfile::Translucent, Bucket::Refractive, true, false, - ) { - Ok(handle) => handle as f64, - Err(e) => { eprintln!("[refractive] compile failed: {:?}", e); 0.0 } - } -} - -#[no_mangle] -pub extern "C" fn bloom_compile_material_transparent(source_ptr: *const u8) -> f64 { - use bloom_shared::renderer::material_pipeline::{FragmentProfile, Bucket}; - let source = str_from_header(source_ptr); - match engine().renderer.compile_material_with_options( - source, FragmentProfile::Translucent, Bucket::Transparent, false, false, - ) { - Ok(handle) => handle as f64, - Err(e) => { eprintln!("[material] compile failed: {:?}", e); 0.0 } - } -} - -#[no_mangle] -pub extern "C" fn bloom_compile_material_additive(source_ptr: *const u8) -> f64 { - use bloom_shared::renderer::material_pipeline::{FragmentProfile, Bucket}; - let source = str_from_header(source_ptr); - match engine().renderer.compile_material_with_options( - source, FragmentProfile::Translucent, Bucket::Additive, false, false, - ) { - Ok(handle) => handle as f64, - Err(e) => { eprintln!("[material] compile failed: {:?}", e); 0.0 } - } -} - -#[no_mangle] -pub extern "C" fn bloom_compile_material_cutout(source_ptr: *const u8) -> f64 { - use bloom_shared::renderer::material_pipeline::{FragmentProfile, Bucket}; - let source = str_from_header(source_ptr); - match engine().renderer.compile_material_with_options( - source, FragmentProfile::Opaque, Bucket::Cutout, false, false, - ) { - Ok(handle) => handle as f64, - Err(e) => { eprintln!("[material] compile failed: {:?}", e); 0.0 } - } -} - -#[no_mangle] -pub extern "C" fn bloom_compile_material_instanced(source_ptr: *const u8) -> f64 { - let source = str_from_header(source_ptr); - match engine().renderer.compile_material_instanced(source) { - Ok(handle) => handle as f64, - Err(e) => { eprintln!("[material] instanced compile failed: {:?}", e); 0.0 } - } -} - -#[no_mangle] -pub extern "C" fn bloom_create_instance_buffer( - data_ptr: *const f64, instance_count: f64, -) -> f64 { - if data_ptr.is_null() || instance_count <= 0.0 { return 0.0; } - let count = instance_count as u32; - let slot_count = (count as usize) * 9; - let raw_f64 = unsafe { std::slice::from_raw_parts(data_ptr, slot_count) }; - let raw_f32: Vec = raw_f64.iter().map(|&v| v as f32).collect(); - engine().renderer.create_instance_buffer(&raw_f32, count) as f64 -} - -#[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, -) { - let eng = engine(); - let handle_bits = mesh_handle.to_bits(); - if let Some(model) = eng.models.get(mesh_handle) { - eng.renderer.cache_model_if_static(handle_bits, &model.meshes); - } - eng.renderer.submit_material_draw_instanced( - material as u32, - handle_bits, - mesh_idx as usize, - instance_buffer as u32, - instance_count as u32, - ); -} - -#[no_mangle] -pub extern "C" fn bloom_destroy_instance_buffer(handle: f64) { - engine().renderer.destroy_instance_buffer(handle as u32); -} - -/// EN-011 — create a planar reflection probe. See macOS lib.rs for the -/// full doc comment; this entry-point exists on every native platform -/// so games can target the same FFI surface across iOS/tvOS/Windows/ -/// Linux/Android. -#[no_mangle] -pub extern "C" fn bloom_create_planar_reflection( - plane_y: f64, nx: f64, ny: f64, nz: f64, resolution: f64, -) -> f64 { - engine().renderer.create_planar_reflection( - plane_y as f32, - [nx as f32, ny as f32, nz as f32], - resolution as u32, - ) as f64 -} - -/// EN-011 — link a material to a planar reflection probe. `probe = 0` -/// reverts the binding to the engine's default 1×1 black texture. -#[no_mangle] -pub extern "C" fn bloom_set_material_reflection_probe( - material: f64, probe: f64, -) { - engine().renderer.set_material_reflection_probe(material as u32, probe as u32); -} - -/// EN-014 — create a texture array from concatenated RGBA8 byte data. -/// See macOS lib.rs for the full doc comment; this entry-point exists -/// on every native platform so a TS game targets the same FFI across -/// iOS / tvOS / Windows / Linux / Android. -#[no_mangle] -pub extern "C" fn bloom_create_texture_array( - data_ptr: *const u8, - data_len: f64, - width: f64, - height: f64, - layer_count: f64, -) -> f64 { - // EN-014 V2 — V1 forwards to _ex with default sRGB / no mips. - bloom_create_texture_array_ex(data_ptr, data_len, width, height, layer_count, 0.0, 1.0) -} - -/// EN-014 V2 — explicit format + mip control. See macOS lib.rs for the -/// reference implementation; this entry-point exists on every native -/// platform so a TS game targets the same FFI surface. -#[no_mangle] -pub extern "C" fn bloom_create_texture_array_ex( - data_ptr: *const u8, - data_len: f64, - width: f64, - height: f64, - layer_count: f64, - format: f64, - mip_levels: f64, -) -> f64 { - if data_ptr.is_null() || data_len <= 0.0 { return 0.0; } - let w = width as u32; - let h = height as u32; - if w == 0 || h == 0 { return 0.0; } - let layers_count = (layer_count as u32) - .min(bloom_shared::renderer::material_system::MAX_TEXTURE_ARRAY_LAYERS); - if layers_count == 0 { return 0.0; } - let layer_size = (w as usize) * (h as usize) * 4; - let total_bytes = (data_len as usize) - .min(layers_count as usize * layer_size); - let bytes = unsafe { std::slice::from_raw_parts(data_ptr, total_bytes) }; - let mut layers: Vec<(&[u8], u32, u32)> = Vec::with_capacity(layers_count as usize); - for i in 0..(layers_count as usize) { - let start = i * layer_size; - let end = start + layer_size; - if end > bytes.len() { break; } - layers.push((&bytes[start..end], w, h)); - } - engine().renderer.create_texture_array_ex(&layers, format as u32, mip_levels as u32) as f64 -} - -/// EN-014 — link a texture-array handle to a material at one of three -/// slots: 0 = albedo (binding 14), 1 = normal (binding 15), -/// 2 = MR (binding 16). Pass `array = 0` to revert to the stub. -#[no_mangle] -pub extern "C" fn bloom_set_material_texture_array( - material: f64, slot: f64, array: f64, -) { - engine().renderer.set_material_texture_array( - material as u32, slot as u32, array as u32, - ); -} - -/// EN-012 — set the shading model for a material (0=default lit, -/// 1=foliage, 2=subsurface V2 stub). -#[no_mangle] -pub extern "C" fn bloom_set_material_shading_model( - material: f64, model: f64, -) { - engine().renderer.set_material_shading_model(material as u32, model as u32); -} - -/// EN-012 — set the foliage shading parameters for a material. -/// Only takes effect when shading_model == 1 (foliage). -#[no_mangle] -pub extern "C" fn bloom_set_material_foliage( - material: f64, - trans_r: f64, trans_g: f64, trans_b: f64, - trans_amount: f64, wrap_factor: f64, -) { - engine().renderer.set_material_foliage( - material as u32, - [trans_r as f32, trans_g as f32, trans_b as f32], - trans_amount as f32, wrap_factor as f32, - ); -} - -#[no_mangle] -pub extern "C" fn bloom_compile_material_from_file( - path_ptr: *const u8, - bucket_kind: f64, -) -> f64 { - use bloom_shared::renderer::material_pipeline::{FragmentProfile, Bucket}; - let path = str_from_header(path_ptr); - let (profile, bucket, reads_scene) = match bucket_kind as u32 { - 0 => (FragmentProfile::Opaque, Bucket::Opaque, false), - 1 => (FragmentProfile::Translucent, Bucket::Transparent, false), - 2 => (FragmentProfile::Translucent, Bucket::Refractive, true), - 3 => (FragmentProfile::Translucent, Bucket::Additive, false), - 4 => (FragmentProfile::Opaque, Bucket::Cutout, false), - _ => { - eprintln!("[material] from_file: unknown bucket_kind {bucket_kind}"); - return 0.0; - } - }; - match engine().renderer.compile_material_from_file( - std::path::Path::new(path), profile, bucket, reads_scene, - ) { - Ok(handle) => handle as f64, - Err(e) => { eprintln!("[material] from_file failed: {e}"); 0.0 } - } -} - -/// EN-017 — compile + install a fullscreen post-pass material. -/// See `bloom-macos::bloom_set_post_pass` for the full ABI. -#[no_mangle] -pub extern "C" fn bloom_set_post_pass(source_ptr: *const u8) -> f64 { - let source = str_from_header(source_ptr); - match engine().renderer.set_post_pass(source) { - Ok(()) => 1.0, - Err(e) => { eprintln!("[post_pass] compile failed: {:?}", e); 0.0 } - } -} - -/// EN-017 — uninstall the active post-pass. -#[no_mangle] -pub extern "C" fn bloom_clear_post_pass() { - engine().renderer.clear_post_pass(); -} - -/// EN-017 V2 — append a post-pass to the stack. -/// See `bloom-macos::bloom_add_post_pass` for the full ABI. -#[no_mangle] -pub extern "C" fn bloom_add_post_pass(source_ptr: *const u8) -> f64 { - let source = str_from_header(source_ptr); - match engine().renderer.add_post_pass(source) { - Ok(h) => h as f64, - Err(e) => { eprintln!("[post_pass] compile failed: {:?}", e); 0.0 } - } -} - -/// EN-017 V2 — wipe the entire post-pass stack. -#[no_mangle] -pub extern "C" fn bloom_clear_all_post_passes() { - engine().renderer.clear_all_post_passes(); -} - -#[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, -) { - let eng = engine(); - let handle_bits = mesh_handle.to_bits(); - if let Some(model) = eng.models.get(mesh_handle) { - eng.renderer.cache_model_if_static(handle_bits, &model.meshes); - } - eng.renderer.submit_material_draw( - material as u32, - handle_bits, - mesh_idx as usize, - [x as f32, y as f32, z as f32], - scale as f32, - [(r / 255.0) as f32, (g / 255.0) as f32, (b / 255.0) as f32, (a / 255.0) as f32], - ); -} - -#[no_mangle] -pub extern "C" fn bloom_load_model_animation(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(resolve_path(path)) { - Ok(data) => engine().models.load_model_animation(&data), - Err(_) => 0.0, - } -} - -#[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_sin: f64, rot_cos: f64) { - let eng = engine(); - eng.models.update_model_animation(handle, anim_index as usize, time as f32); - if let Some(anim) = eng.models.get_animation(handle) { - if !anim.joint_matrices.is_empty() { - eng.renderer.set_joint_matrices_scaled(&anim.joint_matrices, scale as f32, [px as f32, py as f32, pz as f32], rot_sin as f32, rot_cos as f32); - } - } -} - -#[no_mangle] -pub extern "C" fn bloom_get_model_mesh_count(handle: f64) -> f64 { - match engine().models.get(handle) { - Some(model) => model.meshes.len() as f64, - None => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_get_model_material_count(handle: f64) -> f64 { - match engine().models.get(handle) { - Some(model) => model.meshes.len() as f64, - None => 0.0, - } -} - -// ============================================================ -// CoreAudio (iOS) — RemoteIO Audio Unit -// ============================================================ - -type AudioUnit = *mut c_void; -type OSStatus = i32; -type AudioUnitPropertyID = u32; -type AudioUnitScope = u32; -type AudioUnitElement = u32; - -#[repr(C)] -#[derive(Clone, Copy)] -struct AudioComponentDescription { - component_type: u32, - component_sub_type: u32, - component_manufacturer: u32, - component_flags: u32, - component_flags_mask: u32, -} - -#[repr(C)] -#[derive(Clone, Copy)] -struct AudioStreamBasicDescription { - sample_rate: f64, - format_id: u32, - format_flags: u32, - bytes_per_packet: u32, - frames_per_packet: u32, - bytes_per_frame: u32, - channels_per_frame: u32, - bits_per_channel: u32, - reserved: u32, -} - -#[repr(C)] -struct AudioBufferList { - number_buffers: u32, - buffers: [AudioBufferData; 1], -} - -#[repr(C)] -struct AudioBufferData { - number_channels: u32, - data_byte_size: u32, - data: *mut c_void, -} - -type AURenderCallback = unsafe extern "C" fn( - in_ref_con: *mut c_void, - io_action_flags: *mut u32, - in_time_stamp: *const c_void, - in_bus_number: u32, - in_number_frames: u32, - io_data: *mut AudioBufferList, -) -> OSStatus; - -#[repr(C)] -struct AURenderCallbackStruct { - input_proc: AURenderCallback, - input_proc_ref_con: *mut c_void, -} - -type AudioComponent = *mut c_void; - -#[link(name = "AudioToolbox", kind = "framework")] -extern "C" { - fn AudioComponentFindNext(component: AudioComponent, desc: *const AudioComponentDescription) -> AudioComponent; - fn AudioComponentInstanceNew(component: AudioComponent, out: *mut AudioUnit) -> OSStatus; - fn AudioUnitSetProperty( - unit: AudioUnit, - property_id: AudioUnitPropertyID, - scope: AudioUnitScope, - element: AudioUnitElement, - data: *const c_void, - data_size: u32, - ) -> OSStatus; - fn AudioUnitInitialize(unit: AudioUnit) -> OSStatus; - fn AudioOutputUnitStart(unit: AudioUnit) -> OSStatus; - fn AudioOutputUnitStop(unit: AudioUnit) -> OSStatus; - fn AudioComponentInstanceDispose(unit: AudioUnit) -> OSStatus; -} - -const K_AUDIO_UNIT_TYPE_OUTPUT: u32 = u32::from_be_bytes(*b"auou"); -const K_AUDIO_UNIT_SUB_TYPE_REMOTE_IO: u32 = u32::from_be_bytes(*b"rioc"); -const K_AUDIO_UNIT_MANUFACTURER_APPLE: u32 = u32::from_be_bytes(*b"appl"); - -const K_AUDIO_UNIT_PROPERTY_STREAM_FORMAT: AudioUnitPropertyID = 8; -const K_AUDIO_UNIT_PROPERTY_SET_RENDER_CALLBACK: AudioUnitPropertyID = 23; -const K_AUDIO_UNIT_SCOPE_INPUT: AudioUnitScope = 1; - -const K_AUDIO_FORMAT_LINEAR_PCM: u32 = u32::from_be_bytes(*b"lpcm"); -const K_AUDIO_FORMAT_FLAG_IS_FLOAT: u32 = 1; -const K_AUDIO_FORMAT_FLAG_IS_PACKED: u32 = 8; - -struct AudioUnitInstance { unit: AudioUnit } -unsafe impl Send for AudioUnitInstance {} -unsafe impl Sync for AudioUnitInstance {} - -static mut AUDIO_UNIT: Option = None; - -unsafe extern "C" fn audio_render_callback( - _in_ref_con: *mut c_void, - _io_action_flags: *mut u32, - _in_time_stamp: *const c_void, - _in_bus_number: u32, - in_number_frames: u32, - io_data: *mut AudioBufferList, -) -> OSStatus { - let buffer_list = &mut *io_data; - let buffer = &mut buffer_list.buffers[0]; - let num_samples = in_number_frames as usize * 2; - let output = std::slice::from_raw_parts_mut(buffer.data as *mut f32, num_samples); - ENGINE.get_mut().map(|eng| { eng.audio.mix_output(output); }); - 0 -} - -#[no_mangle] -pub extern "C" fn bloom_init_audio() { - unsafe { - let desc = AudioComponentDescription { - component_type: K_AUDIO_UNIT_TYPE_OUTPUT, - component_sub_type: K_AUDIO_UNIT_SUB_TYPE_REMOTE_IO, - component_manufacturer: K_AUDIO_UNIT_MANUFACTURER_APPLE, - component_flags: 0, - component_flags_mask: 0, - }; - - let component = AudioComponentFindNext(std::ptr::null_mut(), &desc); - if component.is_null() { return; } - - let mut unit: AudioUnit = std::ptr::null_mut(); - if AudioComponentInstanceNew(component, &mut unit) != 0 { return; } - - let stream_desc = AudioStreamBasicDescription { - sample_rate: 44100.0, - format_id: K_AUDIO_FORMAT_LINEAR_PCM, - format_flags: K_AUDIO_FORMAT_FLAG_IS_FLOAT | K_AUDIO_FORMAT_FLAG_IS_PACKED, - bytes_per_packet: 8, - frames_per_packet: 1, - bytes_per_frame: 8, - channels_per_frame: 2, - bits_per_channel: 32, - reserved: 0, - }; - - AudioUnitSetProperty( - unit, K_AUDIO_UNIT_PROPERTY_STREAM_FORMAT, K_AUDIO_UNIT_SCOPE_INPUT, 0, - &stream_desc as *const _ as *const c_void, - std::mem::size_of::() as u32, - ); - - let callback_struct = AURenderCallbackStruct { - input_proc: audio_render_callback, - input_proc_ref_con: std::ptr::null_mut(), - }; - - AudioUnitSetProperty( - unit, K_AUDIO_UNIT_PROPERTY_SET_RENDER_CALLBACK, K_AUDIO_UNIT_SCOPE_INPUT, 0, - &callback_struct as *const _ as *const c_void, - std::mem::size_of::() as u32, - ); - - AudioUnitInitialize(unit); - AudioOutputUnitStart(unit); - AUDIO_UNIT = Some(AudioUnitInstance { unit }); - } -} - -#[no_mangle] -pub extern "C" fn bloom_close_audio() { - unsafe { - if let Some(au) = AUDIO_UNIT.take() { - AudioOutputUnitStop(au.unit); - AudioComponentInstanceDispose(au.unit); - } - } -} - -#[no_mangle] -pub extern "C" fn bloom_load_sound(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(resolve_path(path)) { - Ok(data) => { - if let Some(s) = parse_wav(&data) { - engine().audio.load_sound(s) - } else if let Some(s) = parse_ogg(&data) { - engine().audio.load_sound(s) - } else if let Some(s) = parse_mp3(&data) { - engine().audio.load_sound(s) - } else { - 0.0 - } - } - Err(_) => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_play_sound(handle: f64) { engine().audio.play_sound(handle); } -#[no_mangle] -pub extern "C" fn bloom_stop_sound(handle: f64) { engine().audio.stop_sound(handle); } -#[no_mangle] -pub extern "C" fn bloom_set_sound_volume(handle: f64, volume: f64) { engine().audio.set_sound_volume(handle, volume as f32); } -#[no_mangle] -pub extern "C" fn bloom_set_master_volume(volume: f64) { engine().audio.master_volume = volume as f32; } - -#[no_mangle] -pub extern "C" fn bloom_play_sound_3d(handle: f64, x: f64, y: f64, z: f64) { - engine().audio.play_sound_3d(handle, x as f32, y as f32, z as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_listener_position(x: f64, y: f64, z: f64, fx: f64, fy: f64, fz: f64) { - engine().audio.set_listener_position(x as f32, y as f32, z as f32, fx as f32, fy as f32, fz as f32); -} - -// ============================================================ -// Music -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_load_music(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(resolve_path(path)) { - Ok(data) => { - if let Some(s) = parse_ogg(&data) { - engine().audio.load_music(s) - } else if let Some(s) = parse_wav(&data) { - engine().audio.load_music(s) - } else if let Some(s) = parse_mp3(&data) { - engine().audio.load_music(s) - } else { - 0.0 - } - } - Err(_) => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_play_music(handle: f64) { engine().audio.play_music(handle); } -#[no_mangle] -pub extern "C" fn bloom_stop_music(handle: f64) { engine().audio.stop_music(handle); } -#[no_mangle] -pub extern "C" fn bloom_update_music_stream(handle: f64) { engine().audio.update_music_stream(handle); } -#[no_mangle] -pub extern "C" fn bloom_set_music_volume(handle: f64, volume: f64) { engine().audio.set_music_volume(handle, volume as f32); } -#[no_mangle] -pub extern "C" fn bloom_is_music_playing(handle: f64) -> f64 { - if engine().audio.is_music_playing(handle) { 1.0 } else { 0.0 } -} - -// ============================================================ -// Gamepad input -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_is_gamepad_available(_gamepad: f64) -> f64 { - if engine().input.is_gamepad_available() { 1.0 } else { 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_get_gamepad_axis(_gamepad: f64, axis: f64) -> f64 { - engine().input.get_gamepad_axis(axis as usize) as f64 -} - -#[no_mangle] -pub extern "C" fn bloom_is_gamepad_button_pressed(_gamepad: f64, button: f64) -> f64 { - if engine().input.is_gamepad_button_pressed(button as usize) { 1.0 } else { 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_is_gamepad_button_down(_gamepad: f64, button: f64) -> f64 { - if engine().input.is_gamepad_button_down(button as usize) { 1.0 } else { 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_is_gamepad_button_released(_gamepad: f64, button: f64) -> f64 { - if engine().input.is_gamepad_button_released(button as usize) { 1.0 } else { 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_get_gamepad_axis_count(_gamepad: f64) -> f64 { - engine().input.get_gamepad_axis_count() as f64 -} - -// ============================================================ -// Touch input -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_get_touch_x(index: f64) -> f64 { - engine().input.get_touch_x(index as usize) -} - -#[no_mangle] -pub extern "C" fn bloom_get_touch_y(index: f64) -> f64 { - engine().input.get_touch_y(index as usize) -} - -#[no_mangle] -pub extern "C" fn bloom_get_touch_count() -> f64 { - engine().input.get_touch_count() as f64 -} - -// ============================================================ -// Utility -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_toggle_fullscreen() {} - -#[no_mangle] -pub extern "C" fn bloom_set_window_title(_title_ptr: *const u8) {} - -#[no_mangle] -pub extern "C" fn bloom_set_window_icon(_path_ptr: *const u8) {} - -#[no_mangle] -pub extern "C" fn bloom_disable_cursor() { - engine().input.cursor_disabled = true; -} - -#[no_mangle] -pub extern "C" fn bloom_enable_cursor() { - engine().input.cursor_disabled = false; -} - -#[no_mangle] -pub extern "C" fn bloom_get_mouse_delta_x() -> f64 { - engine().input.mouse_delta_x -} - -#[no_mangle] -pub extern "C" fn bloom_get_mouse_delta_y() -> f64 { - engine().input.mouse_delta_y -} - -// Accumulated scroll wheel delta since the last call. Reading consumes the -// value (returns 0 on the next call until the user scrolls again). Used by -// the editor's orbit camera and any scrollable UI panel. -#[no_mangle] -pub extern "C" fn bloom_get_mouse_wheel() -> f64 { - engine().input.consume_mouse_wheel() -} - -#[no_mangle] -pub extern "C" fn bloom_get_char_pressed() -> f64 { - engine().input.pop_char() as f64 -} - -// Q2: Cursor shape -#[no_mangle] -pub extern "C" fn bloom_set_cursor_shape(shape: f64) { - engine().input.cursor_shape = shape as u32; -} - -// E4: Clipboard (stub on this platform) -#[no_mangle] -pub extern "C" fn bloom_set_clipboard_text(_text_ptr: *const u8) {} -#[no_mangle] -pub extern "C" fn bloom_get_clipboard_text() -> *const u8 { std::ptr::null() } - -// E5b: File dialogs (stub on this platform) -#[no_mangle] -pub extern "C" fn bloom_open_file_dialog(_filter_ptr: *const u8, _title_ptr: *const u8) -> *const u8 { std::ptr::null() } -#[no_mangle] -pub extern "C" fn bloom_save_file_dialog(_default_name_ptr: *const u8, _title_ptr: *const u8) -> *const u8 { std::ptr::null() } - -// 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. -#[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 -} -#[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 -} -#[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 -} -#[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 -} -#[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 -} -#[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 -} - -#[no_mangle] -pub extern "C" fn bloom_write_file(path_ptr: *const u8, data_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - let data = str_from_header(data_ptr); - match std::fs::write(path, data.as_bytes()) { - Ok(_) => 1.0, - Err(_) => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_file_exists(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - let resolved = resolve_path(path); - if std::path::Path::new(&resolved).exists() { 1.0 } else { 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_read_file(path_ptr: *const u8) -> *const u8 { - let path = str_from_header(path_ptr); - match std::fs::read_to_string(resolve_path(path)) { - Ok(contents) => { - // Return Perry-format string: StringHeader (length u32 + capacity u32 + refcount u32) followed by UTF-8 data - let bytes = contents.as_bytes(); - let len = bytes.len(); - let total = 12 + len; // 12 bytes header (3 × u32) + data - let layout = std::alloc::Layout::from_size_align(total, 4).unwrap(); - unsafe { - let ptr = std::alloc::alloc(layout); - if ptr.is_null() { return std::ptr::null(); } - *(ptr as *mut u32) = len as u32; // length - *(ptr.add(4) as *mut u32) = len as u32; // capacity - *(ptr.add(8) as *mut u32) = 1; // refcount (unique) - std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(12), len); - ptr - } - } - // A null pointer would NaN-box into a string-typed JS value pointing at - // address 0; `.length`/`.charCodeAt` then dereference the header at a - // negative offset and segfault. Return a valid empty Perry string so - // callers that probe via `data.length === 0` (e.g. level discovery) - // are safe. Mirrors the macOS native crate. - Err(_) => alloc_perry_string(""), - } -} - -#[no_mangle] -pub extern "C" fn bloom_get_time() -> f64 { - engine().get_time() -} - -// ============================================================ -// Input injection + platform detection -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_inject_key_down(key: f64) { - engine().input.set_key_down(key as usize); -} -#[no_mangle] -pub extern "C" fn bloom_inject_key_up(key: f64) { - engine().input.set_key_up(key as usize); -} -#[no_mangle] -pub extern "C" fn bloom_inject_gamepad_axis(axis: f64, value: f64) { - engine().input.set_gamepad_axis(axis as usize, value as f32); -} -#[no_mangle] -pub extern "C" fn bloom_inject_gamepad_button_down(button: f64) { - engine().input.set_gamepad_button_down(button as usize); -} -#[no_mangle] -pub extern "C" fn bloom_inject_gamepad_button_up(button: f64) { - engine().input.set_gamepad_button_up(button as usize); -} -#[no_mangle] -pub extern "C" fn bloom_get_platform() -> f64 { 2.0 } - -/// Preferred OS language packed as `c0*256+c1` (ISO-639 primary subtag). See macos lib for format. -#[no_mangle] -pub extern "C" fn bloom_get_language() -> f64 { - fn pack(code: &str) -> f64 { let l = code.to_ascii_lowercase(); let b = l.as_bytes(); if b.len() >= 2 { (b[0] as f64) * 256.0 + (b[1] as f64) } else { 25966.0 } } - let langs = objc2_foundation::NSLocale::preferredLanguages(); - match langs.firstObject() { Some(s) => pack(&s.to_string()), None => 25966.0 } -} -#[no_mangle] -pub extern "C" fn bloom_is_any_input_pressed() -> f64 { - if engine().input.is_any_input_pressed() { 1.0 } else { 0.0 } -} -#[no_mangle] -pub extern "C" fn bloom_get_crown_rotation() -> f64 { - engine().input.consume_crown_rotation() -} - -// ============================================================ -// Thread-safe staging (for async asset loading via Perry threads) -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_stage_texture(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(resolve_path(path)) { - Ok(data) => bloom_shared::staging::decode_and_stage_texture(&data), - Err(_) => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_stage_model(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - let data = match std::fs::read(resolve_path(path)) { - Ok(d) => d, - Err(_) => return 0.0, - }; - match bloom_shared::models::load_gltf_staged(&data) { - Some(staged) => bloom_shared::staging::stage_model(staged), - None => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_stage_sound(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - let data = match std::fs::read(resolve_path(path)) { - Ok(d) => d, - Err(_) => return 0.0, - }; - let sound_data = if path.ends_with(".ogg") || path.ends_with(".OGG") { - parse_ogg(&data) - } else if path.ends_with(".mp3") || path.ends_with(".MP3") { - parse_mp3(&data) - } else { - parse_wav(&data) - }; - match sound_data { - Some(sd) => bloom_shared::staging::stage_sound(sd), - None => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_commit_texture(staging_handle: f64) -> f64 { - let staged = match bloom_shared::staging::take_texture(staging_handle) { - Some(s) => s, - None => return 0.0, - }; - let eng = engine(); - let bind_group_idx = eng.renderer.register_texture(staged.width, staged.height, &staged.data); - eng.textures.textures.alloc(bloom_shared::textures::TextureData { - bind_group_idx, width: staged.width, height: staged.height, - }) -} - -#[no_mangle] -pub extern "C" fn bloom_commit_model(staging_handle: f64) -> f64 { - let staged = match bloom_shared::staging::take_model(staging_handle) { - Some(s) => s, - None => return 0.0, - }; - let eng = engine(); - let mut tex_map: Vec = Vec::with_capacity(staged.textures.len()); - for tex in &staged.textures { - tex_map.push(eng.renderer.register_texture(tex.width, tex.height, &tex.data)); - } - let mut model = staged.model; - for mesh in &mut model.meshes { - if let Some(ref mut idx) = mesh.texture_idx { - let staged_idx = *idx as usize; - if staged_idx > 0 && staged_idx <= tex_map.len() { - *idx = tex_map[staged_idx - 1]; - } else { - mesh.texture_idx = None; - } - } - } - eng.models.models.alloc(model) -} - -#[no_mangle] -pub extern "C" fn bloom_commit_sound(staging_handle: f64) -> f64 { - match bloom_shared::staging::take_sound(staging_handle) { - Some(sd) => engine().audio.load_sound(sd), - None => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_commit_music(staging_handle: f64) -> f64 { - match bloom_shared::staging::take_sound(staging_handle) { - Some(sd) => engine().audio.load_music(sd), - None => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_run_game(_callback: extern "C" fn(f64)) { - // No-op on native. The TypeScript runGame() helper provides the while loop. -} - - -// Q6: Multi-hit picking -static mut LAST_PICK_ALL: Vec = Vec::new(); - -#[no_mangle] -pub extern "C" fn bloom_scene_pick_all(screen_x: f64, screen_y: f64, max_results: f64) -> f64 { - let eng = engine(); - let inv_vp = eng.renderer.inverse_vp_matrix(); - let cam_pos = eng.renderer.camera_pos(); - let w = eng.renderer.width() as f32; - let h = eng.renderer.height() as f32; - let (origin, direction) = bloom_shared::picking::screen_to_ray( - screen_x as f32, screen_y as f32, w, h, &inv_vp, &cam_pos, - ); - let results = bloom_shared::picking::raycast_scene_all(&eng.scene, &origin, &direction, max_results as usize); - let count = results.len(); - unsafe { LAST_PICK_ALL = results; } - count as f64 -} -#[no_mangle] -pub extern "C" fn bloom_pick_all_handle(index: f64) -> f64 { - let i = index as usize; - unsafe { LAST_PICK_ALL.get(i).map(|r| r.handle).unwrap_or(0.0) } -} -#[no_mangle] -pub extern "C" fn bloom_pick_all_distance(index: f64) -> f64 { - let i = index as usize; - unsafe { LAST_PICK_ALL.get(i).map(|r| r.distance as f64).unwrap_or(0.0) } -} -// ============================================================ - -// ============================================================ -// Render quality toggles (individual + preset) — ticket 011 -// Mirror of the macOS FFI surface added in commit 95da6af; previously -// macOS-only, now exposed on every native platform so non-macOS builds -// don't fail at runtime (missing symbol) when the TS API invokes them. -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_set_quality_preset(preset: f64) { - engine().renderer.apply_quality_preset(preset as u32); -} -#[no_mangle] -pub extern "C" fn bloom_set_shadows_enabled(on: f64) { - engine().renderer.set_shadows_enabled(on != 0.0); -} -#[no_mangle] -pub extern "C" fn bloom_set_shadows_always_fresh(on: f64) { - engine().renderer.set_shadows_always_fresh(on != 0.0); -} -#[no_mangle] -pub extern "C" fn bloom_set_bloom_enabled(on: f64) { - engine().renderer.set_bloom_enabled(on != 0.0); -} -#[no_mangle] -pub extern "C" fn bloom_set_ssao_enabled(on: f64) { - engine().renderer.set_ssao_enabled(on != 0.0); -} -#[no_mangle] -pub extern "C" fn bloom_set_ssao_intensity(value: f64) { - engine().renderer.set_ssao_strength(value as f32); -} -#[no_mangle] -pub extern "C" fn bloom_set_ssao_radius(world_radius: f64) { - engine().renderer.set_ssao_radius(world_radius as f32); -} -#[no_mangle] -pub extern "C" fn bloom_set_wind(dir_x: f64, dir_z: f64, amplitude: f64, frequency: f64) { - engine().renderer.set_wind(dir_x as f32, dir_z as f32, amplitude as f32, frequency as f32); -} -#[no_mangle] -pub extern "C" fn bloom_set_ssr_enabled(on: f64) { - engine().renderer.set_ssr_enabled(on != 0.0); -} -#[no_mangle] -pub extern "C" fn bloom_set_motion_blur_enabled(on: f64) { - engine().renderer.set_motion_blur_enabled(on != 0.0); -} -#[no_mangle] -pub extern "C" fn bloom_set_sss_enabled(on: f64) { - engine().renderer.set_sss_enabled(on != 0.0); -} - -// ============================================================ -// Profiler — CPU phase timings (always available) + GPU timestamps -// (when the adapter supports TIMESTAMP_QUERY). Disabled by default. -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_set_profiler_enabled(on: f64) { - engine().profiler.set_enabled(on != 0.0); -} -#[no_mangle] -pub extern "C" fn bloom_get_profiler_frame_cpu_us() -> f64 { - engine().profiler.avg_frame_cpu_us() -} -#[no_mangle] -pub extern "C" fn bloom_get_profiler_frame_gpu_us() -> f64 { - engine().profiler.avg_frame_gpu_us() -} -#[no_mangle] -pub extern "C" fn bloom_print_profiler_summary() { - print!("{}", engine().profiler.summary()); -} - -// ============================================================ -// Physics (Jolt 5.x) — FFI surface generated from shared macro -// ============================================================ - -#[cfg(feature = "jolt")] -#[inline] -fn bloom_jolt_ffi_physics() -> &'static mut bloom_shared::physics_jolt::JoltPhysics { - &mut engine().jolt +#[cfg(feature = "jolt")] +#[inline] +fn bloom_jolt_ffi_physics() -> &'static mut bloom_shared::physics_jolt::JoltPhysics { + &mut engine().jolt } #[cfg(feature = "jolt")] bloom_shared::define_physics_ffi!(); -// ---- FFI parity port from macos (iOS crate drift fix) ---- -#[no_mangle] -pub extern "C" fn bloom_take_screenshot(path_ptr: *const u8) { - let path = str_from_header(path_ptr).to_string(); - let eng = engine(); - eng.renderer.screenshot_requested = true; - eng.renderer.pending_screenshot_path = Some(path); -} - -#[no_mangle] -pub extern "C" fn bloom_set_env_clear_from_hdr(path_ptr: *const u8) { - use image::ImageDecoder; - let path = str_from_header(path_ptr).to_string(); - let file = match std::fs::File::open(&path) { - Ok(f) => f, - Err(_) => return, - }; - let decoder = match image::codecs::hdr::HdrDecoder::new(std::io::BufReader::new(file)) { - Ok(d) => d, - Err(_) => return, - }; - let (w, h) = decoder.dimensions(); - let byte_len = (w as usize) * (h as usize) * 3 * 4; - let mut buf = vec![0u8; byte_len]; - if decoder.read_image(&mut buf).is_err() { - return; - } - // Reinterpret the byte buffer as f32 RGB triples for the renderer. - let rgb_f32: Vec = buf - .chunks_exact(4) - .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]])) - .collect(); - engine().renderer.load_env_from_hdr(w, h, &rgb_f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_fog(r: f64, g: f64, b: f64, density: f64, height_ref: f64, height_falloff: f64) { - let r_ = engine(); - r_.renderer.set_fog_color(r as f32, g as f32, b as f32); - r_.renderer.set_fog_density(density as f32); - r_.renderer.set_fog_height_falloff(height_ref as f32, height_falloff as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_chromatic_aberration(strength: f64) { - engine().renderer.set_chromatic_aberration(strength as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_vignette(strength: f64, softness: f64) { - engine().renderer.set_vignette(strength as f32, softness as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_film_grain(strength: f64) { - engine().renderer.set_film_grain(strength as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_sun_shafts(strength: f64, decay: f64, r: f64, g: f64, b: f64) { - let eng = engine(); - eng.renderer.set_sun_shaft_strength(strength as f32); - eng.renderer.set_sun_shaft_decay(decay as f32); - eng.renderer.set_sun_shaft_color(r as f32, g as f32, b as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_auto_exposure(on: f64) { - engine().renderer.set_auto_exposure(on != 0.0); -} - -#[no_mangle] -pub extern "C" fn bloom_set_taa_enabled(on: f64) { - engine().renderer.set_taa_enabled(on != 0.0); -} - -#[no_mangle] -pub extern "C" fn bloom_set_render_scale(scale: f64) { - engine().renderer.set_render_scale(scale as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_get_render_scale() -> f64 { - engine().renderer.render_scale() as f64 -} - -#[no_mangle] -pub extern "C" fn bloom_set_upscale_mode(mode: f64) { - engine().renderer.set_upscale_mode(mode as u32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_cas_strength(strength: f64) { - engine().renderer.set_cas_strength(strength as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_get_physical_width() -> f64 { - engine().renderer.physical_width() as f64 -} - -#[no_mangle] -pub extern "C" fn bloom_get_physical_height() -> f64 { - engine().renderer.physical_height() as f64 -} - -#[no_mangle] -pub extern "C" fn bloom_set_auto_resolution(target_hz: f64, enabled: f64) { - let eng = engine(); - if enabled != 0.0 { - let current = eng.renderer.render_scale(); - eng.drs.enable(target_hz as f32, current); - } else { - eng.drs.disable(); - } -} - -#[no_mangle] -pub extern "C" fn bloom_set_manual_exposure(value: f64) { - engine().renderer.set_manual_exposure(value as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_env_intensity(intensity: f64) { - engine().renderer.set_env_intensity(intensity as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_ssgi_enabled(enabled: f64) { - engine().renderer.set_ssgi_enabled(enabled != 0.0); -} - -#[no_mangle] -pub extern "C" fn bloom_set_ssgi_intensity(intensity: f64) { - engine().renderer.set_ssgi_intensity(intensity as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_ssgi_radius(radius: f64) { - engine().renderer.set_ssgi_radius(radius as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_dof(enabled: f64, focus_distance: f64, aperture: f64) { - let r = &mut engine().renderer; - r.set_dof_enabled(enabled != 0.0); - r.set_dof_focus_distance(focus_distance as f32); - r.set_dof_aperture(aperture as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_splat_impulse(x: f64, z: f64, radius: f64, strength: f64) { - engine().renderer.impulse_field.submit_splat( - x as f32, z as f32, radius as f32, strength as f32, - ); -} - -#[no_mangle] -pub extern "C" fn bloom_profiler_frame_history() -> *const u8 { - let hist = engine().profiler.frame_history(); - let mut s = String::with_capacity(hist.len() * 24); - for (cpu, gpu) in &hist { - s.push_str(&format!("{:.2}|{:.2}\n", cpu, gpu)); - } - alloc_perry_string(&s) -} - -#[no_mangle] -pub extern "C" fn bloom_profiler_overlay_text() -> *const u8 { - let snap = engine().profiler.snapshot(); - let mut s = String::with_capacity(snap.len() * 48); - for (label, cpu, gpu) in &snap { - s.push_str(label); - s.push('|'); - s.push_str(&format!("{:.2}", cpu)); - s.push('|'); - match gpu { - Some(g) => s.push_str(&format!("{:.2}", g)), - None => s.push_str("-1"), - } - s.push('\n'); - } - alloc_perry_string(&s) -} - -#[no_mangle] -pub extern "C" fn bloom_register_frame_callback(priority: f64, callback: extern "C" fn(f64)) -> f64 { - engine().frame_callbacks.register(priority as i32, callback) as f64 -} - -#[no_mangle] -pub extern "C" fn bloom_unregister_frame_callback(id: f64) { - engine().frame_callbacks.unregister(id as u64); -} - -#[no_mangle] -pub extern "C" fn bloom_add_directional_light( - dx: f64, dy: f64, dz: f64, - r: f64, g: f64, b: f64, - intensity: f64, -) { - engine().renderer.add_directional_light( - dx as f32, dy as f32, dz as f32, - r as f32, g as f32, b as f32, - intensity as f32, - ); -} - -#[no_mangle] -pub extern "C" fn bloom_add_point_light( - x: f64, y: f64, z: f64, range: f64, - r: f64, g: f64, b: f64, - intensity: f64, -) { - engine().renderer.add_point_light( - x as f32, y as f32, z as f32, range as f32, - r as f32, g as f32, b as f32, - intensity as f32, - ); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_create_node() -> f64 { - engine().scene.create_node() -} - -#[no_mangle] -pub extern "C" fn bloom_scene_destroy_node(handle: f64) { - engine().scene.destroy_node(handle); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_set_visible(handle: f64, visible: f64) { - engine().scene.set_visible(handle, visible != 0.0); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_set_cast_shadow(handle: f64, cast: f64) { - engine().scene.set_cast_shadow(handle, cast != 0.0); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_set_receive_shadow(handle: f64, receive: f64) { - engine().scene.set_receive_shadow(handle, receive != 0.0); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_set_parent(handle: f64, parent: f64) { - engine().scene.set_parent(handle, parent); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_set_transform(handle: f64, mat_ptr: *const f64) { - if mat_ptr.is_null() { return; } - let slice = unsafe { std::slice::from_raw_parts(mat_ptr, 16) }; - let mut mat = [[0.0f32; 4]; 4]; - for col in 0..4 { - for row in 0..4 { - mat[col][row] = slice[col * 4 + row] as f32; - } - } - engine().scene.set_transform(handle, mat); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_update_geometry( - handle: f64, - vert_ptr: *const f64, - vert_count: f64, - idx_ptr: *const f64, - idx_count: f64, -) { - if vert_ptr.is_null() || idx_ptr.is_null() { return; } - let nv = vert_count as usize; - let ni = idx_count as usize; - - let vert_floats = unsafe { std::slice::from_raw_parts(vert_ptr, nv * 12) }; - let idx_floats = unsafe { std::slice::from_raw_parts(idx_ptr, ni) }; - - let mut vertices = Vec::with_capacity(nv); - for i in 0..nv { - let base = i * 12; - vertices.push(bloom_shared::renderer::Vertex3D { - position: [vert_floats[base] as f32, vert_floats[base+1] as f32, vert_floats[base+2] as f32], - normal: [vert_floats[base+3] as f32, vert_floats[base+4] as f32, vert_floats[base+5] as f32], - color: [vert_floats[base+6] as f32, vert_floats[base+7] as f32, vert_floats[base+8] as f32, vert_floats[base+9] as f32], - uv: [vert_floats[base+10] as f32, vert_floats[base+11] as f32], - joints: [0.0; 4], - weights: [0.0; 4], - tangent: [0.0; 4], - }); - } - - let indices: Vec = idx_floats.iter().map(|&v| v as u32).collect(); - - engine().scene.update_geometry(handle, vertices, indices); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_set_material_color(handle: f64, r: f64, g: f64, b: f64, a: f64) { - engine().scene.set_material_color(handle, r as f32, g as f32, b as f32, a as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_set_material_pbr(handle: f64, roughness: f64, metalness: f64) { - engine().scene.set_material_pbr(handle, roughness as f32, metalness as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_set_material_texture(handle: f64, texture_idx: f64) { - engine().scene.set_material_texture(handle, texture_idx as u32); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_node_count() -> f64 { - engine().scene.node_count() as f64 -} - -#[no_mangle] -pub extern "C" fn bloom_scene_node_vertex_count(handle: f64) -> f64 { - match engine().scene.nodes.get(handle) { - Some(node) => node.vertices.len() as f64, - None => -1.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_scene_node_index_count(handle: f64) -> f64 { - match engine().scene.nodes.get(handle) { - Some(node) => node.indices.len() as f64, - None => -1.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_scene_set_material_water(handle: f64, wave_amp: f64, wave_speed: f64, r: f64, g: f64, b: f64, a: f64) { - engine().scene.set_material_water(handle, wave_amp as f32, wave_speed as f32, r as f32, g as f32, b as f32, a as f32); -} - -#[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; - let wn = width_count as usize; - let points = unsafe { std::slice::from_raw_parts(points_ptr as *const f32, n * 3) }; - let widths = unsafe { std::slice::from_raw_parts(widths_ptr as *const f32, wn) }; - engine().models.gen_mesh_spline_ribbon(points, widths) -} - -#[no_mangle] -pub extern "C" fn bloom_load_render_texture(width: f64, height: f64) -> f64 { - let w = width as u32; - let h = height as u32; - let eng = engine(); - let rt_handle = eng.textures.load_render_texture(w, h); - - // Create the GPU texture via the renderer's public method. - let (bind_group_idx, _tex_vec_idx) = eng.renderer.create_render_texture(w, h); - - // Register as a texture handle so drawTexture can sample it. - let tex_handle = eng.textures.textures.alloc(bloom_shared::textures::TextureData { - bind_group_idx, width: w, height: h, - }); - eng.textures.set_render_texture_handle(rt_handle, tex_handle); - - rt_handle -} - -#[no_mangle] -pub extern "C" fn bloom_unload_render_texture(handle: f64) { - engine().textures.unload_render_texture(handle); -} - -#[no_mangle] -pub extern "C" fn bloom_begin_texture_mode(handle: f64) { - let eng = engine(); - let (w, h, bg_idx) = match eng.textures.render_textures.get(handle) { - Some(rt) => { - let tex_handle = rt.texture_handle; - match eng.textures.textures.get(tex_handle) { - Some(td) => (rt.width, rt.height, td.bind_group_idx as usize), - None => return, - } - } - None => return, - }; - if let Some(texture) = eng.renderer.get_texture_ref(bg_idx) { - // We need to call begin_texture_mode with a reference to the texture, - // but get_texture_ref borrows renderer immutably. Clone the texture view - // data we need first, then call the mutable method. - let color_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - // Create depth texture for this RT. - let depth_tex = eng.renderer.device.create_texture(&wgpu::TextureDescriptor { - label: Some("rt_depth"), size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 }, - mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Depth32Float, usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - view_formats: &[], - }); - let depth_view = depth_tex.create_view(&wgpu::TextureViewDescriptor::default()); - eng.renderer.rt_color_view = Some(color_view); - eng.renderer.rt_depth_view = Some(depth_view); - eng.renderer.rt_depth_texture = Some(depth_tex); - eng.renderer.rt_width = w; - eng.renderer.rt_height = h; - } -} - -#[no_mangle] -pub extern "C" fn bloom_end_texture_mode() { - engine().renderer.end_texture_mode(); -} - -#[no_mangle] -pub extern "C" fn bloom_get_render_texture_texture(handle: f64) -> f64 { - engine().textures.get_render_texture_texture(handle) -} - -#[no_mangle] -pub extern "C" fn bloom_scene_get_transform(handle: f64, index: f64) -> f64 { - let mat = engine().scene.get_transform(handle); - let i = index as usize; - let col = i / 4; - let row = i % 4; - if col < 4 && row < 4 { mat[col][row] as f64 } else { 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_scene_get_bounds_min_x(handle: f64) -> f64 { engine().scene.get_bounds(handle).0[0] as f64 } - -#[no_mangle] -pub extern "C" fn bloom_scene_get_bounds_min_y(handle: f64) -> f64 { engine().scene.get_bounds(handle).0[1] as f64 } - -#[no_mangle] -pub extern "C" fn bloom_scene_get_bounds_min_z(handle: f64) -> f64 { engine().scene.get_bounds(handle).0[2] as f64 } - -#[no_mangle] -pub extern "C" fn bloom_scene_get_bounds_max_x(handle: f64) -> f64 { engine().scene.get_bounds(handle).1[0] as f64 } - -#[no_mangle] -pub extern "C" fn bloom_scene_get_bounds_max_y(handle: f64) -> f64 { engine().scene.get_bounds(handle).1[1] as f64 } - -#[no_mangle] -pub extern "C" fn bloom_scene_get_bounds_max_z(handle: f64) -> f64 { engine().scene.get_bounds(handle).1[2] as f64 } - -#[no_mangle] -pub extern "C" fn bloom_scene_set_user_data(handle: f64, data: f64) { - engine().scene.set_user_data(handle, data as i64); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_get_user_data(handle: f64) -> f64 { - engine().scene.get_user_data(handle) as f64 -} - -#[no_mangle] -pub extern "C" fn bloom_scene_extrude_polygon( - handle: f64, - polygon_ptr: *const f64, - polygon_count: f64, - depth: f64, -) { - if polygon_ptr.is_null() { return; } - let n = polygon_count as usize; - let polygon = unsafe { std::slice::from_raw_parts(polygon_ptr, n * 2) }; - - let geo = bloom_shared::geometry::extrude_polygon(polygon, &[], depth); - engine().scene.update_geometry(handle, geo.vertices, geo.indices); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_subtract_box( - handle: f64, - min_x: f64, min_y: f64, min_z: f64, - max_x: f64, max_y: f64, max_z: f64, -) { - let eng = engine(); - if let Some(node) = eng.scene.nodes.get(handle) { - let current = bloom_shared::geometry::GeometryData { - vertices: node.vertices.clone(), - indices: node.indices.clone(), - }; - let result = bloom_shared::geometry::subtract_box( - ¤t, - [min_x as f32, min_y as f32, min_z as f32], - [max_x as f32, max_y as f32, max_z as f32], - ); - eng.scene.update_geometry(handle, result.vertices, result.indices); - } -} - -#[no_mangle] -pub extern "C" fn bloom_enable_shadows() { - engine().renderer.shadow_map.enable(); -} - -#[no_mangle] -pub extern "C" fn bloom_disable_shadows() { - engine().renderer.shadow_map.disable(); -} - -#[no_mangle] -pub extern "C" fn bloom_dump_shadow_map(path_ptr: *const u8) { - let path = str_from_header(path_ptr).to_string(); - engine().renderer.dump_shadow_map(&path); -} - -#[no_mangle] -pub extern "C" fn bloom_enable_postfx() { - let eng = engine(); - let w = eng.renderer.width(); - let h = eng.renderer.height(); - let fmt = eng.renderer.surface_format(); - eng.postfx = Some(bloom_shared::postfx::PostFxPipeline::new( - &eng.renderer.device, w, h, fmt, - )); -} - -#[no_mangle] -pub extern "C" fn bloom_disable_postfx() { - engine().postfx = None; -} - -#[no_mangle] -pub extern "C" fn bloom_postfx_set_selected(handle: f64) { - if let Some(pfx) = &mut engine().postfx { - if handle == 0.0 { - pfx.set_selected(Vec::new()); - } else { - pfx.set_selected(vec![handle]); - } - } -} - -#[no_mangle] -pub extern "C" fn bloom_postfx_set_hovered(handle: f64) { - if let Some(pfx) = &mut engine().postfx { - pfx.set_hovered(handle); - } -} - -#[no_mangle] -pub extern "C" fn bloom_postfx_set_outline_color(r: f64, g: f64, b: f64, a: f64) { - if let Some(pfx) = &mut engine().postfx { - pfx.outline_params.color_selected = [r as f32, g as f32, b as f32, a as f32]; - } -} - -#[no_mangle] -pub extern "C" fn bloom_postfx_set_outline_thickness(thickness: f64) { - if let Some(pfx) = &mut engine().postfx { - pfx.outline_params.thickness[0] = thickness as f32; - } -} - -#[no_mangle] -pub extern "C" fn bloom_scene_attach_model(node_handle: f64, model_handle: f64, mesh_index: f64) { - let eng = engine(); - let mi = mesh_index as usize; - - // Get model mesh data - let model_data = match eng.models.models.get(model_handle) { - Some(md) => md, - None => return, - }; - if mi >= model_data.meshes.len() { return; } - let mesh = &model_data.meshes[mi]; - - // Copy vertices and indices to scene node - let vertices = mesh.vertices.clone(); - let indices = mesh.indices.clone(); - let base_color_tex = mesh.texture_idx; - let normal_tex = mesh.normal_texture_idx; - let mr_tex = mesh.metallic_roughness_texture_idx; - let emissive_tex = mesh.emissive_texture_idx; - let emissive_factor = mesh.emissive_factor; - eng.scene.update_geometry(node_handle, vertices, indices); - - // Pipe PBR textures through to the scene node material so the - // renderer's scene pipeline can sample them. - if let Some(tex_idx) = base_color_tex { - eng.scene.set_material_texture(node_handle, tex_idx); - } - if let Some(tex_idx) = normal_tex { - eng.scene.set_material_normal_texture(node_handle, tex_idx); - } - if let Some(tex_idx) = mr_tex { - eng.scene.set_material_metallic_roughness_texture(node_handle, tex_idx); - } - if let Some(tex_idx) = emissive_tex { - eng.scene.set_material_emissive_texture(node_handle, tex_idx); - } - eng.scene.set_material_emissive_factor( - node_handle, - emissive_factor[0], - emissive_factor[1], - emissive_factor[2], - ); -} - diff --git a/native/linux/src/lib.rs b/native/linux/src/lib.rs index 8898176..b04d399 100644 --- a/native/linux/src/lib.rs +++ b/native/linux/src/lib.rs @@ -12,6 +12,16 @@ static mut GAMEPAD_FD: RawFd = -1; fn engine() -> &'static mut EngineState { unsafe { ENGINE.get_mut().expect("Engine not initialized") } } +/// Asset-path hook for define_core_ffi! — identity on desktop, where game +/// asset paths are valid relative to the working directory. +fn bloom_resolve_asset_path(path: &str) -> std::borrow::Cow<'_, str> { + std::borrow::Cow::Borrowed(path) +} + +// The full shared (non-physics) FFI surface. See bloom_shared::ffi_core +// docs for the contract; tools/validate-ffi.js checks parity in CI. +bloom_shared::define_core_ffi!(); + /// Map X11 keysym to Bloom key code. fn map_keycode(keysym: u32) -> usize { @@ -554,6 +564,11 @@ pub extern "C" fn bloom_init_window(width: f64, height: f64, title_ptr: *const u if supported.contains(wgpu::Features::TIMESTAMP_QUERY) { required_features |= wgpu::Features::TIMESTAMP_QUERY; } + // Cooked BC7 textures (bloom-cook) upload compressed when the + // adapter has BC support; without it they CPU-decode at load. + if supported.contains(wgpu::Features::TEXTURE_COMPRESSION_BC) { + required_features |= wgpu::Features::TEXTURE_COMPRESSION_BC; + } if !force_sw_gi && supported.contains(rt_mask) { required_features |= rt_mask; } @@ -701,141 +716,6 @@ pub extern "C" fn bloom_end_drawing() { engine().end_frame(); } -#[no_mangle] -pub extern "C" fn bloom_clear_background(r: f64, g: f64, b: f64, a: f64) { - engine().renderer.set_clear_color(r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_set_target_fps(fps: f64) { engine().target_fps = fps; } - -#[no_mangle] -pub extern "C" fn bloom_set_direct_2d_mode(on: f64) { engine().direct_2d_mode = on > 0.5; } - -#[no_mangle] -pub extern "C" fn bloom_get_delta_time() -> f64 { engine().delta_time } - -#[no_mangle] -pub extern "C" fn bloom_get_fps() -> f64 { engine().get_fps() } - -#[no_mangle] -pub extern "C" fn bloom_get_screen_width() -> f64 { engine().screen_width() } - -#[no_mangle] -pub extern "C" fn bloom_get_screen_height() -> f64 { engine().screen_height() } - -#[no_mangle] -pub extern "C" fn bloom_is_key_pressed(key: f64) -> f64 { - if engine().input.is_key_pressed(key as usize) { 1.0 } else { 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_is_key_down(key: f64) -> f64 { - if engine().input.is_key_down(key as usize) { 1.0 } else { 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_is_key_released(key: f64) -> f64 { - if engine().input.is_key_released(key as usize) { 1.0 } else { 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_get_mouse_x() -> f64 { engine().input.mouse_x } - -#[no_mangle] -pub extern "C" fn bloom_get_mouse_y() -> f64 { engine().input.mouse_y } - -#[no_mangle] -pub extern "C" fn bloom_is_mouse_button_pressed(btn: f64) -> f64 { - if engine().input.is_mouse_button_pressed(btn as usize) { 1.0 } else { 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_is_mouse_button_down(btn: f64) -> f64 { - if engine().input.is_mouse_button_down(btn as usize) { 1.0 } else { 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_is_mouse_button_released(btn: f64) -> f64 { - if engine().input.is_mouse_button_released(btn as usize) { 1.0 } else { 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_draw_line(x1: f64, y1: f64, x2: f64, y2: f64, thickness: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_line(x1, y1, x2, y2, thickness, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_rect(x: f64, y: f64, w: f64, h: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_rect(x, y, w, h, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_rect_lines(x: f64, y: f64, w: f64, h: f64, thickness: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_rect_lines(x, y, w, h, thickness, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_circle(cx: f64, cy: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_circle(cx, cy, radius, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_circle_lines(cx: f64, cy: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_circle_lines(cx, cy, radius, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_triangle(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_triangle(x1, y1, x2, y2, x3, y3, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_poly(cx: f64, cy: f64, sides: f64, radius: f64, rotation: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_poly(cx, cy, sides, radius, rotation, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_text(text_ptr: *const u8, x: f64, y: f64, size: f64, r: f64, g: f64, b: f64, a: f64) { - let text = str_from_header(text_ptr); - let eng = engine(); - let mut text_renderer = std::mem::replace(&mut eng.text, bloom_shared::text_renderer::TextRenderer::empty()); - text_renderer.draw_text(&mut eng.renderer, text, x, y, size as u32, r, g, b, a); - eng.text = text_renderer; -} - -#[no_mangle] -pub extern "C" fn bloom_measure_text(text_ptr: *const u8, size: f64) -> f64 { - let text = str_from_header(text_ptr); - engine().text.measure_text(text, size as u32) -} - -#[no_mangle] -pub extern "C" fn bloom_load_font(path_ptr: *const u8, size: f64) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(path) { Ok(data) => engine().text.load_font(&data) as f64, Err(_) => 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_unload_font(font_handle: f64) { - engine().text.unload_font(font_handle as usize); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_text_ex(font_handle: f64, text_ptr: *const u8, x: f64, y: f64, size: f64, spacing: f64, r: f64, g: f64, b: f64, a: f64) { - let text = str_from_header(text_ptr); - let eng = engine(); - let mut text_renderer = std::mem::replace(&mut eng.text, bloom_shared::text_renderer::TextRenderer::empty()); - text_renderer.draw_text_ex(&mut eng.renderer, font_handle as usize, text, x, y, size as u32, spacing as f32, r, g, b, a); - eng.text = text_renderer; -} - -#[no_mangle] -pub extern "C" fn bloom_measure_text_ex(font_handle: f64, text_ptr: *const u8, size: f64, spacing: f64) -> f64 { - let text = str_from_header(text_ptr); - engine().text.measure_text_ex(font_handle as usize, text, size as u32, spacing as f32) -} - static AUDIO_RUNNING: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); #[no_mangle] @@ -844,8 +724,11 @@ pub extern "C" fn bloom_init_audio() { { use std::sync::atomic::Ordering; AUDIO_RUNNING.store(true, Ordering::SeqCst); - std::thread::spawn(|| { - alsa_audio_thread(); + // Move the render half into the audio thread; the engine keeps + // only the command-producing control half. + let renderer = engine().audio.take_renderer(); + std::thread::spawn(move || { + alsa_audio_thread(renderer); }); } } @@ -857,7 +740,7 @@ pub extern "C" fn bloom_close_audio() { } #[cfg(target_os = "linux")] -fn alsa_audio_thread() { +fn alsa_audio_thread(mut renderer: Option) { use alsa::pcm::*; use alsa::{Direction, ValueOr}; use std::sync::atomic::Ordering; @@ -890,10 +773,10 @@ fn alsa_audio_thread() { while AUDIO_RUNNING.load(Ordering::SeqCst) { for s in mix_buf.iter_mut() { *s = 0.0; } - unsafe { - ENGINE.get_mut().map(|eng| { - eng.audio.mix_output(&mut mix_buf); - }); + // Renderer is moved into this thread at spawn — no shared + // engine state is touched from here (see audio/mod.rs contract). + if let Some(r) = renderer.as_mut() { + r.mix(&mut mix_buf); } let io = pcm.io_f32().unwrap(); @@ -908,1548 +791,156 @@ fn alsa_audio_thread() { let _ = pcm.drain(); } -#[no_mangle] -pub extern "C" fn bloom_load_sound(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(path) { - Ok(data) => { - if let Some(s) = parse_wav(&data) { engine().audio.load_sound(s) } - else if let Some(s) = parse_ogg(&data) { engine().audio.load_sound(s) } - else if let Some(s) = parse_mp3(&data) { engine().audio.load_sound(s) } - else { 0.0 } - } - Err(_) => 0.0, - } -} +// --- Texture FFI --- -#[no_mangle] -pub extern "C" fn bloom_play_sound(handle: f64) { engine().audio.play_sound(handle); } -#[no_mangle] -pub extern "C" fn bloom_stop_sound(handle: f64) { engine().audio.stop_sound(handle); } -#[no_mangle] -pub extern "C" fn bloom_set_sound_volume(handle: f64, volume: f64) { engine().audio.set_sound_volume(handle, volume as f32); } -#[no_mangle] -pub extern "C" fn bloom_set_master_volume(volume: f64) { engine().audio.master_volume = volume as f32; } +// --- Camera FFI --- -#[no_mangle] -pub extern "C" fn bloom_play_sound_3d(handle: f64, x: f64, y: f64, z: f64) { - engine().audio.play_sound_3d(handle, x as f32, y as f32, z as f32); -} +// --- 3D Drawing FFI --- -#[no_mangle] -pub extern "C" fn bloom_set_listener_position(x: f64, y: f64, z: f64, fx: f64, fy: f64, fz: f64) { - engine().audio.set_listener_position(x as f32, y as f32, z as f32, fx as f32, fy as f32, fz as f32); -} +// --- Model FFI --- -// --- Texture FFI --- +// ============================================================ +// Phase 1c — material system FFI +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_load_texture(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(path) { - Ok(data) => { - let eng = engine(); - let renderer_ptr = &mut eng.renderer as *mut bloom_shared::renderer::Renderer; - eng.textures.load_texture(unsafe { &mut *renderer_ptr }, &data) - } - Err(_) => 0.0, - } -} +// --- Music FFI --- -#[no_mangle] -pub extern "C" fn bloom_unload_texture(handle: f64) { - let eng = engine(); - let renderer_ptr = &mut eng.renderer as *mut bloom_shared::renderer::Renderer; - eng.textures.unload_texture(handle, unsafe { &mut *renderer_ptr }); -} +// --- Gamepad FFI --- -#[no_mangle] -pub extern "C" fn bloom_draw_texture(handle: f64, x: f64, y: f64, r: f64, g: f64, b: f64, a: f64) { - let eng = engine(); - if let Some(tex) = eng.textures.get(handle) { - let idx = tex.bind_group_idx; - eng.renderer.draw_texture(idx, x, y, r, g, b, a); - } -} +// --- Skeletal Animation Debug --- + +// --- Lighting --- + +// --- Utility FFI --- #[no_mangle] -pub extern "C" fn bloom_draw_texture_rec(handle: f64, src_x: f64, src_y: f64, src_w: f64, src_h: f64, dst_x: f64, dst_y: f64, r: f64, g: f64, b: f64, a: f64) { - let eng = engine(); - if let Some(tex) = eng.textures.get(handle) { - let idx = tex.bind_group_idx; - eng.renderer.draw_texture_rec(idx, src_x, src_y, src_w, src_h, dst_x, dst_y, r, g, b, a); - } +pub extern "C" fn bloom_toggle_fullscreen() { + #[cfg(target_os = "linux")] + x11_impl::toggle_fullscreen(); } - #[no_mangle] -pub extern "C" fn bloom_draw_texture_pro(handle: f64, src_x: f64, src_y: f64, src_w: f64, src_h: f64, dst_x: f64, dst_y: f64, dst_w: f64, dst_h: f64, origin_x: f64, origin_y: f64, rotation: f64, r: f64, g: f64, b: f64, a: f64) { - let eng = engine(); - if let Some(tex) = eng.textures.get(handle) { - let idx = tex.bind_group_idx; - eng.renderer.draw_texture_pro(idx, src_x, src_y, src_w, src_h, dst_x, dst_y, dst_w, dst_h, origin_x, origin_y, rotation, r, g, b, a); - } +pub extern "C" fn bloom_set_window_title(title_ptr: *const u8) { + let title = str_from_header(title_ptr); + #[cfg(target_os = "linux")] + x11_impl::set_window_title(title); } - #[no_mangle] -pub extern "C" fn bloom_get_texture_width(handle: f64) -> f64 { - engine().textures.get(handle).map(|t| t.width as f64).unwrap_or(0.0) +pub extern "C" fn bloom_set_window_icon(path_ptr: *const u8) { + let path = str_from_header(path_ptr); + #[cfg(target_os = "linux")] + x11_impl::set_window_icon(path); } #[no_mangle] -pub extern "C" fn bloom_get_texture_height(handle: f64) -> f64 { - engine().textures.get(handle).map(|t| t.height as f64).unwrap_or(0.0) +pub extern "C" fn bloom_disable_cursor() { + let input = &mut engine().input; + input.cursor_disabled = true; + input.clear_mouse_delta(); + #[cfg(target_os = "linux")] + x11_impl::enter_relative_mode(); } #[no_mangle] -pub extern "C" fn bloom_gen_texture_mipmaps(_handle: f64) { - // No-op: wgpu handles mipmaps internally +pub extern "C" fn bloom_enable_cursor() { + engine().input.cursor_disabled = false; + #[cfg(target_os = "linux")] + x11_impl::leave_relative_mode(); } +// E4: Clipboard (arboard, X11/Wayland-aware) #[no_mangle] -pub extern "C" fn bloom_set_texture_filter(handle: f64, mode: f64) { - let eng = engine(); - if let Some(tex) = eng.textures.get(handle) { - let bind_group_idx = tex.bind_group_idx; - eng.renderer.set_texture_filter(bind_group_idx, mode > 0.5); +pub extern "C" fn bloom_set_clipboard_text(text_ptr: *const u8) { + let text = str_from_header(text_ptr); + if let Ok(mut clipboard) = arboard::Clipboard::new() { + let _ = clipboard.set_text(text.to_string()); } } - #[no_mangle] -pub extern "C" fn bloom_load_image(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(path) { Ok(data) => engine().textures.load_image(&data), Err(_) => 0.0 } +pub extern "C" fn bloom_get_clipboard_text() -> *const u8 { + match arboard::Clipboard::new() { + Ok(mut clipboard) => match clipboard.get_text() { + Ok(text) => alloc_perry_string(&text), + Err(_) => alloc_perry_string(""), + }, + Err(_) => alloc_perry_string(""), + } } +// E5b: Native file dialogs (rfd → GTK/zenity/kdialog on Linux) #[no_mangle] -pub extern "C" fn bloom_image_resize(handle: f64, w: f64, h: f64) { - engine().textures.image_resize(handle, w as u32, h as u32); +pub extern "C" fn bloom_open_file_dialog(filter_ptr: *const u8, title_ptr: *const u8) -> *const u8 { + let filter = str_from_header(filter_ptr); + let title = str_from_header(title_ptr); + let mut dialog = rfd::FileDialog::new().set_title(title); + if !filter.is_empty() { + dialog = dialog.add_filter("Files", &[filter]); + } + match dialog.pick_file() { + Some(path) => alloc_perry_string(&path.to_string_lossy()), + None => alloc_perry_string(""), + } } - #[no_mangle] -pub extern "C" fn bloom_image_crop(handle: f64, x: f64, y: f64, w: f64, h: f64) { - engine().textures.image_crop(handle, x as u32, y as u32, w as u32, h as u32); +pub extern "C" fn bloom_save_file_dialog(default_name_ptr: *const u8, title_ptr: *const u8) -> *const u8 { + let default_name = str_from_header(default_name_ptr); + let title = str_from_header(title_ptr); + let dialog = rfd::FileDialog::new() + .set_title(title) + .set_file_name(default_name); + match dialog.save_file() { + Some(path) => alloc_perry_string(&path.to_string_lossy()), + None => alloc_perry_string(""), + } } - #[no_mangle] -pub extern "C" fn bloom_image_flip_h(handle: f64) { - engine().textures.image_flip_h(handle); -} +pub extern "C" fn bloom_get_platform() -> f64 { 4.0 } +/// Preferred OS language packed as `c0*256+c1` (ISO-639 primary subtag), from $LANG/$LC_*. #[no_mangle] -pub extern "C" fn bloom_image_flip_v(handle: f64) { - engine().textures.image_flip_v(handle); +pub extern "C" fn bloom_get_language() -> f64 { + fn pack(code: &str) -> f64 { let l = code.to_ascii_lowercase(); let b = l.as_bytes(); if b.len() >= 2 { (b[0] as f64) * 256.0 + (b[1] as f64) } else { 25966.0 } } + let v = std::env::var("LANG").or_else(|_| std::env::var("LC_ALL")).or_else(|_| std::env::var("LC_MESSAGES")).unwrap_or_default(); + if v.len() >= 2 && !v.starts_with('C') && !v.starts_with("POSIX") { pack(&v) } else { 25966.0 } } -#[no_mangle] -pub extern "C" fn bloom_load_texture_from_image(handle: f64) -> f64 { - let eng = engine(); - let renderer_ptr = &mut eng.renderer as *mut bloom_shared::renderer::Renderer; - eng.textures.load_texture_from_image(handle, unsafe { &mut *renderer_ptr }) -} +// ============================================================ +// Frame callbacks +// ============================================================ -// --- Camera FFI --- +// ============================================================ +// Multiple lights +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_begin_mode_2d(offset_x: f64, offset_y: f64, target_x: f64, target_y: f64, rotation: f64, zoom: f64) { - engine().renderer.begin_mode_2d(offset_x as f32, offset_y as f32, target_x as f32, target_y as f32, rotation as f32, zoom as f32); -} -#[no_mangle] -pub extern "C" fn bloom_end_mode_2d() { engine().renderer.end_mode_2d(); } +// ============================================================ +// Scene graph (retained mode) +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_begin_mode_3d(pos_x: f64, pos_y: f64, pos_z: f64, target_x: f64, target_y: f64, target_z: f64, up_x: f64, up_y: f64, up_z: f64, fovy: f64, projection: f64) { - engine().renderer.begin_mode_3d(pos_x as f32, pos_y as f32, pos_z as f32, target_x as f32, target_y as f32, target_z as f32, up_x as f32, up_y as f32, up_z as f32, fovy as f32, projection as f32); -} -#[no_mangle] -pub extern "C" fn bloom_end_mode_3d() { engine().renderer.end_mode_3d(); } +// ============================================================ +// Geometry generation +// ============================================================ -// --- 3D Drawing FFI --- +// ============================================================ +// Shadow mapping +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_draw_cube(x: f64, y: f64, z: f64, w: f64, h: f64, d: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_cube(x, y, z, w, h, d, r, g, b, a); -} -#[no_mangle] -pub extern "C" fn bloom_draw_cube_wires(x: f64, y: f64, z: f64, w: f64, h: f64, d: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_cube_wires(x, y, z, w, h, d, r, g, b, a); -} -#[no_mangle] -pub extern "C" fn bloom_draw_sphere(x: f64, y: f64, z: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_sphere(x, y, z, radius, r, g, b, a); -} -#[no_mangle] -pub extern "C" fn bloom_draw_sphere_wires(x: f64, y: f64, z: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_sphere_wires(x, y, z, radius, r, g, b, a); -} -#[no_mangle] -pub extern "C" fn bloom_draw_cylinder(x: f64, y: f64, z: f64, rt: f64, rb: f64, h: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_cylinder(x, y, z, rt, rb, h, r, g, b, a); -} -#[no_mangle] -pub extern "C" fn bloom_draw_plane(x: f64, y: f64, z: f64, w: f64, d: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_plane(x, y, z, w, d, r, g, b, a); -} -#[no_mangle] -pub extern "C" fn bloom_draw_grid(slices: f64, spacing: f64) { - engine().renderer.draw_grid(slices as i32, spacing); -} -#[no_mangle] -pub extern "C" fn bloom_draw_ray(ox: f64, oy: f64, oz: f64, dx: f64, dy: f64, dz: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_ray(ox, oy, oz, dx, dy, dz, r, g, b, a); -} +// ============================================================ +// Post-processing +// ============================================================ -// --- Model FFI --- +// ============================================================ +// 3D→2D Projection (for UI overlays positioned in 3D space) +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_load_model(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(path) { Ok(data) => engine().models.load_model(&data), Err(_) => 0.0 } -} -#[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(); - if let Some(model) = eng.models.get(handle) { - let tint = [(r / 255.0) as f32, (g / 255.0) as f32, (b / 255.0) as f32, (a / 255.0) as f32]; - let position = [x as f32, y as f32, z as f32]; - let handle_bits = handle.to_bits(); - if eng.renderer.cache_model_if_static(handle_bits, &model.meshes) { - eng.renderer.draw_model_cached(handle_bits, position, scale as f32, tint); - } else { - for mesh in &model.meshes { - let tex_idx = mesh.texture_idx.unwrap_or(0); - eng.renderer.draw_model_mesh_tinted(&mesh.vertices, &mesh.indices, position, scale as f32, tint, tex_idx); - } - } - } -} -#[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, -) { - let bits = color_packed_argb as u32; - let a = ((bits >> 24) & 0xff) as f32 / 255.0; - let r = ((bits >> 16) & 0xff) as f32 / 255.0; - let g = ((bits >> 8) & 0xff) as f32 / 255.0; - let b = ( bits & 0xff) as f32 / 255.0; - let eng = engine(); - if let Some(model) = eng.models.get(handle) { - let position = [x as f32, y as f32, z as f32]; - let scale = scale as f32; - let tint = [r, g, b, a]; - for mesh in &model.meshes { - let tex_idx = mesh.texture_idx.unwrap_or(0); - eng.renderer.draw_model_mesh_tinted_rotated( - &mesh.vertices, &mesh.indices, position, scale, tint, tex_idx, rot_y as f32, - ); - } - } -} -#[no_mangle] -pub extern "C" fn bloom_unload_model(handle: f64) { engine().models.unload_model(handle); } - -#[no_mangle] -pub extern "C" fn bloom_get_model_mesh_count(handle: f64) -> f64 { - match engine().models.get(handle) { - Some(model) => model.meshes.len() as f64, - None => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_get_model_material_count(handle: f64) -> f64 { - match engine().models.get(handle) { - Some(model) => model.meshes.len() as f64, - None => 0.0, - } -} - -#[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) -} - -#[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(); - if let Some(img) = eng.textures.images.get(image_handle) { - let data = img.data.clone(); - let w = img.width; - let h = img.height; - eng.models.gen_mesh_heightmap(&data, w, h, size_x as f32, size_y as f32, size_z as f32) - } else { - 0.0 - } -} - -#[no_mangle] -pub extern "C" fn bloom_load_shader(source_ptr: *const u8) -> f64 { - let source = str_from_header(source_ptr); - engine().renderer.load_custom_shader(source) as f64 -} - -#[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 - // pointer to that data. A previous version of this FFI declared - // *const f32 / *const u32, which silently read the low 4 bytes - // of each f64 slot as garbage f32/u32 values — meshes registered - // (non-zero handle) but were unrenderable. - // - // Caller must pass `vertex_count` and `index_count` derived from - // a literal-initialized array OR from values it computed itself. - // Don't compute these via `arr.length` after `.push()` — Perry's - // `.length` property currently reflects the literal-init size, - // not the post-push count (a Perry codegen bug). Workaround on - // the TS side: track the count manually or use literal arrays. - if vertex_ptr.is_null() || index_ptr.is_null() { return 0.0; } - let vcount = vertex_count as usize; - let icount = index_count as usize; - let vertex_f64 = unsafe { std::slice::from_raw_parts(vertex_ptr, vcount * 12) }; - let index_f64 = unsafe { std::slice::from_raw_parts(index_ptr, icount) }; - let vertex_data: Vec = vertex_f64.iter().map(|&v| v as f32).collect(); - let index_data: Vec = index_f64.iter().map(|&v| v as u32).collect(); - engine().models.create_mesh(&vertex_data, &index_data) -} - -// ============================================================ -// Phase 1c — material system FFI -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_set_material_params( - handle: f64, - params_ptr: *const f64, - param_count: f64, -) { - let count = param_count as usize; - if count > 64 { - eprintln!("[material] set_material_params: param_count {} > 64 (256-byte UBO cap)", count); - return; - } - let mut bytes = vec![0u8; count * 4]; - if !params_ptr.is_null() && count > 0 { - let slots = unsafe { std::slice::from_raw_parts(params_ptr, count) }; - for (i, &v) in slots.iter().enumerate() { - let f = v as f32; - bytes[i*4..i*4+4].copy_from_slice(&f.to_le_bytes()); - } - } - let eng = engine(); - if let Err(e) = eng.renderer.material_system.set_user_params( - &eng.renderer.device, &eng.renderer.queue, - handle as u32, &bytes, - ) { - eprintln!("[material] set_material_params failed: {}", e); - } -} - -#[no_mangle] -pub extern "C" fn bloom_compile_material(source_ptr: *const u8) -> f64 { - let source = str_from_header(source_ptr); - match engine().renderer.compile_material(source) { - Ok(handle) => handle as f64, - Err(e) => { - eprintln!("[material] compile failed: {:?}", e); - 0.0 - } - } -} - -#[no_mangle] -pub extern "C" fn bloom_compile_material_refractive(source_ptr: *const u8) -> f64 { - use bloom_shared::renderer::material_pipeline::{FragmentProfile, Bucket}; - let source = str_from_header(source_ptr); - match engine().renderer.compile_material_with_options( - source, FragmentProfile::Translucent, Bucket::Refractive, true, false, - ) { - Ok(handle) => handle as f64, - Err(e) => { eprintln!("[refractive] compile failed: {:?}", e); 0.0 } - } -} - -#[no_mangle] -pub extern "C" fn bloom_compile_material_transparent(source_ptr: *const u8) -> f64 { - use bloom_shared::renderer::material_pipeline::{FragmentProfile, Bucket}; - let source = str_from_header(source_ptr); - match engine().renderer.compile_material_with_options( - source, FragmentProfile::Translucent, Bucket::Transparent, false, false, - ) { - Ok(handle) => handle as f64, - Err(e) => { eprintln!("[material] compile failed: {:?}", e); 0.0 } - } -} - -#[no_mangle] -pub extern "C" fn bloom_compile_material_additive(source_ptr: *const u8) -> f64 { - use bloom_shared::renderer::material_pipeline::{FragmentProfile, Bucket}; - let source = str_from_header(source_ptr); - match engine().renderer.compile_material_with_options( - source, FragmentProfile::Translucent, Bucket::Additive, false, false, - ) { - Ok(handle) => handle as f64, - Err(e) => { eprintln!("[material] compile failed: {:?}", e); 0.0 } - } -} - -#[no_mangle] -pub extern "C" fn bloom_compile_material_cutout(source_ptr: *const u8) -> f64 { - use bloom_shared::renderer::material_pipeline::{FragmentProfile, Bucket}; - let source = str_from_header(source_ptr); - match engine().renderer.compile_material_with_options( - source, FragmentProfile::Opaque, Bucket::Cutout, false, false, - ) { - Ok(handle) => handle as f64, - Err(e) => { eprintln!("[material] compile failed: {:?}", e); 0.0 } - } -} - -#[no_mangle] -pub extern "C" fn bloom_compile_material_instanced(source_ptr: *const u8) -> f64 { - let source = str_from_header(source_ptr); - match engine().renderer.compile_material_instanced(source) { - Ok(handle) => handle as f64, - Err(e) => { eprintln!("[material] instanced compile failed: {:?}", e); 0.0 } - } -} - -#[no_mangle] -pub extern "C" fn bloom_create_instance_buffer( - data_ptr: *const f64, instance_count: f64, -) -> f64 { - if data_ptr.is_null() || instance_count <= 0.0 { return 0.0; } - let count = instance_count as u32; - let slot_count = (count as usize) * 9; - let raw_f64 = unsafe { std::slice::from_raw_parts(data_ptr, slot_count) }; - let raw_f32: Vec = raw_f64.iter().map(|&v| v as f32).collect(); - engine().renderer.create_instance_buffer(&raw_f32, count) as f64 -} - -#[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, -) { - let eng = engine(); - let handle_bits = mesh_handle.to_bits(); - if let Some(model) = eng.models.get(mesh_handle) { - eng.renderer.cache_model_if_static(handle_bits, &model.meshes); - } - eng.renderer.submit_material_draw_instanced( - material as u32, - handle_bits, - mesh_idx as usize, - instance_buffer as u32, - instance_count as u32, - ); -} - -#[no_mangle] -pub extern "C" fn bloom_destroy_instance_buffer(handle: f64) { - engine().renderer.destroy_instance_buffer(handle as u32); -} - -/// EN-011 — create a planar reflection probe. See macOS lib.rs for the -/// full doc comment; this entry-point exists on every native platform -/// so games can target the same FFI surface across iOS/tvOS/Windows/ -/// Linux/Android. -#[no_mangle] -pub extern "C" fn bloom_create_planar_reflection( - plane_y: f64, nx: f64, ny: f64, nz: f64, resolution: f64, -) -> f64 { - engine().renderer.create_planar_reflection( - plane_y as f32, - [nx as f32, ny as f32, nz as f32], - resolution as u32, - ) as f64 -} - -/// EN-011 — link a material to a planar reflection probe. `probe = 0` -/// reverts the binding to the engine's default 1×1 black texture. -#[no_mangle] -pub extern "C" fn bloom_set_material_reflection_probe( - material: f64, probe: f64, -) { - engine().renderer.set_material_reflection_probe(material as u32, probe as u32); -} - -/// EN-014 — create a texture array from concatenated RGBA8 byte data. -/// See macOS lib.rs for the full doc comment; this entry-point exists -/// on every native platform so a TS game targets the same FFI across -/// iOS / tvOS / Windows / Linux / Android. -#[no_mangle] -pub extern "C" fn bloom_create_texture_array( - data_ptr: *const u8, - data_len: f64, - width: f64, - height: f64, - layer_count: f64, -) -> f64 { - // EN-014 V2 — V1 forwards to _ex with default sRGB / no mips. - bloom_create_texture_array_ex(data_ptr, data_len, width, height, layer_count, 0.0, 1.0) -} - -/// EN-014 V2 — explicit format + mip control. See macOS lib.rs for docs. -#[no_mangle] -pub extern "C" fn bloom_create_texture_array_ex( - data_ptr: *const u8, - data_len: f64, - width: f64, - height: f64, - layer_count: f64, - format: f64, - mip_levels: f64, -) -> f64 { - if data_ptr.is_null() || data_len <= 0.0 { return 0.0; } - let w = width as u32; - let h = height as u32; - if w == 0 || h == 0 { return 0.0; } - let layers_count = (layer_count as u32) - .min(bloom_shared::renderer::material_system::MAX_TEXTURE_ARRAY_LAYERS); - if layers_count == 0 { return 0.0; } - let layer_size = (w as usize) * (h as usize) * 4; - let total_bytes = (data_len as usize) - .min(layers_count as usize * layer_size); - let bytes = unsafe { std::slice::from_raw_parts(data_ptr, total_bytes) }; - let mut layers: Vec<(&[u8], u32, u32)> = Vec::with_capacity(layers_count as usize); - for i in 0..(layers_count as usize) { - let start = i * layer_size; - let end = start + layer_size; - if end > bytes.len() { break; } - layers.push((&bytes[start..end], w, h)); - } - engine().renderer.create_texture_array_ex(&layers, format as u32, mip_levels as u32) as f64 -} - -/// EN-014 — link a texture-array handle to a material at one of three -/// slots: 0 = albedo (binding 14), 1 = normal (binding 15), -/// 2 = MR (binding 16). Pass `array = 0` to revert to the stub. -#[no_mangle] -pub extern "C" fn bloom_set_material_texture_array( - material: f64, slot: f64, array: f64, -) { - engine().renderer.set_material_texture_array( - material as u32, slot as u32, array as u32, - ); -} - -/// EN-012 — set the shading model for a material (0=default lit, -/// 1=foliage, 2=subsurface V2 stub). -#[no_mangle] -pub extern "C" fn bloom_set_material_shading_model( - material: f64, model: f64, -) { - engine().renderer.set_material_shading_model(material as u32, model as u32); -} - -/// EN-012 — set the foliage shading parameters for a material. -/// Only takes effect when shading_model == 1 (foliage). -#[no_mangle] -pub extern "C" fn bloom_set_material_foliage( - material: f64, - trans_r: f64, trans_g: f64, trans_b: f64, - trans_amount: f64, wrap_factor: f64, -) { - engine().renderer.set_material_foliage( - material as u32, - [trans_r as f32, trans_g as f32, trans_b as f32], - trans_amount as f32, wrap_factor as f32, - ); -} - -#[no_mangle] -pub extern "C" fn bloom_compile_material_from_file( - path_ptr: *const u8, - bucket_kind: f64, -) -> f64 { - use bloom_shared::renderer::material_pipeline::{FragmentProfile, Bucket}; - let path = str_from_header(path_ptr); - let (profile, bucket, reads_scene) = match bucket_kind as u32 { - 0 => (FragmentProfile::Opaque, Bucket::Opaque, false), - 1 => (FragmentProfile::Translucent, Bucket::Transparent, false), - 2 => (FragmentProfile::Translucent, Bucket::Refractive, true), - 3 => (FragmentProfile::Translucent, Bucket::Additive, false), - 4 => (FragmentProfile::Opaque, Bucket::Cutout, false), - _ => { - eprintln!("[material] from_file: unknown bucket_kind {bucket_kind}"); - return 0.0; - } - }; - match engine().renderer.compile_material_from_file( - std::path::Path::new(path), profile, bucket, reads_scene, - ) { - Ok(handle) => handle as f64, - Err(e) => { eprintln!("[material] from_file failed: {e}"); 0.0 } - } -} - -/// EN-017 — compile + install a fullscreen post-pass material. -/// See `bloom-macos::bloom_set_post_pass` for the full ABI. -#[no_mangle] -pub extern "C" fn bloom_set_post_pass(source_ptr: *const u8) -> f64 { - let source = str_from_header(source_ptr); - match engine().renderer.set_post_pass(source) { - Ok(()) => 1.0, - Err(e) => { eprintln!("[post_pass] compile failed: {:?}", e); 0.0 } - } -} - -/// EN-017 — uninstall the active post-pass. -#[no_mangle] -pub extern "C" fn bloom_clear_post_pass() { - engine().renderer.clear_post_pass(); -} - -/// EN-017 V2 — append a post-pass to the stack. -/// See `bloom-macos::bloom_add_post_pass` for the full ABI. -#[no_mangle] -pub extern "C" fn bloom_add_post_pass(source_ptr: *const u8) -> f64 { - let source = str_from_header(source_ptr); - match engine().renderer.add_post_pass(source) { - Ok(h) => h as f64, - Err(e) => { eprintln!("[post_pass] compile failed: {:?}", e); 0.0 } - } -} - -/// EN-017 V2 — wipe the entire post-pass stack. -#[no_mangle] -pub extern "C" fn bloom_clear_all_post_passes() { - engine().renderer.clear_all_post_passes(); -} - -#[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, -) { - let eng = engine(); - let handle_bits = mesh_handle.to_bits(); - if let Some(model) = eng.models.get(mesh_handle) { - eng.renderer.cache_model_if_static(handle_bits, &model.meshes); - } - eng.renderer.submit_material_draw( - material as u32, - handle_bits, - mesh_idx as usize, - [x as f32, y as f32, z as f32], - scale as f32, - [(r / 255.0) as f32, (g / 255.0) as f32, (b / 255.0) as f32, (a / 255.0) as f32], - ); -} - -#[no_mangle] -pub extern "C" fn bloom_load_model_animation(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(path) { - Ok(data) => engine().models.load_model_animation(&data), - Err(_) => 0.0, - } -} - -#[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 - // engine reconstructs both with full precision + correct signs. - // Older callers that passed (rot_sin, rot_cos) hit a Perry-ARM64 - // 9th-arg garbling bug AND a sqrt(1-sin²) workaround that lost - // the sign of cos — model rotation was correct only on half the - // circle. 8-arg signature dodges both issues. Matches macOS. - let rot_y_f = rot_y as f32; - let rot_sin = rot_y_f.sin(); - let rot_cos = rot_y_f.cos(); - let eng = engine(); - eng.models.update_model_animation(handle, anim_index as usize, time as f32); - if let Some(anim) = eng.models.get_animation(handle) { - if !anim.joint_matrices.is_empty() { - eng.renderer.set_joint_matrices_scaled(&anim.joint_matrices, scale as f32, [px as f32, py as f32, pz as f32], rot_sin, rot_cos); - } - } -} - -// --- Music FFI --- - -#[no_mangle] -pub extern "C" fn bloom_load_music(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(path) { - Ok(data) => { - if let Some(s) = parse_ogg(&data) { engine().audio.load_music(s) } - else if let Some(s) = parse_wav(&data) { engine().audio.load_music(s) } - else if let Some(s) = parse_mp3(&data) { engine().audio.load_music(s) } - else { 0.0 } - } - Err(_) => 0.0, - } -} -#[no_mangle] -pub extern "C" fn bloom_play_music(handle: f64) { engine().audio.play_music(handle); } -#[no_mangle] -pub extern "C" fn bloom_stop_music(handle: f64) { engine().audio.stop_music(handle); } -#[no_mangle] -pub extern "C" fn bloom_update_music_stream(handle: f64) { engine().audio.update_music_stream(handle); } -#[no_mangle] -pub extern "C" fn bloom_set_music_volume(handle: f64, volume: f64) { engine().audio.set_music_volume(handle, volume as f32); } -#[no_mangle] -pub extern "C" fn bloom_is_music_playing(handle: f64) -> f64 { if engine().audio.is_music_playing(handle) { 1.0 } else { 0.0 } } - -// --- Gamepad FFI --- - -#[no_mangle] -pub extern "C" fn bloom_is_gamepad_available() -> f64 { if engine().input.is_gamepad_available() { 1.0 } else { 0.0 } } -#[no_mangle] -pub extern "C" fn bloom_get_gamepad_axis(axis: f64) -> f64 { engine().input.get_gamepad_axis(axis as usize) as f64 } -#[no_mangle] -pub extern "C" fn bloom_is_gamepad_button_pressed(btn: f64) -> f64 { if engine().input.is_gamepad_button_pressed(btn as usize) { 1.0 } else { 0.0 } } -#[no_mangle] -pub extern "C" fn bloom_is_gamepad_button_down(btn: f64) -> f64 { if engine().input.is_gamepad_button_down(btn as usize) { 1.0 } else { 0.0 } } -#[no_mangle] -pub extern "C" fn bloom_is_gamepad_button_released(btn: f64) -> f64 { if engine().input.is_gamepad_button_released(btn as usize) { 1.0 } else { 0.0 } } -#[no_mangle] -pub extern "C" fn bloom_get_gamepad_axis_count() -> f64 { engine().input.get_gamepad_axis_count() as f64 } - -// --- Skeletal Animation Debug --- - -#[no_mangle] -pub extern "C" fn bloom_set_joint_test(_joint: f64, _angle: f64) { - // No-op for now — skeletal animation testing -} - -// --- Lighting --- - -#[no_mangle] -pub extern "C" fn bloom_set_ambient_light(r: f64, g: f64, b: f64, intensity: f64) { - engine().renderer.set_ambient_light(r, g, b, intensity); -} - -#[no_mangle] -pub extern "C" fn bloom_set_directional_light(dx: f64, dy: f64, dz: f64, r: f64, g: f64, b: f64, intensity: f64) { - engine().renderer.set_directional_light(dx, dy, dz, r, g, b, intensity); -} - -#[no_mangle] -pub extern "C" fn bloom_set_procedural_sky(enabled: f64, rayleigh_density: f64, mie_density: f64, ground_albedo: f64) { - engine().renderer.set_procedural_sky( - enabled != 0.0, - rayleigh_density as f32, - mie_density as f32, - ground_albedo as f32, - ); -} - -#[no_mangle] -pub extern "C" fn bloom_set_sun_direction(dx: f64, dy: f64, dz: f64, intensity: f64) { - engine().renderer.set_sun_direction(dx as f32, dy as f32, dz as f32, intensity as f32); -} - -// --- Utility FFI --- - -#[no_mangle] -pub extern "C" fn bloom_toggle_fullscreen() { - #[cfg(target_os = "linux")] - x11_impl::toggle_fullscreen(); -} -#[no_mangle] -pub extern "C" fn bloom_set_window_title(title_ptr: *const u8) { - let title = str_from_header(title_ptr); - #[cfg(target_os = "linux")] - x11_impl::set_window_title(title); -} -#[no_mangle] -pub extern "C" fn bloom_set_window_icon(path_ptr: *const u8) { - let path = str_from_header(path_ptr); - #[cfg(target_os = "linux")] - x11_impl::set_window_icon(path); -} - -#[no_mangle] -pub extern "C" fn bloom_disable_cursor() { - let input = &mut engine().input; - input.cursor_disabled = true; - input.clear_mouse_delta(); - #[cfg(target_os = "linux")] - x11_impl::enter_relative_mode(); -} - -#[no_mangle] -pub extern "C" fn bloom_enable_cursor() { - engine().input.cursor_disabled = false; - #[cfg(target_os = "linux")] - x11_impl::leave_relative_mode(); -} - -#[no_mangle] -pub extern "C" fn bloom_get_mouse_delta_x() -> f64 { - engine().input.mouse_delta_x -} - -#[no_mangle] -pub extern "C" fn bloom_get_mouse_delta_y() -> f64 { - engine().input.mouse_delta_y -} - -// Accumulated scroll wheel delta since the last call. Reading consumes the -// value (returns 0 on the next call until the user scrolls again). Used by -// the editor's orbit camera and any scrollable UI panel. -#[no_mangle] -pub extern "C" fn bloom_get_mouse_wheel() -> f64 { - engine().input.consume_mouse_wheel() -} - -#[no_mangle] -pub extern "C" fn bloom_get_char_pressed() -> f64 { - engine().input.pop_char() as f64 -} - -// Q2: Cursor shape -#[no_mangle] -pub extern "C" fn bloom_set_cursor_shape(shape: f64) { - engine().input.cursor_shape = shape as u32; -} - -// E4: Clipboard (arboard, X11/Wayland-aware) -#[no_mangle] -pub extern "C" fn bloom_set_clipboard_text(text_ptr: *const u8) { - let text = str_from_header(text_ptr); - if let Ok(mut clipboard) = arboard::Clipboard::new() { - let _ = clipboard.set_text(text.to_string()); - } -} -#[no_mangle] -pub extern "C" fn bloom_get_clipboard_text() -> *const u8 { - match arboard::Clipboard::new() { - Ok(mut clipboard) => match clipboard.get_text() { - Ok(text) => alloc_perry_string(&text), - Err(_) => alloc_perry_string(""), - }, - Err(_) => alloc_perry_string(""), - } -} - -// E5b: Native file dialogs (rfd → GTK/zenity/kdialog on Linux) -#[no_mangle] -pub extern "C" fn bloom_open_file_dialog(filter_ptr: *const u8, title_ptr: *const u8) -> *const u8 { - let filter = str_from_header(filter_ptr); - let title = str_from_header(title_ptr); - let mut dialog = rfd::FileDialog::new().set_title(title); - if !filter.is_empty() { - dialog = dialog.add_filter("Files", &[filter]); - } - match dialog.pick_file() { - Some(path) => alloc_perry_string(&path.to_string_lossy()), - None => alloc_perry_string(""), - } -} -#[no_mangle] -pub extern "C" fn bloom_save_file_dialog(default_name_ptr: *const u8, title_ptr: *const u8) -> *const u8 { - let default_name = str_from_header(default_name_ptr); - let title = str_from_header(title_ptr); - let dialog = rfd::FileDialog::new() - .set_title(title) - .set_file_name(default_name); - match dialog.save_file() { - Some(path) => alloc_perry_string(&path.to_string_lossy()), - None => alloc_perry_string(""), - } -} - -// 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. -#[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 -} -#[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 -} -#[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 -} -#[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 -} -#[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 -} -#[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 -} - -#[no_mangle] -pub extern "C" fn bloom_write_file(path_ptr: *const u8, data_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - let data = str_from_header(data_ptr); - match std::fs::write(path, data.as_bytes()) { - Ok(_) => 1.0, - Err(_) => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_file_exists(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - if std::path::Path::new(path).exists() { 1.0 } else { 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_read_file(path_ptr: *const u8) -> *const u8 { - let path = str_from_header(path_ptr); - // Always return a valid Perry string. A null pointer would NaN-box into a - // string-typed JS value pointing at address 0; subsequent `.length` / - // `.charCodeAt` reads dereference the bogus StringHeader and segfault. - // Callers detect "missing file" via `data.length === 0` (e.g. the - // jump game's discoverLevels probe across level1..level10 / custom_*). - match std::fs::read_to_string(path) { - Ok(contents) => alloc_perry_string(&contents), - Err(_) => alloc_perry_string(""), - } -} - -#[no_mangle] -pub extern "C" fn bloom_get_touch_x(index: f64) -> f64 { engine().input.get_touch_x(index as usize) } -#[no_mangle] -pub extern "C" fn bloom_get_touch_y(index: f64) -> f64 { engine().input.get_touch_y(index as usize) } -#[no_mangle] -pub extern "C" fn bloom_get_touch_count() -> f64 { engine().input.get_touch_count() as f64 } -#[no_mangle] -pub extern "C" fn bloom_get_time() -> f64 { engine().get_time() } - -// Input injection + platform detection -#[no_mangle] -pub extern "C" fn bloom_inject_key_down(key: f64) { - engine().input.set_key_down(key as usize); -} -#[no_mangle] -pub extern "C" fn bloom_inject_key_up(key: f64) { - engine().input.set_key_up(key as usize); -} -#[no_mangle] -pub extern "C" fn bloom_inject_gamepad_axis(axis: f64, value: f64) { - engine().input.set_gamepad_axis(axis as usize, value as f32); -} -#[no_mangle] -pub extern "C" fn bloom_inject_gamepad_button_down(button: f64) { - engine().input.set_gamepad_button_down(button as usize); -} -#[no_mangle] -pub extern "C" fn bloom_inject_gamepad_button_up(button: f64) { - engine().input.set_gamepad_button_up(button as usize); -} -#[no_mangle] -pub extern "C" fn bloom_get_platform() -> f64 { 4.0 } - -/// Preferred OS language packed as `c0*256+c1` (ISO-639 primary subtag), from $LANG/$LC_*. -#[no_mangle] -pub extern "C" fn bloom_get_language() -> f64 { - fn pack(code: &str) -> f64 { let l = code.to_ascii_lowercase(); let b = l.as_bytes(); if b.len() >= 2 { (b[0] as f64) * 256.0 + (b[1] as f64) } else { 25966.0 } } - let v = std::env::var("LANG").or_else(|_| std::env::var("LC_ALL")).or_else(|_| std::env::var("LC_MESSAGES")).unwrap_or_default(); - if v.len() >= 2 && !v.starts_with('C') && !v.starts_with("POSIX") { pack(&v) } else { 25966.0 } -} -#[no_mangle] -pub extern "C" fn bloom_is_any_input_pressed() -> f64 { - if engine().input.is_any_input_pressed() { 1.0 } else { 0.0 } -} -#[no_mangle] -pub extern "C" fn bloom_get_crown_rotation() -> f64 { - engine().input.consume_crown_rotation() -} - -// ============================================================ -// Frame callbacks -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_register_frame_callback(priority: f64, callback: extern "C" fn(f64)) -> f64 { - engine().frame_callbacks.register(priority as i32, callback) as f64 -} - -#[no_mangle] -pub extern "C" fn bloom_unregister_frame_callback(id: f64) { - engine().frame_callbacks.unregister(id as u64); -} - -#[no_mangle] -pub extern "C" fn bloom_run_game(_callback: extern "C" fn(f64)) { - // No-op on native. The TypeScript runGame() helper provides the while loop. -} - -// ============================================================ -// Multiple lights -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_add_directional_light( - dx: f64, dy: f64, dz: f64, - r: f64, g: f64, b: f64, - intensity: f64, -) { - engine().renderer.add_directional_light( - dx as f32, dy as f32, dz as f32, - r as f32, g as f32, b as f32, - intensity as f32, - ); -} - -#[no_mangle] -pub extern "C" fn bloom_add_point_light( - x: f64, y: f64, z: f64, range: f64, - r: f64, g: f64, b: f64, - intensity: f64, -) { - engine().renderer.add_point_light( - x as f32, y as f32, z as f32, range as f32, - r as f32, g as f32, b as f32, - intensity as f32, - ); -} - -// ============================================================ -// Scene graph (retained mode) -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_scene_create_node() -> f64 { - engine().scene.create_node() -} - -#[no_mangle] -pub extern "C" fn bloom_scene_destroy_node(handle: f64) { - engine().scene.destroy_node(handle); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_set_visible(handle: f64, visible: f64) { - engine().scene.set_visible(handle, visible != 0.0); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_set_cast_shadow(handle: f64, cast: f64) { - engine().scene.set_cast_shadow(handle, cast != 0.0); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_set_receive_shadow(handle: f64, receive: f64) { - engine().scene.set_receive_shadow(handle, receive != 0.0); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_set_parent(handle: f64, parent: f64) { - engine().scene.set_parent(handle, parent); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_set_transform(handle: f64, mat_ptr: *const f64) { - if mat_ptr.is_null() { return; } - let slice = unsafe { std::slice::from_raw_parts(mat_ptr, 16) }; - let mut mat = [[0.0f32; 4]; 4]; - for col in 0..4 { - for row in 0..4 { - mat[col][row] = slice[col * 4 + row] as f32; - } - } - engine().scene.set_transform(handle, mat); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_update_geometry( - handle: f64, - vert_ptr: *const f64, - vert_count: f64, - idx_ptr: *const f64, - idx_count: f64, -) { - if vert_ptr.is_null() || idx_ptr.is_null() { return; } - let nv = vert_count as usize; - let ni = idx_count as usize; - - let vert_floats = unsafe { std::slice::from_raw_parts(vert_ptr, nv * 12) }; - let idx_floats = unsafe { std::slice::from_raw_parts(idx_ptr, ni) }; - - let mut vertices = Vec::with_capacity(nv); - for i in 0..nv { - let base = i * 12; - vertices.push(bloom_shared::renderer::Vertex3D { - position: [vert_floats[base] as f32, vert_floats[base+1] as f32, vert_floats[base+2] as f32], - normal: [vert_floats[base+3] as f32, vert_floats[base+4] as f32, vert_floats[base+5] as f32], - color: [vert_floats[base+6] as f32, vert_floats[base+7] as f32, vert_floats[base+8] as f32, vert_floats[base+9] as f32], - uv: [vert_floats[base+10] as f32, vert_floats[base+11] as f32], - joints: [0.0; 4], - weights: [0.0; 4], - tangent: [0.0; 4], - }); - } - - let indices: Vec = idx_floats.iter().map(|&v| v as u32).collect(); - - engine().scene.update_geometry(handle, vertices, indices); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_set_material_color(handle: f64, r: f64, g: f64, b: f64, a: f64) { - engine().scene.set_material_color(handle, r as f32, g as f32, b as f32, a as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_set_material_pbr(handle: f64, roughness: f64, metalness: f64) { - engine().scene.set_material_pbr(handle, roughness as f32, metalness as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_set_material_texture(handle: f64, texture_idx: f64) { - engine().scene.set_material_texture(handle, texture_idx as u32); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_node_count() -> f64 { - engine().scene.node_count() as f64 -} - -#[no_mangle] -pub extern "C" fn bloom_scene_node_vertex_count(handle: f64) -> f64 { - match engine().scene.nodes.get(handle) { - Some(node) => node.vertices.len() as f64, - None => -1.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_scene_node_index_count(handle: f64) -> f64 { - match engine().scene.nodes.get(handle) { - Some(node) => node.indices.len() as f64, - None => -1.0, - } -} - - -// Q8: Set a water material on a scene node (translucent tint, low roughness). -#[no_mangle] -pub extern "C" fn bloom_scene_set_material_water(handle: f64, wave_amp: f64, wave_speed: f64, r: f64, g: f64, b: f64, a: f64) { - engine().scene.set_material_water(handle, wave_amp as f32, wave_speed as f32, r as f32, g as f32, b as f32, a as f32); -} - -// Q9: Generate a ribbon mesh along a Catmull-Rom spline. -#[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; - let wn = width_count as usize; - let points = unsafe { std::slice::from_raw_parts(points_ptr as *const f32, n * 3) }; - let widths = unsafe { std::slice::from_raw_parts(widths_ptr as *const f32, wn) }; - engine().models.gen_mesh_spline_ribbon(points, widths) -} - -// Q1: Render texture FFI (stub — GPU implementation deferred). -#[no_mangle] -pub extern "C" fn bloom_load_render_texture(width: f64, height: f64) -> f64 { - engine().textures.load_render_texture(width as u32, height as u32) -} -#[no_mangle] -pub extern "C" fn bloom_unload_render_texture(handle: f64) { - engine().textures.unload_render_texture(handle); -} -#[no_mangle] -pub extern "C" fn bloom_begin_texture_mode(_handle: f64) { - // Stub: no-op until GPU render-to-texture is wired. -} -#[no_mangle] -pub extern "C" fn bloom_end_texture_mode() { - // Stub: no-op. -} -#[no_mangle] -pub extern "C" fn bloom_get_render_texture_texture(handle: f64) -> f64 { - engine().textures.get_render_texture_texture(handle) -} - -// Scene graph QoL — Q4/Q5/Q6/Q7 -#[no_mangle] -pub extern "C" fn bloom_scene_get_transform(handle: f64, index: f64) -> f64 { - let mat = engine().scene.get_transform(handle); - let i = index as usize; - let col = i / 4; - let row = i % 4; - if col < 4 && row < 4 { mat[col][row] as f64 } else { 0.0 } -} -#[no_mangle] -pub extern "C" fn bloom_scene_get_bounds_min_x(handle: f64) -> f64 { engine().scene.get_bounds(handle).0[0] as f64 } -#[no_mangle] -pub extern "C" fn bloom_scene_get_bounds_min_y(handle: f64) -> f64 { engine().scene.get_bounds(handle).0[1] as f64 } -#[no_mangle] -pub extern "C" fn bloom_scene_get_bounds_min_z(handle: f64) -> f64 { engine().scene.get_bounds(handle).0[2] as f64 } -#[no_mangle] -pub extern "C" fn bloom_scene_get_bounds_max_x(handle: f64) -> f64 { engine().scene.get_bounds(handle).1[0] as f64 } -#[no_mangle] -pub extern "C" fn bloom_scene_get_bounds_max_y(handle: f64) -> f64 { engine().scene.get_bounds(handle).1[1] as f64 } -#[no_mangle] -pub extern "C" fn bloom_scene_get_bounds_max_z(handle: f64) -> f64 { engine().scene.get_bounds(handle).1[2] as f64 } -#[no_mangle] -pub extern "C" fn bloom_scene_set_user_data(handle: f64, data: f64) { engine().scene.set_user_data(handle, data as i64); } -#[no_mangle] -pub extern "C" fn bloom_scene_get_user_data(handle: f64) -> f64 { engine().scene.get_user_data(handle) as f64 } -// ============================================================ -// Geometry generation -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_scene_extrude_polygon( - handle: f64, - polygon_ptr: *const f64, - polygon_count: f64, - depth: f64, -) { - if polygon_ptr.is_null() { return; } - let n = polygon_count as usize; - let polygon = unsafe { std::slice::from_raw_parts(polygon_ptr, n * 2) }; - - let geo = bloom_shared::geometry::extrude_polygon(polygon, &[], depth); - engine().scene.update_geometry(handle, geo.vertices, geo.indices); -} - -#[no_mangle] -pub extern "C" fn bloom_scene_subtract_box( - handle: f64, - min_x: f64, min_y: f64, min_z: f64, - max_x: f64, max_y: f64, max_z: f64, -) { - let eng = engine(); - if let Some(node) = eng.scene.nodes.get(handle) { - let current = bloom_shared::geometry::GeometryData { - vertices: node.vertices.clone(), - indices: node.indices.clone(), - }; - let result = bloom_shared::geometry::subtract_box( - ¤t, - [min_x as f32, min_y as f32, min_z as f32], - [max_x as f32, max_y as f32, max_z as f32], - ); - eng.scene.update_geometry(handle, result.vertices, result.indices); - } -} - -// ============================================================ -// Shadow mapping -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_enable_shadows() { - engine().renderer.shadow_map.enable(); -} - -#[no_mangle] -pub extern "C" fn bloom_disable_shadows() { - engine().renderer.shadow_map.disable(); -} - -#[no_mangle] -pub extern "C" fn bloom_dump_shadow_map(path_ptr: *const u8) { - let path = str_from_header(path_ptr).to_string(); - engine().renderer.dump_shadow_map(&path); -} - -// ============================================================ -// Post-processing -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_enable_postfx() { - let eng = engine(); - let w = eng.renderer.width(); - let h = eng.renderer.height(); - let fmt = eng.renderer.surface_format(); - eng.postfx = Some(bloom_shared::postfx::PostFxPipeline::new( - &eng.renderer.device, w, h, fmt, - )); -} - -#[no_mangle] -pub extern "C" fn bloom_disable_postfx() { - engine().postfx = None; -} - -#[no_mangle] -pub extern "C" fn bloom_postfx_set_selected(handle: f64) { - if let Some(pfx) = &mut engine().postfx { - if handle == 0.0 { - pfx.set_selected(Vec::new()); - } else { - pfx.set_selected(vec![handle]); - } - } -} - -#[no_mangle] -pub extern "C" fn bloom_postfx_set_hovered(handle: f64) { - if let Some(pfx) = &mut engine().postfx { - pfx.set_hovered(handle); - } -} - -#[no_mangle] -pub extern "C" fn bloom_postfx_set_outline_color(r: f64, g: f64, b: f64, a: f64) { - if let Some(pfx) = &mut engine().postfx { - pfx.outline_params.color_selected = [r as f32, g as f32, b as f32, a as f32]; - } -} - -#[no_mangle] -pub extern "C" fn bloom_postfx_set_outline_thickness(thickness: f64) { - if let Some(pfx) = &mut engine().postfx { - pfx.outline_params.thickness[0] = thickness as f32; - } -} - -// ============================================================ -// 3D→2D Projection (for UI overlays positioned in 3D space) -// ============================================================ - -static mut LAST_PROJECT: (f64, f64) = (0.0, 0.0); - -#[no_mangle] -pub extern "C" fn bloom_project_to_screen(wx: f64, wy: f64, wz: f64) -> f64 { - let eng = engine(); - let vp = eng.renderer.vp_matrix(); - let w = eng.renderer.width() as f32; - let h = eng.renderer.height() as f32; - - let x = wx as f32; - let y = wy as f32; - let z = wz as f32; - let clip_x = vp[0][0]*x + vp[1][0]*y + vp[2][0]*z + vp[3][0]; - let clip_y = vp[0][1]*x + vp[1][1]*y + vp[2][1]*z + vp[3][1]; - let clip_w = vp[0][3]*x + vp[1][3]*y + vp[2][3]*z + vp[3][3]; - - if clip_w <= 0.0 { - unsafe { LAST_PROJECT = (-9999.0, -9999.0); } - return -9999.0; - } - - let ndc_x = clip_x / clip_w; - let ndc_y = clip_y / clip_w; - let screen_x = ((ndc_x + 1.0) * 0.5 * w) as f64; - let screen_y = ((1.0 - ndc_y) * 0.5 * h) as f64; - - unsafe { LAST_PROJECT = (screen_x, screen_y); } - screen_x -} - -#[no_mangle] -pub extern "C" fn bloom_project_screen_y() -> f64 { - unsafe { LAST_PROJECT.1 } -} - -#[no_mangle] -pub extern "C" fn bloom_scene_attach_model(node_handle: f64, model_handle: f64, mesh_index: f64) { - let eng = engine(); - let mi = mesh_index as usize; - - let model_data = match eng.models.models.get(model_handle) { - Some(md) => md, - None => return, - }; - if mi >= model_data.meshes.len() { return; } - let mesh = &model_data.meshes[mi]; - - let vertices = mesh.vertices.clone(); - let indices = mesh.indices.clone(); - let base_color_tex = mesh.texture_idx; - let normal_tex = mesh.normal_texture_idx; - let mr_tex = mesh.metallic_roughness_texture_idx; - let emissive_tex = mesh.emissive_texture_idx; - let emissive_factor = mesh.emissive_factor; - eng.scene.update_geometry(node_handle, vertices, indices); - - if let Some(tex_idx) = base_color_tex { - eng.scene.set_material_texture(node_handle, tex_idx); - } - if let Some(tex_idx) = normal_tex { - eng.scene.set_material_normal_texture(node_handle, tex_idx); - } - if let Some(tex_idx) = mr_tex { - eng.scene.set_material_metallic_roughness_texture(node_handle, tex_idx); - } - if let Some(tex_idx) = emissive_tex { - eng.scene.set_material_emissive_texture(node_handle, tex_idx); - } - eng.scene.set_material_emissive_factor( - node_handle, - emissive_factor[0], - emissive_factor[1], - emissive_factor[2], - ); -} // ============================================================ // Scene picking (raycasting) // ============================================================ -static mut LAST_PICK: Option = None; - -#[no_mangle] -pub extern "C" fn bloom_scene_pick(screen_x: f64, screen_y: f64) -> f64 { - let eng = engine(); - let inv_vp = eng.renderer.inverse_vp_matrix(); - let cam_pos = eng.renderer.camera_pos(); - let w = eng.renderer.width() as f32; - let h = eng.renderer.height() as f32; - - let (origin, direction) = bloom_shared::picking::screen_to_ray( - screen_x as f32, screen_y as f32, - w, h, &inv_vp, &cam_pos, - ); - - let result = bloom_shared::picking::raycast_scene(&eng.scene, &origin, &direction); - let hit = result.hit; - unsafe { LAST_PICK = Some(result); } - if hit { 1.0 } else { 0.0 } -} - -#[no_mangle] -pub extern "C" fn bloom_pick_hit_handle() -> f64 { - unsafe { LAST_PICK.as_ref().map(|r| r.handle).unwrap_or(0.0) } -} - -#[no_mangle] -pub extern "C" fn bloom_pick_hit_distance() -> f64 { - unsafe { LAST_PICK.as_ref().map(|r| r.distance as f64).unwrap_or(0.0) } -} - -#[no_mangle] -pub extern "C" fn bloom_pick_hit_x() -> f64 { - unsafe { LAST_PICK.as_ref().map(|r| r.point[0] as f64).unwrap_or(0.0) } -} - -#[no_mangle] -pub extern "C" fn bloom_pick_hit_y() -> f64 { - unsafe { LAST_PICK.as_ref().map(|r| r.point[1] as f64).unwrap_or(0.0) } -} - -#[no_mangle] -pub extern "C" fn bloom_pick_hit_z() -> f64 { - unsafe { LAST_PICK.as_ref().map(|r| r.point[2] as f64).unwrap_or(0.0) } -} - -#[no_mangle] -pub extern "C" fn bloom_pick_hit_normal_x() -> f64 { - unsafe { LAST_PICK.as_ref().map(|r| r.normal[0] as f64).unwrap_or(0.0) } -} - -#[no_mangle] -pub extern "C" fn bloom_pick_hit_normal_y() -> f64 { - unsafe { LAST_PICK.as_ref().map(|r| r.normal[1] as f64).unwrap_or(0.0) } -} - -#[no_mangle] -pub extern "C" fn bloom_pick_hit_normal_z() -> f64 { - unsafe { LAST_PICK.as_ref().map(|r| r.normal[2] as f64).unwrap_or(0.0) } -} // ============================================================ // Thread-safe staging (for async asset loading via Perry threads) // ============================================================ -#[no_mangle] -pub extern "C" fn bloom_stage_texture(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(path) { - Ok(data) => bloom_shared::staging::decode_and_stage_texture(&data), - Err(_) => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_stage_model(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - let data = match std::fs::read(path) { - Ok(d) => d, - Err(_) => return 0.0, - }; - match bloom_shared::models::load_gltf_staged(&data) { - Some(staged) => bloom_shared::staging::stage_model(staged), - None => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_stage_sound(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - let data = match std::fs::read(path) { - Ok(d) => d, - Err(_) => return 0.0, - }; - let sound_data = if path.ends_with(".ogg") || path.ends_with(".OGG") { - parse_ogg(&data) - } else if path.ends_with(".mp3") || path.ends_with(".MP3") { - parse_mp3(&data) - } else { - parse_wav(&data) - }; - match sound_data { - Some(sd) => bloom_shared::staging::stage_sound(sd), - None => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_commit_texture(staging_handle: f64) -> f64 { - let staged = match bloom_shared::staging::take_texture(staging_handle) { - Some(s) => s, - None => return 0.0, - }; - let eng = engine(); - let bind_group_idx = eng.renderer.register_texture(staged.width, staged.height, &staged.data); - eng.textures.textures.alloc(bloom_shared::textures::TextureData { - bind_group_idx, width: staged.width, height: staged.height, - }) -} - -#[no_mangle] -pub extern "C" fn bloom_commit_model(staging_handle: f64) -> f64 { - let staged = match bloom_shared::staging::take_model(staging_handle) { - Some(s) => s, - None => return 0.0, - }; - let eng = engine(); - let mut tex_map: Vec = Vec::with_capacity(staged.textures.len()); - for tex in &staged.textures { - tex_map.push(eng.renderer.register_texture(tex.width, tex.height, &tex.data)); - } - let mut model = staged.model; - for mesh in &mut model.meshes { - if let Some(ref mut idx) = mesh.texture_idx { - let staged_idx = *idx as usize; - if staged_idx > 0 && staged_idx <= tex_map.len() { - *idx = tex_map[staged_idx - 1]; - } else { - mesh.texture_idx = None; - } - } - } - eng.models.models.alloc(model) -} - -#[no_mangle] -pub extern "C" fn bloom_commit_sound(staging_handle: f64) -> f64 { - match bloom_shared::staging::take_sound(staging_handle) { - Some(sd) => engine().audio.load_sound(sd), - None => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_commit_music(staging_handle: f64) -> f64 { - match bloom_shared::staging::take_sound(staging_handle) { - Some(sd) => engine().audio.load_music(sd), - None => 0.0, - } -} - fn pollster_block_on(future: F) -> F::Output { use std::task::{Context, Poll, Wake, Waker}; use std::pin::Pin; @@ -2470,33 +961,6 @@ fn pollster_block_on(future: F) -> F::Output { // Q6: Multi-hit picking -static mut LAST_PICK_ALL: Vec = Vec::new(); - -#[no_mangle] -pub extern "C" fn bloom_scene_pick_all(screen_x: f64, screen_y: f64, max_results: f64) -> f64 { - let eng = engine(); - let inv_vp = eng.renderer.inverse_vp_matrix(); - let cam_pos = eng.renderer.camera_pos(); - let w = eng.renderer.width() as f32; - let h = eng.renderer.height() as f32; - let (origin, direction) = bloom_shared::picking::screen_to_ray( - screen_x as f32, screen_y as f32, w, h, &inv_vp, &cam_pos, - ); - let results = bloom_shared::picking::raycast_scene_all(&eng.scene, &origin, &direction, max_results as usize); - let count = results.len(); - unsafe { LAST_PICK_ALL = results; } - count as f64 -} -#[no_mangle] -pub extern "C" fn bloom_pick_all_handle(index: f64) -> f64 { - let i = index as usize; - unsafe { LAST_PICK_ALL.get(i).map(|r| r.handle).unwrap_or(0.0) } -} -#[no_mangle] -pub extern "C" fn bloom_pick_all_distance(index: f64) -> f64 { - let i = index as usize; - unsafe { LAST_PICK_ALL.get(i).map(|r| r.distance as f64).unwrap_or(0.0) } -} // ============================================================ // ============================================================ @@ -2506,51 +970,6 @@ pub extern "C" fn bloom_pick_all_distance(index: f64) -> f64 { // don't fail at runtime (missing symbol) when the TS API invokes them. // ============================================================ -#[no_mangle] -pub extern "C" fn bloom_set_quality_preset(preset: f64) { - engine().renderer.apply_quality_preset(preset as u32); -} -#[no_mangle] -pub extern "C" fn bloom_set_shadows_enabled(on: f64) { - engine().renderer.set_shadows_enabled(on != 0.0); -} -#[no_mangle] -pub extern "C" fn bloom_set_shadows_always_fresh(on: f64) { - engine().renderer.set_shadows_always_fresh(on != 0.0); -} -#[no_mangle] -pub extern "C" fn bloom_set_bloom_enabled(on: f64) { - engine().renderer.set_bloom_enabled(on != 0.0); -} -#[no_mangle] -pub extern "C" fn bloom_set_ssao_enabled(on: f64) { - engine().renderer.set_ssao_enabled(on != 0.0); -} -#[no_mangle] -pub extern "C" fn bloom_set_ssao_intensity(value: f64) { - engine().renderer.set_ssao_strength(value as f32); -} -#[no_mangle] -pub extern "C" fn bloom_set_ssao_radius(world_radius: f64) { - engine().renderer.set_ssao_radius(world_radius as f32); -} -#[no_mangle] -pub extern "C" fn bloom_set_wind(dir_x: f64, dir_z: f64, amplitude: f64, frequency: f64) { - engine().renderer.set_wind(dir_x as f32, dir_z as f32, amplitude as f32, frequency as f32); -} -#[no_mangle] -pub extern "C" fn bloom_set_ssr_enabled(on: f64) { - engine().renderer.set_ssr_enabled(on != 0.0); -} -#[no_mangle] -pub extern "C" fn bloom_set_motion_blur_enabled(on: f64) { - engine().renderer.set_motion_blur_enabled(on != 0.0); -} -#[no_mangle] -pub extern "C" fn bloom_set_sss_enabled(on: f64) { - engine().renderer.set_sss_enabled(on != 0.0); -} - // ============================================================ // Render scale / upscale / DRS / post-FX / screenshots / impulse // Ports of the macOS FFI surface so the bloom/core TS layer links @@ -2558,212 +977,12 @@ pub extern "C" fn bloom_set_sss_enabled(on: f64) { // underlying renderer methods, so these wrappers are platform-agnostic. // ============================================================ -#[no_mangle] -pub extern "C" fn bloom_take_screenshot(path_ptr: *const u8) { - let path = str_from_header(path_ptr).to_string(); - let eng = engine(); - eng.renderer.screenshot_requested = true; - eng.renderer.pending_screenshot_path = Some(path); -} - -#[no_mangle] -pub extern "C" fn bloom_set_env_clear_from_hdr(path_ptr: *const u8) { - use image::ImageDecoder; - let path = str_from_header(path_ptr).to_string(); - let file = match std::fs::File::open(&path) { - Ok(f) => f, - Err(_) => return, - }; - let decoder = match image::codecs::hdr::HdrDecoder::new(std::io::BufReader::new(file)) { - Ok(d) => d, - Err(_) => return, - }; - let (w, h) = decoder.dimensions(); - let byte_len = (w as usize) * (h as usize) * 3 * 4; - let mut buf = vec![0u8; byte_len]; - if decoder.read_image(&mut buf).is_err() { - return; - } - let rgb_f32: Vec = buf - .chunks_exact(4) - .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]])) - .collect(); - engine().renderer.load_env_from_hdr(w, h, &rgb_f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_fog(r: f64, g: f64, b: f64, density: f64, height_ref: f64, height_falloff: f64) { - let r_ = engine(); - r_.renderer.set_fog_color(r as f32, g as f32, b as f32); - r_.renderer.set_fog_density(density as f32); - r_.renderer.set_fog_height_falloff(height_ref as f32, height_falloff as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_chromatic_aberration(strength: f64) { - engine().renderer.set_chromatic_aberration(strength as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_vignette(strength: f64, softness: f64) { - engine().renderer.set_vignette(strength as f32, softness as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_film_grain(strength: f64) { - engine().renderer.set_film_grain(strength as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_sun_shafts(strength: f64, decay: f64, r: f64, g: f64, b: f64) { - let eng = engine(); - eng.renderer.set_sun_shaft_strength(strength as f32); - eng.renderer.set_sun_shaft_decay(decay as f32); - eng.renderer.set_sun_shaft_color(r as f32, g as f32, b as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_auto_exposure(on: f64) { - engine().renderer.set_auto_exposure(on != 0.0); -} - -#[no_mangle] -pub extern "C" fn bloom_set_taa_enabled(on: f64) { - engine().renderer.set_taa_enabled(on != 0.0); -} - -#[no_mangle] -pub extern "C" fn bloom_set_render_scale(scale: f64) { - engine().renderer.set_render_scale(scale as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_get_render_scale() -> f64 { - engine().renderer.render_scale() as f64 -} - -#[no_mangle] -pub extern "C" fn bloom_set_upscale_mode(mode: f64) { - engine().renderer.set_upscale_mode(mode as u32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_cas_strength(strength: f64) { - engine().renderer.set_cas_strength(strength as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_get_physical_width() -> f64 { - engine().renderer.physical_width() as f64 -} - -#[no_mangle] -pub extern "C" fn bloom_get_physical_height() -> f64 { - engine().renderer.physical_height() as f64 -} - -#[no_mangle] -pub extern "C" fn bloom_set_auto_resolution(target_hz: f64, enabled: f64) { - let eng = engine(); - if enabled != 0.0 { - let current = eng.renderer.render_scale(); - eng.drs.enable(target_hz as f32, current); - } else { - eng.drs.disable(); - } -} - -#[no_mangle] -pub extern "C" fn bloom_set_manual_exposure(value: f64) { - engine().renderer.set_manual_exposure(value as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_env_intensity(intensity: f64) { - engine().renderer.set_env_intensity(intensity as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_ssgi_enabled(enabled: f64) { - engine().renderer.set_ssgi_enabled(enabled != 0.0); -} - -#[no_mangle] -pub extern "C" fn bloom_set_ssgi_intensity(intensity: f64) { - engine().renderer.set_ssgi_intensity(intensity as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_ssgi_radius(radius: f64) { - engine().renderer.set_ssgi_radius(radius as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_dof(enabled: f64, focus_distance: f64, aperture: f64) { - let r = &mut engine().renderer; - r.set_dof_enabled(enabled != 0.0); - r.set_dof_focus_distance(focus_distance as f32); - r.set_dof_aperture(aperture as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_splat_impulse(x: f64, z: f64, radius: f64, strength: f64) { - engine().renderer.impulse_field.submit_splat( - x as f32, z as f32, radius as f32, strength as f32, - ); -} - -#[no_mangle] -pub extern "C" fn bloom_profiler_frame_history() -> *const u8 { - let hist = engine().profiler.frame_history(); - let mut s = String::with_capacity(hist.len() * 24); - for (cpu, gpu) in &hist { - s.push_str(&format!("{:.2}|{:.2}\n", cpu, gpu)); - } - alloc_perry_string(&s) -} - -#[no_mangle] -pub extern "C" fn bloom_profiler_overlay_text() -> *const u8 { - let snap = engine().profiler.snapshot(); - let mut s = String::with_capacity(snap.len() * 48); - for (label, cpu, gpu) in &snap { - s.push_str(label); - s.push('|'); - s.push_str(&format!("{:.2}", cpu)); - s.push('|'); - match gpu { - Some(g) => s.push_str(&format!("{:.2}", g)), - None => s.push_str("-1"), - } - s.push('\n'); - } - alloc_perry_string(&s) -} - // ============================================================ // Profiler — CPU phase timings (always available) + GPU timestamps // (when the adapter supports TIMESTAMP_QUERY). Disabled by default. // ============================================================ -#[no_mangle] -pub extern "C" fn bloom_set_profiler_enabled(on: f64) { - engine().profiler.set_enabled(on != 0.0); -} -#[no_mangle] -pub extern "C" fn bloom_get_profiler_frame_cpu_us() -> f64 { - engine().profiler.avg_frame_cpu_us() -} -#[no_mangle] -pub extern "C" fn bloom_get_profiler_frame_gpu_us() -> f64 { - engine().profiler.avg_frame_gpu_us() -} -#[no_mangle] -pub extern "C" fn bloom_print_profiler_summary() { - print!("{}", engine().profiler.summary()); -} - // ============================================================ // Geisterhand screenshot integration // ============================================================ @@ -2798,6 +1017,9 @@ extern "C" fn bloom_screenshot_capture(out_len: *mut usize) -> *mut u8 { &eng.renderer.vp_matrix(), &eng.renderer.prev_vp_matrix, eng.renderer.uniform_3d_layout(), + // Screenshot capture renders everything the camera might see — + // never occlusion-cull a one-shot capture. + None, ); eng.scene.prepare_materials(&eng.renderer); { diff --git a/native/macos/src/lib.rs b/native/macos/src/lib.rs index 7546018..24e336f 100644 --- a/native/macos/src/lib.rs +++ b/native/macos/src/lib.rs @@ -9,7 +9,6 @@ use bloom_shared::engine::EngineState; use bloom_shared::renderer::Renderer; use bloom_shared::string_header::{str_from_header, alloc_perry_string}; -use bloom_shared::audio::{parse_wav, parse_ogg, parse_mp3}; use objc2::rc::Retained; use objc2::{msg_send, MainThreadMarker, MainThreadOnly}; @@ -27,10 +26,26 @@ static mut WINDOW: Option> = None; // completion. static mut HEADLESS: bool = false; static mut AUDIO_UNIT: Option = None; +// Render half of the audio system. Moved here from EngineState by +// bloom_init_audio (AudioMixer::take_renderer) BEFORE the CoreAudio +// callback starts; after that it is owned exclusively by the audio +// render thread. The old design mixed through ENGINE from the callback — +// a cross-thread data race with every play/stop call on the main thread. +static mut AUDIO_RENDERER: Option = None; fn engine() -> &'static mut EngineState { unsafe { ENGINE.get_mut().expect("Engine not initialized") } } +/// Asset-path hook for define_core_ffi! — identity on desktop, where game +/// asset paths are valid relative to the working directory. +fn bloom_resolve_asset_path(path: &str) -> std::borrow::Cow<'_, str> { + std::borrow::Cow::Borrowed(path) +} + +// The full shared (non-physics) FFI surface. See bloom_shared::ffi_core +// docs for the contract; tools/validate-ffi.js checks parity in CI. +bloom_shared::define_core_ffi!(); + /// Map macOS virtual key code to Bloom key code. fn map_keycode(keycode: u16) -> usize { @@ -236,9 +251,10 @@ unsafe extern "C" fn audio_render_callback( num_samples, ); - ENGINE.get_mut().map(|eng| { - eng.audio.mix_output(output); - }); + match AUDIO_RENDERER.as_mut() { + Some(r) => r.mix(output), + None => output.iter_mut().for_each(|s| *s = 0.0), + } 0 // noErr } @@ -366,6 +382,11 @@ pub extern "C" fn bloom_init_window(width: f64, height: f64, title_ptr: *const u if supported.contains(wgpu::Features::TIMESTAMP_QUERY) { required_features |= wgpu::Features::TIMESTAMP_QUERY; } + // Cooked BC7 textures (bloom-cook) upload compressed when the + // adapter has BC support; without it they CPU-decode at load. + if supported.contains(wgpu::Features::TEXTURE_COMPRESSION_BC) { + required_features |= wgpu::Features::TEXTURE_COMPRESSION_BC; + } // Ticket 007b: request ray-query + BLAS/TLAS where the adapter // supports both (Apple Silicon Metal, DXR 1.1, VK_KHR_ray_query). // `BLOOM_FORCE_SW_GI=1` forces the SW fallback for testing parity @@ -617,2265 +638,329 @@ pub extern "C" fn bloom_end_drawing() { engine().end_frame(); } -/// Request a PNG screenshot of the next rendered frame. -/// The capture happens during the next end_drawing(), so the caller -/// should call beginDrawing/endDrawing once after this for the file -/// to actually appear on disk. Used by bloom-diff and CI image -/// regression workflows. -#[no_mangle] -pub extern "C" fn bloom_take_screenshot(path_ptr: *const u8) { - let path = str_from_header(path_ptr).to_string(); - let eng = engine(); - eng.renderer.screenshot_requested = true; - eng.renderer.pending_screenshot_path = Some(path); -} - -#[no_mangle] -pub extern "C" fn bloom_clear_background(r: f64, g: f64, b: f64, a: f64) { - engine().renderer.set_clear_color(r, g, b, a); -} +// ============================================================ +// Input - Keyboard +// ============================================================ -#[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 -/// being a flat clear color. The file must be Radiance HDR (.hdr). -#[no_mangle] -pub extern "C" fn bloom_set_env_clear_from_hdr(path_ptr: *const u8) { - use image::ImageDecoder; - let path = str_from_header(path_ptr).to_string(); - let file = match std::fs::File::open(&path) { - Ok(f) => f, - Err(_) => return, - }; - let decoder = match image::codecs::hdr::HdrDecoder::new(std::io::BufReader::new(file)) { - Ok(d) => d, - Err(_) => return, - }; - let (w, h) = decoder.dimensions(); - let byte_len = (w as usize) * (h as usize) * 3 * 4; - let mut buf = vec![0u8; byte_len]; - if decoder.read_image(&mut buf).is_err() { - return; - } - // Reinterpret the byte buffer as f32 RGB triples for the renderer. - let rgb_f32: Vec = buf - .chunks_exact(4) - .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]])) - .collect(); - engine().renderer.load_env_from_hdr(w, h, &rgb_f32); -} +// ============================================================ +// Input - Mouse +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_set_target_fps(fps: f64) { - engine().target_fps = fps; -} +// ============================================================ +// Input - Gamepad +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_set_direct_2d_mode(on: f64) { - engine().direct_2d_mode = on > 0.5; -} +// ============================================================ +// Input - Touch +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_get_delta_time() -> f64 { - engine().delta_time -} +// ============================================================ +// Shapes +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_get_fps() -> f64 { - engine().get_fps() -} +// ============================================================ +// Text +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_get_screen_width() -> f64 { - engine().screen_width() -} +// ============================================================ +// Textures +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_get_screen_height() -> f64 { - engine().screen_height() -} +// ============================================================ +// Camera 2D +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_get_time() -> f64 { - engine().get_time() -} +// ============================================================ +// Camera 3D and 3D drawing +// ============================================================ // ============================================================ -// Input - Keyboard +// Joint test // ============================================================ -#[no_mangle] -pub extern "C" fn bloom_is_key_pressed(key: f64) -> f64 { - if engine().input.is_key_pressed(key as usize) { 1.0 } else { 0.0 } -} +// ============================================================ +// Lighting +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_is_key_down(key: f64) -> f64 { - if engine().input.is_key_down(key as usize) { 1.0 } else { 0.0 } -} +// --- EN-005: procedural sky --- +// Toggle procedural-atmosphere rendering and steer the sun. The +// renderer owns the on/off flag + LUT state; setting the sun marks +// the sky-view LUT dirty so it re-bakes before the next frame. -#[no_mangle] -pub extern "C" fn bloom_is_key_released(key: f64) -> f64 { - if engine().input.is_key_released(key as usize) { 1.0 } else { 0.0 } -} +// --- Post-FX knobs (heuristic visual layer; default-off) --- // ============================================================ -// Input - Mouse +// Render quality toggles (individual + preset) // ============================================================ -#[no_mangle] -pub extern "C" fn bloom_get_mouse_x() -> f64 { - engine().input.mouse_x -} - -#[no_mangle] -pub extern "C" fn bloom_get_mouse_y() -> f64 { - engine().input.mouse_y -} - -#[no_mangle] -pub extern "C" fn bloom_is_mouse_button_pressed(btn: f64) -> f64 { - if engine().input.is_mouse_button_pressed(btn as usize) { 1.0 } else { 0.0 } -} +// ============================================================ +// Profiler — CPU phase timings (always available) + GPU timestamps +// (when the adapter supports TIMESTAMP_QUERY). Disabled by default. +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_is_mouse_button_down(btn: f64) -> f64 { - if engine().input.is_mouse_button_down(btn as usize) { 1.0 } else { 0.0 } -} +// ============================================================ +// Models +// ============================================================ -#[no_mangle] -pub extern "C" fn bloom_is_mouse_button_released(btn: f64) -> f64 { - if engine().input.is_mouse_button_released(btn as usize) { 1.0 } else { 0.0 } -} +// ============================================================ +// Phase 1c — material system FFI +// ============================================================ // ============================================================ -// Input - Gamepad +// Audio // ============================================================ #[no_mangle] -pub extern "C" fn bloom_is_gamepad_available() -> f64 { - if engine().input.is_gamepad_available() { 1.0 } else { 0.0 } -} +pub extern "C" fn bloom_init_audio() { + unsafe { + // Hand the render half to the audio thread before the callback + // can fire. Idempotent: a second init keeps the existing renderer. + if AUDIO_RENDERER.is_none() { + AUDIO_RENDERER = engine().audio.take_renderer(); + } + let desc = AudioComponentDescription { + component_type: K_AUDIO_UNIT_TYPE_OUTPUT, + component_sub_type: K_AUDIO_UNIT_SUB_TYPE_DEFAULT_OUTPUT, + component_manufacturer: K_AUDIO_UNIT_MANUFACTURER_APPLE, + component_flags: 0, + component_flags_mask: 0, + }; -#[no_mangle] -pub extern "C" fn bloom_get_gamepad_axis(axis: f64) -> f64 { - engine().input.get_gamepad_axis(axis as usize) as f64 -} + let component = AudioComponentFindNext(std::ptr::null_mut(), &desc); + if component.is_null() { + return; + } -#[no_mangle] -pub extern "C" fn bloom_is_gamepad_button_pressed(btn: f64) -> f64 { - if engine().input.is_gamepad_button_pressed(btn as usize) { 1.0 } else { 0.0 } -} + let mut unit: AudioUnit = std::ptr::null_mut(); + if AudioComponentInstanceNew(component, &mut unit) != 0 { + return; + } -#[no_mangle] -pub extern "C" fn bloom_is_gamepad_button_down(btn: f64) -> f64 { - if engine().input.is_gamepad_button_down(btn as usize) { 1.0 } else { 0.0 } -} + // Set stream format: 44100 Hz, stereo, float32 + let stream_desc = AudioStreamBasicDescription { + sample_rate: 44100.0, + format_id: K_AUDIO_FORMAT_LINEAR_PCM, + format_flags: K_AUDIO_FORMAT_FLAG_IS_FLOAT | K_AUDIO_FORMAT_FLAG_IS_PACKED, + bytes_per_packet: 8, + frames_per_packet: 1, + bytes_per_frame: 8, + channels_per_frame: 2, + bits_per_channel: 32, + reserved: 0, + }; -#[no_mangle] -pub extern "C" fn bloom_is_gamepad_button_released(btn: f64) -> f64 { - if engine().input.is_gamepad_button_released(btn as usize) { 1.0 } else { 0.0 } -} + AudioUnitSetProperty( + unit, + K_AUDIO_UNIT_PROPERTY_STREAM_FORMAT, + K_AUDIO_UNIT_SCOPE_INPUT, + 0, + &stream_desc as *const _ as *const std::ffi::c_void, + std::mem::size_of::() as u32, + ); -#[no_mangle] -pub extern "C" fn bloom_get_gamepad_axis_count() -> f64 { - engine().input.get_gamepad_axis_count() as f64 -} + // Set render callback + let callback_struct = AURenderCallbackStruct { + input_proc: audio_render_callback, + input_proc_ref_con: std::ptr::null_mut(), + }; -// ============================================================ -// Input - Touch -// ============================================================ + AudioUnitSetProperty( + unit, + K_AUDIO_UNIT_PROPERTY_SET_RENDER_CALLBACK, + K_AUDIO_UNIT_SCOPE_INPUT, + 0, + &callback_struct as *const _ as *const std::ffi::c_void, + std::mem::size_of::() as u32, + ); -#[no_mangle] -pub extern "C" fn bloom_get_touch_x(index: f64) -> f64 { - engine().input.get_touch_x(index as usize) -} + AudioUnitInitialize(unit); + AudioOutputUnitStart(unit); -#[no_mangle] -pub extern "C" fn bloom_get_touch_y(index: f64) -> f64 { - engine().input.get_touch_y(index as usize) + AUDIO_UNIT = Some(AudioUnitInstance { unit }); + } } #[no_mangle] -pub extern "C" fn bloom_get_touch_count() -> f64 { - engine().input.get_touch_count() as f64 +pub extern "C" fn bloom_close_audio() { + unsafe { + if let Some(au) = AUDIO_UNIT.take() { + AudioOutputUnitStop(au.unit); + AudioComponentInstanceDispose(au.unit); + } + } } // ============================================================ -// Shapes +// Music // ============================================================ -#[no_mangle] -pub extern "C" fn bloom_draw_line(x1: f64, y1: f64, x2: f64, y2: f64, thickness: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_line(x1, y1, x2, y2, thickness, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_rect(x: f64, y: f64, w: f64, h: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_rect(x, y, w, h, r, g, b, a); -} +// ============================================================ +// Utility +// ============================================================ #[no_mangle] -pub extern "C" fn bloom_draw_rect_lines(x: f64, y: f64, w: f64, h: f64, thickness: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_rect_lines(x, y, w, h, thickness, r, g, b, a); +pub extern "C" fn bloom_toggle_fullscreen() { + unsafe { + if let Some(window) = &WINDOW { + let _: () = msg_send![window, toggleFullScreen: std::ptr::null::()]; + } + } } #[no_mangle] -pub extern "C" fn bloom_draw_circle(cx: f64, cy: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_circle(cx, cy, radius, r, g, b, a); +pub extern "C" fn bloom_set_window_title(title_ptr: *const u8) { + let title = str_from_header(title_ptr); + unsafe { + if let Some(window) = &WINDOW { + let ns_title = NSString::from_str(title); + window.setTitle(&ns_title); + } + } } #[no_mangle] -pub extern "C" fn bloom_draw_circle_lines(cx: f64, cy: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_circle_lines(cx, cy, radius, r, g, b, a); +pub extern "C" fn bloom_set_window_icon(path_ptr: *const u8) { + let path = str_from_header(path_ptr); + unsafe { + let ns_path = NSString::from_str(path); + let image_cls = objc2::runtime::AnyClass::get(c"NSImage").unwrap(); + let image: *mut objc2::runtime::AnyObject = + msg_send![image_cls, alloc]; + if image.is_null() { return; } + let image: *mut objc2::runtime::AnyObject = + msg_send![image, initWithContentsOfFile: &*ns_path]; + if image.is_null() { return; } + let app = NSApplication::sharedApplication(MainThreadMarker::new_unchecked()); + let _: () = msg_send![&*app, setApplicationIconImage: image]; + } } -#[no_mangle] -pub extern "C" fn bloom_draw_triangle(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_triangle(x1, y1, x2, y2, x3, y3, r, g, b, a); +extern "C" { + fn CGDisplayHideCursor(display: u32) -> i32; + fn CGDisplayShowCursor(display: u32) -> i32; + fn CGAssociateMouseAndMouseCursorPosition(connected: u8) -> i32; } #[no_mangle] -pub extern "C" fn bloom_draw_poly(cx: f64, cy: f64, sides: f64, radius: f64, rotation: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_poly(cx, cy, sides, radius, rotation, r, g, b, a); +pub extern "C" fn bloom_disable_cursor() { + let input = &mut engine().input; + input.cursor_disabled = true; + input.clear_mouse_delta(); + unsafe { + CGDisplayHideCursor(0); + CGAssociateMouseAndMouseCursorPosition(0); // dissociate = relative mode + } } -// ============================================================ -// Text -// ============================================================ - #[no_mangle] -pub extern "C" fn bloom_draw_text(text_ptr: *const u8, x: f64, y: f64, size: f64, r: f64, g: f64, b: f64, a: f64) { - let text = str_from_header(text_ptr); - let eng = engine(); - // Need to split borrow: take text out temporarily - let mut text_renderer = std::mem::replace(&mut eng.text, bloom_shared::text_renderer::TextRenderer::empty()); - text_renderer.draw_text(&mut eng.renderer, text, x, y, size as u32, r, g, b, a); - eng.text = text_renderer; +pub extern "C" fn bloom_enable_cursor() { + engine().input.cursor_disabled = false; + unsafe { + CGAssociateMouseAndMouseCursorPosition(1); + CGDisplayShowCursor(0); + } } +// E4: Clipboard #[no_mangle] -pub extern "C" fn bloom_measure_text(text_ptr: *const u8, size: f64) -> f64 { +pub extern "C" fn bloom_set_clipboard_text(text_ptr: *const u8) { let text = str_from_header(text_ptr); - engine().text.measure_text(text, size as u32) + if let Ok(mut clipboard) = arboard::Clipboard::new() { + let _ = clipboard.set_text(text.to_string()); + } } #[no_mangle] -pub extern "C" fn bloom_load_font(path_ptr: *const u8, _size: f64) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(path) { - Ok(data) => engine().text.load_font(&data) as f64, - Err(_) => 0.0, +pub extern "C" fn bloom_get_clipboard_text() -> *const u8 { + match arboard::Clipboard::new() { + Ok(mut clipboard) => match clipboard.get_text() { + Ok(text) => alloc_perry_string(&text), + Err(_) => alloc_perry_string(""), + }, + Err(_) => alloc_perry_string(""), } } +// E5b: Native file dialogs (via rfd crate) #[no_mangle] -pub extern "C" fn bloom_unload_font(font_handle: f64) { - engine().text.unload_font(font_handle as usize); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_text_ex(font_handle: f64, text_ptr: *const u8, x: f64, y: f64, size: f64, spacing: f64, r: f64, g: f64, b: f64, a: f64) { - let text = str_from_header(text_ptr); - let eng = engine(); - let mut text_renderer = std::mem::replace(&mut eng.text, bloom_shared::text_renderer::TextRenderer::empty()); - text_renderer.draw_text_ex(&mut eng.renderer, font_handle as usize, text, x, y, size as u32, spacing as f32, r, g, b, a); - eng.text = text_renderer; -} - -#[no_mangle] -pub extern "C" fn bloom_measure_text_ex(font_handle: f64, text_ptr: *const u8, size: f64, spacing: f64) -> f64 { - let text = str_from_header(text_ptr); - engine().text.measure_text_ex(font_handle as usize, text, size as u32, spacing as f32) -} - -// ============================================================ -// Textures -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_load_texture(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(path) { - Ok(data) => { - let eng = engine(); - let EngineState { ref mut textures, ref mut renderer, .. } = *eng; - textures.load_texture(renderer, &data) - } - Err(_) => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_unload_texture(handle: f64) { - let eng = engine(); - let EngineState { ref mut textures, ref mut renderer, .. } = *eng; - textures.unload_texture(handle, renderer); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_texture(handle: f64, x: f64, y: f64, tint_r: f64, tint_g: f64, tint_b: f64, tint_a: f64) { - let eng = engine(); - if let Some(tex) = eng.textures.get(handle) { - let bind_group_idx = tex.bind_group_idx; - eng.renderer.draw_texture(bind_group_idx, x, y, tint_r, tint_g, tint_b, tint_a); - } -} - -#[no_mangle] -pub extern "C" fn bloom_draw_texture_pro( - handle: f64, - src_x: f64, src_y: f64, src_w: f64, src_h: f64, - dst_x: f64, dst_y: f64, dst_w: f64, dst_h: f64, - origin_x: f64, origin_y: f64, rotation: f64, - tint_r: f64, tint_g: f64, tint_b: f64, tint_a: f64, -) { - let eng = engine(); - if let Some(tex) = eng.textures.get(handle) { - let bind_group_idx = tex.bind_group_idx; - eng.renderer.draw_texture_pro( - bind_group_idx, - src_x, src_y, src_w, src_h, - dst_x, dst_y, dst_w, dst_h, - origin_x, origin_y, rotation, - tint_r, tint_g, tint_b, tint_a, - ); - } -} - -#[no_mangle] -pub extern "C" fn bloom_draw_texture_rec( - handle: f64, - src_x: f64, src_y: f64, src_w: f64, src_h: f64, - dst_x: f64, dst_y: f64, - tint_r: f64, tint_g: f64, tint_b: f64, tint_a: f64, -) { - let eng = engine(); - if let Some(tex) = eng.textures.get(handle) { - let bind_group_idx = tex.bind_group_idx; - eng.renderer.draw_texture_rec( - bind_group_idx, - src_x, src_y, src_w, src_h, - dst_x, dst_y, - tint_r, tint_g, tint_b, tint_a, - ); - } -} - -#[no_mangle] -pub extern "C" fn bloom_get_texture_width(handle: f64) -> f64 { - let eng = engine(); - eng.textures.get(handle).map(|t| t.width as f64).unwrap_or(0.0) -} - -#[no_mangle] -pub extern "C" fn bloom_get_texture_height(handle: f64) -> f64 { - let eng = engine(); - eng.textures.get(handle).map(|t| t.height as f64).unwrap_or(0.0) -} - -#[no_mangle] -pub extern "C" fn bloom_load_image(path_ptr: *const u8) -> f64 { - let path = str_from_header(path_ptr); - match std::fs::read(path) { - Ok(data) => engine().textures.load_image(&data), - Err(_) => 0.0, - } -} - -#[no_mangle] -pub extern "C" fn bloom_image_resize(handle: f64, w: f64, h: f64) { - engine().textures.image_resize(handle, w as u32, h as u32); -} - -#[no_mangle] -pub extern "C" fn bloom_image_crop(handle: f64, x: f64, y: f64, w: f64, h: f64) { - engine().textures.image_crop(handle, x as u32, y as u32, w as u32, h as u32); -} - -#[no_mangle] -pub extern "C" fn bloom_image_flip_h(handle: f64) { - engine().textures.image_flip_h(handle); -} - -#[no_mangle] -pub extern "C" fn bloom_image_flip_v(handle: f64) { - engine().textures.image_flip_v(handle); -} - -#[no_mangle] -pub extern "C" fn bloom_load_texture_from_image(handle: f64) -> f64 { - let eng = engine(); - let EngineState { ref mut textures, ref mut renderer, .. } = *eng; - textures.load_texture_from_image(handle, renderer) -} - -#[no_mangle] -pub extern "C" fn bloom_gen_texture_mipmaps(_handle: f64) { - // Mipmap generation is handled by the GPU texture creation pipeline - // This is a no-op for now as wgpu handles mipmaps internally -} - -#[no_mangle] -pub extern "C" fn bloom_set_texture_filter(handle: f64, mode: f64) { - let eng = engine(); - if let Some(tex) = eng.textures.get(handle) { - let bind_group_idx = tex.bind_group_idx; - eng.renderer.set_texture_filter(bind_group_idx, mode > 0.5); - } -} - -// ============================================================ -// Camera 2D -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_begin_mode_2d(offset_x: f64, offset_y: f64, target_x: f64, target_y: f64, rotation: f64, zoom: f64) { - engine().renderer.begin_mode_2d( - offset_x as f32, offset_y as f32, - target_x as f32, target_y as f32, - rotation as f32, zoom as f32, - ); -} - -#[no_mangle] -pub extern "C" fn bloom_end_mode_2d() { - engine().renderer.end_mode_2d(); -} - -// ============================================================ -// Camera 3D and 3D drawing -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_begin_mode_3d( - pos_x: f64, pos_y: f64, pos_z: f64, - target_x: f64, target_y: f64, target_z: f64, - up_x: f64, up_y: f64, up_z: f64, - fovy: f64, projection: f64, -) { - engine().renderer.begin_mode_3d( - pos_x as f32, pos_y as f32, pos_z as f32, - target_x as f32, target_y as f32, target_z as f32, - up_x as f32, up_y as f32, up_z as f32, - fovy as f32, projection as f32, - ); -} - -#[no_mangle] -pub extern "C" fn bloom_end_mode_3d() { - engine().renderer.end_mode_3d(); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_cube(x: f64, y: f64, z: f64, w: f64, h: f64, d: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_cube(x, y, z, w, h, d, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_cube_wires(x: f64, y: f64, z: f64, w: f64, h: f64, d: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_cube_wires(x, y, z, w, h, d, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_sphere(cx: f64, cy: f64, cz: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_sphere(cx, cy, cz, radius, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_sphere_wires(cx: f64, cy: f64, cz: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_sphere_wires(cx, cy, cz, radius, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_cylinder(x: f64, y: f64, z: f64, radius_top: f64, radius_bottom: f64, height: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_cylinder(x, y, z, radius_top, radius_bottom, height, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_plane(cx: f64, cy: f64, cz: f64, w: f64, d: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_plane(cx, cy, cz, w, d, r, g, b, a); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_grid(slices: f64, spacing: f64) { - engine().renderer.draw_grid(slices as i32, spacing); -} - -#[no_mangle] -pub extern "C" fn bloom_draw_ray(origin_x: f64, origin_y: f64, origin_z: f64, dir_x: f64, dir_y: f64, dir_z: f64, r: f64, g: f64, b: f64, a: f64) { - engine().renderer.draw_ray(origin_x, origin_y, origin_z, dir_x, dir_y, dir_z, r, g, b, a); -} - -// ============================================================ -// Joint test -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_set_joint_test(joint_index: f64, angle: f64) { - engine().renderer.set_joint_test(joint_index as usize, angle as f32); -} - -// ============================================================ -// Lighting -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_set_ambient_light(r: f64, g: f64, b: f64, intensity: f64) { - engine().renderer.set_ambient_light(r, g, b, intensity); -} - -#[no_mangle] -pub extern "C" fn bloom_set_directional_light(dx: f64, dy: f64, dz: f64, r: f64, g: f64, b: f64, intensity: f64) { - engine().renderer.set_directional_light(dx, dy, dz, r, g, b, intensity); -} - -// --- EN-005: procedural sky --- -// Toggle procedural-atmosphere rendering and steer the sun. The -// renderer owns the on/off flag + LUT state; setting the sun marks -// the sky-view LUT dirty so it re-bakes before the next frame. - -#[no_mangle] -pub extern "C" fn bloom_set_procedural_sky(enabled: f64, rayleigh_density: f64, mie_density: f64, ground_albedo: f64) { - engine().renderer.set_procedural_sky( - enabled != 0.0, - rayleigh_density as f32, - mie_density as f32, - ground_albedo as f32, - ); -} - -#[no_mangle] -pub extern "C" fn bloom_set_sun_direction(dx: f64, dy: f64, dz: f64, intensity: f64) { - engine().renderer.set_sun_direction(dx as f32, dy as f32, dz as f32, intensity as f32); -} - -// --- Post-FX knobs (heuristic visual layer; default-off) --- - -#[no_mangle] -pub extern "C" fn bloom_set_fog(r: f64, g: f64, b: f64, density: f64, height_ref: f64, height_falloff: f64) { - let r_ = engine(); - r_.renderer.set_fog_color(r as f32, g as f32, b as f32); - r_.renderer.set_fog_density(density as f32); - r_.renderer.set_fog_height_falloff(height_ref as f32, height_falloff as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_chromatic_aberration(strength: f64) { - engine().renderer.set_chromatic_aberration(strength as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_vignette(strength: f64, softness: f64) { - engine().renderer.set_vignette(strength as f32, softness as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_film_grain(strength: f64) { - engine().renderer.set_film_grain(strength as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_sun_shafts(strength: f64, decay: f64, r: f64, g: f64, b: f64) { - let eng = engine(); - eng.renderer.set_sun_shaft_strength(strength as f32); - eng.renderer.set_sun_shaft_decay(decay as f32); - eng.renderer.set_sun_shaft_color(r as f32, g as f32, b as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_auto_exposure(on: f64) { - engine().renderer.set_auto_exposure(on != 0.0); -} - -#[no_mangle] -pub extern "C" fn bloom_set_taa_enabled(on: f64) { - engine().renderer.set_taa_enabled(on != 0.0); -} - -#[no_mangle] -pub extern "C" fn bloom_set_render_scale(scale: f64) { - engine().renderer.set_render_scale(scale as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_get_render_scale() -> f64 { - engine().renderer.render_scale() as f64 -} - -#[no_mangle] -pub extern "C" fn bloom_set_upscale_mode(mode: f64) { - engine().renderer.set_upscale_mode(mode as u32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_cas_strength(strength: f64) { - engine().renderer.set_cas_strength(strength as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_get_physical_width() -> f64 { - engine().renderer.physical_width() as f64 -} - -#[no_mangle] -pub extern "C" fn bloom_get_physical_height() -> f64 { - engine().renderer.physical_height() as f64 -} - -#[no_mangle] -pub extern "C" fn bloom_set_auto_resolution(target_hz: f64, enabled: f64) { - let eng = engine(); - if enabled != 0.0 { - let current = eng.renderer.render_scale(); - eng.drs.enable(target_hz as f32, current); - } else { - eng.drs.disable(); - } -} - -#[no_mangle] -pub extern "C" fn bloom_set_manual_exposure(value: f64) { - engine().renderer.set_manual_exposure(value as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_env_intensity(intensity: f64) { - engine().renderer.set_env_intensity(intensity as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_ssgi_enabled(enabled: f64) { - engine().renderer.set_ssgi_enabled(enabled != 0.0); -} - -#[no_mangle] -pub extern "C" fn bloom_set_ssgi_intensity(intensity: f64) { - engine().renderer.set_ssgi_intensity(intensity as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_ssgi_radius(radius: f64) { - engine().renderer.set_ssgi_radius(radius as f32); -} - -#[no_mangle] -pub extern "C" fn bloom_set_dof(enabled: f64, focus_distance: f64, aperture: f64) { - let r = &mut engine().renderer; - r.set_dof_enabled(enabled != 0.0); - r.set_dof_focus_distance(focus_distance as f32); - r.set_dof_aperture(aperture as f32); -} - -// ============================================================ -// Render quality toggles (individual + preset) -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_set_quality_preset(preset: f64) { - engine().renderer.apply_quality_preset(preset as u32); -} -#[no_mangle] -pub extern "C" fn bloom_set_shadows_enabled(on: f64) { - engine().renderer.set_shadows_enabled(on != 0.0); -} -#[no_mangle] -pub extern "C" fn bloom_set_shadows_always_fresh(on: f64) { - engine().renderer.set_shadows_always_fresh(on != 0.0); -} -#[no_mangle] -pub extern "C" fn bloom_set_bloom_enabled(on: f64) { - engine().renderer.set_bloom_enabled(on != 0.0); -} -#[no_mangle] -pub extern "C" fn bloom_set_ssao_enabled(on: f64) { - engine().renderer.set_ssao_enabled(on != 0.0); -} -#[no_mangle] -pub extern "C" fn bloom_set_ssao_intensity(value: f64) { - engine().renderer.set_ssao_strength(value as f32); -} -#[no_mangle] -pub extern "C" fn bloom_set_ssao_radius(world_radius: f64) { - engine().renderer.set_ssao_radius(world_radius as f32); -} -#[no_mangle] -pub extern "C" fn bloom_set_wind(dir_x: f64, dir_z: f64, amplitude: f64, frequency: f64) { - engine().renderer.set_wind(dir_x as f32, dir_z as f32, amplitude as f32, frequency as f32); -} -#[no_mangle] -pub extern "C" fn bloom_set_ssr_enabled(on: f64) { - engine().renderer.set_ssr_enabled(on != 0.0); -} -#[no_mangle] -pub extern "C" fn bloom_set_motion_blur_enabled(on: f64) { - engine().renderer.set_motion_blur_enabled(on != 0.0); -} -#[no_mangle] -pub extern "C" fn bloom_set_sss_enabled(on: f64) { - engine().renderer.set_sss_enabled(on != 0.0); -} - -// ============================================================ -// Profiler — CPU phase timings (always available) + GPU timestamps -// (when the adapter supports TIMESTAMP_QUERY). Disabled by default. -// ============================================================ - -#[no_mangle] -pub extern "C" fn bloom_set_profiler_enabled(on: f64) { - engine().profiler.set_enabled(on != 0.0); -} -#[no_mangle] -pub extern "C" fn bloom_get_profiler_frame_cpu_us() -> f64 { - engine().profiler.avg_frame_cpu_us() -} -#[no_mangle] -pub extern "C" fn bloom_get_profiler_frame_gpu_us() -> f64 { - engine().profiler.avg_frame_gpu_us() -} -#[no_mangle] -pub extern "C" fn bloom_print_profiler_summary() { - print!("{}", engine().profiler.summary()); -} - -/// Phase 8 — formatted per-pass overlay text. Returns a Perry-style -/// header-prefixed string (12-byte header: len, cap, flags, then -/// UTF-8 bytes) with one line per pass sorted by CPU time descending: -/// -/// "