@bloomengine/engine 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -11
- package/native/linux/Cargo.lock +1571 -12
- package/native/linux/Cargo.toml +3 -0
- package/native/linux/src/lib.rs +745 -40
- package/native/macos/src/lib.rs +30 -118
- package/native/shared/build.rs +32 -4
- package/native/shared/src/postfx.rs +16 -10
- package/native/shared/src/renderer/formats.rs +1 -7
- package/native/shared/src/renderer/impulse_field.rs +2 -1
- package/native/shared/src/renderer/material_system.rs +15 -3
- package/native/shared/src/renderer/shaders.rs +7 -1
- package/native/shared/src/renderer/transient.rs +12 -12
- package/native/shared/src/string_header.rs +32 -0
- package/native/third_party/JoltPhysics/Build/Android/PerformanceTest/build.gradle +51 -0
- package/native/third_party/JoltPhysics/Build/Android/PerformanceTest/src/main/AndroidManifest.xml +20 -0
- package/native/third_party/JoltPhysics/Build/Android/PerformanceTest/src/main/cpp/CMakeLists.txt +20 -0
- package/native/third_party/JoltPhysics/Build/Android/UnitTests/build.gradle +51 -0
- package/native/third_party/JoltPhysics/Build/Android/UnitTests/src/main/AndroidManifest.xml +20 -0
- package/native/third_party/JoltPhysics/Build/Android/UnitTests/src/main/cpp/CMakeLists.txt +20 -0
- package/native/third_party/JoltPhysics/Build/Android/build.gradle +17 -0
- package/native/third_party/JoltPhysics/Build/Android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/native/third_party/JoltPhysics/Build/Android/gradle/wrapper/gradle-wrapper.properties +5 -0
- package/native/third_party/JoltPhysics/Build/Android/gradle.properties +21 -0
- package/native/third_party/JoltPhysics/Build/Android/gradlew +185 -0
- package/native/third_party/JoltPhysics/Build/Android/gradlew.bat +89 -0
- package/native/third_party/JoltPhysics/Build/Android/settings.gradle +10 -0
- package/native/third_party/JoltPhysics/Build/CMakeLists.txt +449 -0
- package/native/third_party/JoltPhysics/Build/README.md +250 -0
- package/native/third_party/JoltPhysics/Build/cmake_linux_clang_gcc.sh +28 -0
- package/native/third_party/JoltPhysics/Build/cmake_linux_emscripten.sh +19 -0
- package/native/third_party/JoltPhysics/Build/cmake_linux_mingw.sh +19 -0
- package/native/third_party/JoltPhysics/Build/cmake_vs2022_cl.bat +3 -0
- package/native/third_party/JoltPhysics/Build/cmake_vs2022_cl_32bit.bat +3 -0
- package/native/third_party/JoltPhysics/Build/cmake_vs2022_cl_arm.bat +3 -0
- package/native/third_party/JoltPhysics/Build/cmake_vs2022_cl_arm_32bit.bat +4 -0
- package/native/third_party/JoltPhysics/Build/cmake_vs2022_cl_cross_platform_deterministic.bat +3 -0
- package/native/third_party/JoltPhysics/Build/cmake_vs2022_cl_double.bat +3 -0
- package/native/third_party/JoltPhysics/Build/cmake_vs2022_cl_no_object_stream.bat +3 -0
- package/native/third_party/JoltPhysics/Build/cmake_vs2022_clang.bat +10 -0
- package/native/third_party/JoltPhysics/Build/cmake_vs2022_clang_cross_platform_deterministic.bat +10 -0
- package/native/third_party/JoltPhysics/Build/cmake_vs2022_clang_double.bat +10 -0
- package/native/third_party/JoltPhysics/Build/cmake_vs2022_uwp.bat +5 -0
- package/native/third_party/JoltPhysics/Build/cmake_vs2022_uwp_arm.bat +5 -0
- package/native/third_party/JoltPhysics/Build/cmake_vs2026_cl.bat +3 -0
- package/native/third_party/JoltPhysics/Build/cmake_vs2026_cl_cross_platform_deterministic.bat +3 -0
- package/native/third_party/JoltPhysics/Build/cmake_vs2026_cl_double.bat +3 -0
- package/native/third_party/JoltPhysics/Build/cmake_vs2026_clang.bat +10 -0
- package/native/third_party/JoltPhysics/Build/cmake_vs2026_clang_cross_platform_deterministic.bat +10 -0
- package/native/third_party/JoltPhysics/Build/cmake_vs2026_clang_double.bat +10 -0
- package/native/third_party/JoltPhysics/Build/cmake_windows_mingw.sh +19 -0
- package/native/third_party/JoltPhysics/Build/cmake_xcode_ios.sh +4 -0
- package/native/third_party/JoltPhysics/Build/cmake_xcode_macos.sh +4 -0
- package/native/third_party/JoltPhysics/Build/iOS/JoltViewerInfo.plist +34 -0
- package/native/third_party/JoltPhysics/Build/iOS/SamplesInfo.plist +34 -0
- package/native/third_party/JoltPhysics/Build/iOS/UnitTestsInfo.plist +34 -0
- package/native/third_party/JoltPhysics/Build/macOS/icon.icns +0 -0
- package/native/third_party/JoltPhysics/Build/macos_install_vulkan_sdk.sh +13 -0
- package/native/third_party/JoltPhysics/Build/ubuntu24_install_vulkan_sdk.sh +4 -0
- package/native/third_party/bloom_jolt/CMakeLists.txt +14 -5
- package/native/windows/Cargo.lock +1 -0
- package/native/windows/Cargo.toml +10 -1
- package/native/windows/src/lib.rs +226 -18
- package/package.json +9 -7
- package/src/core/colors.ts +34 -27
- package/src/core/index.ts +1 -2
package/native/linux/src/lib.rs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
use bloom_shared::engine::EngineState;
|
|
2
2
|
use bloom_shared::renderer::Renderer;
|
|
3
|
-
use bloom_shared::string_header::str_from_header;
|
|
3
|
+
use bloom_shared::string_header::{str_from_header, alloc_perry_string};
|
|
4
4
|
use bloom_shared::audio::{parse_wav, parse_ogg, parse_mp3};
|
|
5
5
|
|
|
6
6
|
use std::sync::OnceLock;
|
|
@@ -66,9 +66,29 @@ mod x11_impl {
|
|
|
66
66
|
static mut DISPLAY: *mut x11::xlib::Display = std::ptr::null_mut();
|
|
67
67
|
static mut X11_WINDOW: x11::xlib::Window = 0;
|
|
68
68
|
static mut IS_FULLSCREEN: bool = false;
|
|
69
|
+
static mut HEADLESS: bool = false;
|
|
70
|
+
static mut NO_FULLSCREEN: bool = false;
|
|
71
|
+
static mut CURSOR_HIDDEN: bool = false;
|
|
72
|
+
static mut HIDDEN_CURSOR: x11::xlib::Cursor = 0;
|
|
73
|
+
/// Cached XC_* shape cursors keyed by `cursor_shape` value 0..=6.
|
|
74
|
+
/// Lazily created by `apply_cursor_shape`; reused across frames so
|
|
75
|
+
/// we don't leak a Cursor handle every poll.
|
|
76
|
+
static mut SHAPE_CURSORS: [x11::xlib::Cursor; 8] = [0; 8];
|
|
77
|
+
static mut LAST_APPLIED_SHAPE: u32 = 0xFFFF_FFFF;
|
|
78
|
+
/// When `cursor_disabled` (relative-mode) is on we keep warping the
|
|
79
|
+
/// pointer back to window center each frame; remembering the last warp
|
|
80
|
+
/// target lets motion handlers compute a reliable raw delta and ignore
|
|
81
|
+
/// the synthetic motion event the warp itself generates.
|
|
82
|
+
static mut WARP_CENTER_X: i32 = 0;
|
|
83
|
+
static mut WARP_CENTER_Y: i32 = 0;
|
|
84
|
+
static mut RELATIVE_MODE: bool = false;
|
|
69
85
|
|
|
70
86
|
pub fn set_fullscreen(fullscreen: bool) {
|
|
71
87
|
unsafe {
|
|
88
|
+
// BLOOM_NO_FULLSCREEN=1 hard-disables the fullscreen path so
|
|
89
|
+
// benchmark harnesses on a 4K monitor don't silently 4× their
|
|
90
|
+
// pixel count when an inherited fullscreen Space leaks in.
|
|
91
|
+
if NO_FULLSCREEN && fullscreen { return; }
|
|
72
92
|
if DISPLAY.is_null() || X11_WINDOW == 0 { return; }
|
|
73
93
|
|
|
74
94
|
let wm_state = x11::xlib::XInternAtom(
|
|
@@ -115,8 +135,14 @@ mod x11_impl {
|
|
|
115
135
|
/// are *logical*; on a HiDPI X11 display we multiply by the
|
|
116
136
|
/// monitor's scale factor so the window appears the right size
|
|
117
137
|
/// while its surface is at the screen's physical resolution.
|
|
118
|
-
|
|
138
|
+
///
|
|
139
|
+
/// `headless = true` keeps the X11 window + Vulkan surface alive
|
|
140
|
+
/// (wgpu needs a surface) but never maps the window — so it never
|
|
141
|
+
/// appears on screen and never steals focus. Mirrors the macOS
|
|
142
|
+
/// BLOOM_HEADLESS path for batch / CI rendering harnesses.
|
|
143
|
+
pub fn create_window(width: f64, height: f64, title: &str, headless: bool) -> (u32, u32) {
|
|
119
144
|
unsafe {
|
|
145
|
+
HEADLESS = headless;
|
|
120
146
|
DISPLAY = x11::xlib::XOpenDisplay(std::ptr::null());
|
|
121
147
|
if DISPLAY.is_null() {
|
|
122
148
|
panic!("Failed to open X11 display");
|
|
@@ -129,9 +155,13 @@ mod x11_impl {
|
|
|
129
155
|
let phys_w = (width * scale).round() as u32;
|
|
130
156
|
let phys_h = (height * scale).round() as u32;
|
|
131
157
|
|
|
158
|
+
// Off-screen origin is belt-and-braces: even if some WM
|
|
159
|
+
// chooses to map the window despite us never calling
|
|
160
|
+
// XMapWindow, it'll appear far off the visible desktop.
|
|
161
|
+
let origin_x: i32 = if headless { -20000 } else { 0 };
|
|
132
162
|
X11_WINDOW = x11::xlib::XCreateSimpleWindow(
|
|
133
163
|
DISPLAY, root,
|
|
134
|
-
|
|
164
|
+
origin_x, 0, phys_w, phys_h, 0,
|
|
135
165
|
x11::xlib::XBlackPixel(DISPLAY, screen),
|
|
136
166
|
x11::xlib::XWhitePixel(DISPLAY, screen),
|
|
137
167
|
);
|
|
@@ -144,12 +174,17 @@ mod x11_impl {
|
|
|
144
174
|
x11::xlib::ButtonPressMask | x11::xlib::ButtonReleaseMask |
|
|
145
175
|
x11::xlib::PointerMotionMask | x11::xlib::StructureNotifyMask);
|
|
146
176
|
|
|
147
|
-
|
|
177
|
+
if !headless {
|
|
178
|
+
x11::xlib::XMapWindow(DISPLAY, X11_WINDOW);
|
|
179
|
+
}
|
|
148
180
|
x11::xlib::XFlush(DISPLAY);
|
|
149
181
|
(phys_w, phys_h)
|
|
150
182
|
}
|
|
151
183
|
}
|
|
152
184
|
|
|
185
|
+
pub fn set_no_fullscreen(no_fs: bool) { unsafe { NO_FULLSCREEN = no_fs; } }
|
|
186
|
+
pub fn is_headless() -> bool { unsafe { HEADLESS } }
|
|
187
|
+
|
|
153
188
|
/// Read the current display's DPI scale factor. Computed from
|
|
154
189
|
/// physical screen dimensions (pixels / mm). Snapped to the
|
|
155
190
|
/// nearest 0.25 and clamped to [1.0, 4.0] — EDID often lies
|
|
@@ -173,6 +208,170 @@ mod x11_impl {
|
|
|
173
208
|
pub fn display() -> *mut x11::xlib::Display { unsafe { DISPLAY } }
|
|
174
209
|
pub fn window() -> x11::xlib::Window { unsafe { X11_WINDOW } }
|
|
175
210
|
|
|
211
|
+
pub fn set_window_title(title: &str) {
|
|
212
|
+
unsafe {
|
|
213
|
+
if DISPLAY.is_null() || X11_WINDOW == 0 { return; }
|
|
214
|
+
let cstr = match std::ffi::CString::new(title) { Ok(c) => c, Err(_) => return };
|
|
215
|
+
x11::xlib::XStoreName(DISPLAY, X11_WINDOW, cstr.as_ptr());
|
|
216
|
+
// Modern WMs honour _NET_WM_NAME (UTF-8) over the legacy
|
|
217
|
+
// WM_NAME (Latin-1) set by XStoreName, so write both.
|
|
218
|
+
let net_wm_name = x11::xlib::XInternAtom(
|
|
219
|
+
DISPLAY, b"_NET_WM_NAME\0".as_ptr() as *const _, 0);
|
|
220
|
+
let utf8_string = x11::xlib::XInternAtom(
|
|
221
|
+
DISPLAY, b"UTF8_STRING\0".as_ptr() as *const _, 0);
|
|
222
|
+
if net_wm_name != 0 && utf8_string != 0 {
|
|
223
|
+
x11::xlib::XChangeProperty(
|
|
224
|
+
DISPLAY, X11_WINDOW, net_wm_name, utf8_string, 8,
|
|
225
|
+
x11::xlib::PropModeReplace,
|
|
226
|
+
title.as_ptr(), title.len() as i32,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
x11::xlib::XFlush(DISPLAY);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// Set the EWMH `_NET_WM_ICON` property from an image file. The X11
|
|
234
|
+
/// icon format is a flat array of CARDINALs (`long`s on the wire):
|
|
235
|
+
/// `[width, height, pixel0, pixel1, ...]` with each pixel as ARGB
|
|
236
|
+
/// premultiplied-alpha packed into the low 32 bits of a long.
|
|
237
|
+
pub fn set_window_icon(path: &str) {
|
|
238
|
+
unsafe {
|
|
239
|
+
if DISPLAY.is_null() || X11_WINDOW == 0 { return; }
|
|
240
|
+
let img = match image::open(path) {
|
|
241
|
+
Ok(i) => i.to_rgba8(),
|
|
242
|
+
Err(_) => return,
|
|
243
|
+
};
|
|
244
|
+
let (w, h) = (img.width() as usize, img.height() as usize);
|
|
245
|
+
let mut buf: Vec<std::os::raw::c_long> = Vec::with_capacity(2 + w * h);
|
|
246
|
+
buf.push(w as std::os::raw::c_long);
|
|
247
|
+
buf.push(h as std::os::raw::c_long);
|
|
248
|
+
for px in img.chunks_exact(4) {
|
|
249
|
+
let r = px[0] as u32;
|
|
250
|
+
let g = px[1] as u32;
|
|
251
|
+
let b = px[2] as u32;
|
|
252
|
+
let a = px[3] as u32;
|
|
253
|
+
let argb = (a << 24) | (r << 16) | (g << 8) | b;
|
|
254
|
+
buf.push(argb as std::os::raw::c_long);
|
|
255
|
+
}
|
|
256
|
+
let net_wm_icon = x11::xlib::XInternAtom(
|
|
257
|
+
DISPLAY, b"_NET_WM_ICON\0".as_ptr() as *const _, 0);
|
|
258
|
+
let cardinal = x11::xlib::XInternAtom(
|
|
259
|
+
DISPLAY, b"CARDINAL\0".as_ptr() as *const _, 0);
|
|
260
|
+
x11::xlib::XChangeProperty(
|
|
261
|
+
DISPLAY, X11_WINDOW, net_wm_icon, cardinal, 32,
|
|
262
|
+
x11::xlib::PropModeReplace,
|
|
263
|
+
buf.as_ptr() as *const u8,
|
|
264
|
+
buf.len() as i32,
|
|
265
|
+
);
|
|
266
|
+
x11::xlib::XFlush(DISPLAY);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/// Build (once) a 1x1 fully-transparent cursor — the standard X11
|
|
271
|
+
/// trick for "hide the cursor". Subsequent calls reuse the cached
|
|
272
|
+
/// cursor since X11 leaks Cursor handles otherwise.
|
|
273
|
+
unsafe fn ensure_hidden_cursor() -> x11::xlib::Cursor {
|
|
274
|
+
if HIDDEN_CURSOR != 0 { return HIDDEN_CURSOR; }
|
|
275
|
+
let pixmap = x11::xlib::XCreatePixmap(DISPLAY, X11_WINDOW, 1, 1, 1);
|
|
276
|
+
let mut color: x11::xlib::XColor = std::mem::zeroed();
|
|
277
|
+
let cursor = x11::xlib::XCreatePixmapCursor(
|
|
278
|
+
DISPLAY, pixmap, pixmap, &mut color, &mut color, 0, 0);
|
|
279
|
+
x11::xlib::XFreePixmap(DISPLAY, pixmap);
|
|
280
|
+
HIDDEN_CURSOR = cursor;
|
|
281
|
+
cursor
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
pub fn hide_cursor() {
|
|
285
|
+
unsafe {
|
|
286
|
+
if DISPLAY.is_null() || X11_WINDOW == 0 || CURSOR_HIDDEN { return; }
|
|
287
|
+
let c = ensure_hidden_cursor();
|
|
288
|
+
x11::xlib::XDefineCursor(DISPLAY, X11_WINDOW, c);
|
|
289
|
+
x11::xlib::XFlush(DISPLAY);
|
|
290
|
+
CURSOR_HIDDEN = true;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
pub fn show_cursor() {
|
|
295
|
+
unsafe {
|
|
296
|
+
if DISPLAY.is_null() || X11_WINDOW == 0 || !CURSOR_HIDDEN { return; }
|
|
297
|
+
x11::xlib::XUndefineCursor(DISPLAY, X11_WINDOW);
|
|
298
|
+
x11::xlib::XFlush(DISPLAY);
|
|
299
|
+
CURSOR_HIDDEN = false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/// Warp the pointer to window center and remember where we put it so
|
|
304
|
+
/// the motion handler can compute deltas relative to the warp target.
|
|
305
|
+
pub fn warp_to_center() {
|
|
306
|
+
unsafe {
|
|
307
|
+
if DISPLAY.is_null() || X11_WINDOW == 0 { return; }
|
|
308
|
+
let mut attrs: x11::xlib::XWindowAttributes = std::mem::zeroed();
|
|
309
|
+
x11::xlib::XGetWindowAttributes(DISPLAY, X11_WINDOW, &mut attrs);
|
|
310
|
+
let cx = attrs.width / 2;
|
|
311
|
+
let cy = attrs.height / 2;
|
|
312
|
+
x11::xlib::XWarpPointer(DISPLAY, 0, X11_WINDOW, 0, 0, 0, 0, cx, cy);
|
|
313
|
+
x11::xlib::XFlush(DISPLAY);
|
|
314
|
+
WARP_CENTER_X = cx;
|
|
315
|
+
WARP_CENTER_Y = cy;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
pub fn enter_relative_mode() {
|
|
320
|
+
unsafe {
|
|
321
|
+
RELATIVE_MODE = true;
|
|
322
|
+
hide_cursor();
|
|
323
|
+
warp_to_center();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
pub fn leave_relative_mode() {
|
|
328
|
+
unsafe {
|
|
329
|
+
RELATIVE_MODE = false;
|
|
330
|
+
show_cursor();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
pub fn is_relative_mode() -> bool { unsafe { RELATIVE_MODE } }
|
|
335
|
+
pub fn warp_center_x() -> i32 { unsafe { WARP_CENTER_X } }
|
|
336
|
+
pub fn warp_center_y() -> i32 { unsafe { WARP_CENTER_Y } }
|
|
337
|
+
|
|
338
|
+
/// Apply the requested cursor shape (the same 0..=6 enum macOS uses
|
|
339
|
+
/// in NSCursor calls). XCreateFontCursor uses cursor-font glyph
|
|
340
|
+
/// constants from <X11/cursorfont.h>; we cache one Cursor per shape
|
|
341
|
+
/// so repeat calls don't leak server-side state.
|
|
342
|
+
pub fn apply_cursor_shape(shape: u32) {
|
|
343
|
+
unsafe {
|
|
344
|
+
if DISPLAY.is_null() || X11_WINDOW == 0 || CURSOR_HIDDEN { return; }
|
|
345
|
+
if shape == LAST_APPLIED_SHAPE { return; }
|
|
346
|
+
// X11 cursor-font glyph indices (from cursorfont.h).
|
|
347
|
+
// 0 = default arrow → XC_left_ptr (68)
|
|
348
|
+
// 1 = pointing hand → XC_hand2 (60)
|
|
349
|
+
// 2 = open hand → XC_fleur (52, "move")
|
|
350
|
+
// 3 = I-beam → XC_xterm (152)
|
|
351
|
+
// 4 = resize H → XC_sb_h_double_arrow (108)
|
|
352
|
+
// 5 = resize V → XC_sb_v_double_arrow (116)
|
|
353
|
+
// 6 = crosshair → XC_crosshair (34)
|
|
354
|
+
let glyph: u32 = match shape {
|
|
355
|
+
1 => 60,
|
|
356
|
+
2 => 52,
|
|
357
|
+
3 => 152,
|
|
358
|
+
4 => 108,
|
|
359
|
+
5 => 116,
|
|
360
|
+
6 => 34,
|
|
361
|
+
_ => 68,
|
|
362
|
+
};
|
|
363
|
+
let idx = (shape as usize).min(SHAPE_CURSORS.len() - 1);
|
|
364
|
+
if SHAPE_CURSORS[idx] == 0 {
|
|
365
|
+
SHAPE_CURSORS[idx] = x11::xlib::XCreateFontCursor(DISPLAY, glyph);
|
|
366
|
+
}
|
|
367
|
+
if SHAPE_CURSORS[idx] != 0 {
|
|
368
|
+
x11::xlib::XDefineCursor(DISPLAY, X11_WINDOW, SHAPE_CURSORS[idx]);
|
|
369
|
+
x11::xlib::XFlush(DISPLAY);
|
|
370
|
+
LAST_APPLIED_SHAPE = shape;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
176
375
|
pub fn poll_events() {
|
|
177
376
|
unsafe {
|
|
178
377
|
while x11::xlib::XPending(DISPLAY) > 0 {
|
|
@@ -181,7 +380,6 @@ mod x11_impl {
|
|
|
181
380
|
|
|
182
381
|
match event.type_ {
|
|
183
382
|
x11::xlib::KeyPress => {
|
|
184
|
-
let key_event = event.key;
|
|
185
383
|
let keysym = x11::xlib::XLookupKeysym(
|
|
186
384
|
&mut event.key as *mut _ as *mut _,
|
|
187
385
|
0,
|
|
@@ -190,6 +388,27 @@ mod x11_impl {
|
|
|
190
388
|
if bloom_key > 0 {
|
|
191
389
|
engine().input.set_key_down(bloom_key);
|
|
192
390
|
}
|
|
391
|
+
// Decode UTF-8 typed text via XLookupString so the
|
|
392
|
+
// editor's text-input widget receives characters.
|
|
393
|
+
let mut buf = [0u8; 32];
|
|
394
|
+
let mut ks: x11::xlib::KeySym = 0;
|
|
395
|
+
let len = x11::xlib::XLookupString(
|
|
396
|
+
&mut event.key as *mut _,
|
|
397
|
+
buf.as_mut_ptr() as *mut i8,
|
|
398
|
+
buf.len() as i32,
|
|
399
|
+
&mut ks,
|
|
400
|
+
std::ptr::null_mut(),
|
|
401
|
+
);
|
|
402
|
+
if len > 0 {
|
|
403
|
+
if let Ok(s) = std::str::from_utf8(&buf[..len as usize]) {
|
|
404
|
+
for c in s.chars() {
|
|
405
|
+
let cp = c as u32;
|
|
406
|
+
if cp >= 32 || cp == 8 || cp == 13 || cp == 9 {
|
|
407
|
+
engine().input.push_char(cp);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
193
412
|
}
|
|
194
413
|
x11::xlib::KeyRelease => {
|
|
195
414
|
let keysym = x11::xlib::XLookupKeysym(
|
|
@@ -203,7 +422,21 @@ mod x11_impl {
|
|
|
203
422
|
}
|
|
204
423
|
x11::xlib::MotionNotify => {
|
|
205
424
|
let motion = event.motion;
|
|
206
|
-
|
|
425
|
+
if RELATIVE_MODE {
|
|
426
|
+
// Ignore the motion event we generated by warping
|
|
427
|
+
// back to center — it would otherwise add a stray
|
|
428
|
+
// -delta cancelling the user's actual movement.
|
|
429
|
+
if motion.x == WARP_CENTER_X && motion.y == WARP_CENTER_Y {
|
|
430
|
+
// synthetic warp event; skip
|
|
431
|
+
} else {
|
|
432
|
+
let dx = (motion.x - WARP_CENTER_X) as f64;
|
|
433
|
+
let dy = (motion.y - WARP_CENTER_Y) as f64;
|
|
434
|
+
engine().input.accumulate_mouse_delta(dx, dy);
|
|
435
|
+
warp_to_center();
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
engine().input.set_mouse_position(motion.x as f64, motion.y as f64);
|
|
439
|
+
}
|
|
207
440
|
}
|
|
208
441
|
x11::xlib::ButtonPress => {
|
|
209
442
|
let button = event.button.button;
|
|
@@ -211,6 +444,12 @@ mod x11_impl {
|
|
|
211
444
|
1 => engine().input.set_mouse_button_down(0),
|
|
212
445
|
3 => engine().input.set_mouse_button_down(1),
|
|
213
446
|
2 => engine().input.set_mouse_button_down(2),
|
|
447
|
+
// X11 maps wheel up/down to button 4/5 and
|
|
448
|
+
// horizontal scroll to 6/7. macOS uses an
|
|
449
|
+
// accumulator with positive = scroll up; flip
|
|
450
|
+
// the sign on the down case to match.
|
|
451
|
+
4 => engine().input.accumulate_mouse_wheel(1.0),
|
|
452
|
+
5 => engine().input.accumulate_mouse_wheel(-1.0),
|
|
214
453
|
_ => {}
|
|
215
454
|
}
|
|
216
455
|
}
|
|
@@ -256,7 +495,23 @@ pub extern "C" fn bloom_init_window(width: f64, height: f64, title_ptr: *const u
|
|
|
256
495
|
|
|
257
496
|
#[cfg(target_os = "linux")]
|
|
258
497
|
{
|
|
259
|
-
|
|
498
|
+
// Headless mode: BLOOM_HEADLESS=1 keeps the X11 window + Vulkan
|
|
499
|
+
// surface alive (wgpu requires a real surface) but never maps
|
|
500
|
+
// the window so it's invisible and never steals focus. Lets an
|
|
501
|
+
// agent spin up the renderer in a batch loop without disturbing
|
|
502
|
+
// the user's desktop.
|
|
503
|
+
let headless = std::env::var("BLOOM_HEADLESS")
|
|
504
|
+
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
|
505
|
+
.unwrap_or(false);
|
|
506
|
+
// BLOOM_NO_FULLSCREEN=1 hard-disables fullscreen capability for
|
|
507
|
+
// benchmark harnesses where a 4K-display fullscreen path would
|
|
508
|
+
// silently quadruple render cost.
|
|
509
|
+
let no_fullscreen = std::env::var("BLOOM_NO_FULLSCREEN")
|
|
510
|
+
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
|
511
|
+
.unwrap_or(false);
|
|
512
|
+
x11_impl::set_no_fullscreen(no_fullscreen);
|
|
513
|
+
|
|
514
|
+
let (phys_w, phys_h) = x11_impl::create_window(width, height, title, headless);
|
|
260
515
|
|
|
261
516
|
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
|
262
517
|
backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
|
|
@@ -308,6 +563,10 @@ pub extern "C" fn bloom_init_window(width: f64, height: f64, title_ptr: *const u
|
|
|
308
563
|
wgpu::ExperimentalFeatures::disabled()
|
|
309
564
|
};
|
|
310
565
|
let mut required_limits = wgpu::Limits::default();
|
|
566
|
+
// The material ABI declares 5 bind groups (PerFrame, PerView,
|
|
567
|
+
// PerMaterial, PerDraw, SceneInputs). wgpu's default limit is
|
|
568
|
+
// 4. Vulkan supports at least 7 here, so 5 is universally safe.
|
|
569
|
+
required_limits.max_bind_groups = 5;
|
|
311
570
|
if required_features.intersects(rt_mask) {
|
|
312
571
|
required_limits = required_limits
|
|
313
572
|
.using_minimum_supported_acceleration_structure_values();
|
|
@@ -346,6 +605,12 @@ pub extern "C" fn bloom_init_window(width: f64, height: f64, title_ptr: *const u
|
|
|
346
605
|
if fullscreen != 0.0 {
|
|
347
606
|
x11_impl::set_fullscreen(true);
|
|
348
607
|
}
|
|
608
|
+
|
|
609
|
+
// Register Bloom's GPU screenshot capture with perry-geisterhand.
|
|
610
|
+
// perry-runtime always exposes these symbols, so the link is direct
|
|
611
|
+
// (not weak); the registry no-ops gracefully if the editor isn't
|
|
612
|
+
// running.
|
|
613
|
+
bloom_register_geisterhand_screenshot();
|
|
349
614
|
}
|
|
350
615
|
|
|
351
616
|
#[cfg(not(target_os = "linux"))]
|
|
@@ -418,12 +683,23 @@ pub extern "C" fn bloom_begin_drawing() {
|
|
|
418
683
|
{
|
|
419
684
|
x11_impl::poll_events();
|
|
420
685
|
poll_linux_gamepad();
|
|
686
|
+
// Apply Q2 cursor shape — mirrors macOS NSCursor calls in its
|
|
687
|
+
// event loop. set_cursor_shape just stores the value; the X11
|
|
688
|
+
// cursor only changes when we actually call XDefineCursor.
|
|
689
|
+
x11_impl::apply_cursor_shape(engine().input.cursor_shape);
|
|
421
690
|
}
|
|
422
691
|
engine().begin_frame();
|
|
423
692
|
}
|
|
424
693
|
|
|
425
694
|
#[no_mangle]
|
|
426
|
-
pub extern "C" fn bloom_end_drawing() {
|
|
695
|
+
pub extern "C" fn bloom_end_drawing() {
|
|
696
|
+
// Pump geisterhand BEFORE end_frame. Mirrors macOS — the screenshot
|
|
697
|
+
// function re-renders inline using the captured VP + vertex buffers.
|
|
698
|
+
extern "C" { fn perry_geisterhand_pump(); }
|
|
699
|
+
unsafe { perry_geisterhand_pump(); }
|
|
700
|
+
|
|
701
|
+
engine().end_frame();
|
|
702
|
+
}
|
|
427
703
|
|
|
428
704
|
#[no_mangle]
|
|
429
705
|
pub extern "C" fn bloom_clear_background(r: f64, g: f64, b: f64, a: f64) {
|
|
@@ -914,13 +1190,27 @@ pub extern "C" fn bloom_load_shader(source_ptr: *const u8) -> f64 {
|
|
|
914
1190
|
}
|
|
915
1191
|
|
|
916
1192
|
#[no_mangle]
|
|
917
|
-
pub extern "C" fn bloom_create_mesh(vertex_ptr: *const
|
|
1193
|
+
pub extern "C" fn bloom_create_mesh(vertex_ptr: *const f64, vertex_count: f64, index_ptr: *const f64, index_count: f64) -> f64 {
|
|
1194
|
+
// Perry's TS `number[]` is f64-laid-out in memory; Perry passes a
|
|
1195
|
+
// pointer to that data. A previous version of this FFI declared
|
|
1196
|
+
// *const f32 / *const u32, which silently read the low 4 bytes
|
|
1197
|
+
// of each f64 slot as garbage f32/u32 values — meshes registered
|
|
1198
|
+
// (non-zero handle) but were unrenderable.
|
|
1199
|
+
//
|
|
1200
|
+
// Caller must pass `vertex_count` and `index_count` derived from
|
|
1201
|
+
// a literal-initialized array OR from values it computed itself.
|
|
1202
|
+
// Don't compute these via `arr.length` after `.push()` — Perry's
|
|
1203
|
+
// `.length` property currently reflects the literal-init size,
|
|
1204
|
+
// not the post-push count (a Perry codegen bug). Workaround on
|
|
1205
|
+
// the TS side: track the count manually or use literal arrays.
|
|
918
1206
|
if vertex_ptr.is_null() || index_ptr.is_null() { return 0.0; }
|
|
919
1207
|
let vcount = vertex_count as usize;
|
|
920
1208
|
let icount = index_count as usize;
|
|
921
|
-
let
|
|
922
|
-
let
|
|
923
|
-
|
|
1209
|
+
let vertex_f64 = unsafe { std::slice::from_raw_parts(vertex_ptr, vcount * 12) };
|
|
1210
|
+
let index_f64 = unsafe { std::slice::from_raw_parts(index_ptr, icount) };
|
|
1211
|
+
let vertex_data: Vec<f32> = vertex_f64.iter().map(|&v| v as f32).collect();
|
|
1212
|
+
let index_data: Vec<u32> = index_f64.iter().map(|&v| v as u32).collect();
|
|
1213
|
+
engine().models.create_mesh(&vertex_data, &index_data)
|
|
924
1214
|
}
|
|
925
1215
|
|
|
926
1216
|
// ============================================================
|
|
@@ -1261,12 +1551,21 @@ pub extern "C" fn bloom_load_model_animation(path_ptr: *const u8) -> f64 {
|
|
|
1261
1551
|
}
|
|
1262
1552
|
|
|
1263
1553
|
#[no_mangle]
|
|
1264
|
-
pub extern "C" fn bloom_update_model_animation(handle: f64, anim_index: f64, time: f64, scale: f64, px: f64, py: f64, pz: f64,
|
|
1554
|
+
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) {
|
|
1555
|
+
// Take a single Y-axis angle (radians) instead of sin/cos, so the
|
|
1556
|
+
// engine reconstructs both with full precision + correct signs.
|
|
1557
|
+
// Older callers that passed (rot_sin, rot_cos) hit a Perry-ARM64
|
|
1558
|
+
// 9th-arg garbling bug AND a sqrt(1-sin²) workaround that lost
|
|
1559
|
+
// the sign of cos — model rotation was correct only on half the
|
|
1560
|
+
// circle. 8-arg signature dodges both issues. Matches macOS.
|
|
1561
|
+
let rot_y_f = rot_y as f32;
|
|
1562
|
+
let rot_sin = rot_y_f.sin();
|
|
1563
|
+
let rot_cos = rot_y_f.cos();
|
|
1265
1564
|
let eng = engine();
|
|
1266
1565
|
eng.models.update_model_animation(handle, anim_index as usize, time as f32);
|
|
1267
1566
|
if let Some(anim) = eng.models.get_animation(handle) {
|
|
1268
1567
|
if !anim.joint_matrices.is_empty() {
|
|
1269
|
-
eng.renderer.set_joint_matrices_scaled(&anim.joint_matrices, scale as f32, [px as f32, py as f32, pz as f32], rot_sin
|
|
1568
|
+
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);
|
|
1270
1569
|
}
|
|
1271
1570
|
}
|
|
1272
1571
|
}
|
|
@@ -1354,18 +1653,32 @@ pub extern "C" fn bloom_toggle_fullscreen() {
|
|
|
1354
1653
|
x11_impl::toggle_fullscreen();
|
|
1355
1654
|
}
|
|
1356
1655
|
#[no_mangle]
|
|
1357
|
-
pub extern "C" fn bloom_set_window_title(title_ptr: *const u8) {
|
|
1656
|
+
pub extern "C" fn bloom_set_window_title(title_ptr: *const u8) {
|
|
1657
|
+
let title = str_from_header(title_ptr);
|
|
1658
|
+
#[cfg(target_os = "linux")]
|
|
1659
|
+
x11_impl::set_window_title(title);
|
|
1660
|
+
}
|
|
1358
1661
|
#[no_mangle]
|
|
1359
|
-
pub extern "C" fn bloom_set_window_icon(path_ptr: *const u8) {
|
|
1662
|
+
pub extern "C" fn bloom_set_window_icon(path_ptr: *const u8) {
|
|
1663
|
+
let path = str_from_header(path_ptr);
|
|
1664
|
+
#[cfg(target_os = "linux")]
|
|
1665
|
+
x11_impl::set_window_icon(path);
|
|
1666
|
+
}
|
|
1360
1667
|
|
|
1361
1668
|
#[no_mangle]
|
|
1362
1669
|
pub extern "C" fn bloom_disable_cursor() {
|
|
1363
|
-
engine().input
|
|
1670
|
+
let input = &mut engine().input;
|
|
1671
|
+
input.cursor_disabled = true;
|
|
1672
|
+
input.clear_mouse_delta();
|
|
1673
|
+
#[cfg(target_os = "linux")]
|
|
1674
|
+
x11_impl::enter_relative_mode();
|
|
1364
1675
|
}
|
|
1365
1676
|
|
|
1366
1677
|
#[no_mangle]
|
|
1367
1678
|
pub extern "C" fn bloom_enable_cursor() {
|
|
1368
1679
|
engine().input.cursor_disabled = false;
|
|
1680
|
+
#[cfg(target_os = "linux")]
|
|
1681
|
+
x11_impl::leave_relative_mode();
|
|
1369
1682
|
}
|
|
1370
1683
|
|
|
1371
1684
|
#[no_mangle]
|
|
@@ -1397,17 +1710,51 @@ pub extern "C" fn bloom_set_cursor_shape(shape: f64) {
|
|
|
1397
1710
|
engine().input.cursor_shape = shape as u32;
|
|
1398
1711
|
}
|
|
1399
1712
|
|
|
1400
|
-
// E4: Clipboard (
|
|
1713
|
+
// E4: Clipboard (arboard, X11/Wayland-aware)
|
|
1401
1714
|
#[no_mangle]
|
|
1402
|
-
pub extern "C" fn bloom_set_clipboard_text(
|
|
1715
|
+
pub extern "C" fn bloom_set_clipboard_text(text_ptr: *const u8) {
|
|
1716
|
+
let text = str_from_header(text_ptr);
|
|
1717
|
+
if let Ok(mut clipboard) = arboard::Clipboard::new() {
|
|
1718
|
+
let _ = clipboard.set_text(text.to_string());
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1403
1721
|
#[no_mangle]
|
|
1404
|
-
pub extern "C" fn bloom_get_clipboard_text() -> *const u8 {
|
|
1722
|
+
pub extern "C" fn bloom_get_clipboard_text() -> *const u8 {
|
|
1723
|
+
match arboard::Clipboard::new() {
|
|
1724
|
+
Ok(mut clipboard) => match clipboard.get_text() {
|
|
1725
|
+
Ok(text) => alloc_perry_string(&text),
|
|
1726
|
+
Err(_) => alloc_perry_string(""),
|
|
1727
|
+
},
|
|
1728
|
+
Err(_) => alloc_perry_string(""),
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1405
1731
|
|
|
1406
|
-
// E5b:
|
|
1732
|
+
// E5b: Native file dialogs (rfd → GTK/zenity/kdialog on Linux)
|
|
1407
1733
|
#[no_mangle]
|
|
1408
|
-
pub extern "C" fn bloom_open_file_dialog(
|
|
1734
|
+
pub extern "C" fn bloom_open_file_dialog(filter_ptr: *const u8, title_ptr: *const u8) -> *const u8 {
|
|
1735
|
+
let filter = str_from_header(filter_ptr);
|
|
1736
|
+
let title = str_from_header(title_ptr);
|
|
1737
|
+
let mut dialog = rfd::FileDialog::new().set_title(title);
|
|
1738
|
+
if !filter.is_empty() {
|
|
1739
|
+
dialog = dialog.add_filter("Files", &[filter]);
|
|
1740
|
+
}
|
|
1741
|
+
match dialog.pick_file() {
|
|
1742
|
+
Some(path) => alloc_perry_string(&path.to_string_lossy()),
|
|
1743
|
+
None => alloc_perry_string(""),
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1409
1746
|
#[no_mangle]
|
|
1410
|
-
pub extern "C" fn bloom_save_file_dialog(
|
|
1747
|
+
pub extern "C" fn bloom_save_file_dialog(default_name_ptr: *const u8, title_ptr: *const u8) -> *const u8 {
|
|
1748
|
+
let default_name = str_from_header(default_name_ptr);
|
|
1749
|
+
let title = str_from_header(title_ptr);
|
|
1750
|
+
let dialog = rfd::FileDialog::new()
|
|
1751
|
+
.set_title(title)
|
|
1752
|
+
.set_file_name(default_name);
|
|
1753
|
+
match dialog.save_file() {
|
|
1754
|
+
Some(path) => alloc_perry_string(&path.to_string_lossy()),
|
|
1755
|
+
None => alloc_perry_string(""),
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1411
1758
|
|
|
1412
1759
|
// Model bounds accessors. Return the axis-aligned bounding box of a loaded
|
|
1413
1760
|
// model in model-local coordinates. Editors use these to size gizmos, auto-
|
|
@@ -1456,24 +1803,14 @@ pub extern "C" fn bloom_file_exists(path_ptr: *const u8) -> f64 {
|
|
|
1456
1803
|
#[no_mangle]
|
|
1457
1804
|
pub extern "C" fn bloom_read_file(path_ptr: *const u8) -> *const u8 {
|
|
1458
1805
|
let path = str_from_header(path_ptr);
|
|
1806
|
+
// Always return a valid Perry string. A null pointer would NaN-box into a
|
|
1807
|
+
// string-typed JS value pointing at address 0; subsequent `.length` /
|
|
1808
|
+
// `.charCodeAt` reads dereference the bogus StringHeader and segfault.
|
|
1809
|
+
// Callers detect "missing file" via `data.length === 0` (e.g. the
|
|
1810
|
+
// jump game's discoverLevels probe across level1..level10 / custom_*).
|
|
1459
1811
|
match std::fs::read_to_string(path) {
|
|
1460
|
-
Ok(contents) =>
|
|
1461
|
-
|
|
1462
|
-
let bytes = contents.as_bytes();
|
|
1463
|
-
let len = bytes.len();
|
|
1464
|
-
let total = 12 + len; // 12 bytes header (3 × u32) + data
|
|
1465
|
-
let layout = std::alloc::Layout::from_size_align(total, 4).unwrap();
|
|
1466
|
-
unsafe {
|
|
1467
|
-
let ptr = std::alloc::alloc(layout);
|
|
1468
|
-
if ptr.is_null() { return std::ptr::null(); }
|
|
1469
|
-
*(ptr as *mut u32) = len as u32; // length
|
|
1470
|
-
*(ptr.add(4) as *mut u32) = len as u32; // capacity
|
|
1471
|
-
*(ptr.add(8) as *mut u32) = 1; // refcount (unique)
|
|
1472
|
-
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(12), len);
|
|
1473
|
-
ptr
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
Err(_) => std::ptr::null(),
|
|
1812
|
+
Ok(contents) => alloc_perry_string(&contents),
|
|
1813
|
+
Err(_) => alloc_perry_string(""),
|
|
1477
1814
|
}
|
|
1478
1815
|
}
|
|
1479
1816
|
|
|
@@ -1802,6 +2139,12 @@ pub extern "C" fn bloom_disable_shadows() {
|
|
|
1802
2139
|
engine().renderer.shadow_map.disable();
|
|
1803
2140
|
}
|
|
1804
2141
|
|
|
2142
|
+
#[no_mangle]
|
|
2143
|
+
pub extern "C" fn bloom_dump_shadow_map(path_ptr: *const u8) {
|
|
2144
|
+
let path = str_from_header(path_ptr).to_string();
|
|
2145
|
+
engine().renderer.dump_shadow_map(&path);
|
|
2146
|
+
}
|
|
2147
|
+
|
|
1805
2148
|
// ============================================================
|
|
1806
2149
|
// Post-processing
|
|
1807
2150
|
// ============================================================
|
|
@@ -2200,6 +2543,197 @@ pub extern "C" fn bloom_set_sss_enabled(on: f64) {
|
|
|
2200
2543
|
engine().renderer.set_sss_enabled(on != 0.0);
|
|
2201
2544
|
}
|
|
2202
2545
|
|
|
2546
|
+
// ============================================================
|
|
2547
|
+
// Render scale / upscale / DRS / post-FX / screenshots / impulse
|
|
2548
|
+
// Ports of the macOS FFI surface so the bloom/core TS layer links
|
|
2549
|
+
// cleanly on Linux. EngineState in bloom-shared already exposes the
|
|
2550
|
+
// underlying renderer methods, so these wrappers are platform-agnostic.
|
|
2551
|
+
// ============================================================
|
|
2552
|
+
|
|
2553
|
+
#[no_mangle]
|
|
2554
|
+
pub extern "C" fn bloom_take_screenshot(path_ptr: *const u8) {
|
|
2555
|
+
let path = str_from_header(path_ptr).to_string();
|
|
2556
|
+
let eng = engine();
|
|
2557
|
+
eng.renderer.screenshot_requested = true;
|
|
2558
|
+
eng.renderer.pending_screenshot_path = Some(path);
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
#[no_mangle]
|
|
2562
|
+
pub extern "C" fn bloom_set_env_clear_from_hdr(path_ptr: *const u8) {
|
|
2563
|
+
use image::ImageDecoder;
|
|
2564
|
+
let path = str_from_header(path_ptr).to_string();
|
|
2565
|
+
let file = match std::fs::File::open(&path) {
|
|
2566
|
+
Ok(f) => f,
|
|
2567
|
+
Err(_) => return,
|
|
2568
|
+
};
|
|
2569
|
+
let decoder = match image::codecs::hdr::HdrDecoder::new(std::io::BufReader::new(file)) {
|
|
2570
|
+
Ok(d) => d,
|
|
2571
|
+
Err(_) => return,
|
|
2572
|
+
};
|
|
2573
|
+
let (w, h) = decoder.dimensions();
|
|
2574
|
+
let byte_len = (w as usize) * (h as usize) * 3 * 4;
|
|
2575
|
+
let mut buf = vec![0u8; byte_len];
|
|
2576
|
+
if decoder.read_image(&mut buf).is_err() {
|
|
2577
|
+
return;
|
|
2578
|
+
}
|
|
2579
|
+
let rgb_f32: Vec<f32> = buf
|
|
2580
|
+
.chunks_exact(4)
|
|
2581
|
+
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
|
|
2582
|
+
.collect();
|
|
2583
|
+
engine().renderer.load_env_from_hdr(w, h, &rgb_f32);
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
#[no_mangle]
|
|
2587
|
+
pub extern "C" fn bloom_set_fog(r: f64, g: f64, b: f64, density: f64, height_ref: f64, height_falloff: f64) {
|
|
2588
|
+
let r_ = engine();
|
|
2589
|
+
r_.renderer.set_fog_color(r as f32, g as f32, b as f32);
|
|
2590
|
+
r_.renderer.set_fog_density(density as f32);
|
|
2591
|
+
r_.renderer.set_fog_height_falloff(height_ref as f32, height_falloff as f32);
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
#[no_mangle]
|
|
2595
|
+
pub extern "C" fn bloom_set_chromatic_aberration(strength: f64) {
|
|
2596
|
+
engine().renderer.set_chromatic_aberration(strength as f32);
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
#[no_mangle]
|
|
2600
|
+
pub extern "C" fn bloom_set_vignette(strength: f64, softness: f64) {
|
|
2601
|
+
engine().renderer.set_vignette(strength as f32, softness as f32);
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
#[no_mangle]
|
|
2605
|
+
pub extern "C" fn bloom_set_film_grain(strength: f64) {
|
|
2606
|
+
engine().renderer.set_film_grain(strength as f32);
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
#[no_mangle]
|
|
2610
|
+
pub extern "C" fn bloom_set_sun_shafts(strength: f64, decay: f64, r: f64, g: f64, b: f64) {
|
|
2611
|
+
let eng = engine();
|
|
2612
|
+
eng.renderer.set_sun_shaft_strength(strength as f32);
|
|
2613
|
+
eng.renderer.set_sun_shaft_decay(decay as f32);
|
|
2614
|
+
eng.renderer.set_sun_shaft_color(r as f32, g as f32, b as f32);
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
#[no_mangle]
|
|
2618
|
+
pub extern "C" fn bloom_set_auto_exposure(on: f64) {
|
|
2619
|
+
engine().renderer.set_auto_exposure(on != 0.0);
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
#[no_mangle]
|
|
2623
|
+
pub extern "C" fn bloom_set_taa_enabled(on: f64) {
|
|
2624
|
+
engine().renderer.set_taa_enabled(on != 0.0);
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
#[no_mangle]
|
|
2628
|
+
pub extern "C" fn bloom_set_render_scale(scale: f64) {
|
|
2629
|
+
engine().renderer.set_render_scale(scale as f32);
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
#[no_mangle]
|
|
2633
|
+
pub extern "C" fn bloom_get_render_scale() -> f64 {
|
|
2634
|
+
engine().renderer.render_scale() as f64
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
#[no_mangle]
|
|
2638
|
+
pub extern "C" fn bloom_set_upscale_mode(mode: f64) {
|
|
2639
|
+
engine().renderer.set_upscale_mode(mode as u32);
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
#[no_mangle]
|
|
2643
|
+
pub extern "C" fn bloom_set_cas_strength(strength: f64) {
|
|
2644
|
+
engine().renderer.set_cas_strength(strength as f32);
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
#[no_mangle]
|
|
2648
|
+
pub extern "C" fn bloom_get_physical_width() -> f64 {
|
|
2649
|
+
engine().renderer.physical_width() as f64
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
#[no_mangle]
|
|
2653
|
+
pub extern "C" fn bloom_get_physical_height() -> f64 {
|
|
2654
|
+
engine().renderer.physical_height() as f64
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
#[no_mangle]
|
|
2658
|
+
pub extern "C" fn bloom_set_auto_resolution(target_hz: f64, enabled: f64) {
|
|
2659
|
+
let eng = engine();
|
|
2660
|
+
if enabled != 0.0 {
|
|
2661
|
+
let current = eng.renderer.render_scale();
|
|
2662
|
+
eng.drs.enable(target_hz as f32, current);
|
|
2663
|
+
} else {
|
|
2664
|
+
eng.drs.disable();
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
#[no_mangle]
|
|
2669
|
+
pub extern "C" fn bloom_set_manual_exposure(value: f64) {
|
|
2670
|
+
engine().renderer.set_manual_exposure(value as f32);
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
#[no_mangle]
|
|
2674
|
+
pub extern "C" fn bloom_set_env_intensity(intensity: f64) {
|
|
2675
|
+
engine().renderer.set_env_intensity(intensity as f32);
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
#[no_mangle]
|
|
2679
|
+
pub extern "C" fn bloom_set_ssgi_enabled(enabled: f64) {
|
|
2680
|
+
engine().renderer.set_ssgi_enabled(enabled != 0.0);
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
#[no_mangle]
|
|
2684
|
+
pub extern "C" fn bloom_set_ssgi_intensity(intensity: f64) {
|
|
2685
|
+
engine().renderer.set_ssgi_intensity(intensity as f32);
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
#[no_mangle]
|
|
2689
|
+
pub extern "C" fn bloom_set_ssgi_radius(radius: f64) {
|
|
2690
|
+
engine().renderer.set_ssgi_radius(radius as f32);
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
#[no_mangle]
|
|
2694
|
+
pub extern "C" fn bloom_set_dof(enabled: f64, focus_distance: f64, aperture: f64) {
|
|
2695
|
+
let r = &mut engine().renderer;
|
|
2696
|
+
r.set_dof_enabled(enabled != 0.0);
|
|
2697
|
+
r.set_dof_focus_distance(focus_distance as f32);
|
|
2698
|
+
r.set_dof_aperture(aperture as f32);
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
#[no_mangle]
|
|
2702
|
+
pub extern "C" fn bloom_splat_impulse(x: f64, z: f64, radius: f64, strength: f64) {
|
|
2703
|
+
engine().renderer.impulse_field.submit_splat(
|
|
2704
|
+
x as f32, z as f32, radius as f32, strength as f32,
|
|
2705
|
+
);
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
#[no_mangle]
|
|
2709
|
+
pub extern "C" fn bloom_profiler_frame_history() -> *const u8 {
|
|
2710
|
+
let hist = engine().profiler.frame_history();
|
|
2711
|
+
let mut s = String::with_capacity(hist.len() * 24);
|
|
2712
|
+
for (cpu, gpu) in &hist {
|
|
2713
|
+
s.push_str(&format!("{:.2}|{:.2}\n", cpu, gpu));
|
|
2714
|
+
}
|
|
2715
|
+
alloc_perry_string(&s)
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
#[no_mangle]
|
|
2719
|
+
pub extern "C" fn bloom_profiler_overlay_text() -> *const u8 {
|
|
2720
|
+
let snap = engine().profiler.snapshot();
|
|
2721
|
+
let mut s = String::with_capacity(snap.len() * 48);
|
|
2722
|
+
for (label, cpu, gpu) in &snap {
|
|
2723
|
+
s.push_str(label);
|
|
2724
|
+
s.push('|');
|
|
2725
|
+
s.push_str(&format!("{:.2}", cpu));
|
|
2726
|
+
s.push('|');
|
|
2727
|
+
match gpu {
|
|
2728
|
+
Some(g) => s.push_str(&format!("{:.2}", g)),
|
|
2729
|
+
None => s.push_str("-1"),
|
|
2730
|
+
}
|
|
2731
|
+
s.push('\n');
|
|
2732
|
+
}
|
|
2733
|
+
alloc_perry_string(&s)
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
|
|
2203
2737
|
// ============================================================
|
|
2204
2738
|
// Profiler — CPU phase timings (always available) + GPU timestamps
|
|
2205
2739
|
// (when the adapter supports TIMESTAMP_QUERY). Disabled by default.
|
|
@@ -2222,6 +2756,177 @@ pub extern "C" fn bloom_print_profiler_summary() {
|
|
|
2222
2756
|
print!("{}", engine().profiler.summary());
|
|
2223
2757
|
}
|
|
2224
2758
|
|
|
2759
|
+
// ============================================================
|
|
2760
|
+
// Geisterhand screenshot integration
|
|
2761
|
+
// ============================================================
|
|
2762
|
+
|
|
2763
|
+
/// Register Bloom's GPU-based screenshot capture with perry-geisterhand.
|
|
2764
|
+
/// Mirrors macOS — replaces the platform-default window-grabber with a
|
|
2765
|
+
/// direct wgpu texture readback that works against the same Vulkan/GL
|
|
2766
|
+
/// surface bloom is already drawing into.
|
|
2767
|
+
fn bloom_register_geisterhand_screenshot() {
|
|
2768
|
+
extern "C" {
|
|
2769
|
+
fn perry_geisterhand_register_screenshot_capture(
|
|
2770
|
+
f: extern "C" fn(*mut usize) -> *mut u8,
|
|
2771
|
+
);
|
|
2772
|
+
}
|
|
2773
|
+
unsafe {
|
|
2774
|
+
perry_geisterhand_register_screenshot_capture(bloom_screenshot_capture);
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
/// Capture the Bloom framebuffer as PNG. Called from the geisterhand pump
|
|
2779
|
+
/// BEFORE end_frame in bloom_end_drawing. The vertices_3d/2d and VP matrix
|
|
2780
|
+
/// from the game loop are still populated; we render to a fresh surface
|
|
2781
|
+
/// texture with screenshot capture, producing the same visual output as
|
|
2782
|
+
/// the real frame.
|
|
2783
|
+
extern "C" fn bloom_screenshot_capture(out_len: *mut usize) -> *mut u8 {
|
|
2784
|
+
let eng = engine();
|
|
2785
|
+
|
|
2786
|
+
eng.renderer.screenshot_requested = true;
|
|
2787
|
+
eng.scene.prepare(
|
|
2788
|
+
&eng.renderer.device,
|
|
2789
|
+
&eng.renderer.queue,
|
|
2790
|
+
&eng.renderer.vp_matrix(),
|
|
2791
|
+
&eng.renderer.prev_vp_matrix,
|
|
2792
|
+
eng.renderer.uniform_3d_layout(),
|
|
2793
|
+
);
|
|
2794
|
+
eng.scene.prepare_materials(&eng.renderer);
|
|
2795
|
+
{
|
|
2796
|
+
let t = eng.get_time() as f32;
|
|
2797
|
+
let dt = eng.delta_time as f32;
|
|
2798
|
+
eng.renderer.material_system_begin_frame(t, dt);
|
|
2799
|
+
}
|
|
2800
|
+
eng.renderer.end_frame_with_scene(&mut eng.scene, &mut eng.profiler);
|
|
2801
|
+
|
|
2802
|
+
match eng.renderer.screenshot_data.take() {
|
|
2803
|
+
Some((width, height, rgba)) => {
|
|
2804
|
+
match encode_png(width, height, &rgba) {
|
|
2805
|
+
Some(png_data) => {
|
|
2806
|
+
let len = png_data.len();
|
|
2807
|
+
let ptr = unsafe { libc::malloc(len) as *mut u8 };
|
|
2808
|
+
if ptr.is_null() {
|
|
2809
|
+
unsafe { *out_len = 0; }
|
|
2810
|
+
return std::ptr::null_mut();
|
|
2811
|
+
}
|
|
2812
|
+
unsafe {
|
|
2813
|
+
std::ptr::copy_nonoverlapping(png_data.as_ptr(), ptr, len);
|
|
2814
|
+
*out_len = len;
|
|
2815
|
+
}
|
|
2816
|
+
ptr
|
|
2817
|
+
}
|
|
2818
|
+
None => {
|
|
2819
|
+
unsafe { *out_len = 0; }
|
|
2820
|
+
std::ptr::null_mut()
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
None => {
|
|
2825
|
+
unsafe { *out_len = 0; }
|
|
2826
|
+
std::ptr::null_mut()
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
/// Minimal PNG encoder (no external dependency). Matches the macOS
|
|
2832
|
+
/// implementation byte-for-byte so screenshots are identical across
|
|
2833
|
+
/// platforms.
|
|
2834
|
+
fn encode_png(width: u32, height: u32, rgba: &[u8]) -> Option<Vec<u8>> {
|
|
2835
|
+
use std::io::Write;
|
|
2836
|
+
|
|
2837
|
+
let mut png = Vec::new();
|
|
2838
|
+
png.write_all(&[137, 80, 78, 71, 13, 10, 26, 10]).ok()?;
|
|
2839
|
+
|
|
2840
|
+
let mut ihdr = Vec::new();
|
|
2841
|
+
ihdr.extend_from_slice(&width.to_be_bytes());
|
|
2842
|
+
ihdr.extend_from_slice(&height.to_be_bytes());
|
|
2843
|
+
ihdr.push(8);
|
|
2844
|
+
ihdr.push(6);
|
|
2845
|
+
ihdr.push(0);
|
|
2846
|
+
ihdr.push(0);
|
|
2847
|
+
ihdr.push(0);
|
|
2848
|
+
write_png_chunk(&mut png, b"IHDR", &ihdr);
|
|
2849
|
+
|
|
2850
|
+
let row_bytes = (width * 4) as usize;
|
|
2851
|
+
let mut raw = Vec::with_capacity((row_bytes + 1) * height as usize);
|
|
2852
|
+
for y in 0..height as usize {
|
|
2853
|
+
raw.push(0);
|
|
2854
|
+
let start = y * row_bytes;
|
|
2855
|
+
for x in 0..width as usize {
|
|
2856
|
+
let idx = start + x * 4;
|
|
2857
|
+
// wgpu Bgra8UnormSrgb: byte order is B, G, R, A
|
|
2858
|
+
raw.push(rgba[idx + 2]);
|
|
2859
|
+
raw.push(rgba[idx + 1]);
|
|
2860
|
+
raw.push(rgba[idx + 0]);
|
|
2861
|
+
raw.push(255);
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
let deflated = deflate_store(&raw);
|
|
2866
|
+
write_png_chunk(&mut png, b"IDAT", &deflated);
|
|
2867
|
+
write_png_chunk(&mut png, b"IEND", &[]);
|
|
2868
|
+
Some(png)
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
fn write_png_chunk(out: &mut Vec<u8>, chunk_type: &[u8; 4], data: &[u8]) {
|
|
2872
|
+
let len = data.len() as u32;
|
|
2873
|
+
out.extend_from_slice(&len.to_be_bytes());
|
|
2874
|
+
out.extend_from_slice(chunk_type);
|
|
2875
|
+
out.extend_from_slice(data);
|
|
2876
|
+
let crc = crc32(&[chunk_type.as_slice(), data].concat());
|
|
2877
|
+
out.extend_from_slice(&crc.to_be_bytes());
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
fn crc32(data: &[u8]) -> u32 {
|
|
2881
|
+
let mut crc: u32 = 0xFFFFFFFF;
|
|
2882
|
+
for &byte in data {
|
|
2883
|
+
crc ^= byte as u32;
|
|
2884
|
+
for _ in 0..8 {
|
|
2885
|
+
if crc & 1 != 0 {
|
|
2886
|
+
crc = (crc >> 1) ^ 0xEDB88320;
|
|
2887
|
+
} else {
|
|
2888
|
+
crc >>= 1;
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
!crc
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
fn deflate_store(data: &[u8]) -> Vec<u8> {
|
|
2896
|
+
let mut out = Vec::new();
|
|
2897
|
+
out.push(0x78);
|
|
2898
|
+
out.push(0x01);
|
|
2899
|
+
|
|
2900
|
+
let mut remaining = data.len();
|
|
2901
|
+
let mut offset = 0;
|
|
2902
|
+
while remaining > 0 {
|
|
2903
|
+
let block_size = remaining.min(65535);
|
|
2904
|
+
let is_last = remaining <= 65535;
|
|
2905
|
+
out.push(if is_last { 1 } else { 0 });
|
|
2906
|
+
let len = block_size as u16;
|
|
2907
|
+
let nlen = !len;
|
|
2908
|
+
out.extend_from_slice(&len.to_le_bytes());
|
|
2909
|
+
out.extend_from_slice(&nlen.to_le_bytes());
|
|
2910
|
+
out.extend_from_slice(&data[offset..offset + block_size]);
|
|
2911
|
+
offset += block_size;
|
|
2912
|
+
remaining -= block_size;
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
let adler = adler32(data);
|
|
2916
|
+
out.extend_from_slice(&adler.to_be_bytes());
|
|
2917
|
+
out
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
fn adler32(data: &[u8]) -> u32 {
|
|
2921
|
+
let mut a: u32 = 1;
|
|
2922
|
+
let mut b: u32 = 0;
|
|
2923
|
+
for &byte in data {
|
|
2924
|
+
a = (a + byte as u32) % 65521;
|
|
2925
|
+
b = (b + a) % 65521;
|
|
2926
|
+
}
|
|
2927
|
+
(b << 16) | a
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2225
2930
|
// ============================================================
|
|
2226
2931
|
// Physics (Jolt 5.x) — FFI surface generated from shared macro
|
|
2227
2932
|
// ============================================================
|