@bornengine/engine 0.4.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +231 -0
- package/native/android/Cargo.lock +1848 -0
- package/native/android/Cargo.toml +24 -0
- package/native/android/src/lib.rs +702 -0
- package/native/ios/Cargo.lock +1690 -0
- package/native/ios/Cargo.toml +32 -0
- package/native/ios/src/lib.rs +1267 -0
- package/native/linux/Cargo.lock +3279 -0
- package/native/linux/Cargo.toml +29 -0
- package/native/linux/src/lib.rs +1331 -0
- package/native/macos/Cargo.lock +3310 -0
- package/native/macos/Cargo.toml +46 -0
- package/native/macos/src/lib.rs +1302 -0
- package/native/shared/Cargo.lock +1899 -0
- package/native/shared/Cargo.toml +62 -0
- package/native/shared/assets/default_font.ttf +0 -0
- package/native/shared/build.rs +270 -0
- package/native/shared/shaders/common/clouds.wgsl +122 -0
- package/native/shared/shaders/common/fog.wgsl +16 -0
- package/native/shared/shaders/common/foliage_wind.wgsl +98 -0
- package/native/shared/shaders/common/imposter.wgsl +112 -0
- package/native/shared/shaders/common/pbr.wgsl +186 -0
- package/native/shared/shaders/common/shadows.wgsl +186 -0
- package/native/shared/shaders/common/sky.wgsl +8 -0
- package/native/shared/shaders/common/tonemap.wgsl +25 -0
- package/native/shared/shaders/impulse_field.wgsl +57 -0
- package/native/shared/shaders/material_abi.wgsl +383 -0
- package/native/shared/shaders/materials/test_minimal.wgsl +42 -0
- package/native/shared/src/anim_mixer.rs +61 -0
- package/native/shared/src/attach.rs +263 -0
- package/native/shared/src/audio/decode.rs +123 -0
- package/native/shared/src/audio/mod.rs +863 -0
- package/native/shared/src/audio/render.rs +892 -0
- package/native/shared/src/audio/spsc.rs +156 -0
- package/native/shared/src/audio/stream.rs +226 -0
- package/native/shared/src/custom_shaders.rs +104 -0
- package/native/shared/src/decals.rs +245 -0
- package/native/shared/src/drs.rs +211 -0
- package/native/shared/src/engine.rs +261 -0
- package/native/shared/src/ffi.rs +116 -0
- package/native/shared/src/ffi_core/assets.rs +388 -0
- package/native/shared/src/ffi_core/audio_ffi.rs +184 -0
- package/native/shared/src/ffi_core/draw.rs +334 -0
- package/native/shared/src/ffi_core/game_loop.rs +577 -0
- package/native/shared/src/ffi_core/input.rs +234 -0
- package/native/shared/src/ffi_core/mod.rs +127 -0
- package/native/shared/src/ffi_core/models.rs +1154 -0
- package/native/shared/src/ffi_core/ragdoll_ffi.rs +261 -0
- package/native/shared/src/ffi_core/scene.rs +626 -0
- package/native/shared/src/ffi_core/vfx.rs +212 -0
- package/native/shared/src/ffi_core/visual.rs +691 -0
- package/native/shared/src/frame_callbacks.rs +122 -0
- package/native/shared/src/geometry.rs +236 -0
- package/native/shared/src/handles.rs +182 -0
- package/native/shared/src/input.rs +448 -0
- package/native/shared/src/jolt_sys.rs +822 -0
- package/native/shared/src/lib.rs +55 -0
- package/native/shared/src/models.rs +1093 -0
- package/native/shared/src/models_gltf.rs +1280 -0
- package/native/shared/src/particles.rs +391 -0
- package/native/shared/src/physics_jolt.rs +1908 -0
- package/native/shared/src/picking.rs +298 -0
- package/native/shared/src/postfx.rs +345 -0
- package/native/shared/src/profiler.rs +492 -0
- package/native/shared/src/ragdoll.rs +474 -0
- package/native/shared/src/renderer/atmosphere_lut.rs +573 -0
- package/native/shared/src/renderer/brdf_lut.rs +154 -0
- package/native/shared/src/renderer/draw2d.rs +143 -0
- package/native/shared/src/renderer/formats.rs +822 -0
- package/native/shared/src/renderer/froxel.rs +421 -0
- package/native/shared/src/renderer/gi_bake.rs +653 -0
- package/native/shared/src/renderer/graph.rs +462 -0
- package/native/shared/src/renderer/hiz.rs +269 -0
- package/native/shared/src/renderer/hot_reload.rs +390 -0
- package/native/shared/src/renderer/impulse_field.rs +456 -0
- package/native/shared/src/renderer/lighting.rs +154 -0
- package/native/shared/src/renderer/material_instancing.rs +171 -0
- package/native/shared/src/renderer/material_pipeline.rs +700 -0
- package/native/shared/src/renderer/material_system.rs +1996 -0
- package/native/shared/src/renderer/material_system_tests.rs +601 -0
- package/native/shared/src/renderer/material_system_wasm.rs +41 -0
- package/native/shared/src/renderer/mod.rs +12556 -0
- package/native/shared/src/renderer/model_draw.rs +641 -0
- package/native/shared/src/renderer/occlusion.rs +429 -0
- package/native/shared/src/renderer/planar_pass.rs +593 -0
- package/native/shared/src/renderer/planar_reflection.rs +499 -0
- package/native/shared/src/renderer/post_pass.rs +249 -0
- package/native/shared/src/renderer/postfx_chain.rs +728 -0
- package/native/shared/src/renderer/pt_pass.rs +577 -0
- package/native/shared/src/renderer/scene_pass.rs +607 -0
- package/native/shared/src/renderer/shader_include.rs +205 -0
- package/native/shared/src/renderer/shader_library.rs +135 -0
- package/native/shared/src/renderer/shaders/ao.rs +570 -0
- package/native/shared/src/renderer/shaders/core.rs +1243 -0
- package/native/shared/src/renderer/shaders/env.rs +907 -0
- package/native/shared/src/renderer/shaders/gi.rs +810 -0
- package/native/shared/src/renderer/shaders/mod.rs +19 -0
- package/native/shared/src/renderer/shaders/post.rs +1558 -0
- package/native/shared/src/renderer/shaders/pt.rs +1859 -0
- package/native/shared/src/renderer/shaders/ssgi.rs +1586 -0
- package/native/shared/src/renderer/shadow_pass.rs +731 -0
- package/native/shared/src/renderer/ssgi_pass.rs +392 -0
- package/native/shared/src/renderer/ssr_pass.rs +188 -0
- package/native/shared/src/renderer/texture_store.rs +473 -0
- package/native/shared/src/renderer/transient.rs +591 -0
- package/native/shared/src/renderer/types.rs +941 -0
- package/native/shared/src/renderer/util.rs +152 -0
- package/native/shared/src/scene.rs +1362 -0
- package/native/shared/src/sdf_cache.rs +274 -0
- package/native/shared/src/shadows.rs +1036 -0
- package/native/shared/src/staging.rs +102 -0
- package/native/shared/src/string_header.rs +266 -0
- package/native/shared/src/text_renderer.rs +502 -0
- package/native/shared/src/textures.rs +197 -0
- package/native/tvos/Cargo.lock +1693 -0
- package/native/tvos/Cargo.toml +36 -0
- package/native/tvos/metal-patched/Cargo.toml +178 -0
- package/native/tvos/metal-patched/LICENSE-APACHE +201 -0
- package/native/tvos/metal-patched/LICENSE-MIT +25 -0
- package/native/tvos/metal-patched/src/acceleration_structure.rs +667 -0
- package/native/tvos/metal-patched/src/acceleration_structure_pass.rs +108 -0
- package/native/tvos/metal-patched/src/argument.rs +366 -0
- package/native/tvos/metal-patched/src/blitpass.rs +102 -0
- package/native/tvos/metal-patched/src/buffer.rs +71 -0
- package/native/tvos/metal-patched/src/capturedescriptor.rs +76 -0
- package/native/tvos/metal-patched/src/capturemanager.rs +113 -0
- package/native/tvos/metal-patched/src/commandbuffer.rs +192 -0
- package/native/tvos/metal-patched/src/commandqueue.rs +44 -0
- package/native/tvos/metal-patched/src/computepass.rs +107 -0
- package/native/tvos/metal-patched/src/constants.rs +152 -0
- package/native/tvos/metal-patched/src/counters.rs +119 -0
- package/native/tvos/metal-patched/src/depthstencil.rs +190 -0
- package/native/tvos/metal-patched/src/device.rs +2134 -0
- package/native/tvos/metal-patched/src/drawable.rs +39 -0
- package/native/tvos/metal-patched/src/encoder.rs +2041 -0
- package/native/tvos/metal-patched/src/heap.rs +281 -0
- package/native/tvos/metal-patched/src/indirect_encoder.rs +344 -0
- package/native/tvos/metal-patched/src/lib.rs +657 -0
- package/native/tvos/metal-patched/src/library.rs +902 -0
- package/native/tvos/metal-patched/src/mps.rs +575 -0
- package/native/tvos/metal-patched/src/pipeline/compute.rs +475 -0
- package/native/tvos/metal-patched/src/pipeline/mod.rs +71 -0
- package/native/tvos/metal-patched/src/pipeline/render.rs +762 -0
- package/native/tvos/metal-patched/src/renderpass.rs +443 -0
- package/native/tvos/metal-patched/src/resource.rs +182 -0
- package/native/tvos/metal-patched/src/sampler.rs +165 -0
- package/native/tvos/metal-patched/src/sync.rs +178 -0
- package/native/tvos/metal-patched/src/texture.rs +352 -0
- package/native/tvos/metal-patched/src/types.rs +90 -0
- package/native/tvos/metal-patched/src/vertexdescriptor.rs +250 -0
- package/native/tvos/src/audio_backend.rs +197 -0
- package/native/tvos/src/lib.rs +1891 -0
- package/native/visionos/Cargo.lock +1693 -0
- package/native/visionos/Cargo.toml +40 -0
- package/native/visionos/src/audio_backend.rs +197 -0
- package/native/visionos/src/lib.rs +1887 -0
- package/native/watchos/Cargo.lock +16 -0
- package/native/watchos/Cargo.toml +19 -0
- package/native/watchos/shaders/bloom_postfx.metal +99 -0
- package/native/watchos/src/BloomWatchApp.swift +1267 -0
- package/native/watchos/src/BloomWatchAudio.swift +179 -0
- package/native/watchos/src/audio.rs +55 -0
- package/native/watchos/src/draw_list.rs +229 -0
- package/native/watchos/src/ffi_stubs.rs +915 -0
- package/native/watchos/src/ffi_stubs_manual.rs +35 -0
- package/native/watchos/src/lib.rs +1124 -0
- package/native/watchos/src/models.rs +746 -0
- package/native/watchos/src/postfx.rs +95 -0
- package/native/watchos/src/scene.rs +534 -0
- package/native/watchos/src/textures.rs +184 -0
- package/native/web/Cargo.lock +1657 -0
- package/native/web/Cargo.toml +43 -0
- package/native/web/bloom_glue.js +695 -0
- package/native/web/build.sh +131 -0
- package/native/web/index.html +35 -0
- package/native/web/jolt_bridge.js +1519 -0
- package/native/web/src/input_ffi.rs +286 -0
- package/native/web/src/lib.rs +1796 -0
- package/native/web/src/material_ffi.rs +710 -0
- package/native/web/src/parity_ffi.rs +343 -0
- package/native/web/src/physics_ffi.rs +643 -0
- package/native/web/src/ragdoll_ffi.rs +250 -0
- package/native/web/src/render_settings.rs +98 -0
- package/native/windows/Cargo.lock +1815 -0
- package/native/windows/Cargo.toml +68 -0
- package/native/windows/src/lib.rs +1486 -0
- package/package.json +4279 -0
- package/src/audio/index.ts +315 -0
- package/src/core/colors.ts +63 -0
- package/src/core/index.ts +1206 -0
- package/src/core/keys.ts +63 -0
- package/src/core/types.ts +104 -0
- package/src/index.ts +171 -0
- package/src/math/index.ts +516 -0
- package/src/mobile/index.ts +294 -0
- package/src/models/index.ts +1258 -0
- package/src/physics/index.ts +1134 -0
- package/src/scene/index.ts +698 -0
- package/src/shapes/index.ts +120 -0
- package/src/text/index.ts +48 -0
- package/src/textures/index.ts +187 -0
- package/src/vfx/index.ts +191 -0
- package/src/world/index.ts +24 -0
- package/src/world/loader.ts +423 -0
- package/src/world/prefab.ts +217 -0
- package/src/world/render.ts +172 -0
- package/src/world/saver.ts +108 -0
- package/src/world/serialize.ts +301 -0
- package/src/world/terrain.ts +355 -0
- package/src/world/types.ts +160 -0
- package/src/world/validate.ts +319 -0
- package/src/world/version.ts +114 -0
|
@@ -0,0 +1,1331 @@
|
|
|
1
|
+
use bloom_shared::engine::EngineState;
|
|
2
|
+
use bloom_shared::renderer::Renderer;
|
|
3
|
+
use bloom_shared::string_header::{alloc_perry_string, str_from_header};
|
|
4
|
+
|
|
5
|
+
use std::os::unix::io::RawFd;
|
|
6
|
+
#[cfg(feature = "jolt")]
|
|
7
|
+
use std::ops::{Deref, DerefMut};
|
|
8
|
+
use std::sync::{Mutex, MutexGuard, OnceLock};
|
|
9
|
+
|
|
10
|
+
static ENGINE: OnceLock<Mutex<EngineState>> = OnceLock::new();
|
|
11
|
+
// X11 and joystick state below are confined to Perry's main run-loop thread.
|
|
12
|
+
// EngineState uses a Mutex because FFI callbacks can re-enter the engine;
|
|
13
|
+
// these platform handles are not accessed from worker threads.
|
|
14
|
+
static mut GAMEPAD_FD: RawFd = -1;
|
|
15
|
+
|
|
16
|
+
fn engine() -> MutexGuard<'static, EngineState> {
|
|
17
|
+
ENGINE
|
|
18
|
+
.get()
|
|
19
|
+
.expect("Engine not initialized")
|
|
20
|
+
.lock()
|
|
21
|
+
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
22
|
+
}
|
|
23
|
+
/// Asset-path hook for define_core_ffi! — identity on desktop, where game
|
|
24
|
+
/// asset paths are valid relative to the working directory.
|
|
25
|
+
fn bloom_resolve_asset_path(path: &str) -> std::borrow::Cow<'_, str> {
|
|
26
|
+
std::borrow::Cow::Borrowed(path)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The full shared (non-physics) FFI surface. See bloom_shared::ffi_core
|
|
30
|
+
// docs for the contract; tools/validate-ffi.js checks parity in CI.
|
|
31
|
+
bloom_shared::define_core_ffi!();
|
|
32
|
+
|
|
33
|
+
/// Map X11 keysym to Bloom key code.
|
|
34
|
+
fn map_keycode(keysym: u32) -> usize {
|
|
35
|
+
match keysym {
|
|
36
|
+
0x61..=0x7a => (keysym - 0x61 + 65) as usize, // a-z → A-Z (65-90)
|
|
37
|
+
0x41..=0x5a => keysym as usize, // A-Z direct
|
|
38
|
+
0x30..=0x39 => keysym as usize, // 0-9
|
|
39
|
+
0xff52 => 256, // XK_Up
|
|
40
|
+
0xff54 => 257, // XK_Down
|
|
41
|
+
0xff51 => 258, // XK_Left
|
|
42
|
+
0xff53 => 259, // XK_Right
|
|
43
|
+
0x20 => 32, // space
|
|
44
|
+
0xff0d => 265, // XK_Return → Bloom ENTER
|
|
45
|
+
0xff1b => 27, // XK_Escape
|
|
46
|
+
0xff09 => 9, // XK_Tab
|
|
47
|
+
0xff08 => 8, // XK_BackSpace
|
|
48
|
+
0xffff => 127, // XK_Delete
|
|
49
|
+
0xff63 => 260, // XK_Insert
|
|
50
|
+
0xff50 => 261, // XK_Home
|
|
51
|
+
0xff57 => 262, // XK_End
|
|
52
|
+
0xff55 => 263, // XK_Page_Up
|
|
53
|
+
0xff56 => 264, // XK_Page_Down
|
|
54
|
+
0xffe1 => 280, // XK_Shift_L
|
|
55
|
+
0xffe2 => 281, // XK_Shift_R
|
|
56
|
+
0xffe3 => 282, // XK_Control_L
|
|
57
|
+
0xffe4 => 283, // XK_Control_R
|
|
58
|
+
0xffe9 => 284, // XK_Alt_L
|
|
59
|
+
0xffea => 285, // XK_Alt_R
|
|
60
|
+
0xffeb => 286, // XK_Super_L
|
|
61
|
+
0xffec => 287, // XK_Super_R
|
|
62
|
+
// Function keys
|
|
63
|
+
0xffbe => 112, // XK_F1
|
|
64
|
+
0xffbf => 113, // XK_F2
|
|
65
|
+
0xffc0 => 114, // XK_F3
|
|
66
|
+
0xffc1 => 115, // XK_F4
|
|
67
|
+
0xffc2 => 116, // XK_F5
|
|
68
|
+
0xffc3 => 117, // XK_F6
|
|
69
|
+
0xffc4 => 118, // XK_F7
|
|
70
|
+
0xffc5 => 119, // XK_F8
|
|
71
|
+
0xffc6 => 120, // XK_F9
|
|
72
|
+
0xffc7 => 121, // XK_F10
|
|
73
|
+
0xffc8 => 122, // XK_F11
|
|
74
|
+
0xffc9 => 123, // XK_F12
|
|
75
|
+
_ => 0,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#[cfg(target_os = "linux")]
|
|
80
|
+
mod x11_impl {
|
|
81
|
+
use super::*;
|
|
82
|
+
|
|
83
|
+
static mut DISPLAY: *mut x11::xlib::Display = std::ptr::null_mut();
|
|
84
|
+
static mut X11_WINDOW: x11::xlib::Window = 0;
|
|
85
|
+
static mut IS_FULLSCREEN: bool = false;
|
|
86
|
+
static mut NO_FULLSCREEN: bool = false;
|
|
87
|
+
static mut CURSOR_HIDDEN: bool = false;
|
|
88
|
+
static mut HIDDEN_CURSOR: x11::xlib::Cursor = 0;
|
|
89
|
+
/// Cached XC_* shape cursors keyed by `cursor_shape` value 0..=6.
|
|
90
|
+
/// Lazily created by `apply_cursor_shape`; reused across frames so
|
|
91
|
+
/// we don't leak a Cursor handle every poll.
|
|
92
|
+
struct CursorCache {
|
|
93
|
+
cursors: [x11::xlib::Cursor; 8],
|
|
94
|
+
last_applied_shape: u32,
|
|
95
|
+
}
|
|
96
|
+
static CURSOR_CACHE: Mutex<CursorCache> = Mutex::new(CursorCache {
|
|
97
|
+
cursors: [0; 8],
|
|
98
|
+
last_applied_shape: 0xFFFF_FFFF,
|
|
99
|
+
});
|
|
100
|
+
/// When `cursor_disabled` (relative-mode) is on we keep warping the
|
|
101
|
+
/// pointer back to window center each frame; remembering the last warp
|
|
102
|
+
/// target lets motion handlers compute a reliable raw delta and ignore
|
|
103
|
+
/// the synthetic motion event the warp itself generates.
|
|
104
|
+
static mut WARP_CENTER_X: i32 = 0;
|
|
105
|
+
static mut WARP_CENTER_Y: i32 = 0;
|
|
106
|
+
static mut RELATIVE_MODE: bool = false;
|
|
107
|
+
/// WM_PROTOCOLS / WM_DELETE_WINDOW, interned once in `create_window`.
|
|
108
|
+
/// Cached because `poll_events` compares against them every event.
|
|
109
|
+
static mut WM_PROTOCOLS: x11::xlib::Atom = 0;
|
|
110
|
+
static mut WM_DELETE_WINDOW: x11::xlib::Atom = 0;
|
|
111
|
+
|
|
112
|
+
pub fn wm_protocols_atom() -> x11::xlib::Atom { unsafe { WM_PROTOCOLS } }
|
|
113
|
+
pub fn wm_delete_window_atom() -> x11::xlib::Atom { unsafe { WM_DELETE_WINDOW } }
|
|
114
|
+
|
|
115
|
+
pub fn set_fullscreen(fullscreen: bool) {
|
|
116
|
+
unsafe {
|
|
117
|
+
// BLOOM_NO_FULLSCREEN=1 hard-disables the fullscreen path so
|
|
118
|
+
// benchmark harnesses on a 4K monitor don't silently 4× their
|
|
119
|
+
// pixel count when an inherited fullscreen Space leaks in.
|
|
120
|
+
if NO_FULLSCREEN && fullscreen { return; }
|
|
121
|
+
if DISPLAY.is_null() || X11_WINDOW == 0 { return; }
|
|
122
|
+
|
|
123
|
+
let wm_state = x11::xlib::XInternAtom(
|
|
124
|
+
DISPLAY,
|
|
125
|
+
b"_NET_WM_STATE\0".as_ptr() as *const _,
|
|
126
|
+
0,
|
|
127
|
+
);
|
|
128
|
+
let wm_fullscreen = x11::xlib::XInternAtom(
|
|
129
|
+
DISPLAY,
|
|
130
|
+
b"_NET_WM_STATE_FULLSCREEN\0".as_ptr() as *const _,
|
|
131
|
+
0,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
let action = if fullscreen { 1 } else { 0 }; // _NET_WM_STATE_ADD / _REMOVE
|
|
135
|
+
|
|
136
|
+
let mut event: x11::xlib::XClientMessageEvent = std::mem::zeroed();
|
|
137
|
+
event.type_ = x11::xlib::ClientMessage;
|
|
138
|
+
event.window = X11_WINDOW;
|
|
139
|
+
event.message_type = wm_state;
|
|
140
|
+
event.format = 32;
|
|
141
|
+
event.data.set_long(0, action);
|
|
142
|
+
event.data.set_long(1, wm_fullscreen as i64);
|
|
143
|
+
event.data.set_long(2, 0);
|
|
144
|
+
event.data.set_long(3, 1); // source: normal application
|
|
145
|
+
|
|
146
|
+
let root = x11::xlib::XDefaultRootWindow(DISPLAY);
|
|
147
|
+
x11::xlib::XSendEvent(
|
|
148
|
+
DISPLAY,
|
|
149
|
+
root,
|
|
150
|
+
0,
|
|
151
|
+
x11::xlib::SubstructureRedirectMask | x11::xlib::SubstructureNotifyMask,
|
|
152
|
+
&mut event as *mut x11::xlib::XClientMessageEvent as *mut x11::xlib::XEvent,
|
|
153
|
+
);
|
|
154
|
+
x11::xlib::XFlush(DISPLAY);
|
|
155
|
+
IS_FULLSCREEN = fullscreen;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
pub fn toggle_fullscreen() {
|
|
160
|
+
unsafe { set_fullscreen(!IS_FULLSCREEN); }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Returns (physical_w, physical_h). Caller's `width`/`height`
|
|
164
|
+
/// are *logical*; on a HiDPI X11 display we multiply by the
|
|
165
|
+
/// monitor's scale factor so the window appears the right size
|
|
166
|
+
/// while its surface is at the screen's physical resolution.
|
|
167
|
+
///
|
|
168
|
+
/// `headless = true` keeps the X11 window + Vulkan surface alive
|
|
169
|
+
/// (wgpu needs a surface) but never maps the window — so it never
|
|
170
|
+
/// appears on screen and never steals focus. Mirrors the macOS
|
|
171
|
+
/// BLOOM_HEADLESS path for batch / CI rendering harnesses.
|
|
172
|
+
pub fn create_window(width: f64, height: f64, title: &str, headless: bool) -> (u32, u32) {
|
|
173
|
+
unsafe {
|
|
174
|
+
DISPLAY = x11::xlib::XOpenDisplay(std::ptr::null());
|
|
175
|
+
if DISPLAY.is_null() {
|
|
176
|
+
panic!("Failed to open X11 display");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let screen = x11::xlib::XDefaultScreen(DISPLAY);
|
|
180
|
+
let root = x11::xlib::XRootWindow(DISPLAY, screen);
|
|
181
|
+
|
|
182
|
+
let scale = display_scale(DISPLAY, screen);
|
|
183
|
+
let phys_w = (width * scale).round() as u32;
|
|
184
|
+
let phys_h = (height * scale).round() as u32;
|
|
185
|
+
|
|
186
|
+
// Off-screen origin is belt-and-braces: even if some WM
|
|
187
|
+
// chooses to map the window despite us never calling
|
|
188
|
+
// XMapWindow, it'll appear far off the visible desktop.
|
|
189
|
+
let origin_x: i32 = if headless { -20000 } else { 0 };
|
|
190
|
+
X11_WINDOW = x11::xlib::XCreateSimpleWindow(
|
|
191
|
+
DISPLAY, root,
|
|
192
|
+
origin_x, 0, phys_w, phys_h, 0,
|
|
193
|
+
x11::xlib::XBlackPixel(DISPLAY, screen),
|
|
194
|
+
x11::xlib::XWhitePixel(DISPLAY, screen),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
let title_cstr = std::ffi::CString::new(title).unwrap();
|
|
198
|
+
x11::xlib::XStoreName(DISPLAY, X11_WINDOW, title_cstr.as_ptr());
|
|
199
|
+
|
|
200
|
+
x11::xlib::XSelectInput(DISPLAY, X11_WINDOW,
|
|
201
|
+
x11::xlib::ExposureMask | x11::xlib::KeyPressMask | x11::xlib::KeyReleaseMask |
|
|
202
|
+
x11::xlib::ButtonPressMask | x11::xlib::ButtonReleaseMask |
|
|
203
|
+
x11::xlib::PointerMotionMask | x11::xlib::StructureNotifyMask);
|
|
204
|
+
|
|
205
|
+
// Opt into the WM close protocol before mapping, so the titlebar
|
|
206
|
+
// ✕ / Alt-F4 arrives as a ClientMessage we can turn into
|
|
207
|
+
// `windowShouldClose()` instead of the WM dropping our X
|
|
208
|
+
// connection and Xlib exit(1)-ing the game mid-frame.
|
|
209
|
+
WM_PROTOCOLS = x11::xlib::XInternAtom(
|
|
210
|
+
DISPLAY, b"WM_PROTOCOLS\0".as_ptr() as *const _, 0);
|
|
211
|
+
WM_DELETE_WINDOW = x11::xlib::XInternAtom(
|
|
212
|
+
DISPLAY, b"WM_DELETE_WINDOW\0".as_ptr() as *const _, 0);
|
|
213
|
+
if WM_DELETE_WINDOW != 0 {
|
|
214
|
+
let mut protocols = [WM_DELETE_WINDOW];
|
|
215
|
+
x11::xlib::XSetWMProtocols(
|
|
216
|
+
DISPLAY, X11_WINDOW, protocols.as_mut_ptr(), 1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if !headless {
|
|
220
|
+
x11::xlib::XMapWindow(DISPLAY, X11_WINDOW);
|
|
221
|
+
}
|
|
222
|
+
x11::xlib::XFlush(DISPLAY);
|
|
223
|
+
(phys_w, phys_h)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
pub fn set_no_fullscreen(no_fs: bool) { unsafe { NO_FULLSCREEN = no_fs; } }
|
|
228
|
+
|
|
229
|
+
/// Read the current display's DPI scale factor. Computed from
|
|
230
|
+
/// physical screen dimensions (pixels / mm). Snapped to the
|
|
231
|
+
/// nearest 0.25 and clamped to [1.0, 4.0] — EDID often lies
|
|
232
|
+
/// about millimetres so we want stable integer-ish steps. A
|
|
233
|
+
/// real Wayland-style desktop environment with explicit scale
|
|
234
|
+
/// settings is out of scope here; this matches what most X11
|
|
235
|
+
/// users actually run (fractional scaling via Xft.dpi is the
|
|
236
|
+
/// only thing GTK/Qt honour by default).
|
|
237
|
+
pub fn display_scale(display: *mut x11::xlib::Display, screen: i32) -> f64 {
|
|
238
|
+
unsafe {
|
|
239
|
+
let pixels = x11::xlib::XDisplayWidth(display, screen) as f64;
|
|
240
|
+
let mm = x11::xlib::XDisplayWidthMM(display, screen) as f64;
|
|
241
|
+
if mm <= 0.0 || pixels <= 0.0 { return 1.0; }
|
|
242
|
+
let dpi = pixels / (mm / 25.4);
|
|
243
|
+
// 96 DPI = scale 1.0. Snap to 0.25 steps.
|
|
244
|
+
let raw = (dpi / 96.0).max(1.0).min(4.0);
|
|
245
|
+
(raw * 4.0).round() / 4.0
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
pub fn display() -> *mut x11::xlib::Display { unsafe { DISPLAY } }
|
|
250
|
+
pub fn window() -> x11::xlib::Window { unsafe { X11_WINDOW } }
|
|
251
|
+
|
|
252
|
+
pub fn set_window_title(title: &str) {
|
|
253
|
+
unsafe {
|
|
254
|
+
if DISPLAY.is_null() || X11_WINDOW == 0 { return; }
|
|
255
|
+
let cstr = match std::ffi::CString::new(title) { Ok(c) => c, Err(_) => return };
|
|
256
|
+
x11::xlib::XStoreName(DISPLAY, X11_WINDOW, cstr.as_ptr());
|
|
257
|
+
// Modern WMs honour _NET_WM_NAME (UTF-8) over the legacy
|
|
258
|
+
// WM_NAME (Latin-1) set by XStoreName, so write both.
|
|
259
|
+
let net_wm_name = x11::xlib::XInternAtom(
|
|
260
|
+
DISPLAY, b"_NET_WM_NAME\0".as_ptr() as *const _, 0);
|
|
261
|
+
let utf8_string = x11::xlib::XInternAtom(
|
|
262
|
+
DISPLAY, b"UTF8_STRING\0".as_ptr() as *const _, 0);
|
|
263
|
+
if net_wm_name != 0 && utf8_string != 0 {
|
|
264
|
+
x11::xlib::XChangeProperty(
|
|
265
|
+
DISPLAY, X11_WINDOW, net_wm_name, utf8_string, 8,
|
|
266
|
+
x11::xlib::PropModeReplace,
|
|
267
|
+
title.as_ptr(), title.len() as i32,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
x11::xlib::XFlush(DISPLAY);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/// Set the EWMH `_NET_WM_ICON` property from an image file. The X11
|
|
275
|
+
/// icon format is a flat array of CARDINALs (`long`s on the wire):
|
|
276
|
+
/// `[width, height, pixel0, pixel1, ...]` with each pixel as ARGB
|
|
277
|
+
/// premultiplied-alpha packed into the low 32 bits of a long.
|
|
278
|
+
pub fn set_window_icon(path: &str) {
|
|
279
|
+
unsafe {
|
|
280
|
+
if DISPLAY.is_null() || X11_WINDOW == 0 { return; }
|
|
281
|
+
let img = match image::open(path) {
|
|
282
|
+
Ok(i) => i.to_rgba8(),
|
|
283
|
+
Err(_) => return,
|
|
284
|
+
};
|
|
285
|
+
let (w, h) = (img.width() as usize, img.height() as usize);
|
|
286
|
+
let mut buf: Vec<std::os::raw::c_long> = Vec::with_capacity(2 + w * h);
|
|
287
|
+
buf.push(w as std::os::raw::c_long);
|
|
288
|
+
buf.push(h as std::os::raw::c_long);
|
|
289
|
+
for px in img.chunks_exact(4) {
|
|
290
|
+
let r = px[0] as u32;
|
|
291
|
+
let g = px[1] as u32;
|
|
292
|
+
let b = px[2] as u32;
|
|
293
|
+
let a = px[3] as u32;
|
|
294
|
+
let argb = (a << 24) | (r << 16) | (g << 8) | b;
|
|
295
|
+
buf.push(argb as std::os::raw::c_long);
|
|
296
|
+
}
|
|
297
|
+
let net_wm_icon = x11::xlib::XInternAtom(
|
|
298
|
+
DISPLAY, b"_NET_WM_ICON\0".as_ptr() as *const _, 0);
|
|
299
|
+
let cardinal = x11::xlib::XInternAtom(
|
|
300
|
+
DISPLAY, b"CARDINAL\0".as_ptr() as *const _, 0);
|
|
301
|
+
x11::xlib::XChangeProperty(
|
|
302
|
+
DISPLAY, X11_WINDOW, net_wm_icon, cardinal, 32,
|
|
303
|
+
x11::xlib::PropModeReplace,
|
|
304
|
+
buf.as_ptr() as *const u8,
|
|
305
|
+
buf.len() as i32,
|
|
306
|
+
);
|
|
307
|
+
x11::xlib::XFlush(DISPLAY);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/// Build (once) a 1x1 fully-transparent cursor — the standard X11
|
|
312
|
+
/// trick for "hide the cursor". Subsequent calls reuse the cached
|
|
313
|
+
/// cursor since X11 leaks Cursor handles otherwise.
|
|
314
|
+
unsafe fn ensure_hidden_cursor() -> x11::xlib::Cursor {
|
|
315
|
+
if HIDDEN_CURSOR != 0 { return HIDDEN_CURSOR; }
|
|
316
|
+
let pixmap = x11::xlib::XCreatePixmap(DISPLAY, X11_WINDOW, 1, 1, 1);
|
|
317
|
+
let mut color: x11::xlib::XColor = std::mem::zeroed();
|
|
318
|
+
let cursor = x11::xlib::XCreatePixmapCursor(
|
|
319
|
+
DISPLAY, pixmap, pixmap, &mut color, &mut color, 0, 0);
|
|
320
|
+
x11::xlib::XFreePixmap(DISPLAY, pixmap);
|
|
321
|
+
HIDDEN_CURSOR = cursor;
|
|
322
|
+
cursor
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
pub fn hide_cursor() {
|
|
326
|
+
unsafe {
|
|
327
|
+
if DISPLAY.is_null() || X11_WINDOW == 0 || CURSOR_HIDDEN { return; }
|
|
328
|
+
let c = ensure_hidden_cursor();
|
|
329
|
+
x11::xlib::XDefineCursor(DISPLAY, X11_WINDOW, c);
|
|
330
|
+
x11::xlib::XFlush(DISPLAY);
|
|
331
|
+
CURSOR_HIDDEN = true;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
pub fn show_cursor() {
|
|
336
|
+
unsafe {
|
|
337
|
+
if DISPLAY.is_null() || X11_WINDOW == 0 || !CURSOR_HIDDEN { return; }
|
|
338
|
+
x11::xlib::XUndefineCursor(DISPLAY, X11_WINDOW);
|
|
339
|
+
x11::xlib::XFlush(DISPLAY);
|
|
340
|
+
CURSOR_HIDDEN = false;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/// Warp the pointer to window center and remember where we put it so
|
|
345
|
+
/// the motion handler can compute deltas relative to the warp target.
|
|
346
|
+
pub fn warp_to_center() {
|
|
347
|
+
unsafe {
|
|
348
|
+
if DISPLAY.is_null() || X11_WINDOW == 0 { return; }
|
|
349
|
+
let mut attrs: x11::xlib::XWindowAttributes = std::mem::zeroed();
|
|
350
|
+
x11::xlib::XGetWindowAttributes(DISPLAY, X11_WINDOW, &mut attrs);
|
|
351
|
+
let cx = attrs.width / 2;
|
|
352
|
+
let cy = attrs.height / 2;
|
|
353
|
+
x11::xlib::XWarpPointer(DISPLAY, 0, X11_WINDOW, 0, 0, 0, 0, cx, cy);
|
|
354
|
+
x11::xlib::XFlush(DISPLAY);
|
|
355
|
+
WARP_CENTER_X = cx;
|
|
356
|
+
WARP_CENTER_Y = cy;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
pub fn enter_relative_mode() {
|
|
361
|
+
unsafe {
|
|
362
|
+
RELATIVE_MODE = true;
|
|
363
|
+
hide_cursor();
|
|
364
|
+
warp_to_center();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
pub fn leave_relative_mode() {
|
|
369
|
+
unsafe {
|
|
370
|
+
RELATIVE_MODE = false;
|
|
371
|
+
show_cursor();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/// Apply the requested cursor shape (the same 0..=6 enum macOS uses
|
|
376
|
+
/// in NSCursor calls). XCreateFontCursor uses cursor-font glyph
|
|
377
|
+
/// constants from <X11/cursorfont.h>; we cache one Cursor per shape
|
|
378
|
+
/// so repeat calls don't leak server-side state.
|
|
379
|
+
pub fn apply_cursor_shape(shape: u32) {
|
|
380
|
+
unsafe {
|
|
381
|
+
if DISPLAY.is_null() || X11_WINDOW == 0 || CURSOR_HIDDEN { return; }
|
|
382
|
+
let mut cache = CURSOR_CACHE
|
|
383
|
+
.lock()
|
|
384
|
+
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
|
385
|
+
if shape == cache.last_applied_shape { return; }
|
|
386
|
+
// X11 cursor-font glyph indices (from cursorfont.h).
|
|
387
|
+
// 0 = default arrow → XC_left_ptr (68)
|
|
388
|
+
// 1 = pointing hand → XC_hand2 (60)
|
|
389
|
+
// 2 = open hand → XC_fleur (52, "move")
|
|
390
|
+
// 3 = I-beam → XC_xterm (152)
|
|
391
|
+
// 4 = resize H → XC_sb_h_double_arrow (108)
|
|
392
|
+
// 5 = resize V → XC_sb_v_double_arrow (116)
|
|
393
|
+
// 6 = crosshair → XC_crosshair (34)
|
|
394
|
+
let glyph: u32 = match shape {
|
|
395
|
+
1 => 60,
|
|
396
|
+
2 => 52,
|
|
397
|
+
3 => 152,
|
|
398
|
+
4 => 108,
|
|
399
|
+
5 => 116,
|
|
400
|
+
6 => 34,
|
|
401
|
+
_ => 68,
|
|
402
|
+
};
|
|
403
|
+
let idx = (shape as usize).min(cache.cursors.len() - 1);
|
|
404
|
+
if cache.cursors[idx] == 0 {
|
|
405
|
+
cache.cursors[idx] = x11::xlib::XCreateFontCursor(DISPLAY, glyph);
|
|
406
|
+
}
|
|
407
|
+
if cache.cursors[idx] != 0 {
|
|
408
|
+
x11::xlib::XDefineCursor(DISPLAY, X11_WINDOW, cache.cursors[idx]);
|
|
409
|
+
x11::xlib::XFlush(DISPLAY);
|
|
410
|
+
cache.last_applied_shape = shape;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
pub fn poll_events() {
|
|
416
|
+
unsafe {
|
|
417
|
+
while x11::xlib::XPending(DISPLAY) > 0 {
|
|
418
|
+
let mut event = std::mem::zeroed::<x11::xlib::XEvent>();
|
|
419
|
+
x11::xlib::XNextEvent(DISPLAY, &mut event);
|
|
420
|
+
|
|
421
|
+
match event.type_ {
|
|
422
|
+
x11::xlib::KeyPress => {
|
|
423
|
+
let keysym = x11::xlib::XLookupKeysym(
|
|
424
|
+
&mut event.key as *mut _ as *mut _,
|
|
425
|
+
0,
|
|
426
|
+
);
|
|
427
|
+
let bloom_key = map_keycode(keysym as u32);
|
|
428
|
+
if bloom_key > 0 {
|
|
429
|
+
engine().input.set_key_down(bloom_key);
|
|
430
|
+
}
|
|
431
|
+
// Decode UTF-8 typed text via XLookupString so the
|
|
432
|
+
// editor's text-input widget receives characters.
|
|
433
|
+
let mut buf = [0u8; 32];
|
|
434
|
+
let mut ks: x11::xlib::KeySym = 0;
|
|
435
|
+
let len = x11::xlib::XLookupString(
|
|
436
|
+
&mut event.key as *mut _,
|
|
437
|
+
buf.as_mut_ptr() as *mut i8,
|
|
438
|
+
buf.len() as i32,
|
|
439
|
+
&mut ks,
|
|
440
|
+
std::ptr::null_mut(),
|
|
441
|
+
);
|
|
442
|
+
if len > 0 {
|
|
443
|
+
if let Ok(s) = std::str::from_utf8(&buf[..len as usize]) {
|
|
444
|
+
for c in s.chars() {
|
|
445
|
+
let cp = c as u32;
|
|
446
|
+
if cp >= 32 || cp == 8 || cp == 13 || cp == 9 {
|
|
447
|
+
engine().input.push_char(cp);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
x11::xlib::KeyRelease => {
|
|
454
|
+
let keysym = x11::xlib::XLookupKeysym(
|
|
455
|
+
&mut event.key as *mut _ as *mut _,
|
|
456
|
+
0,
|
|
457
|
+
);
|
|
458
|
+
let bloom_key = map_keycode(keysym as u32);
|
|
459
|
+
if bloom_key > 0 {
|
|
460
|
+
engine().input.set_key_up(bloom_key);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
x11::xlib::MotionNotify => {
|
|
464
|
+
let motion = event.motion;
|
|
465
|
+
if RELATIVE_MODE {
|
|
466
|
+
// Ignore the motion event we generated by warping
|
|
467
|
+
// back to center — it would otherwise add a stray
|
|
468
|
+
// -delta cancelling the user's actual movement.
|
|
469
|
+
if motion.x == WARP_CENTER_X && motion.y == WARP_CENTER_Y {
|
|
470
|
+
// synthetic warp event; skip
|
|
471
|
+
} else {
|
|
472
|
+
let dx = (motion.x - WARP_CENTER_X) as f64;
|
|
473
|
+
let dy = (motion.y - WARP_CENTER_Y) as f64;
|
|
474
|
+
engine().input.accumulate_mouse_delta(dx, dy);
|
|
475
|
+
warp_to_center();
|
|
476
|
+
}
|
|
477
|
+
} else {
|
|
478
|
+
engine().input.set_mouse_position(motion.x as f64, motion.y as f64);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
x11::xlib::ButtonPress => {
|
|
482
|
+
let button = event.button.button;
|
|
483
|
+
match button {
|
|
484
|
+
1 => engine().input.set_mouse_button_down(0),
|
|
485
|
+
3 => engine().input.set_mouse_button_down(1),
|
|
486
|
+
2 => engine().input.set_mouse_button_down(2),
|
|
487
|
+
// X11 maps wheel up/down to button 4/5 and
|
|
488
|
+
// horizontal scroll to 6/7. macOS uses an
|
|
489
|
+
// accumulator with positive = scroll up; flip
|
|
490
|
+
// the sign on the down case to match.
|
|
491
|
+
4 => engine().input.accumulate_mouse_wheel(1.0),
|
|
492
|
+
5 => engine().input.accumulate_mouse_wheel(-1.0),
|
|
493
|
+
_ => {}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
x11::xlib::ButtonRelease => {
|
|
497
|
+
let button = event.button.button;
|
|
498
|
+
match button {
|
|
499
|
+
1 => engine().input.set_mouse_button_up(0),
|
|
500
|
+
3 => engine().input.set_mouse_button_up(1),
|
|
501
|
+
2 => engine().input.set_mouse_button_up(2),
|
|
502
|
+
_ => {}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
x11::xlib::ConfigureNotify => {
|
|
506
|
+
let configure = event.configure;
|
|
507
|
+
let phys_w = configure.width as u32;
|
|
508
|
+
let phys_h = configure.height as u32;
|
|
509
|
+
if phys_w > 0 && phys_h > 0 {
|
|
510
|
+
let mut eng = engine();
|
|
511
|
+
if phys_w != eng.renderer.physical_width()
|
|
512
|
+
|| phys_h != eng.renderer.physical_height()
|
|
513
|
+
{
|
|
514
|
+
let screen = x11::xlib::XDefaultScreen(DISPLAY);
|
|
515
|
+
let scale = display_scale(DISPLAY, screen);
|
|
516
|
+
let log_w = ((phys_w as f64) / scale).round() as u32;
|
|
517
|
+
let log_h = ((phys_h as f64) / scale).round() as u32;
|
|
518
|
+
eng.renderer.resize(phys_w, phys_h, log_w, log_h);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
x11::xlib::ClientMessage => {
|
|
523
|
+
// The WM asking us to close (titlebar ✕, Alt-F4, or a
|
|
524
|
+
// session logout). Without WM_DELETE_WINDOW in our
|
|
525
|
+
// WM_PROTOCOLS the WM instead destroys the connection,
|
|
526
|
+
// and Xlib's default IO-error handler calls exit(1)
|
|
527
|
+
// from under the game — `windowShouldClose()` never
|
|
528
|
+
// goes true, so `closeWindow()` and every shutdown
|
|
529
|
+
// path after it (audio teardown, save-on-exit) are
|
|
530
|
+
// skipped. Observed as:
|
|
531
|
+
// XIO: fatal IO error 62 (Timer expired)
|
|
532
|
+
if event.client_message.message_type == wm_protocols_atom()
|
|
533
|
+
&& event.client_message.data.get_long(0)
|
|
534
|
+
== wm_delete_window_atom() as i64
|
|
535
|
+
{
|
|
536
|
+
engine().should_close = true;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
x11::xlib::DestroyNotify => {
|
|
540
|
+
engine().should_close = true;
|
|
541
|
+
}
|
|
542
|
+
_ => {}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
#[no_mangle]
|
|
550
|
+
pub extern "C" fn bloom_init_window(width: f64, height: f64, title_ptr: *const u8, fullscreen: f64) {
|
|
551
|
+
let title = str_from_header(title_ptr);
|
|
552
|
+
|
|
553
|
+
#[cfg(target_os = "linux")]
|
|
554
|
+
{
|
|
555
|
+
// Headless mode: BLOOM_HEADLESS=1 keeps the X11 window + Vulkan
|
|
556
|
+
// surface alive (wgpu requires a real surface) but never maps
|
|
557
|
+
// the window so it's invisible and never steals focus. Lets an
|
|
558
|
+
// agent spin up the renderer in a batch loop without disturbing
|
|
559
|
+
// the user's desktop.
|
|
560
|
+
let headless = std::env::var("BLOOM_HEADLESS")
|
|
561
|
+
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
|
562
|
+
.unwrap_or(false);
|
|
563
|
+
// BLOOM_NO_FULLSCREEN=1 hard-disables fullscreen capability for
|
|
564
|
+
// benchmark harnesses where a 4K-display fullscreen path would
|
|
565
|
+
// silently quadruple render cost.
|
|
566
|
+
let no_fullscreen = std::env::var("BLOOM_NO_FULLSCREEN")
|
|
567
|
+
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
|
568
|
+
.unwrap_or(false);
|
|
569
|
+
x11_impl::set_no_fullscreen(no_fullscreen);
|
|
570
|
+
|
|
571
|
+
let (phys_w, phys_h) = x11_impl::create_window(width, height, title, headless);
|
|
572
|
+
|
|
573
|
+
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
|
574
|
+
backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
|
|
575
|
+
..wgpu::InstanceDescriptor::new_without_display_handle()
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
let surface = unsafe {
|
|
579
|
+
let raw_window = raw_window_handle::RawWindowHandle::Xlib(
|
|
580
|
+
raw_window_handle::XlibWindowHandle::new(x11_impl::window())
|
|
581
|
+
);
|
|
582
|
+
let raw_display = raw_window_handle::RawDisplayHandle::Xlib(
|
|
583
|
+
raw_window_handle::XlibDisplayHandle::new(
|
|
584
|
+
std::ptr::NonNull::new(x11_impl::display() as *mut _),
|
|
585
|
+
0,
|
|
586
|
+
)
|
|
587
|
+
);
|
|
588
|
+
instance.create_surface_unsafe(wgpu::SurfaceTargetUnsafe::RawHandle {
|
|
589
|
+
raw_display_handle: Some(raw_display),
|
|
590
|
+
raw_window_handle: raw_window,
|
|
591
|
+
}).expect("Failed to create surface")
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
let adapter = pollster_block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
|
|
595
|
+
compatible_surface: Some(&surface),
|
|
596
|
+
..Default::default()
|
|
597
|
+
})).expect("No adapter found");
|
|
598
|
+
|
|
599
|
+
// Ticket 007b: HW ray-query via VK_KHR_ray_query on RT-capable
|
|
600
|
+
// desktop Linux GPUs. Older integrated GPUs will fall back to
|
|
601
|
+
// the SW path through this gate.
|
|
602
|
+
let supported = adapter.features();
|
|
603
|
+
let force_sw_gi = std::env::var("BLOOM_FORCE_SW_GI")
|
|
604
|
+
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
|
605
|
+
.unwrap_or(false);
|
|
606
|
+
let rt_mask = wgpu::Features::EXPERIMENTAL_RAY_QUERY;
|
|
607
|
+
let mut required_features = wgpu::Features::empty();
|
|
608
|
+
// Ticket 011: request TIMESTAMP_QUERY when supported so the profiler
|
|
609
|
+
// can record GPU timings. Optional — profiler falls back to CPU-only
|
|
610
|
+
// when the adapter doesn't grant it.
|
|
611
|
+
if supported.contains(wgpu::Features::TIMESTAMP_QUERY) {
|
|
612
|
+
required_features |= wgpu::Features::TIMESTAMP_QUERY;
|
|
613
|
+
}
|
|
614
|
+
// Cooked BC7 textures (bloom-cook) upload compressed when the
|
|
615
|
+
// adapter has BC support; without it they CPU-decode at load.
|
|
616
|
+
if supported.contains(wgpu::Features::TEXTURE_COMPRESSION_BC) {
|
|
617
|
+
required_features |= wgpu::Features::TEXTURE_COMPRESSION_BC;
|
|
618
|
+
}
|
|
619
|
+
if !force_sw_gi && supported.contains(rt_mask) {
|
|
620
|
+
required_features |= rt_mask;
|
|
621
|
+
}
|
|
622
|
+
// PT-2: texture binding array + non-uniform indexing for textured
|
|
623
|
+
// path-trace hit shading. Both or neither (the kernel indexes the
|
|
624
|
+
// array with a per-thread material id).
|
|
625
|
+
let pt_tex_mask = wgpu::Features::TEXTURE_BINDING_ARRAY
|
|
626
|
+
| wgpu::Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING;
|
|
627
|
+
if supported.contains(pt_tex_mask) {
|
|
628
|
+
required_features |= pt_tex_mask;
|
|
629
|
+
}
|
|
630
|
+
let experimental_features = if required_features.intersects(rt_mask) {
|
|
631
|
+
unsafe { wgpu::ExperimentalFeatures::enabled() }
|
|
632
|
+
} else {
|
|
633
|
+
wgpu::ExperimentalFeatures::disabled()
|
|
634
|
+
};
|
|
635
|
+
let mut required_limits = wgpu::Limits::default();
|
|
636
|
+
// The material ABI declares 5 bind groups (PerFrame, PerView,
|
|
637
|
+
// PerMaterial, PerDraw, SceneInputs). wgpu's default limit is
|
|
638
|
+
// 4. Vulkan supports at least 7 here, so 5 is universally safe.
|
|
639
|
+
required_limits.max_bind_groups = 5;
|
|
640
|
+
// The refractive/translucent material profile binds up to 19
|
|
641
|
+
// sampled textures in the fragment stage (5 material maps + env/
|
|
642
|
+
// BRDF/3 shadow cascades/env-diffuse + planar reflection + 3 texture
|
|
643
|
+
// arrays + the group-4 scene_color/scene_depth/impulse/motion inputs).
|
|
644
|
+
// wgpu's default is 16. Raise to whatever the adapter actually
|
|
645
|
+
// supports — every real Vulkan/GL GPU exposes ≥128 — so opaque/
|
|
646
|
+
// transparent materials are unaffected and refractive ones link.
|
|
647
|
+
let adapter_limits = adapter.limits();
|
|
648
|
+
required_limits.max_sampled_textures_per_shader_stage = required_limits
|
|
649
|
+
.max_sampled_textures_per_shader_stage
|
|
650
|
+
.max(adapter_limits.max_sampled_textures_per_shader_stage);
|
|
651
|
+
required_limits.max_samplers_per_shader_stage = required_limits
|
|
652
|
+
.max_samplers_per_shader_stage
|
|
653
|
+
.max(adapter_limits.max_samplers_per_shader_stage);
|
|
654
|
+
// PT-2: binding arrays have their own element budget, default 0.
|
|
655
|
+
// Take whatever the adapter offers; the renderer checks the
|
|
656
|
+
// granted value against its fixed array size before compiling
|
|
657
|
+
// the textured kernel variant.
|
|
658
|
+
if required_features.contains(pt_tex_mask) {
|
|
659
|
+
required_limits.max_binding_array_elements_per_shader_stage =
|
|
660
|
+
adapter_limits.max_binding_array_elements_per_shader_stage;
|
|
661
|
+
}
|
|
662
|
+
// PT-4: the path-trace kernel binds 9 storage buffers (accum +
|
|
663
|
+
// moments + reservoir ping-pongs on top of instance/geo data);
|
|
664
|
+
// the wgpu default limit is 8.
|
|
665
|
+
required_limits.max_storage_buffers_per_shader_stage = required_limits
|
|
666
|
+
.max_storage_buffers_per_shader_stage
|
|
667
|
+
.max(adapter_limits.max_storage_buffers_per_shader_stage.min(16));
|
|
668
|
+
if required_features.intersects(rt_mask) {
|
|
669
|
+
required_limits = required_limits
|
|
670
|
+
.using_minimum_supported_acceleration_structure_values();
|
|
671
|
+
}
|
|
672
|
+
let (device, queue) = pollster_block_on(adapter.request_device(
|
|
673
|
+
&wgpu::DeviceDescriptor {
|
|
674
|
+
label: Some("bloom_device"),
|
|
675
|
+
required_features,
|
|
676
|
+
required_limits,
|
|
677
|
+
experimental_features,
|
|
678
|
+
..Default::default()
|
|
679
|
+
},
|
|
680
|
+
)).expect("Failed to create device");
|
|
681
|
+
|
|
682
|
+
let surface_caps = surface.get_capabilities(&adapter);
|
|
683
|
+
let format = surface_caps.formats[0];
|
|
684
|
+
// Surface sized at the *physical* pixel count we computed
|
|
685
|
+
// from XDisplayWidth/MM; renderer is told the caller's
|
|
686
|
+
// logical size separately so screenWidth() etc. stay
|
|
687
|
+
// DPI-independent.
|
|
688
|
+
let surface_config = wgpu::SurfaceConfiguration {
|
|
689
|
+
// COPY_SRC: bloom_take_screenshot reads the swapchain back;
|
|
690
|
+
// without it the readback copy is a validation error that
|
|
691
|
+
// aborts the process the first time a game calls it.
|
|
692
|
+
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
|
693
|
+
| wgpu::TextureUsages::COPY_SRC,
|
|
694
|
+
format,
|
|
695
|
+
width: phys_w,
|
|
696
|
+
height: phys_h,
|
|
697
|
+
present_mode: wgpu::PresentMode::Fifo,
|
|
698
|
+
alpha_mode: surface_caps.alpha_modes[0],
|
|
699
|
+
view_formats: vec![],
|
|
700
|
+
desired_maximum_frame_latency: 2,
|
|
701
|
+
};
|
|
702
|
+
surface.configure(&device, &surface_config);
|
|
703
|
+
|
|
704
|
+
let renderer = Renderer::new(device, queue, surface, surface_config, width as u32, height as u32);
|
|
705
|
+
let _ = ENGINE.set(Mutex::new(EngineState::new(renderer)));
|
|
706
|
+
|
|
707
|
+
if fullscreen != 0.0 {
|
|
708
|
+
x11_impl::set_fullscreen(true);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Register Bloom's GPU screenshot capture with perry-geisterhand.
|
|
712
|
+
// perry-runtime always exposes these symbols, so the link is direct
|
|
713
|
+
// (not weak); the registry no-ops gracefully if the editor isn't
|
|
714
|
+
// running.
|
|
715
|
+
bloom_register_geisterhand_screenshot();
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
#[cfg(not(target_os = "linux"))]
|
|
719
|
+
panic!("bloom-linux can only run on Linux");
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/// Attach the engine to a host-owned surface (PerryTS/perry#5519).
|
|
723
|
+
///
|
|
724
|
+
/// Not yet wired on Linux: Perry UI's GTK4 `BloomView` hands out a
|
|
725
|
+
/// `GtkWidget*`, and turning that into a wgpu surface needs the widget
|
|
726
|
+
/// realized/mapped and its `GdkSurface` bridged to an X11 `Window` (or a
|
|
727
|
+
/// Wayland `wl_surface`) — the GTK4 dmabuf/`GtkGLArea` path the issue
|
|
728
|
+
/// calls out as the larger follow-up. Until that lands this returns 0.0
|
|
729
|
+
/// (failure) so hosts fall back to the windowed `bloom_init_window`
|
|
730
|
+
/// path. The symbol exists so the FFI surface is uniform across
|
|
731
|
+
/// platforms (the shared bring-up is `bloom_shared::attach::attach_engine`,
|
|
732
|
+
/// already used by the Apple/Android/Windows attach paths).
|
|
733
|
+
#[no_mangle]
|
|
734
|
+
pub extern "C" fn bloom_attach_native(handle: i64, width: f64, height: f64) -> f64 {
|
|
735
|
+
let _ = (handle, width, height);
|
|
736
|
+
0.0
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/// Resize the engine's surface (#70 parity; used by host-driven
|
|
740
|
+
/// BloomViews on layout changes). `phys_*` physical px, `log_*` logical.
|
|
741
|
+
#[no_mangle]
|
|
742
|
+
pub extern "C" fn bloom_resize(phys_w: f64, phys_h: f64, log_w: f64, log_h: f64) {
|
|
743
|
+
if let Some(lock) = ENGINE.get() {
|
|
744
|
+
let mut eng = lock
|
|
745
|
+
.lock()
|
|
746
|
+
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
|
747
|
+
eng.renderer.resize(phys_w as u32, phys_h as u32, log_w as u32, log_h as u32);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/// HWND host-embed (#70) — Windows only; a no-op here for FFI-manifest
|
|
752
|
+
/// parity. Non-Windows hosts attach via `bloom_attach_native`.
|
|
753
|
+
#[no_mangle]
|
|
754
|
+
pub extern "C" fn bloom_attach_hwnd(_hwnd_bits: f64, _width: f64, _height: f64) {}
|
|
755
|
+
|
|
756
|
+
#[no_mangle]
|
|
757
|
+
pub extern "C" fn bloom_close_window() {
|
|
758
|
+
// Request a clean exit — the game loop's next windowShouldClose() check
|
|
759
|
+
// reports true (mirrors the Windows implementation; was an empty stub).
|
|
760
|
+
engine().should_close = true;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
#[no_mangle]
|
|
764
|
+
pub extern "C" fn bloom_window_should_close() -> f64 {
|
|
765
|
+
if engine().should_close { 1.0 } else { 0.0 }
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
#[cfg(target_os = "linux")]
|
|
769
|
+
fn poll_linux_gamepad() {
|
|
770
|
+
unsafe {
|
|
771
|
+
// Try to open gamepad if not already
|
|
772
|
+
if GAMEPAD_FD < 0 {
|
|
773
|
+
let path = b"/dev/input/js0\0";
|
|
774
|
+
// Open non-blocking
|
|
775
|
+
GAMEPAD_FD = libc::open(path.as_ptr() as *const libc::c_char, libc::O_RDONLY | libc::O_NONBLOCK);
|
|
776
|
+
if GAMEPAD_FD >= 0 {
|
|
777
|
+
engine().input.gamepad_available = true;
|
|
778
|
+
engine().input.gamepad_axis_count = 6;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
if GAMEPAD_FD < 0 { return; }
|
|
782
|
+
|
|
783
|
+
// Linux joystick event structure
|
|
784
|
+
#[repr(C)]
|
|
785
|
+
struct JsEvent {
|
|
786
|
+
time: u32,
|
|
787
|
+
value: i16,
|
|
788
|
+
event_type: u8,
|
|
789
|
+
number: u8,
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
loop {
|
|
793
|
+
let mut event = std::mem::zeroed::<JsEvent>();
|
|
794
|
+
let n = libc::read(
|
|
795
|
+
GAMEPAD_FD,
|
|
796
|
+
&mut event as *mut _ as *mut libc::c_void,
|
|
797
|
+
std::mem::size_of::<JsEvent>(),
|
|
798
|
+
);
|
|
799
|
+
if n != std::mem::size_of::<JsEvent>() as isize { break; }
|
|
800
|
+
|
|
801
|
+
let type_masked = event.event_type & 0x7f; // strip JS_EVENT_INIT
|
|
802
|
+
if type_masked == 1 {
|
|
803
|
+
// Button
|
|
804
|
+
let idx = event.number as usize;
|
|
805
|
+
if event.value != 0 {
|
|
806
|
+
engine().input.set_gamepad_button_down(idx);
|
|
807
|
+
} else {
|
|
808
|
+
engine().input.set_gamepad_button_up(idx);
|
|
809
|
+
}
|
|
810
|
+
} else if type_masked == 2 {
|
|
811
|
+
// Axis
|
|
812
|
+
let idx = event.number as usize;
|
|
813
|
+
let value = event.value as f32 / 32767.0;
|
|
814
|
+
engine().input.set_gamepad_axis(idx, value);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
#[no_mangle]
|
|
821
|
+
pub extern "C" fn bloom_begin_drawing() {
|
|
822
|
+
#[cfg(target_os = "linux")]
|
|
823
|
+
{
|
|
824
|
+
x11_impl::poll_events();
|
|
825
|
+
poll_linux_gamepad();
|
|
826
|
+
// Apply Q2 cursor shape — mirrors macOS NSCursor calls in its
|
|
827
|
+
// event loop. set_cursor_shape just stores the value; the X11
|
|
828
|
+
// cursor only changes when we actually call XDefineCursor.
|
|
829
|
+
x11_impl::apply_cursor_shape(engine().input.cursor_shape);
|
|
830
|
+
}
|
|
831
|
+
let (delta_time, callbacks) = {
|
|
832
|
+
let mut eng = engine();
|
|
833
|
+
eng.begin_frame_without_callbacks();
|
|
834
|
+
eng.begin_frame_callbacks()
|
|
835
|
+
};
|
|
836
|
+
for callback in callbacks {
|
|
837
|
+
callback(delta_time);
|
|
838
|
+
}
|
|
839
|
+
engine().finish_frame_callbacks();
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
#[no_mangle]
|
|
843
|
+
pub extern "C" fn bloom_end_drawing() {
|
|
844
|
+
// Pump geisterhand BEFORE end_frame. Mirrors macOS — the screenshot
|
|
845
|
+
// function re-renders inline using the captured VP + vertex buffers.
|
|
846
|
+
extern "C" { fn perry_geisterhand_pump(); }
|
|
847
|
+
unsafe { perry_geisterhand_pump(); }
|
|
848
|
+
|
|
849
|
+
engine().end_frame();
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
static AUDIO_RUNNING: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
|
853
|
+
|
|
854
|
+
#[no_mangle]
|
|
855
|
+
pub extern "C" fn bloom_init_audio() {
|
|
856
|
+
#[cfg(target_os = "linux")]
|
|
857
|
+
{
|
|
858
|
+
use std::sync::atomic::Ordering;
|
|
859
|
+
AUDIO_RUNNING.store(true, Ordering::SeqCst);
|
|
860
|
+
// Move the render half into the audio thread; the engine keeps
|
|
861
|
+
// only the command-producing control half.
|
|
862
|
+
let renderer = engine().audio.take_renderer();
|
|
863
|
+
std::thread::spawn(move || {
|
|
864
|
+
alsa_audio_thread(renderer);
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
#[no_mangle]
|
|
870
|
+
pub extern "C" fn bloom_close_audio() {
|
|
871
|
+
AUDIO_RUNNING.store(false, std::sync::atomic::Ordering::SeqCst);
|
|
872
|
+
std::thread::sleep(std::time::Duration::from_millis(50));
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
#[cfg(target_os = "linux")]
|
|
876
|
+
fn alsa_audio_thread(mut renderer: Option<bloom_shared::audio::AudioRenderer>) {
|
|
877
|
+
use alsa::pcm::*;
|
|
878
|
+
use alsa::{Direction, ValueOr};
|
|
879
|
+
use std::sync::atomic::Ordering;
|
|
880
|
+
|
|
881
|
+
let pcm = match PCM::new("default", Direction::Playback, false) {
|
|
882
|
+
Ok(p) => p,
|
|
883
|
+
Err(_) => return,
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
let sample_rate = 44100u32;
|
|
887
|
+
let channels = 2u32;
|
|
888
|
+
let period_size = 1024;
|
|
889
|
+
|
|
890
|
+
{
|
|
891
|
+
let hwp = HwParams::any(&pcm).unwrap();
|
|
892
|
+
let _ = hwp.set_channels(channels);
|
|
893
|
+
let _ = hwp.set_rate(sample_rate, ValueOr::Nearest);
|
|
894
|
+
let _ = hwp.set_format(Format::float());
|
|
895
|
+
let _ = hwp.set_access(Access::RWInterleaved);
|
|
896
|
+
let _ = hwp.set_period_size(period_size, ValueOr::Nearest);
|
|
897
|
+
let _ = hwp.set_buffer_size(period_size * 4);
|
|
898
|
+
let _ = pcm.hw_params(&hwp);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
let _ = pcm.prepare();
|
|
902
|
+
|
|
903
|
+
let frames = period_size as usize;
|
|
904
|
+
let mut mix_buf = vec![0.0f32; frames * channels as usize];
|
|
905
|
+
|
|
906
|
+
while AUDIO_RUNNING.load(Ordering::SeqCst) {
|
|
907
|
+
for s in mix_buf.iter_mut() { *s = 0.0; }
|
|
908
|
+
|
|
909
|
+
// Renderer is moved into this thread at spawn — no shared
|
|
910
|
+
// engine state is touched from here (see audio/mod.rs contract).
|
|
911
|
+
if let Some(r) = renderer.as_mut() {
|
|
912
|
+
r.mix(&mut mix_buf);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
let io = pcm.io_f32().unwrap();
|
|
916
|
+
match io.writei(&mix_buf) {
|
|
917
|
+
Ok(_) => {}
|
|
918
|
+
Err(e) => {
|
|
919
|
+
let _ = pcm.try_recover(e, true);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
let _ = pcm.drain();
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// --- Texture FFI ---
|
|
928
|
+
|
|
929
|
+
// --- Camera FFI ---
|
|
930
|
+
|
|
931
|
+
// --- 3D Drawing FFI ---
|
|
932
|
+
|
|
933
|
+
// --- Model FFI ---
|
|
934
|
+
|
|
935
|
+
// ============================================================
|
|
936
|
+
// Phase 1c — material system FFI
|
|
937
|
+
// ============================================================
|
|
938
|
+
|
|
939
|
+
// --- Music FFI ---
|
|
940
|
+
|
|
941
|
+
// --- Gamepad FFI ---
|
|
942
|
+
|
|
943
|
+
// --- Skeletal Animation Debug ---
|
|
944
|
+
|
|
945
|
+
// --- Lighting ---
|
|
946
|
+
|
|
947
|
+
// --- Utility FFI ---
|
|
948
|
+
|
|
949
|
+
#[no_mangle]
|
|
950
|
+
pub extern "C" fn bloom_toggle_fullscreen() {
|
|
951
|
+
#[cfg(target_os = "linux")]
|
|
952
|
+
x11_impl::toggle_fullscreen();
|
|
953
|
+
}
|
|
954
|
+
#[no_mangle]
|
|
955
|
+
pub extern "C" fn bloom_set_window_title(title_ptr: *const u8) {
|
|
956
|
+
let title = str_from_header(title_ptr);
|
|
957
|
+
#[cfg(target_os = "linux")]
|
|
958
|
+
x11_impl::set_window_title(title);
|
|
959
|
+
}
|
|
960
|
+
#[no_mangle]
|
|
961
|
+
pub extern "C" fn bloom_set_window_icon(path_ptr: *const u8) {
|
|
962
|
+
let path = str_from_header(path_ptr);
|
|
963
|
+
#[cfg(target_os = "linux")]
|
|
964
|
+
x11_impl::set_window_icon(path);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
#[no_mangle]
|
|
968
|
+
pub extern "C" fn bloom_disable_cursor() {
|
|
969
|
+
let input = &mut engine().input;
|
|
970
|
+
input.cursor_disabled = true;
|
|
971
|
+
input.clear_mouse_delta();
|
|
972
|
+
#[cfg(target_os = "linux")]
|
|
973
|
+
x11_impl::enter_relative_mode();
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
#[no_mangle]
|
|
977
|
+
pub extern "C" fn bloom_enable_cursor() {
|
|
978
|
+
engine().input.cursor_disabled = false;
|
|
979
|
+
#[cfg(target_os = "linux")]
|
|
980
|
+
x11_impl::leave_relative_mode();
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// E4: Clipboard (arboard, X11/Wayland-aware)
|
|
984
|
+
#[no_mangle]
|
|
985
|
+
pub extern "C" fn bloom_set_clipboard_text(text_ptr: *const u8) {
|
|
986
|
+
let text = str_from_header(text_ptr);
|
|
987
|
+
if let Ok(mut clipboard) = arboard::Clipboard::new() {
|
|
988
|
+
let _ = clipboard.set_text(text.to_string());
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
#[no_mangle]
|
|
992
|
+
pub extern "C" fn bloom_get_clipboard_text() -> *const u8 {
|
|
993
|
+
match arboard::Clipboard::new() {
|
|
994
|
+
Ok(mut clipboard) => match clipboard.get_text() {
|
|
995
|
+
Ok(text) => alloc_perry_string(&text),
|
|
996
|
+
Err(_) => alloc_perry_string(""),
|
|
997
|
+
},
|
|
998
|
+
Err(_) => alloc_perry_string(""),
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// E5b: Native file dialogs (rfd → GTK/zenity/kdialog on Linux)
|
|
1003
|
+
#[no_mangle]
|
|
1004
|
+
pub extern "C" fn bloom_open_file_dialog(filter_ptr: *const u8, title_ptr: *const u8) -> *const u8 {
|
|
1005
|
+
let filter = str_from_header(filter_ptr);
|
|
1006
|
+
let title = str_from_header(title_ptr);
|
|
1007
|
+
let mut dialog = rfd::FileDialog::new().set_title(title);
|
|
1008
|
+
if !filter.is_empty() {
|
|
1009
|
+
dialog = dialog.add_filter("Files", &[filter]);
|
|
1010
|
+
}
|
|
1011
|
+
match dialog.pick_file() {
|
|
1012
|
+
Some(path) => alloc_perry_string(&path.to_string_lossy()),
|
|
1013
|
+
None => alloc_perry_string(""),
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
#[no_mangle]
|
|
1017
|
+
pub extern "C" fn bloom_save_file_dialog(default_name_ptr: *const u8, title_ptr: *const u8) -> *const u8 {
|
|
1018
|
+
let default_name = str_from_header(default_name_ptr);
|
|
1019
|
+
let title = str_from_header(title_ptr);
|
|
1020
|
+
let dialog = rfd::FileDialog::new()
|
|
1021
|
+
.set_title(title)
|
|
1022
|
+
.set_file_name(default_name);
|
|
1023
|
+
match dialog.save_file() {
|
|
1024
|
+
Some(path) => alloc_perry_string(&path.to_string_lossy()),
|
|
1025
|
+
None => alloc_perry_string(""),
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
#[no_mangle]
|
|
1029
|
+
pub extern "C" fn bloom_get_platform() -> f64 { 4.0 }
|
|
1030
|
+
|
|
1031
|
+
/// Preferred OS language packed as `c0*256+c1` (ISO-639 primary subtag), from $LANG/$LC_*.
|
|
1032
|
+
#[no_mangle]
|
|
1033
|
+
pub extern "C" fn bloom_get_language() -> f64 {
|
|
1034
|
+
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 } }
|
|
1035
|
+
let v = std::env::var("LANG").or_else(|_| std::env::var("LC_ALL")).or_else(|_| std::env::var("LC_MESSAGES")).unwrap_or_default();
|
|
1036
|
+
if v.len() >= 2 && !v.starts_with('C') && !v.starts_with("POSIX") { pack(&v) } else { 25966.0 }
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// ============================================================
|
|
1040
|
+
// Frame callbacks
|
|
1041
|
+
// ============================================================
|
|
1042
|
+
|
|
1043
|
+
// ============================================================
|
|
1044
|
+
// Multiple lights
|
|
1045
|
+
// ============================================================
|
|
1046
|
+
|
|
1047
|
+
// ============================================================
|
|
1048
|
+
// Scene graph (retained mode)
|
|
1049
|
+
// ============================================================
|
|
1050
|
+
|
|
1051
|
+
// ============================================================
|
|
1052
|
+
// Geometry generation
|
|
1053
|
+
// ============================================================
|
|
1054
|
+
|
|
1055
|
+
// ============================================================
|
|
1056
|
+
// Shadow mapping
|
|
1057
|
+
// ============================================================
|
|
1058
|
+
|
|
1059
|
+
// ============================================================
|
|
1060
|
+
// Post-processing
|
|
1061
|
+
// ============================================================
|
|
1062
|
+
|
|
1063
|
+
// ============================================================
|
|
1064
|
+
// 3D→2D Projection (for UI overlays positioned in 3D space)
|
|
1065
|
+
// ============================================================
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
// ============================================================
|
|
1069
|
+
// Scene picking (raycasting)
|
|
1070
|
+
// ============================================================
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
// ============================================================
|
|
1074
|
+
// Thread-safe staging (for async asset loading via Perry threads)
|
|
1075
|
+
// ============================================================
|
|
1076
|
+
|
|
1077
|
+
fn pollster_block_on<F: std::future::Future>(future: F) -> F::Output {
|
|
1078
|
+
use std::task::{Context, Poll, Wake, Waker};
|
|
1079
|
+
use std::pin::Pin;
|
|
1080
|
+
use std::sync::Arc;
|
|
1081
|
+
struct NoopWaker;
|
|
1082
|
+
impl Wake for NoopWaker { fn wake(self: Arc<Self>) {} }
|
|
1083
|
+
let waker = Waker::from(Arc::new(NoopWaker));
|
|
1084
|
+
let mut cx = Context::from_waker(&waker);
|
|
1085
|
+
let mut future = unsafe { Pin::new_unchecked(Box::new(future)) };
|
|
1086
|
+
loop {
|
|
1087
|
+
match future.as_mut().poll(&mut cx) {
|
|
1088
|
+
Poll::Ready(result) => return result,
|
|
1089
|
+
Poll::Pending => std::thread::yield_now(),
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
// Q6: Multi-hit picking
|
|
1097
|
+
// ============================================================
|
|
1098
|
+
|
|
1099
|
+
// ============================================================
|
|
1100
|
+
// Render quality toggles (individual + preset) — ticket 011
|
|
1101
|
+
// Mirror of the macOS FFI surface added in commit 95da6af; previously
|
|
1102
|
+
// macOS-only, now exposed on every native platform so non-macOS builds
|
|
1103
|
+
// don't fail at runtime (missing symbol) when the TS API invokes them.
|
|
1104
|
+
// ============================================================
|
|
1105
|
+
|
|
1106
|
+
// ============================================================
|
|
1107
|
+
// Render scale / upscale / DRS / post-FX / screenshots / impulse
|
|
1108
|
+
// Ports of the macOS FFI surface so the bloom/core TS layer links
|
|
1109
|
+
// cleanly on Linux. EngineState in bloom-shared already exposes the
|
|
1110
|
+
// underlying renderer methods, so these wrappers are platform-agnostic.
|
|
1111
|
+
// ============================================================
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
// ============================================================
|
|
1115
|
+
// Profiler — CPU phase timings (always available) + GPU timestamps
|
|
1116
|
+
// (when the adapter supports TIMESTAMP_QUERY). Disabled by default.
|
|
1117
|
+
// ============================================================
|
|
1118
|
+
|
|
1119
|
+
// ============================================================
|
|
1120
|
+
// Geisterhand screenshot integration
|
|
1121
|
+
// ============================================================
|
|
1122
|
+
|
|
1123
|
+
/// Register Bloom's GPU-based screenshot capture with perry-geisterhand.
|
|
1124
|
+
/// Mirrors macOS — replaces the platform-default window-grabber with a
|
|
1125
|
+
/// direct wgpu texture readback that works against the same Vulkan/GL
|
|
1126
|
+
/// surface bloom is already drawing into.
|
|
1127
|
+
fn bloom_register_geisterhand_screenshot() {
|
|
1128
|
+
extern "C" {
|
|
1129
|
+
fn perry_geisterhand_register_screenshot_capture(
|
|
1130
|
+
f: extern "C" fn(*mut usize) -> *mut u8,
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
unsafe {
|
|
1134
|
+
perry_geisterhand_register_screenshot_capture(bloom_screenshot_capture);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/// Capture the Bloom framebuffer as PNG. Called from the geisterhand pump
|
|
1139
|
+
/// BEFORE end_frame in bloom_end_drawing. The vertices_3d/2d and VP matrix
|
|
1140
|
+
/// from the game loop are still populated; we render to a fresh surface
|
|
1141
|
+
/// texture with screenshot capture, producing the same visual output as
|
|
1142
|
+
/// the real frame.
|
|
1143
|
+
extern "C" fn bloom_screenshot_capture(out_len: *mut usize) -> *mut u8 {
|
|
1144
|
+
let mut eng = engine();
|
|
1145
|
+
let time = eng.get_time() as f32;
|
|
1146
|
+
let delta_time = eng.delta_time as f32;
|
|
1147
|
+
let bloom_shared::engine::EngineState {
|
|
1148
|
+
renderer,
|
|
1149
|
+
scene,
|
|
1150
|
+
profiler,
|
|
1151
|
+
..
|
|
1152
|
+
} = &mut *eng;
|
|
1153
|
+
|
|
1154
|
+
renderer.screenshot_requested = true;
|
|
1155
|
+
scene.prepare(
|
|
1156
|
+
&renderer.device,
|
|
1157
|
+
&renderer.queue,
|
|
1158
|
+
&renderer.vp_matrix(),
|
|
1159
|
+
&renderer.prev_vp_matrix,
|
|
1160
|
+
renderer.uniform_3d_layout(),
|
|
1161
|
+
// Screenshot capture renders everything the camera might see —
|
|
1162
|
+
// never occlusion-cull a one-shot capture.
|
|
1163
|
+
None,
|
|
1164
|
+
);
|
|
1165
|
+
scene.prepare_materials(renderer);
|
|
1166
|
+
{
|
|
1167
|
+
renderer.material_system_begin_frame(time, delta_time);
|
|
1168
|
+
}
|
|
1169
|
+
renderer.end_frame_with_scene(scene, profiler);
|
|
1170
|
+
|
|
1171
|
+
match renderer.screenshot_data.take() {
|
|
1172
|
+
Some((width, height, rgba)) => {
|
|
1173
|
+
match encode_png(width, height, &rgba) {
|
|
1174
|
+
Some(png_data) => {
|
|
1175
|
+
let len = png_data.len();
|
|
1176
|
+
let ptr = unsafe { libc::malloc(len) as *mut u8 };
|
|
1177
|
+
if ptr.is_null() {
|
|
1178
|
+
unsafe { *out_len = 0; }
|
|
1179
|
+
return std::ptr::null_mut();
|
|
1180
|
+
}
|
|
1181
|
+
unsafe {
|
|
1182
|
+
std::ptr::copy_nonoverlapping(png_data.as_ptr(), ptr, len);
|
|
1183
|
+
*out_len = len;
|
|
1184
|
+
}
|
|
1185
|
+
ptr
|
|
1186
|
+
}
|
|
1187
|
+
None => {
|
|
1188
|
+
unsafe { *out_len = 0; }
|
|
1189
|
+
std::ptr::null_mut()
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
None => {
|
|
1194
|
+
unsafe { *out_len = 0; }
|
|
1195
|
+
std::ptr::null_mut()
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/// Minimal PNG encoder (no external dependency). Matches the macOS
|
|
1201
|
+
/// implementation byte-for-byte so screenshots are identical across
|
|
1202
|
+
/// platforms.
|
|
1203
|
+
fn encode_png(width: u32, height: u32, rgba: &[u8]) -> Option<Vec<u8>> {
|
|
1204
|
+
use std::io::Write;
|
|
1205
|
+
|
|
1206
|
+
let mut png = Vec::new();
|
|
1207
|
+
png.write_all(&[137, 80, 78, 71, 13, 10, 26, 10]).ok()?;
|
|
1208
|
+
|
|
1209
|
+
let mut ihdr = Vec::new();
|
|
1210
|
+
ihdr.extend_from_slice(&width.to_be_bytes());
|
|
1211
|
+
ihdr.extend_from_slice(&height.to_be_bytes());
|
|
1212
|
+
ihdr.push(8);
|
|
1213
|
+
ihdr.push(6);
|
|
1214
|
+
ihdr.push(0);
|
|
1215
|
+
ihdr.push(0);
|
|
1216
|
+
ihdr.push(0);
|
|
1217
|
+
write_png_chunk(&mut png, b"IHDR", &ihdr);
|
|
1218
|
+
|
|
1219
|
+
let row_bytes = (width * 4) as usize;
|
|
1220
|
+
let mut raw = Vec::with_capacity((row_bytes + 1) * height as usize);
|
|
1221
|
+
for y in 0..height as usize {
|
|
1222
|
+
raw.push(0);
|
|
1223
|
+
let start = y * row_bytes;
|
|
1224
|
+
for x in 0..width as usize {
|
|
1225
|
+
let idx = start + x * 4;
|
|
1226
|
+
// wgpu Bgra8UnormSrgb: byte order is B, G, R, A
|
|
1227
|
+
raw.push(rgba[idx + 2]);
|
|
1228
|
+
raw.push(rgba[idx + 1]);
|
|
1229
|
+
raw.push(rgba[idx + 0]);
|
|
1230
|
+
raw.push(255);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
let deflated = deflate_store(&raw);
|
|
1235
|
+
write_png_chunk(&mut png, b"IDAT", &deflated);
|
|
1236
|
+
write_png_chunk(&mut png, b"IEND", &[]);
|
|
1237
|
+
Some(png)
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
fn write_png_chunk(out: &mut Vec<u8>, chunk_type: &[u8; 4], data: &[u8]) {
|
|
1241
|
+
let len = data.len() as u32;
|
|
1242
|
+
out.extend_from_slice(&len.to_be_bytes());
|
|
1243
|
+
out.extend_from_slice(chunk_type);
|
|
1244
|
+
out.extend_from_slice(data);
|
|
1245
|
+
let crc = crc32(&[chunk_type.as_slice(), data].concat());
|
|
1246
|
+
out.extend_from_slice(&crc.to_be_bytes());
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
fn crc32(data: &[u8]) -> u32 {
|
|
1250
|
+
let mut crc: u32 = 0xFFFFFFFF;
|
|
1251
|
+
for &byte in data {
|
|
1252
|
+
crc ^= byte as u32;
|
|
1253
|
+
for _ in 0..8 {
|
|
1254
|
+
if crc & 1 != 0 {
|
|
1255
|
+
crc = (crc >> 1) ^ 0xEDB88320;
|
|
1256
|
+
} else {
|
|
1257
|
+
crc >>= 1;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
!crc
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
fn deflate_store(data: &[u8]) -> Vec<u8> {
|
|
1265
|
+
let mut out = Vec::new();
|
|
1266
|
+
out.push(0x78);
|
|
1267
|
+
out.push(0x01);
|
|
1268
|
+
|
|
1269
|
+
let mut remaining = data.len();
|
|
1270
|
+
let mut offset = 0;
|
|
1271
|
+
while remaining > 0 {
|
|
1272
|
+
let block_size = remaining.min(65535);
|
|
1273
|
+
let is_last = remaining <= 65535;
|
|
1274
|
+
out.push(if is_last { 1 } else { 0 });
|
|
1275
|
+
let len = block_size as u16;
|
|
1276
|
+
let nlen = !len;
|
|
1277
|
+
out.extend_from_slice(&len.to_le_bytes());
|
|
1278
|
+
out.extend_from_slice(&nlen.to_le_bytes());
|
|
1279
|
+
out.extend_from_slice(&data[offset..offset + block_size]);
|
|
1280
|
+
offset += block_size;
|
|
1281
|
+
remaining -= block_size;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
let adler = adler32(data);
|
|
1285
|
+
out.extend_from_slice(&adler.to_be_bytes());
|
|
1286
|
+
out
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
fn adler32(data: &[u8]) -> u32 {
|
|
1290
|
+
let mut a: u32 = 1;
|
|
1291
|
+
let mut b: u32 = 0;
|
|
1292
|
+
for &byte in data {
|
|
1293
|
+
a = (a + byte as u32) % 65521;
|
|
1294
|
+
b = (b + a) % 65521;
|
|
1295
|
+
}
|
|
1296
|
+
(b << 16) | a
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// ============================================================
|
|
1300
|
+
// Physics (Jolt 5.x) — FFI surface generated from shared macro
|
|
1301
|
+
// ============================================================
|
|
1302
|
+
|
|
1303
|
+
#[cfg(feature = "jolt")]
|
|
1304
|
+
struct JoltEngineGuard {
|
|
1305
|
+
engine: MutexGuard<'static, EngineState>,
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
#[cfg(feature = "jolt")]
|
|
1309
|
+
impl Deref for JoltEngineGuard {
|
|
1310
|
+
type Target = bloom_shared::physics_jolt::JoltPhysics;
|
|
1311
|
+
|
|
1312
|
+
fn deref(&self) -> &Self::Target {
|
|
1313
|
+
&self.engine.jolt
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
#[cfg(feature = "jolt")]
|
|
1318
|
+
impl DerefMut for JoltEngineGuard {
|
|
1319
|
+
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
1320
|
+
&mut self.engine.jolt
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
#[cfg(feature = "jolt")]
|
|
1325
|
+
#[inline]
|
|
1326
|
+
fn bloom_jolt_ffi_physics() -> JoltEngineGuard {
|
|
1327
|
+
JoltEngineGuard { engine: engine() }
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
#[cfg(feature = "jolt")]
|
|
1331
|
+
bloom_shared::define_physics_ffi!();
|