From 59b10569c4def5bc74457b03698e31acc07c707f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 12 Jun 2026 10:16:19 +0200 Subject: [PATCH 01/30] =?UTF-8?q?feat(ffi):=20define=5Fcore=5Fffi!=20?= =?UTF-8?q?=E2=80=94=20single=20shared=20source=20for=20the=20non-physics?= =?UTF-8?q?=20FFI=20surface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ~250 non-physics bloom_* functions were hand-copied into six platform crates (~9k duplicated lines) and drifted constantly: Android shipped 60 functions behind then patched the gap with silent no-op stubs, Windows stubbed the whole scene-graph/lighting/picking/post-FX surface, iOS/tvOS gamepad functions took an extra leading param the manifest doesn't declare (axis/button reads were off by one register), and bloom_create_mesh / bloom_gen_mesh_spline_ribbon read *const f32 where Perry passes f64 arrays. This adds the same cure the physics surface already had (define_physics_ffi!): one macro in bloom-shared generating all 247 functions, expanded per platform. Platform crates keep only genuinely platform-specific code (window/event loop, audio backend, clipboard, dialogs, cursor, locale). - new bloom_shared::ffi: panic guard at every FFI entry point (log-once + safe default instead of unwinding into Perry code), platform-aware error logging (logcat on Android), feature_off_warn_once moved here - new bloom_shared::ffi_core: define_core_ffi! with models3d / image-extras gate pairs (warn-once stubs when off — symbols never silently vanish or no-op) - audio::decode_audio: unified extension-dispatch + format-sniff decode (was two divergent per-platform behaviors) - Renderer::set_env_clear_from_hdr_file: HDR decode hoisted from per-platform wrappers - macro expansion compile-checked in shared unit tests via mock hooks - macos migrated as reference platform: lib.rs 3567→1194 lines, release staticlib exports verified identical pre/post (382 symbols, 0 diff) --- native/macos/src/lib.rs | 2829 +++------------------------ native/shared/src/audio.rs | 38 + native/shared/src/ffi.rs | 109 ++ native/shared/src/ffi_core.rs | 2954 +++++++++++++++++++++++++++++ native/shared/src/lib.rs | 3 + native/shared/src/renderer/mod.rs | 32 + 6 files changed, 3364 insertions(+), 2601 deletions(-) create mode 100644 native/shared/src/ffi.rs create mode 100644 native/shared/src/ffi_core.rs diff --git a/native/macos/src/lib.rs b/native/macos/src/lib.rs index 7546018..d740c33 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}; @@ -31,6 +30,16 @@ static mut AUDIO_UNIT: 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 { @@ -617,1709 +626,245 @@ 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 { + 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) -} - -#[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, + if let Ok(mut clipboard) = arboard::Clipboard::new() { + let _ = clipboard.set_text(text.to_string()); } } #[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_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(""), + } } -#[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: -/// -/// "