@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,1486 @@
|
|
|
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
|
+
use bloom_shared::audio::{parse_wav, parse_ogg, parse_mp3};
|
|
5
|
+
|
|
6
|
+
use std::sync::OnceLock;
|
|
7
|
+
|
|
8
|
+
static mut ENGINE: OnceLock<EngineState> = OnceLock::new();
|
|
9
|
+
|
|
10
|
+
/// True when the engine renders into a host-provided child window (a Perry UI
|
|
11
|
+
/// `BloomView`) rather than a Bloom-owned top-level window. In embedded mode
|
|
12
|
+
/// the host owns the Win32 message loop, so `bloom_begin_drawing` must not pump
|
|
13
|
+
/// messages itself and `bloom_window_should_close` always reports "stay open".
|
|
14
|
+
#[cfg(windows)]
|
|
15
|
+
static mut EMBEDDED: bool = false;
|
|
16
|
+
|
|
17
|
+
fn engine() -> &'static mut EngineState {
|
|
18
|
+
unsafe { ENGINE.get_mut().expect("Engine not initialized") }
|
|
19
|
+
}
|
|
20
|
+
/// Asset-path hook for define_core_ffi! — identity on desktop, where game
|
|
21
|
+
/// asset paths are valid relative to the working directory.
|
|
22
|
+
fn bloom_resolve_asset_path(path: &str) -> std::borrow::Cow<'_, str> {
|
|
23
|
+
std::borrow::Cow::Borrowed(path)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The full shared (non-physics) FFI surface. See bloom_shared::ffi_core
|
|
27
|
+
// docs for the contract; tools/validate-ffi.js checks parity in CI.
|
|
28
|
+
bloom_shared::define_core_ffi!();
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
/// Map Win32 virtual key code to Bloom key code.
|
|
32
|
+
fn map_keycode(vk: u32) -> usize {
|
|
33
|
+
match vk {
|
|
34
|
+
0x41..=0x5A => vk as usize, // A-Z map directly (65-90)
|
|
35
|
+
0x30..=0x39 => vk as usize, // 0-9 map directly (48-57)
|
|
36
|
+
0x70..=0x7B => (vk - 0x70 + 112) as usize, // F1-F12
|
|
37
|
+
0x26 => 256, // VK_UP
|
|
38
|
+
0x28 => 257, // VK_DOWN
|
|
39
|
+
0x25 => 258, // VK_LEFT
|
|
40
|
+
0x27 => 259, // VK_RIGHT
|
|
41
|
+
0x20 => 32, // VK_SPACE
|
|
42
|
+
0x0D => 265, // VK_RETURN → Bloom ENTER
|
|
43
|
+
0x1B => 27, // VK_ESCAPE
|
|
44
|
+
0x09 => 9, // VK_TAB
|
|
45
|
+
0x08 => 8, // VK_BACK
|
|
46
|
+
0x2E => 127, // VK_DELETE
|
|
47
|
+
0x2D => 260, // VK_INSERT
|
|
48
|
+
0x24 => 261, // VK_HOME
|
|
49
|
+
0x23 => 262, // VK_END
|
|
50
|
+
0x21 => 263, // VK_PRIOR (Page Up)
|
|
51
|
+
0x22 => 264, // VK_NEXT (Page Down)
|
|
52
|
+
0xA0 => 280, // VK_LSHIFT
|
|
53
|
+
0xA1 => 281, // VK_RSHIFT
|
|
54
|
+
0xA2 => 282, // VK_LCONTROL
|
|
55
|
+
0xA3 => 283, // VK_RCONTROL
|
|
56
|
+
0xA4 => 284, // VK_LMENU (Left Alt)
|
|
57
|
+
0xA5 => 285, // VK_RMENU (Right Alt)
|
|
58
|
+
0x5B => 286, // VK_LWIN
|
|
59
|
+
0x5C => 287, // VK_RWIN
|
|
60
|
+
_ => 0,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Windows reports the GENERIC modifier VKs — VK_SHIFT (0x10), VK_CONTROL (0x11),
|
|
65
|
+
// VK_MENU (0x12) — in WM_KEYDOWN/UP for BOTH the left and right keys. The
|
|
66
|
+
// side-specific codes (VK_LSHIFT 0xA0, etc.) come back only from GetKeyState,
|
|
67
|
+
// never in the message wParam. map_keycode knows only the side-specific codes,
|
|
68
|
+
// so a raw generic VK mapped to 0 and was dropped: every Shift and Ctrl press
|
|
69
|
+
// vanished, which is why sprint (Shift) and dodge (Ctrl) silently never fired on
|
|
70
|
+
// Windows. Recover the side from lParam — scancode for Shift (LShift = 0x2A,
|
|
71
|
+
// RShift = 0x36), the extended-key bit for Ctrl/Alt (the right-hand ones are
|
|
72
|
+
// flagged extended).
|
|
73
|
+
fn resolve_modifier_vk(vk: u32, lparam: isize) -> u32 {
|
|
74
|
+
let scancode = ((lparam >> 16) & 0xFF) as u32;
|
|
75
|
+
let extended = (lparam >> 24) & 1 != 0;
|
|
76
|
+
match vk {
|
|
77
|
+
0x10 => if scancode == 0x36 { 0xA1 } else { 0xA0 }, // VK_SHIFT -> R/L
|
|
78
|
+
0x11 => if extended { 0xA3 } else { 0xA2 }, // VK_CONTROL -> R/L
|
|
79
|
+
0x12 => if extended { 0xA5 } else { 0xA4 }, // VK_MENU -> R/L
|
|
80
|
+
_ => vk,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Crash reporting (title-freeze / EN-020 investigation): the game was dying
|
|
86
|
+
// with empty stderr, no WER event, and no dump — i.e. invisibly. Catch
|
|
87
|
+
// unhandled SEH exceptions, print code + module-relative address
|
|
88
|
+
// (symbolizable against main.pdb via llvm-symbolizer), and write our own
|
|
89
|
+
// minidump. Fast-fail (0xC0000409) bypasses this filter by design, so a
|
|
90
|
+
// death with no output at all narrows to fail-fast / TerminateProcess.
|
|
91
|
+
#[cfg(windows)]
|
|
92
|
+
mod crash_report {
|
|
93
|
+
use std::os::windows::io::AsRawHandle;
|
|
94
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
95
|
+
use windows::core::{s, w};
|
|
96
|
+
use windows::Win32::Foundation::BOOL;
|
|
97
|
+
use windows::Win32::System::Diagnostics::Debug::{
|
|
98
|
+
SetUnhandledExceptionFilter, EXCEPTION_POINTERS, MINIDUMP_EXCEPTION_INFORMATION,
|
|
99
|
+
};
|
|
100
|
+
use windows::Win32::System::LibraryLoader::{GetModuleHandleW, GetProcAddress, LoadLibraryW};
|
|
101
|
+
use windows::Win32::System::Threading::{
|
|
102
|
+
GetCurrentProcess, GetCurrentProcessId, GetCurrentThreadId,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// dbghelp.dll is loaded at crash time — its import lib is not on
|
|
106
|
+
// perry's link line, and adding manifest libs would force a
|
|
107
|
+
// .perry-cache clear for every consumer of the engine.
|
|
108
|
+
type MiniDumpWriteDumpFn = unsafe extern "system" fn(
|
|
109
|
+
hprocess: *mut core::ffi::c_void,
|
|
110
|
+
processid: u32,
|
|
111
|
+
hfile: *mut core::ffi::c_void,
|
|
112
|
+
dumptype: i32,
|
|
113
|
+
exceptionparam: *const MINIDUMP_EXCEPTION_INFORMATION,
|
|
114
|
+
userstreamparam: *const core::ffi::c_void,
|
|
115
|
+
callbackparam: *const core::ffi::c_void,
|
|
116
|
+
) -> i32;
|
|
117
|
+
|
|
118
|
+
// MiniDumpNormal | MiniDumpWithIndirectlyReferencedMemory | MiniDumpWithThreadInfo
|
|
119
|
+
const DUMP_FLAGS: i32 = 0x0000_0040 | 0x0000_1000;
|
|
120
|
+
|
|
121
|
+
static INSTALLED: AtomicBool = AtomicBool::new(false);
|
|
122
|
+
|
|
123
|
+
pub fn install() {
|
|
124
|
+
if INSTALLED.swap(true, Ordering::SeqCst) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
unsafe {
|
|
128
|
+
SetUnhandledExceptionFilter(Some(filter));
|
|
129
|
+
}
|
|
130
|
+
eprintln!("bloom: crash filter installed (dumps -> tools/.testout/dumps)");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
unsafe extern "system" fn filter(info: *const EXCEPTION_POINTERS) -> i32 {
|
|
134
|
+
let (code, addr) = {
|
|
135
|
+
let mut c = 0u32;
|
|
136
|
+
let mut a = 0usize;
|
|
137
|
+
if !info.is_null() {
|
|
138
|
+
let rec = (*info).ExceptionRecord;
|
|
139
|
+
if !rec.is_null() {
|
|
140
|
+
c = (*rec).ExceptionCode.0 as u32;
|
|
141
|
+
a = (*rec).ExceptionAddress as usize;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
(c, a)
|
|
145
|
+
};
|
|
146
|
+
let base = GetModuleHandleW(None).map(|m| m.0 as usize).unwrap_or(0);
|
|
147
|
+
let rel = addr.wrapping_sub(base);
|
|
148
|
+
eprintln!("bloom: FATAL unhandled exception {code:#010x} at {addr:#x} (main.exe+{rel:#x})");
|
|
149
|
+
|
|
150
|
+
let _ = std::fs::create_dir_all("tools/.testout/dumps");
|
|
151
|
+
let stamp = std::time::SystemTime::now()
|
|
152
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
153
|
+
.map(|d| d.as_secs())
|
|
154
|
+
.unwrap_or(0);
|
|
155
|
+
let path = format!(
|
|
156
|
+
"tools/.testout/dumps/crash_main_{}_{stamp:x}.dmp",
|
|
157
|
+
GetCurrentProcessId()
|
|
158
|
+
);
|
|
159
|
+
let write_dump = LoadLibraryW(w!("dbghelp.dll"))
|
|
160
|
+
.ok()
|
|
161
|
+
.and_then(|h| GetProcAddress(h, s!("MiniDumpWriteDump")))
|
|
162
|
+
.map(|p| std::mem::transmute::<_, MiniDumpWriteDumpFn>(p));
|
|
163
|
+
match (std::fs::File::create(&path), write_dump) {
|
|
164
|
+
(Ok(f), Some(dump_fn)) => {
|
|
165
|
+
let exc = MINIDUMP_EXCEPTION_INFORMATION {
|
|
166
|
+
ThreadId: GetCurrentThreadId(),
|
|
167
|
+
ExceptionPointers: info as *mut EXCEPTION_POINTERS,
|
|
168
|
+
ClientPointers: BOOL(0),
|
|
169
|
+
};
|
|
170
|
+
let ok = dump_fn(
|
|
171
|
+
GetCurrentProcess().0,
|
|
172
|
+
GetCurrentProcessId(),
|
|
173
|
+
f.as_raw_handle() as *mut core::ffi::c_void,
|
|
174
|
+
DUMP_FLAGS,
|
|
175
|
+
&exc,
|
|
176
|
+
core::ptr::null(),
|
|
177
|
+
core::ptr::null(),
|
|
178
|
+
);
|
|
179
|
+
if ok != 0 {
|
|
180
|
+
eprintln!("bloom: minidump written: {path}");
|
|
181
|
+
} else {
|
|
182
|
+
eprintln!("bloom: minidump FAILED (MiniDumpWriteDump returned FALSE)");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
(Err(e), _) => eprintln!("bloom: minidump file create failed: {e}"),
|
|
186
|
+
(_, None) => eprintln!("bloom: minidump unavailable (dbghelp load failed)"),
|
|
187
|
+
}
|
|
188
|
+
1 // EXCEPTION_EXECUTE_HANDLER — let the process die
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Win32 windowing implementation
|
|
193
|
+
#[cfg(windows)]
|
|
194
|
+
mod win32 {
|
|
195
|
+
use super::*;
|
|
196
|
+
use windows::Win32::UI::WindowsAndMessaging::*;
|
|
197
|
+
use windows::Win32::UI::HiDpi::*;
|
|
198
|
+
use windows::Win32::Foundation::*;
|
|
199
|
+
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
|
|
200
|
+
use windows::Win32::Graphics::Gdi::*;
|
|
201
|
+
use windows::core::*;
|
|
202
|
+
use raw_window_handle::{RawWindowHandle, Win32WindowHandle, RawDisplayHandle, WindowsDisplayHandle};
|
|
203
|
+
|
|
204
|
+
static mut HWND_GLOBAL: Option<HWND> = None;
|
|
205
|
+
|
|
206
|
+
/// The Bloom-owned top-level window, for platform features that need an
|
|
207
|
+
/// owner (dialogs, SetWindowTextW, clipboard). None in embedded mode.
|
|
208
|
+
pub fn main_hwnd() -> Option<HWND> {
|
|
209
|
+
unsafe { HWND_GLOBAL }
|
|
210
|
+
}
|
|
211
|
+
static mut IS_FULLSCREEN: bool = false;
|
|
212
|
+
static mut WINDOWED_STYLE: u32 = 0;
|
|
213
|
+
static mut WINDOWED_RECT: RECT = RECT { left: 0, top: 0, right: 0, bottom: 0 };
|
|
214
|
+
|
|
215
|
+
pub fn set_fullscreen(fullscreen: bool) {
|
|
216
|
+
unsafe {
|
|
217
|
+
let Some(hwnd) = HWND_GLOBAL else { return };
|
|
218
|
+
|
|
219
|
+
if fullscreen && !IS_FULLSCREEN {
|
|
220
|
+
// Save current style and window rect for restore
|
|
221
|
+
WINDOWED_STYLE = GetWindowLongW(hwnd, GWL_STYLE) as u32;
|
|
222
|
+
let _ = GetWindowRect(hwnd, &mut WINDOWED_RECT);
|
|
223
|
+
|
|
224
|
+
// Get monitor dimensions
|
|
225
|
+
let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
|
|
226
|
+
let mut mi: MONITORINFO = std::mem::zeroed();
|
|
227
|
+
mi.cbSize = std::mem::size_of::<MONITORINFO>() as u32;
|
|
228
|
+
let _ = GetMonitorInfoW(monitor, &mut mi);
|
|
229
|
+
|
|
230
|
+
// Set borderless fullscreen
|
|
231
|
+
SetWindowLongW(hwnd, GWL_STYLE, (WS_POPUP | WS_VISIBLE).0 as i32);
|
|
232
|
+
let _ = SetWindowPos(
|
|
233
|
+
hwnd, HWND_TOP,
|
|
234
|
+
mi.rcMonitor.left, mi.rcMonitor.top,
|
|
235
|
+
mi.rcMonitor.right - mi.rcMonitor.left,
|
|
236
|
+
mi.rcMonitor.bottom - mi.rcMonitor.top,
|
|
237
|
+
SWP_FRAMECHANGED | SWP_NOOWNERZORDER,
|
|
238
|
+
);
|
|
239
|
+
IS_FULLSCREEN = true;
|
|
240
|
+
} else if !fullscreen && IS_FULLSCREEN {
|
|
241
|
+
// Restore windowed mode
|
|
242
|
+
SetWindowLongW(hwnd, GWL_STYLE, WINDOWED_STYLE as i32);
|
|
243
|
+
let _ = SetWindowPos(
|
|
244
|
+
hwnd, None,
|
|
245
|
+
WINDOWED_RECT.left, WINDOWED_RECT.top,
|
|
246
|
+
WINDOWED_RECT.right - WINDOWED_RECT.left,
|
|
247
|
+
WINDOWED_RECT.bottom - WINDOWED_RECT.top,
|
|
248
|
+
SWP_FRAMECHANGED | SWP_NOOWNERZORDER | SWP_NOZORDER,
|
|
249
|
+
);
|
|
250
|
+
IS_FULLSCREEN = false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
pub fn toggle_fullscreen() {
|
|
256
|
+
unsafe { set_fullscreen(!IS_FULLSCREEN); }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
unsafe extern "system" fn wndproc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
|
|
260
|
+
match msg {
|
|
261
|
+
WM_CLOSE => {
|
|
262
|
+
// Diagnostic (title-freeze investigation): who closes us?
|
|
263
|
+
eprintln!("bloom: WM_CLOSE received");
|
|
264
|
+
DefWindowProcW(hwnd, msg, wparam, lparam)
|
|
265
|
+
}
|
|
266
|
+
WM_DESTROY => {
|
|
267
|
+
eprintln!("bloom: WM_DESTROY — should_close set, main loop will exit cleanly");
|
|
268
|
+
PostQuitMessage(0);
|
|
269
|
+
if let Some(eng) = ENGINE.get_mut() {
|
|
270
|
+
eng.should_close = true;
|
|
271
|
+
}
|
|
272
|
+
LRESULT(0)
|
|
273
|
+
}
|
|
274
|
+
WM_KEYDOWN => {
|
|
275
|
+
let bloom_key = map_keycode(resolve_modifier_vk(wparam.0 as u32, lparam.0));
|
|
276
|
+
if bloom_key > 0 {
|
|
277
|
+
if let Some(eng) = ENGINE.get_mut() {
|
|
278
|
+
eng.input.set_key_down(bloom_key);
|
|
279
|
+
// lParam bit 30 = previous key state: 1 means this is
|
|
280
|
+
// an OS auto-repeat, surfaced as its own isKeyRepeated
|
|
281
|
+
// edge (isKeyPressed stays initial-press-only).
|
|
282
|
+
if (lparam.0 >> 30) & 1 == 1 {
|
|
283
|
+
eng.input.queue_key_repeat(bloom_key);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// F12 — native screenshot hotkey. Perry currently drops
|
|
288
|
+
// the game-side takeScreenshot FFI call entirely
|
|
289
|
+
// (PerryTS/perry#6087), so the capture is triggered here
|
|
290
|
+
// where no compiler sits in the path. Initial press only
|
|
291
|
+
// (lparam bit 30 = previous key state) so holding the key
|
|
292
|
+
// doesn't machine-gun PNGs. The file lands in the working
|
|
293
|
+
// directory with a timestamped name; the readback runs at
|
|
294
|
+
// this frame's end_frame.
|
|
295
|
+
if wparam.0 as u32 == 0x7B && (lparam.0 as u32 >> 30) & 1 == 0 {
|
|
296
|
+
if let Some(eng) = ENGINE.get_mut() {
|
|
297
|
+
let ms = std::time::SystemTime::now()
|
|
298
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
299
|
+
.map(|d| d.as_millis())
|
|
300
|
+
.unwrap_or(0);
|
|
301
|
+
let path = format!("screenshot_{}.png", ms);
|
|
302
|
+
eprintln!("bloom: F12 screenshot -> {}", path);
|
|
303
|
+
eng.renderer.screenshot_requested = true;
|
|
304
|
+
eng.renderer.pending_screenshot_path = Some(path);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
DefWindowProcW(hwnd, msg, wparam, lparam)
|
|
308
|
+
}
|
|
309
|
+
WM_KEYUP => {
|
|
310
|
+
let bloom_key = map_keycode(resolve_modifier_vk(wparam.0 as u32, lparam.0));
|
|
311
|
+
if bloom_key > 0 {
|
|
312
|
+
if let Some(eng) = ENGINE.get_mut() {
|
|
313
|
+
eng.input.set_key_up(bloom_key);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
DefWindowProcW(hwnd, msg, wparam, lparam)
|
|
317
|
+
}
|
|
318
|
+
WM_MOUSEMOVE => {
|
|
319
|
+
// lParam carries PHYSICAL client pixels. The rest of the
|
|
320
|
+
// engine — screenWidth, all 2D drawing, picking — works in
|
|
321
|
+
// LOGICAL units (see the WM_SIZE handler), so divide by the
|
|
322
|
+
// window's DPI scale before handing coordinates in. Without
|
|
323
|
+
// this, every hit-test on a scaled display (125%/150%…) lands
|
|
324
|
+
// scale× away from the pointer — buttons near the top-left
|
|
325
|
+
// almost work, everything else misses.
|
|
326
|
+
let x = (lparam.0 & 0xFFFF) as i16 as f64;
|
|
327
|
+
let y = ((lparam.0 >> 16) & 0xFFFF) as i16 as f64;
|
|
328
|
+
let scale = dpi_scale(hwnd);
|
|
329
|
+
if let Some(eng) = ENGINE.get_mut() {
|
|
330
|
+
if eng.input.cursor_disabled {
|
|
331
|
+
// FPS-style capture: accumulate the movement away from
|
|
332
|
+
// the window centre as a raw delta, then snap the OS
|
|
333
|
+
// cursor back to centre so look never hits the screen
|
|
334
|
+
// edge. (begin_frame consumes raw_delta when the cursor
|
|
335
|
+
// is disabled.) The recenter generates another
|
|
336
|
+
// WM_MOUSEMOVE at the centre with zero delta — no loop.
|
|
337
|
+
// Centre math and SetCursorPos stay PHYSICAL (that is
|
|
338
|
+
// what GetClientRect/ClientToScreen speak); only the
|
|
339
|
+
// delta handed to the engine converts to logical, so
|
|
340
|
+
// look sensitivity matches macOS point-deltas.
|
|
341
|
+
let mut rect = RECT::default();
|
|
342
|
+
let _ = GetClientRect(hwnd, &mut rect);
|
|
343
|
+
let cx = ((rect.right - rect.left) / 2) as f64;
|
|
344
|
+
let cy = ((rect.bottom - rect.top) / 2) as f64;
|
|
345
|
+
let dx = x - cx;
|
|
346
|
+
let dy = y - cy;
|
|
347
|
+
if dx != 0.0 || dy != 0.0 {
|
|
348
|
+
eng.input.accumulate_mouse_delta(dx / scale, dy / scale);
|
|
349
|
+
let mut pt = POINT { x: cx as i32, y: cy as i32 };
|
|
350
|
+
let _ = ClientToScreen(hwnd, &mut pt);
|
|
351
|
+
let _ = SetCursorPos(pt.x, pt.y);
|
|
352
|
+
}
|
|
353
|
+
} else {
|
|
354
|
+
eng.input.set_mouse_position(x / scale, y / scale);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
DefWindowProcW(hwnd, msg, wparam, lparam)
|
|
358
|
+
}
|
|
359
|
+
WM_LBUTTONDOWN => {
|
|
360
|
+
if let Some(eng) = ENGINE.get_mut() {
|
|
361
|
+
eng.input.set_mouse_button_down(0);
|
|
362
|
+
}
|
|
363
|
+
DefWindowProcW(hwnd, msg, wparam, lparam)
|
|
364
|
+
}
|
|
365
|
+
WM_LBUTTONUP => {
|
|
366
|
+
if let Some(eng) = ENGINE.get_mut() {
|
|
367
|
+
eng.input.set_mouse_button_up(0);
|
|
368
|
+
}
|
|
369
|
+
DefWindowProcW(hwnd, msg, wparam, lparam)
|
|
370
|
+
}
|
|
371
|
+
WM_RBUTTONDOWN => {
|
|
372
|
+
if let Some(eng) = ENGINE.get_mut() {
|
|
373
|
+
eng.input.set_mouse_button_down(1);
|
|
374
|
+
}
|
|
375
|
+
DefWindowProcW(hwnd, msg, wparam, lparam)
|
|
376
|
+
}
|
|
377
|
+
WM_RBUTTONUP => {
|
|
378
|
+
if let Some(eng) = ENGINE.get_mut() {
|
|
379
|
+
eng.input.set_mouse_button_up(1);
|
|
380
|
+
}
|
|
381
|
+
DefWindowProcW(hwnd, msg, wparam, lparam)
|
|
382
|
+
}
|
|
383
|
+
0x0005 /* WM_SIZE */ => {
|
|
384
|
+
// lParam carries the new client-area size in *physical*
|
|
385
|
+
// pixels (Per-Monitor-Aware-V2). Derive logical via
|
|
386
|
+
// current DPI so the rest of the engine sees the same
|
|
387
|
+
// logical/physical split macOS does.
|
|
388
|
+
let phys_w = (lparam.0 & 0xFFFF) as u32;
|
|
389
|
+
let phys_h = ((lparam.0 >> 16) & 0xFFFF) as u32;
|
|
390
|
+
if phys_w > 0 && phys_h > 0 {
|
|
391
|
+
if let Some(eng) = ENGINE.get_mut() {
|
|
392
|
+
if phys_w != eng.renderer.physical_width()
|
|
393
|
+
|| phys_h != eng.renderer.physical_height()
|
|
394
|
+
{
|
|
395
|
+
let scale = dpi_scale(hwnd);
|
|
396
|
+
let log_w = ((phys_w as f64) / scale).round() as u32;
|
|
397
|
+
let log_h = ((phys_h as f64) / scale).round() as u32;
|
|
398
|
+
eng.renderer.resize(phys_w, phys_h, log_w, log_h);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
DefWindowProcW(hwnd, msg, wparam, lparam)
|
|
403
|
+
}
|
|
404
|
+
0x02E0 /* WM_DPICHANGED */ => {
|
|
405
|
+
// The window dragged onto a different-DPI monitor.
|
|
406
|
+
// lParam holds a suggested RECT — Windows wants us to
|
|
407
|
+
// accept this geometry to keep the window's apparent
|
|
408
|
+
// size constant across the DPI change. The follow-up
|
|
409
|
+
// WM_SIZE handles the renderer resize.
|
|
410
|
+
let suggested = &*(lparam.0 as *const RECT);
|
|
411
|
+
let _ = SetWindowPos(
|
|
412
|
+
hwnd, None,
|
|
413
|
+
suggested.left, suggested.top,
|
|
414
|
+
suggested.right - suggested.left,
|
|
415
|
+
suggested.bottom - suggested.top,
|
|
416
|
+
SWP_NOZORDER | SWP_NOACTIVATE,
|
|
417
|
+
);
|
|
418
|
+
LRESULT(0)
|
|
419
|
+
}
|
|
420
|
+
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/// Returns (hwnd, physical_w, physical_h). Caller's `width`/`height`
|
|
425
|
+
/// are *logical* — on a HiDPI monitor with scale 2.0 the window
|
|
426
|
+
/// will appear logically the right size while its client area is
|
|
427
|
+
/// scaled-up physical pixels for the renderer to fill.
|
|
428
|
+
pub fn create_window(width: f64, height: f64, title: &str) -> (HWND, u32, u32) {
|
|
429
|
+
unsafe {
|
|
430
|
+
// Per-Monitor-Aware-V2: the window's DPI tracks the monitor
|
|
431
|
+
// it's currently on, and Windows fires WM_DPICHANGED when
|
|
432
|
+
// it moves between monitors of different DPI. Without this
|
|
433
|
+
// call, Win32 silently virtualises us to 96 DPI on HiDPI
|
|
434
|
+
// displays — a 4K monitor would render at ~2560×1440.
|
|
435
|
+
// Safe to call early; ignored on pre-Win10 1703.
|
|
436
|
+
let _ = SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
|
437
|
+
|
|
438
|
+
let hmodule = GetModuleHandleW(None).unwrap();
|
|
439
|
+
let hinstance: HINSTANCE = hmodule.into();
|
|
440
|
+
let class_name = w!("BloomWindowClass");
|
|
441
|
+
|
|
442
|
+
let wc = WNDCLASSEXW {
|
|
443
|
+
cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
|
|
444
|
+
style: CS_HREDRAW | CS_VREDRAW,
|
|
445
|
+
lpfnWndProc: Some(wndproc),
|
|
446
|
+
hInstance: hinstance,
|
|
447
|
+
lpszClassName: class_name,
|
|
448
|
+
hCursor: LoadCursorW(None, IDC_ARROW).unwrap(),
|
|
449
|
+
..Default::default()
|
|
450
|
+
};
|
|
451
|
+
RegisterClassExW(&wc);
|
|
452
|
+
|
|
453
|
+
let title_wide: Vec<u16> = title.encode_utf16().chain(std::iter::once(0)).collect();
|
|
454
|
+
|
|
455
|
+
// Initial window size in physical pixels. We don't have a
|
|
456
|
+
// window handle yet, so use the system DPI — close enough,
|
|
457
|
+
// and WM_DPICHANGED will resize on the first move if the
|
|
458
|
+
// user lands on a different-DPI monitor.
|
|
459
|
+
let system_dpi = GetDpiForSystem().max(96);
|
|
460
|
+
let scale = system_dpi as f64 / 96.0;
|
|
461
|
+
let phys_w = (width * scale).round() as i32;
|
|
462
|
+
let phys_h = (height * scale).round() as i32;
|
|
463
|
+
|
|
464
|
+
let hwnd = CreateWindowExW(
|
|
465
|
+
WINDOW_EX_STYLE::default(),
|
|
466
|
+
class_name,
|
|
467
|
+
PCWSTR(title_wide.as_ptr()),
|
|
468
|
+
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
|
|
469
|
+
CW_USEDEFAULT, CW_USEDEFAULT,
|
|
470
|
+
phys_w, phys_h,
|
|
471
|
+
None, None, Some(&hinstance), None,
|
|
472
|
+
).unwrap();
|
|
473
|
+
|
|
474
|
+
ShowWindow(hwnd, SW_SHOW);
|
|
475
|
+
HWND_GLOBAL = Some(hwnd);
|
|
476
|
+
|
|
477
|
+
// After the window exists, query the actual client-area
|
|
478
|
+
// size. Includes whatever DWM trimmed for borders and
|
|
479
|
+
// reflects this monitor's DPI rather than the system one.
|
|
480
|
+
let mut rect = RECT::default();
|
|
481
|
+
let _ = GetClientRect(hwnd, &mut rect);
|
|
482
|
+
let actual_w = (rect.right - rect.left).max(1) as u32;
|
|
483
|
+
let actual_h = (rect.bottom - rect.top).max(1) as u32;
|
|
484
|
+
(hwnd, actual_w, actual_h)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/// Current per-monitor DPI scale for this window (1.0 on a 96-DPI
|
|
489
|
+
/// monitor, 2.0 on a Retina-class 192-DPI monitor, etc.). Falls
|
|
490
|
+
/// back to 1.0 if the API is missing (pre-Win10 1607).
|
|
491
|
+
pub fn dpi_scale(hwnd: HWND) -> f64 {
|
|
492
|
+
unsafe {
|
|
493
|
+
let dpi = GetDpiForWindow(hwnd);
|
|
494
|
+
if dpi == 0 { 1.0 } else { dpi as f64 / 96.0 }
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
pub fn poll_events() {
|
|
499
|
+
unsafe {
|
|
500
|
+
let mut msg = MSG::default();
|
|
501
|
+
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).into() {
|
|
502
|
+
TranslateMessage(&msg);
|
|
503
|
+
DispatchMessageW(&msg);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ---- Embedded mode (Perry UI BloomView host window) ----
|
|
509
|
+
//
|
|
510
|
+
// When Bloom renders into a Perry UI child window, that window already has
|
|
511
|
+
// Perry's own WNDPROC. We classic-subclass it so Bloom sees the WM_SIZE /
|
|
512
|
+
// keyboard / mouse it needs (its own `wndproc` above never runs for a
|
|
513
|
+
// foreign-class window), then chain to the original proc. The host's
|
|
514
|
+
// message loop dispatches these — Bloom never pumps in embedded mode.
|
|
515
|
+
static mut EMBED_ORIG_WNDPROC: isize = 0;
|
|
516
|
+
|
|
517
|
+
unsafe extern "system" fn embedded_wndproc(
|
|
518
|
+
hwnd: HWND,
|
|
519
|
+
msg: u32,
|
|
520
|
+
wparam: WPARAM,
|
|
521
|
+
lparam: LPARAM,
|
|
522
|
+
) -> LRESULT {
|
|
523
|
+
match msg {
|
|
524
|
+
0x0005 /* WM_SIZE */ => {
|
|
525
|
+
let phys_w = (lparam.0 & 0xFFFF) as u32;
|
|
526
|
+
let phys_h = ((lparam.0 >> 16) & 0xFFFF) as u32;
|
|
527
|
+
if phys_w > 0 && phys_h > 0 {
|
|
528
|
+
if let Some(eng) = ENGINE.get_mut() {
|
|
529
|
+
if phys_w != eng.renderer.physical_width()
|
|
530
|
+
|| phys_h != eng.renderer.physical_height()
|
|
531
|
+
{
|
|
532
|
+
let scale = dpi_scale(hwnd);
|
|
533
|
+
let log_w = ((phys_w as f64) / scale).round() as u32;
|
|
534
|
+
let log_h = ((phys_h as f64) / scale).round() as u32;
|
|
535
|
+
eng.renderer.resize(phys_w, phys_h, log_w, log_h);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
WM_KEYDOWN => {
|
|
541
|
+
let k = map_keycode(resolve_modifier_vk(wparam.0 as u32, lparam.0));
|
|
542
|
+
if k > 0 {
|
|
543
|
+
if let Some(eng) = ENGINE.get_mut() {
|
|
544
|
+
eng.input.set_key_down(k);
|
|
545
|
+
if (lparam.0 >> 30) & 1 == 1 { eng.input.queue_key_repeat(k); }
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
WM_KEYUP => {
|
|
550
|
+
let k = map_keycode(resolve_modifier_vk(wparam.0 as u32, lparam.0));
|
|
551
|
+
if k > 0 { if let Some(eng) = ENGINE.get_mut() { eng.input.set_key_up(k); } }
|
|
552
|
+
}
|
|
553
|
+
WM_MOUSEMOVE => {
|
|
554
|
+
// Physical -> logical, same as the standalone window proc.
|
|
555
|
+
let scale = dpi_scale(hwnd);
|
|
556
|
+
let x = ((lparam.0 & 0xFFFF) as i16 as f64) / scale;
|
|
557
|
+
let y = (((lparam.0 >> 16) & 0xFFFF) as i16 as f64) / scale;
|
|
558
|
+
if let Some(eng) = ENGINE.get_mut() { eng.input.set_mouse_position(x, y); }
|
|
559
|
+
}
|
|
560
|
+
WM_LBUTTONDOWN => { if let Some(eng) = ENGINE.get_mut() { eng.input.set_mouse_button_down(0); } }
|
|
561
|
+
WM_LBUTTONUP => { if let Some(eng) = ENGINE.get_mut() { eng.input.set_mouse_button_up(0); } }
|
|
562
|
+
WM_RBUTTONDOWN => { if let Some(eng) = ENGINE.get_mut() { eng.input.set_mouse_button_down(1); } }
|
|
563
|
+
WM_RBUTTONUP => { if let Some(eng) = ENGINE.get_mut() { eng.input.set_mouse_button_up(1); } }
|
|
564
|
+
_ => {}
|
|
565
|
+
}
|
|
566
|
+
let orig: WNDPROC = std::mem::transmute(EMBED_ORIG_WNDPROC);
|
|
567
|
+
CallWindowProcW(orig, hwnd, msg, wparam, lparam)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/// Subclass a host-provided child window so Bloom receives resize/input.
|
|
571
|
+
pub unsafe fn attach_subclass(hwnd: HWND) {
|
|
572
|
+
let prev = SetWindowLongPtrW(hwnd, GWLP_WNDPROC, embedded_wndproc as usize as isize);
|
|
573
|
+
EMBED_ORIG_WNDPROC = prev;
|
|
574
|
+
HWND_GLOBAL = Some(hwnd);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
#[no_mangle]
|
|
579
|
+
pub extern "C" fn bloom_init_window(width: f64, height: f64, title_ptr: *const u8, fullscreen: f64) {
|
|
580
|
+
let title = str_from_header(title_ptr);
|
|
581
|
+
|
|
582
|
+
#[cfg(windows)]
|
|
583
|
+
{
|
|
584
|
+
crash_report::install();
|
|
585
|
+
let (hwnd, phys_w, phys_h) = win32::create_window(width, height, title);
|
|
586
|
+
unsafe { init_engine_for_hwnd(hwnd, width as u32, height as u32, phys_w, phys_h); }
|
|
587
|
+
if fullscreen != 0.0 {
|
|
588
|
+
win32::set_fullscreen(true);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
#[cfg(not(windows))]
|
|
593
|
+
{
|
|
594
|
+
let _ = (width, height, title, fullscreen);
|
|
595
|
+
panic!("bloom-windows can only run on Windows");
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/// Build the wgpu surface + engine on an existing HWND (top-level window or
|
|
600
|
+
/// host-provided child). Shared by `bloom_init_window` (Bloom owns the window)
|
|
601
|
+
/// and `bloom_attach_hwnd` (a Perry UI `BloomView` child window). `logical_*`
|
|
602
|
+
/// are DPI-independent sizes; `phys_*` are the surface's physical client size.
|
|
603
|
+
#[cfg(windows)]
|
|
604
|
+
unsafe fn init_engine_for_hwnd(
|
|
605
|
+
hwnd: windows::Win32::Foundation::HWND,
|
|
606
|
+
logical_w: u32,
|
|
607
|
+
logical_h: u32,
|
|
608
|
+
phys_w: u32,
|
|
609
|
+
phys_h: u32,
|
|
610
|
+
) {
|
|
611
|
+
// Compile shaders with DXC, not FXC.
|
|
612
|
+
//
|
|
613
|
+
// This is not a nicety. wgpu's DX12 backend reports the adapter's
|
|
614
|
+
// shader model as min(device, compiler), and FXC — its default — caps
|
|
615
|
+
// that at 5.1. Hardware ray query requires 6.5 (wgpu-hal
|
|
616
|
+
// dx12/adapter.rs: `supports_ray_tracing`), so with FXC,
|
|
617
|
+
// EXPERIMENTAL_RAY_QUERY is never exposed on DX12 on *any* GPU, no
|
|
618
|
+
// matter how capable. Lumen then silently takes its software path and
|
|
619
|
+
// the frame quietly loses its hardware-traced GI. That is what the
|
|
620
|
+
// `ray_query=false` in the boot line has been telling us.
|
|
621
|
+
//
|
|
622
|
+
// DXC is loaded at runtime from `dxcompiler.dll` + `dxil.dll`. wgpu's
|
|
623
|
+
// `static-dxc` feature would avoid the DLLs but needs MSVC's ATL,
|
|
624
|
+
// which is not part of a default toolchain install. Both DLLs ship
|
|
625
|
+
// with the Windows SDK; `tools/fetch-dxc.ps1` copies them next to the
|
|
626
|
+
// binary. If they are missing, wgpu falls back to FXC on its own — we
|
|
627
|
+
// lose HW ray query, exactly as before, and nothing else breaks.
|
|
628
|
+
let mut backend_options = wgpu::BackendOptions::default();
|
|
629
|
+
backend_options.dx12.shader_compiler = wgpu::Dx12Compiler::DynamicDxc {
|
|
630
|
+
dxc_path: String::from("dxcompiler.dll"),
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
|
634
|
+
backends: wgpu::Backends::DX12 | wgpu::Backends::VULKAN,
|
|
635
|
+
backend_options,
|
|
636
|
+
..wgpu::InstanceDescriptor::new_without_display_handle()
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
let surface = unsafe {
|
|
640
|
+
let mut handle = raw_window_handle::Win32WindowHandle::new(
|
|
641
|
+
std::num::NonZeroIsize::new(hwnd.0 as isize).unwrap()
|
|
642
|
+
);
|
|
643
|
+
let raw = raw_window_handle::RawWindowHandle::Win32(handle);
|
|
644
|
+
instance.create_surface_unsafe(wgpu::SurfaceTargetUnsafe::RawHandle {
|
|
645
|
+
raw_display_handle: Some(raw_window_handle::RawDisplayHandle::Windows(
|
|
646
|
+
raw_window_handle::WindowsDisplayHandle::new()
|
|
647
|
+
)),
|
|
648
|
+
raw_window_handle: raw,
|
|
649
|
+
}).expect("Failed to create surface")
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
// Pick the adapter that can actually trace rays.
|
|
653
|
+
//
|
|
654
|
+
// We used to take whatever `request_adapter` handed back, which on
|
|
655
|
+
// Windows means DX12 — and wgpu's DX12 backend only reports
|
|
656
|
+
// EXPERIMENTAL_RAY_QUERY when the driver advertises D3D12 raytracing
|
|
657
|
+
// tier 1.1. The same GPU under Vulkan can expose VK_KHR_ray_query
|
|
658
|
+
// when DX12 does not. The result was silent: Lumen's hardware trace
|
|
659
|
+
// was never selected, the software SDF path ran instead, and nobody
|
|
660
|
+
// saw a reason why. So enumerate the candidates, say out loud what
|
|
661
|
+
// each one offers, and prefer one that supports ray query.
|
|
662
|
+
//
|
|
663
|
+
// BLOOM_FORCE_SW_GI keeps its meaning: it also stops us from picking
|
|
664
|
+
// a backend *for* ray tracing, so the software path can be tested on
|
|
665
|
+
// hardware that would otherwise take the fast route.
|
|
666
|
+
let want_rt = !std::env::var("BLOOM_FORCE_SW_GI")
|
|
667
|
+
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
|
668
|
+
.unwrap_or(false);
|
|
669
|
+
|
|
670
|
+
let candidates = pollster_block_on(
|
|
671
|
+
instance.enumerate_adapters(wgpu::Backends::DX12 | wgpu::Backends::VULKAN),
|
|
672
|
+
);
|
|
673
|
+
let mut rt_adapter: Option<wgpu::Adapter> = None;
|
|
674
|
+
for cand in candidates {
|
|
675
|
+
let info = cand.get_info();
|
|
676
|
+
let surf_ok = cand.is_surface_supported(&surface);
|
|
677
|
+
let has_rt = cand
|
|
678
|
+
.features()
|
|
679
|
+
.contains(wgpu::Features::EXPERIMENTAL_RAY_QUERY);
|
|
680
|
+
eprintln!(
|
|
681
|
+
"bloom: candidate '{}' ({:?}), ray_query={}, surface={}",
|
|
682
|
+
info.name, info.backend, has_rt, surf_ok,
|
|
683
|
+
);
|
|
684
|
+
if want_rt && has_rt && surf_ok && rt_adapter.is_none() {
|
|
685
|
+
rt_adapter = Some(cand);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
let adapter = match rt_adapter {
|
|
690
|
+
Some(a) => a,
|
|
691
|
+
None => pollster_block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
|
|
692
|
+
compatible_surface: Some(&surface),
|
|
693
|
+
power_preference: wgpu::PowerPreference::HighPerformance,
|
|
694
|
+
..Default::default()
|
|
695
|
+
}))
|
|
696
|
+
.expect("No adapter found"),
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
{
|
|
700
|
+
// One line of boot truth: which GPU we got and whether the
|
|
701
|
+
// features that silently reshape the frame (HW ray query for
|
|
702
|
+
// GI, timestamps for the profiler) are available on it.
|
|
703
|
+
let info = adapter.get_info();
|
|
704
|
+
eprintln!(
|
|
705
|
+
"bloom: adapter '{}' ({:?}), ray_query={}, timestamps={}, tex_arrays={}",
|
|
706
|
+
info.name,
|
|
707
|
+
info.backend,
|
|
708
|
+
adapter.features().contains(wgpu::Features::EXPERIMENTAL_RAY_QUERY),
|
|
709
|
+
adapter.features().contains(wgpu::Features::TIMESTAMP_QUERY),
|
|
710
|
+
adapter.features().contains(
|
|
711
|
+
wgpu::Features::TEXTURE_BINDING_ARRAY
|
|
712
|
+
| wgpu::Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING
|
|
713
|
+
),
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Ticket 007b: HW ray-query via DXR 1.1 / VK_KHR_ray_query.
|
|
718
|
+
let supported = adapter.features();
|
|
719
|
+
// 2026-07-16: HW ray query is OPT-IN now, not granted-by-default.
|
|
720
|
+
// Measured on the shooter (760M, PT off): merely granting the
|
|
721
|
+
// feature flips SSGI/WSRC to the HW trace and registers every
|
|
722
|
+
// skinned draw for per-frame pre-skin + BLAS builds — costing
|
|
723
|
+
// +20 ms/frame (14.4 vs 20.3 fps) AND losing the sun's cast
|
|
724
|
+
// shadows (screenshot-confirmed twice). Until the HW path is
|
|
725
|
+
// both cheaper and visually correct, nobody should get it by
|
|
726
|
+
// accident just because dxcompiler.dll sits beside the exe.
|
|
727
|
+
// Opt in with BLOOM_HW_GI=1; --pt / BLOOM_PT keep working (the
|
|
728
|
+
// path tracer needs the feature at device creation).
|
|
729
|
+
let want_hw = std::env::var("BLOOM_HW_GI")
|
|
730
|
+
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
|
731
|
+
.unwrap_or(false)
|
|
732
|
+
|| std::env::var("BLOOM_PT").is_ok()
|
|
733
|
+
|| std::env::args().any(|a| a == "--pt");
|
|
734
|
+
let force_sw_gi = !want_hw
|
|
735
|
+
|| std::env::var("BLOOM_FORCE_SW_GI")
|
|
736
|
+
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
|
737
|
+
.unwrap_or(false);
|
|
738
|
+
let rt_mask = wgpu::Features::EXPERIMENTAL_RAY_QUERY;
|
|
739
|
+
eprintln!("bloom: hw-gi opt-in: want_hw={want_hw} force_sw_gi={force_sw_gi}");
|
|
740
|
+
let mut required_features = wgpu::Features::empty();
|
|
741
|
+
// Ticket 011: request TIMESTAMP_QUERY when supported so the profiler
|
|
742
|
+
// can record GPU timings. Optional — profiler falls back to CPU-only
|
|
743
|
+
// when the adapter doesn't grant it.
|
|
744
|
+
if supported.contains(wgpu::Features::TIMESTAMP_QUERY) {
|
|
745
|
+
required_features |= wgpu::Features::TIMESTAMP_QUERY;
|
|
746
|
+
}
|
|
747
|
+
// Cooked BC7 textures (bloom-cook) upload compressed when the
|
|
748
|
+
// adapter has BC support; without it they CPU-decode at load.
|
|
749
|
+
if supported.contains(wgpu::Features::TEXTURE_COMPRESSION_BC) {
|
|
750
|
+
required_features |= wgpu::Features::TEXTURE_COMPRESSION_BC;
|
|
751
|
+
}
|
|
752
|
+
if !force_sw_gi && supported.contains(rt_mask) {
|
|
753
|
+
required_features |= rt_mask;
|
|
754
|
+
}
|
|
755
|
+
// PT-2: texture binding array + non-uniform indexing for textured
|
|
756
|
+
// path-trace hit shading. Both or neither (the kernel indexes the
|
|
757
|
+
// array with a per-thread material id).
|
|
758
|
+
let pt_tex_mask = wgpu::Features::TEXTURE_BINDING_ARRAY
|
|
759
|
+
| wgpu::Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING;
|
|
760
|
+
if supported.contains(pt_tex_mask) {
|
|
761
|
+
required_features |= pt_tex_mask;
|
|
762
|
+
}
|
|
763
|
+
let experimental_features = if required_features.intersects(rt_mask) {
|
|
764
|
+
unsafe { wgpu::ExperimentalFeatures::enabled() }
|
|
765
|
+
} else {
|
|
766
|
+
wgpu::ExperimentalFeatures::disabled()
|
|
767
|
+
};
|
|
768
|
+
let mut required_limits = wgpu::Limits::default();
|
|
769
|
+
// Phase 1c: the material ABI declares 5 bind groups (PerFrame,
|
|
770
|
+
// PerView, PerMaterial, PerDraw, SceneInputs). wgpu's default
|
|
771
|
+
// limit is 4. Metal / Vulkan / D3D12 support at least 7, so 5 is
|
|
772
|
+
// safely within every real backend's capabilities.
|
|
773
|
+
required_limits.max_bind_groups = 5;
|
|
774
|
+
// The refractive/translucent material profile binds up to 19
|
|
775
|
+
// sampled textures in the fragment stage (5 material maps + env/
|
|
776
|
+
// BRDF/3 shadow cascades/env-diffuse + planar reflection + 3 texture
|
|
777
|
+
// arrays + the group-4 scene_color/scene_depth/impulse/motion inputs).
|
|
778
|
+
// wgpu's default is 16. Raise to whatever the adapter actually
|
|
779
|
+
// supports — every real D3D12/Vulkan/Metal GPU exposes ≥128 — so
|
|
780
|
+
// opaque/transparent materials are unaffected and refractive ones link.
|
|
781
|
+
let adapter_limits = adapter.limits();
|
|
782
|
+
required_limits.max_sampled_textures_per_shader_stage =
|
|
783
|
+
adapter_limits.max_sampled_textures_per_shader_stage;
|
|
784
|
+
required_limits.max_samplers_per_shader_stage =
|
|
785
|
+
adapter_limits.max_samplers_per_shader_stage;
|
|
786
|
+
// PT-2: binding arrays have their own element budget, default 0.
|
|
787
|
+
// Take whatever the adapter offers; the renderer checks the
|
|
788
|
+
// granted value against its fixed array size before compiling
|
|
789
|
+
// the textured kernel variant.
|
|
790
|
+
if required_features.contains(pt_tex_mask) {
|
|
791
|
+
required_limits.max_binding_array_elements_per_shader_stage =
|
|
792
|
+
adapter_limits.max_binding_array_elements_per_shader_stage;
|
|
793
|
+
}
|
|
794
|
+
// PT-4: the path-trace kernel binds 9 storage buffers (accum +
|
|
795
|
+
// moments + reservoir ping-pongs on top of instance/geo data);
|
|
796
|
+
// the wgpu default limit is 8.
|
|
797
|
+
required_limits.max_storage_buffers_per_shader_stage = required_limits
|
|
798
|
+
.max_storage_buffers_per_shader_stage
|
|
799
|
+
.max(adapter_limits.max_storage_buffers_per_shader_stage.min(16));
|
|
800
|
+
if required_features.intersects(rt_mask) {
|
|
801
|
+
required_limits = required_limits
|
|
802
|
+
.using_minimum_supported_acceleration_structure_values();
|
|
803
|
+
}
|
|
804
|
+
let (device, queue) = pollster_block_on(adapter.request_device(
|
|
805
|
+
&wgpu::DeviceDescriptor {
|
|
806
|
+
label: Some("bloom_device"),
|
|
807
|
+
required_features,
|
|
808
|
+
required_limits,
|
|
809
|
+
experimental_features,
|
|
810
|
+
..Default::default()
|
|
811
|
+
},
|
|
812
|
+
)).expect("Failed to create device");
|
|
813
|
+
|
|
814
|
+
let surface_caps = surface.get_capabilities(&adapter);
|
|
815
|
+
let format = surface_caps.formats[0];
|
|
816
|
+
// Surface is configured at the *physical* client-area size we
|
|
817
|
+
// got back from create_window; Renderer::new takes the
|
|
818
|
+
// caller's logical size separately so screenWidth() etc. keep
|
|
819
|
+
// returning DPI-independent numbers.
|
|
820
|
+
let surface_config = wgpu::SurfaceConfiguration {
|
|
821
|
+
// COPY_SRC: bloom_take_screenshot reads the swapchain back;
|
|
822
|
+
// without it the readback copy is a swallowed validation
|
|
823
|
+
// error and screenshots silently produce nothing on Windows.
|
|
824
|
+
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
|
825
|
+
| wgpu::TextureUsages::COPY_SRC,
|
|
826
|
+
format,
|
|
827
|
+
width: phys_w,
|
|
828
|
+
height: phys_h,
|
|
829
|
+
present_mode: wgpu::PresentMode::Fifo,
|
|
830
|
+
alpha_mode: surface_caps.alpha_modes[0],
|
|
831
|
+
view_formats: vec![],
|
|
832
|
+
desired_maximum_frame_latency: 2,
|
|
833
|
+
};
|
|
834
|
+
surface.configure(&device, &surface_config);
|
|
835
|
+
|
|
836
|
+
// `Renderer::new` routes initial target creation through `resize`
|
|
837
|
+
// itself, so the construction-time targets already honour
|
|
838
|
+
// render_scale — no explicit resize needed here (it used to live
|
|
839
|
+
// here, which left every non-Windows host with the bug).
|
|
840
|
+
let renderer = Renderer::new(device, queue, surface, surface_config, logical_w, logical_h);
|
|
841
|
+
let _ = ENGINE.set(EngineState::new(renderer));
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/// Attach the engine to a host-owned `HWND` instead of creating its own
|
|
845
|
+
/// top-level window (PerryTS/perry#5519). `handle` is the child `HWND`
|
|
846
|
+
/// the host (Perry UI's `BloomView`) owns; `width`/`height` are its
|
|
847
|
+
/// client size in physical pixels. Returns 1.0 on success, 0.0 on a
|
|
848
|
+
/// null/invalid handle or surface bring-up failure. Idempotent once
|
|
849
|
+
/// attached.
|
|
850
|
+
#[no_mangle]
|
|
851
|
+
pub extern "C" fn bloom_attach_native(handle: i64, width: f64, height: f64) -> f64 {
|
|
852
|
+
if handle == 0 {
|
|
853
|
+
return 0.0;
|
|
854
|
+
}
|
|
855
|
+
if unsafe { ENGINE.get() }.is_some() {
|
|
856
|
+
return 1.0;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
#[cfg(windows)]
|
|
860
|
+
{
|
|
861
|
+
let Some(hwnd_nz) = std::num::NonZeroIsize::new(handle as isize) else {
|
|
862
|
+
return 0.0;
|
|
863
|
+
};
|
|
864
|
+
let target = {
|
|
865
|
+
let h = raw_window_handle::Win32WindowHandle::new(hwnd_nz);
|
|
866
|
+
wgpu::SurfaceTargetUnsafe::RawHandle {
|
|
867
|
+
raw_display_handle: Some(raw_window_handle::RawDisplayHandle::Windows(
|
|
868
|
+
raw_window_handle::WindowsDisplayHandle::new(),
|
|
869
|
+
)),
|
|
870
|
+
raw_window_handle: raw_window_handle::RawWindowHandle::Win32(h),
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
match unsafe {
|
|
874
|
+
bloom_shared::attach::attach_engine(
|
|
875
|
+
target,
|
|
876
|
+
bloom_shared::attach::AttachParams {
|
|
877
|
+
backends: wgpu::Backends::DX12 | wgpu::Backends::VULKAN,
|
|
878
|
+
logical_w: (width as u32).max(1),
|
|
879
|
+
logical_h: (height as u32).max(1),
|
|
880
|
+
physical_w: (width as u32).max(1),
|
|
881
|
+
physical_h: (height as u32).max(1),
|
|
882
|
+
format: bloom_shared::attach::FormatPreference::First,
|
|
883
|
+
},
|
|
884
|
+
)
|
|
885
|
+
} {
|
|
886
|
+
Ok(es) => {
|
|
887
|
+
unsafe {
|
|
888
|
+
let _ = ENGINE.set(es);
|
|
889
|
+
}
|
|
890
|
+
1.0
|
|
891
|
+
}
|
|
892
|
+
Err(_) => 0.0,
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
#[cfg(not(windows))]
|
|
897
|
+
{
|
|
898
|
+
let _ = (width, height);
|
|
899
|
+
0.0
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/// Request a clean exit: makes `bloom_window_should_close` report true so the
|
|
904
|
+
/// game loop (blocking `while` or `runGame`) falls out on its next check.
|
|
905
|
+
/// Was an empty stub — callable from a game's quit path since SH-054 turned
|
|
906
|
+
/// the shooter's `break`-based exits into closeWindow() calls.
|
|
907
|
+
#[no_mangle]
|
|
908
|
+
pub extern "C" fn bloom_close_window() {
|
|
909
|
+
engine().should_close = true;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/// Attach the engine to a host-provided child window (a Perry UI `BloomView`).
|
|
913
|
+
/// `hwnd_bits` is the raw HWND value as an integer (from `bloomViewGetHwnd`).
|
|
914
|
+
/// `width`/`height` are the logical viewport size. The engine builds its wgpu
|
|
915
|
+
/// surface on this window and subclasses it for resize/input; the host drives
|
|
916
|
+
/// frames via `bloom_begin_drawing` / `bloom_end_drawing`.
|
|
917
|
+
#[no_mangle]
|
|
918
|
+
pub extern "C" fn bloom_attach_hwnd(hwnd_bits: f64, width: f64, height: f64) {
|
|
919
|
+
#[cfg(windows)]
|
|
920
|
+
unsafe {
|
|
921
|
+
use windows::Win32::Foundation::{HWND, RECT};
|
|
922
|
+
use windows::Win32::UI::WindowsAndMessaging::GetClientRect;
|
|
923
|
+
let hwnd = HWND(hwnd_bits as i64 as isize as *mut core::ffi::c_void);
|
|
924
|
+
let mut rect = RECT::default();
|
|
925
|
+
let _ = GetClientRect(hwnd, &mut rect);
|
|
926
|
+
let phys_w = (rect.right - rect.left).max(1) as u32;
|
|
927
|
+
let phys_h = (rect.bottom - rect.top).max(1) as u32;
|
|
928
|
+
if ENGINE.get().is_none() {
|
|
929
|
+
init_engine_for_hwnd(hwnd, width as u32, height as u32, phys_w, phys_h);
|
|
930
|
+
}
|
|
931
|
+
EMBEDDED = true;
|
|
932
|
+
win32::attach_subclass(hwnd);
|
|
933
|
+
}
|
|
934
|
+
#[cfg(not(windows))]
|
|
935
|
+
{ let _ = (hwnd_bits, width, height); }
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/// Resize the engine's surface. `phys_*` are physical pixels, `log_*` logical.
|
|
939
|
+
/// Embedded `BloomView`s resize automatically via the subclassed WM_SIZE; this
|
|
940
|
+
/// is exposed for hosts that need to drive the size explicitly.
|
|
941
|
+
#[no_mangle]
|
|
942
|
+
pub extern "C" fn bloom_resize(phys_w: f64, phys_h: f64, log_w: f64, log_h: f64) {
|
|
943
|
+
#[cfg(windows)]
|
|
944
|
+
unsafe {
|
|
945
|
+
if let Some(eng) = ENGINE.get_mut() {
|
|
946
|
+
eng.renderer.resize(phys_w as u32, phys_h as u32, log_w as u32, log_h as u32);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
#[cfg(not(windows))]
|
|
950
|
+
{ let _ = (phys_w, phys_h, log_w, log_h); }
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
#[no_mangle]
|
|
954
|
+
pub extern "C" fn bloom_window_should_close() -> f64 {
|
|
955
|
+
#[cfg(windows)]
|
|
956
|
+
unsafe { if EMBEDDED { return 0.0; } }
|
|
957
|
+
if engine().should_close { 1.0 } else { 0.0 }
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
#[cfg(windows)]
|
|
961
|
+
fn poll_xinput_gamepad() {
|
|
962
|
+
use windows::Win32::UI::Input::XboxController::*;
|
|
963
|
+
let eng = engine();
|
|
964
|
+
let mut state = XINPUT_STATE::default();
|
|
965
|
+
let result = unsafe { XInputGetState(0, &mut state) };
|
|
966
|
+
if result == 0 {
|
|
967
|
+
// ERROR_SUCCESS
|
|
968
|
+
eng.input.gamepad_available = true;
|
|
969
|
+
let gp = &state.Gamepad;
|
|
970
|
+
|
|
971
|
+
// Axes: left stick X/Y, right stick X/Y, triggers
|
|
972
|
+
let normalize = |v: i16| -> f32 {
|
|
973
|
+
if v > 0 { v as f32 / 32767.0 } else { v as f32 / 32768.0 }
|
|
974
|
+
};
|
|
975
|
+
eng.input.set_gamepad_axis(0, normalize(gp.sThumbLX));
|
|
976
|
+
eng.input.set_gamepad_axis(1, -normalize(gp.sThumbLY)); // invert Y
|
|
977
|
+
eng.input.set_gamepad_axis(2, normalize(gp.sThumbRX));
|
|
978
|
+
eng.input.set_gamepad_axis(3, -normalize(gp.sThumbRY));
|
|
979
|
+
eng.input.set_gamepad_axis(4, gp.bLeftTrigger as f32 / 255.0);
|
|
980
|
+
eng.input.set_gamepad_axis(5, gp.bRightTrigger as f32 / 255.0);
|
|
981
|
+
eng.input.gamepad_axis_count = 6;
|
|
982
|
+
|
|
983
|
+
// Buttons
|
|
984
|
+
let buttons = gp.wButtons;
|
|
985
|
+
let mappings: &[(u16, usize)] = &[
|
|
986
|
+
(0x1000, 0), // A
|
|
987
|
+
(0x2000, 1), // B
|
|
988
|
+
(0x4000, 2), // X
|
|
989
|
+
(0x8000, 3), // Y
|
|
990
|
+
(0x0100, 4), // Left bumper
|
|
991
|
+
(0x0200, 5), // Right bumper
|
|
992
|
+
(0x0020, 6), // Back/Select
|
|
993
|
+
(0x0010, 7), // Start
|
|
994
|
+
(0x0040, 8), // Left stick press
|
|
995
|
+
(0x0080, 9), // Right stick press
|
|
996
|
+
(0x0001, 10), // DPad Up
|
|
997
|
+
(0x0002, 11), // DPad Down
|
|
998
|
+
(0x0004, 12), // DPad Left
|
|
999
|
+
(0x0008, 13), // DPad Right
|
|
1000
|
+
];
|
|
1001
|
+
for &(mask, idx) in mappings {
|
|
1002
|
+
if buttons.0 & mask != 0 {
|
|
1003
|
+
eng.input.set_gamepad_button_down(idx);
|
|
1004
|
+
} else {
|
|
1005
|
+
eng.input.set_gamepad_button_up(idx);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// EN-031 — rumble. The FFI writes (low, high, seconds) into shared
|
|
1010
|
+
// input state; we own the motors and the countdown. Only push a new
|
|
1011
|
+
// XInputSetState when the commanded level actually changes, since the
|
|
1012
|
+
// call goes out over USB/BT and doing it every frame is wasteful.
|
|
1013
|
+
let dt = eng.delta_time as f32;
|
|
1014
|
+
let (lo, hi, mut left) = (eng.input.rumble[0], eng.input.rumble[1], eng.input.rumble[2]);
|
|
1015
|
+
let (want_lo, want_hi) = if left > 0.0 { (lo, hi) } else { (0.0, 0.0) };
|
|
1016
|
+
unsafe {
|
|
1017
|
+
if (want_lo, want_hi) != LAST_RUMBLE {
|
|
1018
|
+
let mut vib = XINPUT_VIBRATION {
|
|
1019
|
+
wLeftMotorSpeed: (want_lo * 65535.0) as u16,
|
|
1020
|
+
wRightMotorSpeed: (want_hi * 65535.0) as u16,
|
|
1021
|
+
};
|
|
1022
|
+
let _ = XInputSetState(0, &mut vib);
|
|
1023
|
+
LAST_RUMBLE = (want_lo, want_hi);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
if left > 0.0 {
|
|
1027
|
+
left = (left - dt).max(0.0);
|
|
1028
|
+
eng.input.rumble[2] = left;
|
|
1029
|
+
}
|
|
1030
|
+
} else {
|
|
1031
|
+
eng.input.gamepad_available = false;
|
|
1032
|
+
// Pad unplugged mid-rumble: forget the commanded level so a
|
|
1033
|
+
// reconnecting pad doesn't inherit a stale "still buzzing" state.
|
|
1034
|
+
unsafe { LAST_RUMBLE = (0.0, 0.0); }
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/// Last vibration level actually pushed to the pad, so we only call
|
|
1039
|
+
/// XInputSetState on change.
|
|
1040
|
+
#[cfg(windows)]
|
|
1041
|
+
static mut LAST_RUMBLE: (f32, f32) = (0.0, 0.0);
|
|
1042
|
+
|
|
1043
|
+
#[no_mangle]
|
|
1044
|
+
pub extern "C" fn bloom_begin_drawing() {
|
|
1045
|
+
#[cfg(windows)]
|
|
1046
|
+
{
|
|
1047
|
+
// In embedded mode the host (Perry UI) owns the message loop and
|
|
1048
|
+
// dispatches our subclassed window's messages — pumping here would
|
|
1049
|
+
// steal messages from the host. Only poll when Bloom owns the window.
|
|
1050
|
+
unsafe { if !EMBEDDED { win32::poll_events(); } }
|
|
1051
|
+
poll_xinput_gamepad();
|
|
1052
|
+
}
|
|
1053
|
+
engine().begin_frame();
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
#[no_mangle]
|
|
1057
|
+
pub extern "C" fn bloom_end_drawing() { engine().end_frame(); }
|
|
1058
|
+
|
|
1059
|
+
static AUDIO_RUNNING: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
|
1060
|
+
|
|
1061
|
+
#[no_mangle]
|
|
1062
|
+
pub extern "C" fn bloom_init_audio() {
|
|
1063
|
+
#[cfg(windows)]
|
|
1064
|
+
{
|
|
1065
|
+
use std::sync::atomic::Ordering;
|
|
1066
|
+
AUDIO_RUNNING.store(true, Ordering::SeqCst);
|
|
1067
|
+
|
|
1068
|
+
// Move the render half into the audio thread; the engine keeps
|
|
1069
|
+
// only the command-producing control half.
|
|
1070
|
+
let renderer = engine().audio.take_renderer();
|
|
1071
|
+
std::thread::spawn(move || {
|
|
1072
|
+
unsafe { wasapi_audio_thread(renderer); }
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
#[no_mangle]
|
|
1078
|
+
pub extern "C" fn bloom_close_audio() {
|
|
1079
|
+
AUDIO_RUNNING.store(false, std::sync::atomic::Ordering::SeqCst);
|
|
1080
|
+
std::thread::sleep(std::time::Duration::from_millis(50));
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
#[cfg(windows)]
|
|
1084
|
+
unsafe fn wasapi_audio_thread(mut renderer: Option<bloom_shared::audio::AudioRenderer>) {
|
|
1085
|
+
use windows::Win32::Media::Audio::*;
|
|
1086
|
+
use windows::Win32::System::Com::*;
|
|
1087
|
+
use windows::core::*;
|
|
1088
|
+
use std::sync::atomic::Ordering;
|
|
1089
|
+
|
|
1090
|
+
// Initialize COM on this thread
|
|
1091
|
+
let _ = CoInitializeEx(None, COINIT_MULTITHREADED);
|
|
1092
|
+
|
|
1093
|
+
// Create device enumerator and get default output device
|
|
1094
|
+
let enumerator: IMMDeviceEnumerator = match CoCreateInstance(
|
|
1095
|
+
&MMDeviceEnumerator,
|
|
1096
|
+
None,
|
|
1097
|
+
CLSCTX_ALL,
|
|
1098
|
+
) {
|
|
1099
|
+
Ok(e) => e,
|
|
1100
|
+
Err(_) => return,
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
let device = match enumerator.GetDefaultAudioEndpoint(eRender, eConsole) {
|
|
1104
|
+
Ok(d) => d,
|
|
1105
|
+
Err(_) => return,
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
let audio_client: IAudioClient = match device.Activate(CLSCTX_ALL, None) {
|
|
1109
|
+
Ok(c) => c,
|
|
1110
|
+
Err(_) => return,
|
|
1111
|
+
};
|
|
1112
|
+
|
|
1113
|
+
// Get mix format
|
|
1114
|
+
let mix_format_ptr = match audio_client.GetMixFormat() {
|
|
1115
|
+
Ok(f) => f,
|
|
1116
|
+
Err(_) => return,
|
|
1117
|
+
};
|
|
1118
|
+
let mix_format = &*mix_format_ptr;
|
|
1119
|
+
let sample_rate = mix_format.nSamplesPerSec;
|
|
1120
|
+
let channels = mix_format.nChannels as usize;
|
|
1121
|
+
|
|
1122
|
+
// Initialize in shared mode with 20ms buffer
|
|
1123
|
+
let buffer_duration = 200_000; // 20ms in 100-nanosecond units
|
|
1124
|
+
if audio_client.Initialize(
|
|
1125
|
+
AUDCLNT_SHAREMODE_SHARED,
|
|
1126
|
+
0,
|
|
1127
|
+
buffer_duration,
|
|
1128
|
+
0,
|
|
1129
|
+
mix_format_ptr,
|
|
1130
|
+
None,
|
|
1131
|
+
).is_err() {
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
let buffer_size = match audio_client.GetBufferSize() {
|
|
1136
|
+
Ok(s) => s as usize,
|
|
1137
|
+
Err(_) => return,
|
|
1138
|
+
};
|
|
1139
|
+
|
|
1140
|
+
let render_client: IAudioRenderClient = match audio_client.GetService() {
|
|
1141
|
+
Ok(r) => r,
|
|
1142
|
+
Err(_) => return,
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
let _ = audio_client.Start();
|
|
1146
|
+
|
|
1147
|
+
// EN-062 — tell the renderer the DEVICE rate. It defaulted to 44.1 kHz
|
|
1148
|
+
// forever, so on the typical 48 kHz endpoint every filter cutoff and
|
|
1149
|
+
// block-dt was ~9% off (and, pre-resampler, every sound played sharp).
|
|
1150
|
+
if let Some(r) = renderer.as_mut() {
|
|
1151
|
+
r.set_sample_rate(sample_rate as f32);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Temporary buffer for mixing (always stereo f32 from our mixer)
|
|
1155
|
+
let mut mix_buf = vec![0.0f32; buffer_size * 2];
|
|
1156
|
+
|
|
1157
|
+
while AUDIO_RUNNING.load(Ordering::SeqCst) {
|
|
1158
|
+
let padding = audio_client.GetCurrentPadding().unwrap_or(0) as usize;
|
|
1159
|
+
let available = buffer_size - padding;
|
|
1160
|
+
if available == 0 {
|
|
1161
|
+
std::thread::sleep(std::time::Duration::from_millis(2));
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
let buffer_ptr = match render_client.GetBuffer(available as u32) {
|
|
1166
|
+
Ok(p) => p,
|
|
1167
|
+
Err(_) => { std::thread::sleep(std::time::Duration::from_millis(2)); continue; }
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
// Mix audio
|
|
1171
|
+
let mix_samples = available * 2; // stereo
|
|
1172
|
+
for i in 0..mix_samples { mix_buf[i] = 0.0; }
|
|
1173
|
+
// Renderer is moved into this thread at spawn — no shared
|
|
1174
|
+
// engine state is touched from here (see audio/mod.rs contract).
|
|
1175
|
+
if let Some(r) = renderer.as_mut() {
|
|
1176
|
+
r.mix(&mut mix_buf[..mix_samples]);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Write to WASAPI buffer (format is typically f32 or i16, assume float since we requested shared mode)
|
|
1180
|
+
let bits = mix_format.wBitsPerSample;
|
|
1181
|
+
let out_channels = channels;
|
|
1182
|
+
if bits == 32 {
|
|
1183
|
+
let out = std::slice::from_raw_parts_mut(buffer_ptr as *mut f32, available * out_channels);
|
|
1184
|
+
for i in 0..available {
|
|
1185
|
+
let l = mix_buf[i * 2];
|
|
1186
|
+
let r = if i * 2 + 1 < mix_samples { mix_buf[i * 2 + 1] } else { l };
|
|
1187
|
+
if out_channels >= 2 {
|
|
1188
|
+
out[i * out_channels] = l;
|
|
1189
|
+
out[i * out_channels + 1] = r;
|
|
1190
|
+
for c in 2..out_channels { out[i * out_channels + c] = 0.0; }
|
|
1191
|
+
} else {
|
|
1192
|
+
out[i] = (l + r) * 0.5;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
} else if bits == 16 {
|
|
1196
|
+
let out = std::slice::from_raw_parts_mut(buffer_ptr as *mut i16, available * out_channels);
|
|
1197
|
+
for i in 0..available {
|
|
1198
|
+
let l = mix_buf[i * 2];
|
|
1199
|
+
let r = if i * 2 + 1 < mix_samples { mix_buf[i * 2 + 1] } else { l };
|
|
1200
|
+
if out_channels >= 2 {
|
|
1201
|
+
out[i * out_channels] = (l * 32767.0) as i16;
|
|
1202
|
+
out[i * out_channels + 1] = (r * 32767.0) as i16;
|
|
1203
|
+
for c in 2..out_channels { out[i * out_channels + c] = 0; }
|
|
1204
|
+
} else {
|
|
1205
|
+
out[i] = ((l + r) * 0.5 * 32767.0) as i16;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
let _ = render_client.ReleaseBuffer(available as u32, 0);
|
|
1211
|
+
std::thread::sleep(std::time::Duration::from_millis(2));
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
let _ = audio_client.Stop();
|
|
1215
|
+
CoUninitialize();
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// --- Texture FFI ---
|
|
1219
|
+
|
|
1220
|
+
// --- Camera FFI ---
|
|
1221
|
+
|
|
1222
|
+
// --- 3D Drawing FFI ---
|
|
1223
|
+
|
|
1224
|
+
// --- Model FFI ---
|
|
1225
|
+
|
|
1226
|
+
// ============================================================
|
|
1227
|
+
// Phase 1c — material system FFI
|
|
1228
|
+
// ============================================================
|
|
1229
|
+
|
|
1230
|
+
// --- Music FFI ---
|
|
1231
|
+
|
|
1232
|
+
// --- Gamepad FFI ---
|
|
1233
|
+
|
|
1234
|
+
// --- Skeletal Animation Debug ---
|
|
1235
|
+
|
|
1236
|
+
// --- Lighting ---
|
|
1237
|
+
|
|
1238
|
+
// --- Utility FFI ---
|
|
1239
|
+
|
|
1240
|
+
#[no_mangle]
|
|
1241
|
+
pub extern "C" fn bloom_toggle_fullscreen() {
|
|
1242
|
+
#[cfg(windows)]
|
|
1243
|
+
win32::toggle_fullscreen();
|
|
1244
|
+
}
|
|
1245
|
+
#[no_mangle]
|
|
1246
|
+
pub extern "C" fn bloom_set_window_title(title_ptr: *const u8) {
|
|
1247
|
+
// Real since 2026-07-17 — the stub read the string and discarded it.
|
|
1248
|
+
use windows::Win32::UI::WindowsAndMessaging::SetWindowTextW;
|
|
1249
|
+
use windows::core::PCWSTR;
|
|
1250
|
+
let title = str_from_header(title_ptr);
|
|
1251
|
+
if let Some(hwnd) = win32::main_hwnd() {
|
|
1252
|
+
let wide: Vec<u16> = title.encode_utf16().chain(std::iter::once(0)).collect();
|
|
1253
|
+
unsafe { let _ = SetWindowTextW(hwnd, PCWSTR(wide.as_ptr())); }
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
#[no_mangle]
|
|
1257
|
+
pub extern "C" fn bloom_set_window_icon(path_ptr: *const u8) { let _ = str_from_header(path_ptr); }
|
|
1258
|
+
|
|
1259
|
+
#[no_mangle]
|
|
1260
|
+
pub extern "C" fn bloom_disable_cursor() {
|
|
1261
|
+
engine().input.cursor_disabled = true;
|
|
1262
|
+
// Hide the OS cursor (ShowCursor keeps a counter; loop until it's hidden).
|
|
1263
|
+
unsafe { while windows::Win32::UI::WindowsAndMessaging::ShowCursor(false) >= 0 {} }
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
#[no_mangle]
|
|
1267
|
+
pub extern "C" fn bloom_enable_cursor() {
|
|
1268
|
+
engine().input.cursor_disabled = false;
|
|
1269
|
+
unsafe { while windows::Win32::UI::WindowsAndMessaging::ShowCursor(true) < 0 {} }
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// E4: Clipboard — real Win32 implementation (was a stub until 2026-07-17,
|
|
1273
|
+
// so paste in the editor's text fields silently never worked on Windows).
|
|
1274
|
+
#[no_mangle]
|
|
1275
|
+
pub extern "C" fn bloom_set_clipboard_text(text_ptr: *const u8) {
|
|
1276
|
+
use windows::Win32::System::DataExchange::{
|
|
1277
|
+
OpenClipboard, CloseClipboard, EmptyClipboard, SetClipboardData,
|
|
1278
|
+
};
|
|
1279
|
+
use windows::Win32::System::Memory::{GlobalAlloc, GlobalLock, GlobalUnlock, GMEM_MOVEABLE};
|
|
1280
|
+
use windows::Win32::System::Ole::CF_UNICODETEXT;
|
|
1281
|
+
use windows::Win32::Foundation::{HANDLE, HWND};
|
|
1282
|
+
|
|
1283
|
+
let text = str_from_header(text_ptr);
|
|
1284
|
+
unsafe {
|
|
1285
|
+
let owner = win32::main_hwnd().unwrap_or(HWND(std::ptr::null_mut()));
|
|
1286
|
+
if OpenClipboard(owner).is_err() {
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
let _ = EmptyClipboard();
|
|
1290
|
+
let wide: Vec<u16> = text.encode_utf16().chain(std::iter::once(0)).collect();
|
|
1291
|
+
if let Ok(hmem) = GlobalAlloc(GMEM_MOVEABLE, wide.len() * 2) {
|
|
1292
|
+
let dst = GlobalLock(hmem) as *mut u16;
|
|
1293
|
+
if !dst.is_null() {
|
|
1294
|
+
std::ptr::copy_nonoverlapping(wide.as_ptr(), dst, wide.len());
|
|
1295
|
+
let _ = GlobalUnlock(hmem);
|
|
1296
|
+
// The system owns the memory on success; on failure we leak a
|
|
1297
|
+
// small block rather than double-free.
|
|
1298
|
+
let _ = SetClipboardData(CF_UNICODETEXT.0 as u32, HANDLE(hmem.0));
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
let _ = CloseClipboard();
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
#[no_mangle]
|
|
1306
|
+
pub extern "C" fn bloom_get_clipboard_text() -> *const u8 {
|
|
1307
|
+
use windows::Win32::System::DataExchange::{OpenClipboard, CloseClipboard, GetClipboardData};
|
|
1308
|
+
use windows::Win32::System::Memory::{GlobalLock, GlobalUnlock};
|
|
1309
|
+
use windows::Win32::System::Ole::CF_UNICODETEXT;
|
|
1310
|
+
use windows::Win32::Foundation::{HGLOBAL, HWND};
|
|
1311
|
+
|
|
1312
|
+
unsafe {
|
|
1313
|
+
let owner = win32::main_hwnd().unwrap_or(HWND(std::ptr::null_mut()));
|
|
1314
|
+
if OpenClipboard(owner).is_err() {
|
|
1315
|
+
return alloc_perry_string("");
|
|
1316
|
+
}
|
|
1317
|
+
let mut out = String::new();
|
|
1318
|
+
if let Ok(handle) = GetClipboardData(CF_UNICODETEXT.0 as u32) {
|
|
1319
|
+
let hglobal = HGLOBAL(handle.0);
|
|
1320
|
+
let ptr = GlobalLock(hglobal) as *const u16;
|
|
1321
|
+
if !ptr.is_null() {
|
|
1322
|
+
let mut len = 0usize;
|
|
1323
|
+
while *ptr.add(len) != 0 {
|
|
1324
|
+
len += 1;
|
|
1325
|
+
}
|
|
1326
|
+
out = String::from_utf16_lossy(std::slice::from_raw_parts(ptr, len));
|
|
1327
|
+
let _ = GlobalUnlock(hglobal);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
let _ = CloseClipboard();
|
|
1331
|
+
alloc_perry_string(&out)
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// E5b: File dialogs — real Win32 implementation (stubs until 2026-07-17: the
|
|
1336
|
+
// editor's Open/Save buttons silently did nothing on Windows). The filter
|
|
1337
|
+
// argument is a simple pattern like "*.world.json"; empty means all files.
|
|
1338
|
+
// OFN_NOCHANGEDIR is load-bearing: the common dialogs change the process CWD
|
|
1339
|
+
// by default, which would break every relative asset path afterward.
|
|
1340
|
+
#[cfg(windows)]
|
|
1341
|
+
fn run_file_dialog(filter: &str, title: &str, save: bool, default_name: &str) -> String {
|
|
1342
|
+
use windows::Win32::UI::Controls::Dialogs::{
|
|
1343
|
+
GetOpenFileNameW, GetSaveFileNameW, OPENFILENAMEW,
|
|
1344
|
+
OFN_FILEMUSTEXIST, OFN_PATHMUSTEXIST, OFN_NOCHANGEDIR, OFN_OVERWRITEPROMPT,
|
|
1345
|
+
};
|
|
1346
|
+
use windows::core::{PCWSTR, PWSTR};
|
|
1347
|
+
|
|
1348
|
+
// Filter block: "Matching files\0<pattern>\0All files\0*.*\0\0".
|
|
1349
|
+
let pattern = if filter.is_empty() { "*.*" } else { filter };
|
|
1350
|
+
let mut filter_w: Vec<u16> = Vec::new();
|
|
1351
|
+
filter_w.extend("Matching files".encode_utf16());
|
|
1352
|
+
filter_w.push(0);
|
|
1353
|
+
filter_w.extend(pattern.encode_utf16());
|
|
1354
|
+
filter_w.push(0);
|
|
1355
|
+
filter_w.extend("All files".encode_utf16());
|
|
1356
|
+
filter_w.push(0);
|
|
1357
|
+
filter_w.extend("*.*".encode_utf16());
|
|
1358
|
+
filter_w.push(0);
|
|
1359
|
+
filter_w.push(0);
|
|
1360
|
+
|
|
1361
|
+
let title_w: Vec<u16> = title.encode_utf16().chain(std::iter::once(0)).collect();
|
|
1362
|
+
|
|
1363
|
+
let mut file_buf = [0u16; 4096];
|
|
1364
|
+
if save && !default_name.is_empty() {
|
|
1365
|
+
for (i, c) in default_name.encode_utf16().take(4000).enumerate() {
|
|
1366
|
+
file_buf[i] = c;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
let mut ofn = OPENFILENAMEW::default();
|
|
1371
|
+
ofn.lStructSize = std::mem::size_of::<OPENFILENAMEW>() as u32;
|
|
1372
|
+
ofn.hwndOwner = win32::main_hwnd().unwrap_or_default();
|
|
1373
|
+
ofn.lpstrFilter = PCWSTR(filter_w.as_ptr());
|
|
1374
|
+
ofn.lpstrFile = PWSTR(file_buf.as_mut_ptr());
|
|
1375
|
+
ofn.nMaxFile = file_buf.len() as u32;
|
|
1376
|
+
if !title.is_empty() {
|
|
1377
|
+
ofn.lpstrTitle = PCWSTR(title_w.as_ptr());
|
|
1378
|
+
}
|
|
1379
|
+
ofn.Flags = if save {
|
|
1380
|
+
OFN_OVERWRITEPROMPT | OFN_NOCHANGEDIR
|
|
1381
|
+
} else {
|
|
1382
|
+
OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR
|
|
1383
|
+
};
|
|
1384
|
+
|
|
1385
|
+
let ok = unsafe {
|
|
1386
|
+
if save { GetSaveFileNameW(&mut ofn) } else { GetOpenFileNameW(&mut ofn) }
|
|
1387
|
+
};
|
|
1388
|
+
if !ok.as_bool() {
|
|
1389
|
+
return String::new();
|
|
1390
|
+
}
|
|
1391
|
+
let len = file_buf.iter().position(|&c| c == 0).unwrap_or(0);
|
|
1392
|
+
String::from_utf16_lossy(&file_buf[..len])
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
#[no_mangle]
|
|
1396
|
+
pub extern "C" fn bloom_open_file_dialog(filter_ptr: *const u8, title_ptr: *const u8) -> *const u8 {
|
|
1397
|
+
let filter = str_from_header(filter_ptr);
|
|
1398
|
+
let title = str_from_header(title_ptr);
|
|
1399
|
+
let path = run_file_dialog(&filter, &title, false, "");
|
|
1400
|
+
alloc_perry_string(&path)
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
#[no_mangle]
|
|
1404
|
+
pub extern "C" fn bloom_save_file_dialog(default_name_ptr: *const u8, title_ptr: *const u8) -> *const u8 {
|
|
1405
|
+
let default_name = str_from_header(default_name_ptr);
|
|
1406
|
+
let title = str_from_header(title_ptr);
|
|
1407
|
+
let path = run_file_dialog("", &title, true, &default_name);
|
|
1408
|
+
alloc_perry_string(&path)
|
|
1409
|
+
}
|
|
1410
|
+
#[no_mangle]
|
|
1411
|
+
pub extern "C" fn bloom_get_platform() -> f64 { 3.0 }
|
|
1412
|
+
|
|
1413
|
+
/// Preferred OS language packed as `c0*256+c1` (ISO-639 primary subtag), from
|
|
1414
|
+
/// `GetUserDefaultLocaleName` (e.g. "en-US" -> "en"). Falls back to "en".
|
|
1415
|
+
#[no_mangle]
|
|
1416
|
+
pub extern "C" fn bloom_get_language() -> f64 {
|
|
1417
|
+
use windows::Win32::Globalization::GetUserDefaultLocaleName;
|
|
1418
|
+
let mut buf = [0u16; 85]; // LOCALE_NAME_MAX_LENGTH
|
|
1419
|
+
let n = unsafe { GetUserDefaultLocaleName(&mut buf) };
|
|
1420
|
+
if n >= 2 {
|
|
1421
|
+
let lc = |c: u16| -> u8 { let b = c as u8; if b.is_ascii_uppercase() { b + 32 } else { b } };
|
|
1422
|
+
let (c0, c1) = (lc(buf[0]), lc(buf[1]));
|
|
1423
|
+
if c0.is_ascii_alphabetic() && c1.is_ascii_alphabetic() {
|
|
1424
|
+
return (c0 as f64) * 256.0 + (c1 as f64);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
25966.0
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// ============================================================
|
|
1431
|
+
// Thread-safe staging (for async asset loading via Perry threads)
|
|
1432
|
+
// ============================================================
|
|
1433
|
+
|
|
1434
|
+
fn pollster_block_on<F: std::future::Future>(future: F) -> F::Output {
|
|
1435
|
+
use std::task::{Context, Poll, Wake, Waker};
|
|
1436
|
+
use std::pin::Pin;
|
|
1437
|
+
use std::sync::Arc;
|
|
1438
|
+
struct NoopWaker;
|
|
1439
|
+
impl Wake for NoopWaker { fn wake(self: Arc<Self>) {} }
|
|
1440
|
+
let waker = Waker::from(Arc::new(NoopWaker));
|
|
1441
|
+
let mut cx = Context::from_waker(&waker);
|
|
1442
|
+
let mut future = unsafe { Pin::new_unchecked(Box::new(future)) };
|
|
1443
|
+
loop {
|
|
1444
|
+
match future.as_mut().poll(&mut cx) {
|
|
1445
|
+
Poll::Ready(result) => return result,
|
|
1446
|
+
Poll::Pending => std::thread::yield_now(),
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
|
|
1452
|
+
// Q6: Multi-hit picking
|
|
1453
|
+
// ============================================================
|
|
1454
|
+
|
|
1455
|
+
// ============================================================
|
|
1456
|
+
// Render quality toggles (individual + preset) — ticket 011
|
|
1457
|
+
// Mirror of the macOS FFI surface added in commit 95da6af; previously
|
|
1458
|
+
// macOS-only, now exposed on every native platform so non-macOS builds
|
|
1459
|
+
// don't fail at runtime (missing symbol) when the TS API invokes them.
|
|
1460
|
+
// ============================================================
|
|
1461
|
+
|
|
1462
|
+
// ============================================================
|
|
1463
|
+
// Render scale / upscale / DRS / post-FX / screenshots / impulse
|
|
1464
|
+
// Ports of the macOS / Linux FFI surface so the bloom/core TS layer
|
|
1465
|
+
// links cleanly on Windows. EngineState in bloom-shared already
|
|
1466
|
+
// exposes the underlying renderer methods, so these wrappers are
|
|
1467
|
+
// platform-agnostic.
|
|
1468
|
+
// ============================================================
|
|
1469
|
+
|
|
1470
|
+
// ============================================================
|
|
1471
|
+
// Profiler — CPU phase timings (always available) + GPU timestamps
|
|
1472
|
+
// (when the adapter supports TIMESTAMP_QUERY). Disabled by default.
|
|
1473
|
+
// ============================================================
|
|
1474
|
+
|
|
1475
|
+
// ============================================================
|
|
1476
|
+
// Physics (Jolt 5.x) — FFI surface generated from shared macro
|
|
1477
|
+
// ============================================================
|
|
1478
|
+
|
|
1479
|
+
#[cfg(feature = "jolt")]
|
|
1480
|
+
#[inline]
|
|
1481
|
+
fn bloom_jolt_ffi_physics() -> &'static mut bloom_shared::physics_jolt::JoltPhysics {
|
|
1482
|
+
&mut engine().jolt
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
#[cfg(feature = "jolt")]
|
|
1486
|
+
bloom_shared::define_physics_ffi!();
|