@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,653 @@
|
|
|
1
|
+
//! GI bake methods — scene-wide SDF clipmap (binned + sliced amortized
|
|
2
|
+
//! bake), WSRC radiance cascades, and per-mesh SDF drain. Split out of
|
|
3
|
+
//! renderer/mod.rs to keep it under the file-line ceiling (see
|
|
4
|
+
//! tools/check-file-lines.js); pure code move, no behaviour change.
|
|
5
|
+
|
|
6
|
+
use super::*;
|
|
7
|
+
|
|
8
|
+
// EN-054 — cached world-triangle soup for the clipmap rebake. The soup is a
|
|
9
|
+
// pure function of the scene graph (node set, geometry, transforms,
|
|
10
|
+
// visibility), and every one of those bumps `tlas_version` — so a travel-
|
|
11
|
+
// triggered rebake only needs to RE-BIN against its new origin, not re-gather
|
|
12
|
+
// the whole scene. Before this, every ~10 m of camera travel re-transformed
|
|
13
|
+
// every vertex of every node and allocated two multi-MB Vecs on the frame
|
|
14
|
+
// that starts the rebake (measured 2–9 ms gather per event on the shooter's
|
|
15
|
+
// 203k-triangle scene, on top of the origin-dependent binning that stays).
|
|
16
|
+
// Module-local static, same pattern as staging.rs's stores — the ratcheted
|
|
17
|
+
// renderer/mod.rs gains no field.
|
|
18
|
+
struct SdfTriCache {
|
|
19
|
+
version: u64,
|
|
20
|
+
vertices: Vec<f32>,
|
|
21
|
+
indices: Vec<u32>,
|
|
22
|
+
tri_count: u32,
|
|
23
|
+
}
|
|
24
|
+
fn sdf_tri_cache() -> &'static std::sync::Mutex<Option<SdfTriCache>> {
|
|
25
|
+
static INSTANCE: std::sync::OnceLock<std::sync::Mutex<Option<SdfTriCache>>> =
|
|
26
|
+
std::sync::OnceLock::new();
|
|
27
|
+
INSTANCE.get_or_init(|| std::sync::Mutex::new(None))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
impl Renderer {
|
|
31
|
+
/// Ticket 014 V2 — bake the scene-wide SDF clipmap once, on the
|
|
32
|
+
/// frame when all per-mesh queues (BLAS, cards, per-mesh SDFs)
|
|
33
|
+
/// have drained. Gathers every visible mesh's triangles into a
|
|
34
|
+
/// world-space buffer via `scene.build_world_triangles()` and
|
|
35
|
+
/// runs `SDF_BAKE_WGSL` against the unified data with the
|
|
36
|
+
/// clipmap's fixed world-space AABB. 64³ voxel × scene triangle
|
|
37
|
+
/// count = expensive one-shot (~100-200 ms on Sponza), but
|
|
38
|
+
/// happens after a visible frame and never repeats for static
|
|
39
|
+
/// scenes.
|
|
40
|
+
/// Ticket 014 V5 — camera world-space position. Uses
|
|
41
|
+
/// `current_camera_pos`, which `begin_mode_3d` writes every frame
|
|
42
|
+
/// from the user-supplied camera position (cheaper than inverting
|
|
43
|
+
/// the view matrix and always in sync with what the game sees).
|
|
44
|
+
pub(super) fn current_camera_world_pos(&self) -> [f32; 3] {
|
|
45
|
+
self.current_camera_pos
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Ticket 014 V5 — flag the SDF clipmap for a re-bake if the camera
|
|
49
|
+
/// has moved past the rebake threshold from the current clipmap
|
|
50
|
+
/// centre. Fullscreen-lag fix: instead of clearing `built` (which
|
|
51
|
+
/// used to fire a full-volume single-dispatch rebake that stalled
|
|
52
|
+
/// weak GPUs for seconds), this only raises `rebake_needed`; the
|
|
53
|
+
/// live clipmap keeps serving traces while the amortized job bakes
|
|
54
|
+
/// the re-centred volume a few Z-slices per frame.
|
|
55
|
+
pub(super) fn maybe_invalidate_sdf_clipmap(&mut self) {
|
|
56
|
+
// A job in flight already re-centres on its own origin — let it
|
|
57
|
+
// land before measuring drift again.
|
|
58
|
+
if !self.scene_sdf_clipmap_built || self.sdf_clipmap_job.is_some() {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
let cam = self.current_camera_world_pos();
|
|
62
|
+
let dx = cam[0] - self.scene_sdf_clipmap_origin[0];
|
|
63
|
+
let dy = cam[1] - self.scene_sdf_clipmap_origin[1];
|
|
64
|
+
let dz = cam[2] - self.scene_sdf_clipmap_origin[2];
|
|
65
|
+
let dist_sq = dx * dx + dy * dy + dz * dz;
|
|
66
|
+
let threshold = SCENE_SDF_CLIPMAP_EXTENT * SCENE_SDF_CLIPMAP_REBAKE_THRESHOLD;
|
|
67
|
+
if dist_sq > threshold * threshold {
|
|
68
|
+
self.scene_sdf_clipmap_rebake_needed = true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
pub(super) fn bake_scene_sdf_clipmap(
|
|
73
|
+
&mut self,
|
|
74
|
+
scene: &crate::scene::SceneGraph,
|
|
75
|
+
encoder: &mut wgpu::CommandEncoder,
|
|
76
|
+
) {
|
|
77
|
+
// Continue an in-flight job first: one slice batch per frame.
|
|
78
|
+
if let Some(job) = self.sdf_clipmap_job.take() {
|
|
79
|
+
self.encode_clipmap_bake_slices(job, encoder);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if !self.scene_sdf_clipmap_rebake_needed {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// Wait for all per-mesh queues to drain — builds the clipmap
|
|
86
|
+
// from a fully-loaded scene rather than a partial one, and
|
|
87
|
+
// keeps first-frame cost spread across the card/BLAS work
|
|
88
|
+
// already scheduled.
|
|
89
|
+
if !scene.pending_blas_builds.is_empty()
|
|
90
|
+
|| !scene.pending_card_captures.is_empty()
|
|
91
|
+
|| !scene.pending_sdf_bakes.is_empty()
|
|
92
|
+
{
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let mut cache_guard = sdf_tri_cache().lock().unwrap();
|
|
97
|
+
let stale = match &*cache_guard {
|
|
98
|
+
Some(c) => c.version != scene.tlas_version,
|
|
99
|
+
None => true,
|
|
100
|
+
};
|
|
101
|
+
if stale {
|
|
102
|
+
let (v, i, n) = scene.build_world_triangles();
|
|
103
|
+
*cache_guard = Some(SdfTriCache {
|
|
104
|
+
version: scene.tlas_version,
|
|
105
|
+
vertices: v,
|
|
106
|
+
indices: i,
|
|
107
|
+
tri_count: n,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
let cached = cache_guard.as_ref().unwrap();
|
|
111
|
+
let vertices = &cached.vertices;
|
|
112
|
+
let indices = &cached.indices;
|
|
113
|
+
let tri_count = cached.tri_count;
|
|
114
|
+
if tri_count == 0 {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// V5 — centre the clipmap on the current camera position,
|
|
119
|
+
// voxel-snapped for sampling stability (sub-voxel shifts
|
|
120
|
+
// would change which voxel each sphere-trace step reads).
|
|
121
|
+
// The live origin flips only when the job completes.
|
|
122
|
+
let half = SCENE_SDF_CLIPMAP_EXTENT * 0.5;
|
|
123
|
+
let voxel = SCENE_SDF_CLIPMAP_EXTENT / SCENE_SDF_CLIPMAP_RES as f32;
|
|
124
|
+
let cam = self.current_camera_world_pos();
|
|
125
|
+
let origin = [
|
|
126
|
+
(cam[0] / voxel).round() * voxel,
|
|
127
|
+
(cam[1] / voxel).round() * voxel,
|
|
128
|
+
(cam[2] / voxel).round() * voxel,
|
|
129
|
+
];
|
|
130
|
+
let aabb_min = [origin[0] - half, origin[1] - half, origin[2] - half, 0.0];
|
|
131
|
+
let aabb_max = [origin[0] + half, origin[1] + half, origin[2] + half, 0.0];
|
|
132
|
+
|
|
133
|
+
// Bin triangles into BIN_CELLS³ cells, each list expanded by one
|
|
134
|
+
// cell (the shader's narrow band) so the per-cell clamp stays a
|
|
135
|
+
// conservative lower bound for sphere tracing. Two-pass counting
|
|
136
|
+
// sort: count, prefix-sum, fill.
|
|
137
|
+
let cells = SCENE_SDF_CLIPMAP_BIN_CELLS as usize;
|
|
138
|
+
let cell_size = SCENE_SDF_CLIPMAP_EXTENT / cells as f32;
|
|
139
|
+
let grid_min = [aabb_min[0], aabb_min[1], aabb_min[2]];
|
|
140
|
+
let cell_range = |tri: usize| -> Option<([usize; 3], [usize; 3])> {
|
|
141
|
+
let i0 = indices[tri * 3] as usize * 12;
|
|
142
|
+
let i1 = indices[tri * 3 + 1] as usize * 12;
|
|
143
|
+
let i2 = indices[tri * 3 + 2] as usize * 12;
|
|
144
|
+
let mut lo = [f32::MAX; 3];
|
|
145
|
+
let mut hi = [f32::MIN; 3];
|
|
146
|
+
for base in [i0, i1, i2] {
|
|
147
|
+
for a in 0..3 {
|
|
148
|
+
let v = vertices[base + a];
|
|
149
|
+
lo[a] = lo[a].min(v);
|
|
150
|
+
hi[a] = hi[a].max(v);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
let mut c_lo = [0usize; 3];
|
|
154
|
+
let mut c_hi = [0usize; 3];
|
|
155
|
+
for a in 0..3 {
|
|
156
|
+
// Expand by one cell width (= the shader's band).
|
|
157
|
+
let lo_c = ((lo[a] - cell_size - grid_min[a]) / cell_size).floor();
|
|
158
|
+
let hi_c = ((hi[a] + cell_size - grid_min[a]) / cell_size).floor();
|
|
159
|
+
if hi_c < 0.0 || lo_c >= cells as f32 {
|
|
160
|
+
return None; // entirely outside the clipmap volume
|
|
161
|
+
}
|
|
162
|
+
c_lo[a] = lo_c.max(0.0) as usize;
|
|
163
|
+
c_hi[a] = hi_c.min(cells as f32 - 1.0) as usize;
|
|
164
|
+
}
|
|
165
|
+
Some((c_lo, c_hi))
|
|
166
|
+
};
|
|
167
|
+
let cell_count = cells * cells * cells;
|
|
168
|
+
let mut counts = vec![0u32; cell_count];
|
|
169
|
+
for t in 0..tri_count as usize {
|
|
170
|
+
if let Some((lo, hi)) = cell_range(t) {
|
|
171
|
+
for z in lo[2]..=hi[2] {
|
|
172
|
+
for y in lo[1]..=hi[1] {
|
|
173
|
+
for x in lo[0]..=hi[0] {
|
|
174
|
+
counts[(z * cells + y) * cells + x] += 1;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
let mut offsets = vec![0u32; cell_count + 1];
|
|
181
|
+
for i in 0..cell_count {
|
|
182
|
+
offsets[i + 1] = offsets[i] + counts[i];
|
|
183
|
+
}
|
|
184
|
+
let total_refs = offsets[cell_count] as usize;
|
|
185
|
+
let mut cursor: Vec<u32> = offsets[..cell_count].to_vec();
|
|
186
|
+
// wgpu rejects zero-sized buffers — keep one dummy entry when no
|
|
187
|
+
// triangle touches the volume (all cells then read empty ranges).
|
|
188
|
+
let mut tri_refs = vec![0u32; total_refs.max(1)];
|
|
189
|
+
for t in 0..tri_count as usize {
|
|
190
|
+
if let Some((lo, hi)) = cell_range(t) {
|
|
191
|
+
for z in lo[2]..=hi[2] {
|
|
192
|
+
for y in lo[1]..=hi[1] {
|
|
193
|
+
for x in lo[0]..=hi[0] {
|
|
194
|
+
let ci = (z * cells + y) * cells + x;
|
|
195
|
+
tri_refs[cursor[ci] as usize] = t as u32;
|
|
196
|
+
cursor[ci] += 1;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let vbuf = self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
204
|
+
label: Some("scene_sdf_bake_vbuf"),
|
|
205
|
+
contents: bytemuck::cast_slice(vertices.as_slice()),
|
|
206
|
+
usage: wgpu::BufferUsages::STORAGE,
|
|
207
|
+
});
|
|
208
|
+
let ibuf = self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
209
|
+
label: Some("scene_sdf_bake_ibuf"),
|
|
210
|
+
contents: bytemuck::cast_slice(indices.as_slice()),
|
|
211
|
+
usage: wgpu::BufferUsages::STORAGE,
|
|
212
|
+
});
|
|
213
|
+
let cell_offsets_buf = self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
214
|
+
label: Some("scene_sdf_bake_cell_offsets"),
|
|
215
|
+
contents: bytemuck::cast_slice(&offsets),
|
|
216
|
+
usage: wgpu::BufferUsages::STORAGE,
|
|
217
|
+
});
|
|
218
|
+
let cell_tris_buf = self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
219
|
+
label: Some("scene_sdf_bake_cell_tris"),
|
|
220
|
+
contents: bytemuck::cast_slice(&tri_refs),
|
|
221
|
+
usage: wgpu::BufferUsages::STORAGE,
|
|
222
|
+
});
|
|
223
|
+
// Per-job uniform: sharing sdf_bake_uniform would alias with the
|
|
224
|
+
// per-mesh bakes — queue.write_buffer applies before any of this
|
|
225
|
+
// frame's commands, so the last write would win for every pass.
|
|
226
|
+
let uniform = self.device.create_buffer(&wgpu::BufferDescriptor {
|
|
227
|
+
label: Some("scene_sdf_clipmap_bake_uniform"),
|
|
228
|
+
size: std::mem::size_of::<SdfBakeParams>() as u64,
|
|
229
|
+
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
|
230
|
+
mapped_at_creation: false,
|
|
231
|
+
});
|
|
232
|
+
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
233
|
+
label: Some("scene_sdf_clipmap_bake_bg"),
|
|
234
|
+
layout: &self.sdf_clipmap_bake_layout,
|
|
235
|
+
entries: &[
|
|
236
|
+
wgpu::BindGroupEntry { binding: 0, resource: uniform.as_entire_binding() },
|
|
237
|
+
wgpu::BindGroupEntry { binding: 1, resource: vbuf.as_entire_binding() },
|
|
238
|
+
wgpu::BindGroupEntry { binding: 2, resource: ibuf.as_entire_binding() },
|
|
239
|
+
wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(&self.scene_sdf_clipmap_staging_view) },
|
|
240
|
+
wgpu::BindGroupEntry { binding: 4, resource: cell_offsets_buf.as_entire_binding() },
|
|
241
|
+
wgpu::BindGroupEntry { binding: 5, resource: cell_tris_buf.as_entire_binding() },
|
|
242
|
+
],
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
self.scene_sdf_clipmap_rebake_needed = false;
|
|
246
|
+
let job = SdfClipmapBakeJob {
|
|
247
|
+
origin,
|
|
248
|
+
aabb_min,
|
|
249
|
+
aabb_max,
|
|
250
|
+
uniform,
|
|
251
|
+
bind_group,
|
|
252
|
+
next_z: 0,
|
|
253
|
+
};
|
|
254
|
+
// Encode the first slice batch right away so a full rebake takes
|
|
255
|
+
// exactly RES / LAYERS_PER_FRAME frames end to end.
|
|
256
|
+
self.encode_clipmap_bake_slices(job, encoder);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/// Fullscreen-lag fix — encode this frame's slice batch of the
|
|
260
|
+
/// in-flight clipmap bake. On the final batch, copy the staging
|
|
261
|
+
/// volume over the live clipmap and flip the origin — the copy is
|
|
262
|
+
/// encoded before this frame's probe traces, so the swap is atomic
|
|
263
|
+
/// from the tracer's point of view.
|
|
264
|
+
pub(super) fn encode_clipmap_bake_slices(
|
|
265
|
+
&mut self,
|
|
266
|
+
mut job: SdfClipmapBakeJob,
|
|
267
|
+
encoder: &mut wgpu::CommandEncoder,
|
|
268
|
+
) {
|
|
269
|
+
let res = SCENE_SDF_CLIPMAP_RES;
|
|
270
|
+
let layers = SCENE_SDF_CLIPMAP_LAYERS_PER_FRAME.min(res - job.next_z);
|
|
271
|
+
let params = SdfBakeParams {
|
|
272
|
+
aabb_min: job.aabb_min,
|
|
273
|
+
aabb_max: job.aabb_max,
|
|
274
|
+
counts: [SCENE_SDF_CLIPMAP_BIN_CELLS, res, job.next_z, 0],
|
|
275
|
+
};
|
|
276
|
+
self.queue.write_buffer(&job.uniform, 0, bytemuck::bytes_of(¶ms));
|
|
277
|
+
|
|
278
|
+
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
|
279
|
+
label: Some("scene_sdf_clipmap_bake_slice"),
|
|
280
|
+
timestamp_writes: None,
|
|
281
|
+
});
|
|
282
|
+
pass.set_pipeline(&self.sdf_clipmap_bake_pipeline);
|
|
283
|
+
pass.set_bind_group(0, &job.bind_group, &[]);
|
|
284
|
+
pass.dispatch_workgroups(res / 4, res / 4, layers / 4);
|
|
285
|
+
drop(pass);
|
|
286
|
+
|
|
287
|
+
job.next_z += layers;
|
|
288
|
+
if job.next_z >= res {
|
|
289
|
+
encoder.copy_texture_to_texture(
|
|
290
|
+
self.scene_sdf_clipmap_staging_tex.as_image_copy(),
|
|
291
|
+
self.scene_sdf_clipmap_tex.as_image_copy(),
|
|
292
|
+
wgpu::Extent3d {
|
|
293
|
+
width: res,
|
|
294
|
+
height: res,
|
|
295
|
+
depth_or_array_layers: res,
|
|
296
|
+
},
|
|
297
|
+
);
|
|
298
|
+
self.scene_sdf_clipmap_origin = job.origin;
|
|
299
|
+
self.scene_sdf_clipmap_built = true;
|
|
300
|
+
} else {
|
|
301
|
+
self.sdf_clipmap_job = Some(job);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/// Ticket 014 V6/V7/V12/V13 — invalidate WSRC cascades on
|
|
306
|
+
/// camera travel OR meaningful lighting change. V13 runs the
|
|
307
|
+
/// V12 hysteresis checks per cascade, so a sun rotation or
|
|
308
|
+
/// camera shift only rebakes the affected cascade(s). Typical
|
|
309
|
+
/// pattern: camera moves 10 m → near cascade (1.875 m cell,
|
|
310
|
+
/// ~0.47 m threshold) rebakes every few frames, mid cascade
|
|
311
|
+
/// (7.5 m cell, ~1.9 m threshold) rebakes occasionally, far
|
|
312
|
+
/// cascade (31 m cell, ~7.8 m threshold) stays cached for
|
|
313
|
+
/// much longer.
|
|
314
|
+
pub(super) fn maybe_invalidate_wsrc(&mut self) {
|
|
315
|
+
let cam = self.current_camera_world_pos();
|
|
316
|
+
let ld = self.lighting_uniforms.light_dir;
|
|
317
|
+
let lc = self.lighting_uniforms.light_color;
|
|
318
|
+
let amb = self.lighting_uniforms.ambient;
|
|
319
|
+
let cur_sun_color = [lc[0] * ld[3], lc[1] * ld[3], lc[2] * ld[3]];
|
|
320
|
+
let cur_sky_color = [amb[0] * amb[3], amb[1] * amb[3], amb[2] * amb[3]];
|
|
321
|
+
|
|
322
|
+
fn luma(c: [f32; 3]) -> f32 {
|
|
323
|
+
c[0] * 0.2126 + c[1] * 0.7152 + c[2] * 0.0722
|
|
324
|
+
}
|
|
325
|
+
fn rel_diff(a: f32, b: f32) -> f32 {
|
|
326
|
+
(a - b).abs() / a.max(b).max(1e-4)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
for c in 0..WSRC_CASCADE_COUNT as usize {
|
|
330
|
+
if !self.wsrc_built[c] {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
// Camera travel — per-cascade threshold scales with the
|
|
334
|
+
// cascade's extent, so each cascade has its own
|
|
335
|
+
// "moved enough" metric.
|
|
336
|
+
let extent = WSRC_CASCADE_EXTENTS[c];
|
|
337
|
+
let origin = self.wsrc_origin[c];
|
|
338
|
+
let dx = cam[0] - origin[0];
|
|
339
|
+
let dy = cam[1] - origin[1];
|
|
340
|
+
let dz = cam[2] - origin[2];
|
|
341
|
+
let dist_sq = dx * dx + dy * dy + dz * dz;
|
|
342
|
+
let threshold = extent * WSRC_REBAKE_THRESHOLD;
|
|
343
|
+
if dist_sq > threshold * threshold {
|
|
344
|
+
self.wsrc_built[c] = false;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// V12 hysteresis — angular sun + 5% relative luma.
|
|
349
|
+
let last = self.wsrc_last_sun_dir[c];
|
|
350
|
+
let sun_dot = ld[0] * last[0] + ld[1] * last[1] + ld[2] * last[2];
|
|
351
|
+
if sun_dot < 0.99985 {
|
|
352
|
+
self.wsrc_built[c] = false;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if rel_diff(luma(cur_sun_color), luma(self.wsrc_last_sun_color[c])) > 0.05 {
|
|
356
|
+
self.wsrc_built[c] = false;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if rel_diff(luma(cur_sky_color), luma(self.wsrc_last_sky_color[c])) > 0.05 {
|
|
360
|
+
self.wsrc_built[c] = false;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/// Ticket 014 V6 — bake the world-space radiance cache. One
|
|
366
|
+
/// dispatch covers all `WSRC_GRID_RES³` probes × 64 octel texels.
|
|
367
|
+
/// Cheap: per-texel work is one shadow-cascade lookup + analytic
|
|
368
|
+
/// sun/sky math, roughly matching a single card-lighting pixel.
|
|
369
|
+
/// Runs at most once per `WSRC_REBAKE_THRESHOLD × extent` of
|
|
370
|
+
/// camera travel — same amortisation pattern as the clipmap.
|
|
371
|
+
pub(super) fn bake_wsrc(
|
|
372
|
+
&mut self,
|
|
373
|
+
encoder: &mut wgpu::CommandEncoder,
|
|
374
|
+
) {
|
|
375
|
+
// V13 — bake only cascades that are marked not-built. Each
|
|
376
|
+
// cascade snaps to its own cell grid (cell = extent / 16)
|
|
377
|
+
// and writes into its own 16-slice block of the shared
|
|
378
|
+
// atlas.
|
|
379
|
+
if self.wsrc_built.iter().all(|b| *b) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// V14 — pick the HW ray-traced bake when the adapter has
|
|
384
|
+
// ray-query AND the TLAS is ready. The SW path stays the
|
|
385
|
+
// fallback for non-RT adapters and for the early frames
|
|
386
|
+
// before BLAS / TLAS have been built.
|
|
387
|
+
let use_hw = self.hw_rt_enabled
|
|
388
|
+
&& self.wsrc_bake_hw_pipeline.is_some()
|
|
389
|
+
&& self.tlas.is_some()
|
|
390
|
+
&& self.tlas_instance_data_buffer.is_some();
|
|
391
|
+
|
|
392
|
+
// Resolve a single set of lighting params — they're the same
|
|
393
|
+
// across all cascades in one frame. Per-cascade differences
|
|
394
|
+
// come from the origin + extent passed through the uniform.
|
|
395
|
+
let ld = self.lighting_uniforms.light_dir;
|
|
396
|
+
let inv_len = 1.0 / (ld[0]*ld[0] + ld[1]*ld[1] + ld[2]*ld[2]).sqrt().max(1e-4);
|
|
397
|
+
let sun_dir_ws = [-ld[0]*inv_len, -ld[1]*inv_len, -ld[2]*inv_len, ld[3]];
|
|
398
|
+
let lc = self.lighting_uniforms.light_color;
|
|
399
|
+
let sun_intensity = ld[3].max(0.0);
|
|
400
|
+
let sun_color = [
|
|
401
|
+
lc[0] * sun_intensity,
|
|
402
|
+
lc[1] * sun_intensity,
|
|
403
|
+
lc[2] * sun_intensity,
|
|
404
|
+
0.0,
|
|
405
|
+
];
|
|
406
|
+
let amb = self.lighting_uniforms.ambient;
|
|
407
|
+
let sky_intensity = amb[3].max(0.0);
|
|
408
|
+
let sky_color = [
|
|
409
|
+
amb[0] * sky_intensity,
|
|
410
|
+
amb[1] * sky_intensity,
|
|
411
|
+
amb[2] * sky_intensity,
|
|
412
|
+
0.0,
|
|
413
|
+
];
|
|
414
|
+
|
|
415
|
+
let shadows_enabled = self.shadow_map.enabled;
|
|
416
|
+
let shadow_vps: [[[f32; 4]; 4]; 3] = if shadows_enabled {
|
|
417
|
+
self.shadow_map.light_vps
|
|
418
|
+
} else {
|
|
419
|
+
[IDENTITY_MAT4; 3]
|
|
420
|
+
};
|
|
421
|
+
let shadow_splits = if shadows_enabled {
|
|
422
|
+
let s = self.shadow_map.cascade_splits;
|
|
423
|
+
[s[0], s[1], s[2], 0.0]
|
|
424
|
+
} else {
|
|
425
|
+
[f32::INFINITY, f32::INFINITY, f32::INFINITY, 0.0]
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
// Lazy-build whichever bind group the selected path needs.
|
|
429
|
+
// The two caches are independent — switching between paths
|
|
430
|
+
// (e.g. if TLAS becomes available mid-session) is fine.
|
|
431
|
+
if use_hw {
|
|
432
|
+
if self.wsrc_bake_hw_bg_cache.is_none() {
|
|
433
|
+
let tlas = self.tlas.as_ref().unwrap();
|
|
434
|
+
let instance_buf = self.tlas_instance_data_buffer.as_ref().unwrap();
|
|
435
|
+
self.wsrc_bake_hw_bg_cache = Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
436
|
+
label: Some("wsrc_bake_hw_bg"),
|
|
437
|
+
layout: self.wsrc_bake_hw_layout.as_ref().unwrap(),
|
|
438
|
+
entries: &[
|
|
439
|
+
wgpu::BindGroupEntry { binding: 0, resource: self.wsrc_bake_uniform.as_entire_binding() },
|
|
440
|
+
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&self.shadow_map.depth_views[0]) },
|
|
441
|
+
wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(&self.shadow_map.depth_views[1]) },
|
|
442
|
+
wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(&self.shadow_map.depth_views[2]) },
|
|
443
|
+
wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::Sampler(&self.shadow_map.sampler) },
|
|
444
|
+
wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::TextureView(&self.wsrc_atlas_view) },
|
|
445
|
+
wgpu::BindGroupEntry { binding: 6, resource: tlas.as_binding() },
|
|
446
|
+
wgpu::BindGroupEntry { binding: 7, resource: instance_buf.as_entire_binding() },
|
|
447
|
+
wgpu::BindGroupEntry { binding: 8, resource: wgpu::BindingResource::TextureView(&self.mesh_card_radiance_view) },
|
|
448
|
+
wgpu::BindGroupEntry { binding: 9, resource: wgpu::BindingResource::Sampler(&self.mesh_card_atlas_sampler) },
|
|
449
|
+
],
|
|
450
|
+
}));
|
|
451
|
+
}
|
|
452
|
+
} else if self.wsrc_bake_bg_cache.is_none() {
|
|
453
|
+
self.wsrc_bake_bg_cache = Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
454
|
+
label: Some("wsrc_bake_bg"),
|
|
455
|
+
layout: &self.wsrc_bake_layout,
|
|
456
|
+
entries: &[
|
|
457
|
+
wgpu::BindGroupEntry { binding: 0, resource: self.wsrc_bake_uniform.as_entire_binding() },
|
|
458
|
+
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&self.shadow_map.depth_views[0]) },
|
|
459
|
+
wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(&self.shadow_map.depth_views[1]) },
|
|
460
|
+
wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(&self.shadow_map.depth_views[2]) },
|
|
461
|
+
wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::Sampler(&self.shadow_map.sampler) },
|
|
462
|
+
wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::TextureView(&self.wsrc_atlas_view) },
|
|
463
|
+
],
|
|
464
|
+
}));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
let cam = self.current_camera_world_pos();
|
|
468
|
+
|
|
469
|
+
// At most ONE cascade per frame. Besides amortizing the cost,
|
|
470
|
+
// this fixes a params-aliasing bug: all cascades share
|
|
471
|
+
// `wsrc_bake_uniform`, and `queue.write_buffer` applies every
|
|
472
|
+
// write before any of this frame's dispatches execute — baking
|
|
473
|
+
// two cascades in one frame made both dispatches read the last
|
|
474
|
+
// cascade's params (wrong extent + wrong atlas slice flag).
|
|
475
|
+
let mut baked_one = false;
|
|
476
|
+
for c in 0..WSRC_CASCADE_COUNT as usize {
|
|
477
|
+
if self.wsrc_built[c] || baked_one {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
let extent = WSRC_CASCADE_EXTENTS[c];
|
|
481
|
+
let cell = extent / WSRC_GRID_RES as f32;
|
|
482
|
+
let origin = [
|
|
483
|
+
(cam[0] / cell).round() * cell,
|
|
484
|
+
(cam[1] / cell).round() * cell,
|
|
485
|
+
(cam[2] / cell).round() * cell,
|
|
486
|
+
];
|
|
487
|
+
self.wsrc_origin[c] = origin;
|
|
488
|
+
|
|
489
|
+
let params = WsrcBakeParams {
|
|
490
|
+
sun_dir: sun_dir_ws,
|
|
491
|
+
sun_color,
|
|
492
|
+
sky_color,
|
|
493
|
+
grid: [origin[0], origin[1], origin[2], extent],
|
|
494
|
+
shadow_vps,
|
|
495
|
+
shadow_splits,
|
|
496
|
+
flags: [
|
|
497
|
+
0.002,
|
|
498
|
+
if shadows_enabled { 1.0 } else { 0.0 },
|
|
499
|
+
c as f32,
|
|
500
|
+
0.0,
|
|
501
|
+
],
|
|
502
|
+
ground_albedo: [
|
|
503
|
+
self.gi_scene_avg_albedo[0],
|
|
504
|
+
self.gi_scene_avg_albedo[1],
|
|
505
|
+
self.gi_scene_avg_albedo[2],
|
|
506
|
+
0.0,
|
|
507
|
+
],
|
|
508
|
+
};
|
|
509
|
+
self.queue.write_buffer(
|
|
510
|
+
&self.wsrc_bake_uniform,
|
|
511
|
+
0,
|
|
512
|
+
bytemuck::bytes_of(¶ms),
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
|
516
|
+
label: Some(if use_hw { "wsrc_bake_hw_pass" } else { "wsrc_bake_pass" }),
|
|
517
|
+
timestamp_writes: None,
|
|
518
|
+
});
|
|
519
|
+
if use_hw {
|
|
520
|
+
pass.set_pipeline(self.wsrc_bake_hw_pipeline.as_ref().unwrap());
|
|
521
|
+
pass.set_bind_group(0, self.wsrc_bake_hw_bg_cache.as_ref().unwrap(), &[]);
|
|
522
|
+
} else {
|
|
523
|
+
pass.set_pipeline(&self.wsrc_bake_pipeline);
|
|
524
|
+
pass.set_bind_group(0, self.wsrc_bake_bg_cache.as_ref().unwrap(), &[]);
|
|
525
|
+
}
|
|
526
|
+
// One workgroup per probe in this cascade (16³),
|
|
527
|
+
// 10×10 threads per workgroup (padded octel).
|
|
528
|
+
pass.dispatch_workgroups(WSRC_GRID_RES, WSRC_GRID_RES, WSRC_GRID_RES);
|
|
529
|
+
drop(pass);
|
|
530
|
+
|
|
531
|
+
self.wsrc_built[c] = true;
|
|
532
|
+
self.wsrc_last_sun_dir[c] = ld;
|
|
533
|
+
self.wsrc_last_sun_color[c] = [sun_color[0], sun_color[1], sun_color[2]];
|
|
534
|
+
self.wsrc_last_sky_color[c] = [sky_color[0], sky_color[1], sky_color[2]];
|
|
535
|
+
baked_one = true;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/// Ticket 014 V1 — bake per-mesh unsigned distance fields via
|
|
540
|
+
/// the compute pipeline. Drains `scene.pending_sdf_bakes` with a
|
|
541
|
+
/// per-frame budget; expensive workload (O(voxels × triangles)
|
|
542
|
+
/// per mesh), so the rate-limit keeps first-frame stutter
|
|
543
|
+
/// bounded. Static scenes amortise and never re-bake.
|
|
544
|
+
///
|
|
545
|
+
/// Ticket 022 — after each dispatch, encode a copy_texture_to_buffer
|
|
546
|
+
/// against a fresh staging buffer and stash (hash, buffer) on
|
|
547
|
+
/// `sdf_cache_writes`. The frame's main submit picks up the copies
|
|
548
|
+
/// alongside the bake; `flush_sdf_cache_writes` then maps and
|
|
549
|
+
/// persists each buffer to the on-disk cache so the next launch
|
|
550
|
+
/// hits the load path in scene.rs and skips the bake entirely.
|
|
551
|
+
pub(super) fn bake_pending_sdfs(
|
|
552
|
+
&mut self,
|
|
553
|
+
scene: &mut crate::scene::SceneGraph,
|
|
554
|
+
encoder: &mut wgpu::CommandEncoder,
|
|
555
|
+
) {
|
|
556
|
+
if !self.hw_rt_enabled || scene.pending_sdf_bakes.is_empty() {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const SDF_BAKE_MAX_PER_FRAME: usize = 8;
|
|
560
|
+
let take = scene.pending_sdf_bakes.len().min(SDF_BAKE_MAX_PER_FRAME);
|
|
561
|
+
let pending: Vec<f64> = scene.pending_sdf_bakes.drain(..take).collect();
|
|
562
|
+
|
|
563
|
+
for handle in pending {
|
|
564
|
+
let (sdf_tex, sdf_view, vb_ptr, ib_ptr, bmin, bmax, index_count, mesh_hash) = {
|
|
565
|
+
let Some(node) = scene.nodes.get(handle) else { continue; };
|
|
566
|
+
let Some(sdf_tex) = node.mesh_sdf.as_ref() else { continue; };
|
|
567
|
+
let Some(sdf_view) = node.mesh_sdf_view.as_ref() else { continue; };
|
|
568
|
+
let Some(vb) = node.gpu_vb.as_ref() else { continue; };
|
|
569
|
+
let Some(ib) = node.gpu_ib.as_ref() else { continue; };
|
|
570
|
+
(
|
|
571
|
+
sdf_tex.clone(),
|
|
572
|
+
sdf_view.clone(),
|
|
573
|
+
vb.clone(),
|
|
574
|
+
ib.clone(),
|
|
575
|
+
node.bounds_min,
|
|
576
|
+
node.bounds_max,
|
|
577
|
+
node.gpu_index_count,
|
|
578
|
+
node.mesh_hash,
|
|
579
|
+
)
|
|
580
|
+
};
|
|
581
|
+
if index_count == 0 {
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
let tri_count = index_count / 3;
|
|
585
|
+
let params = SdfBakeParams {
|
|
586
|
+
aabb_min: [bmin[0], bmin[1], bmin[2], 0.0],
|
|
587
|
+
aabb_max: [bmax[0], bmax[1], bmax[2], 0.0],
|
|
588
|
+
counts: [tri_count, MESH_SDF_RES, 0, 0],
|
|
589
|
+
};
|
|
590
|
+
self.queue.write_buffer(
|
|
591
|
+
&self.sdf_bake_uniform,
|
|
592
|
+
0,
|
|
593
|
+
bytemuck::bytes_of(¶ms),
|
|
594
|
+
);
|
|
595
|
+
let bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
596
|
+
label: Some("sdf_bake_bg"),
|
|
597
|
+
layout: &self.sdf_bake_layout,
|
|
598
|
+
entries: &[
|
|
599
|
+
wgpu::BindGroupEntry { binding: 0, resource: self.sdf_bake_uniform.as_entire_binding() },
|
|
600
|
+
wgpu::BindGroupEntry { binding: 1, resource: vb_ptr.as_entire_binding() },
|
|
601
|
+
wgpu::BindGroupEntry { binding: 2, resource: ib_ptr.as_entire_binding() },
|
|
602
|
+
wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(&sdf_view) },
|
|
603
|
+
],
|
|
604
|
+
});
|
|
605
|
+
{
|
|
606
|
+
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
|
607
|
+
label: Some("sdf_bake_pass"),
|
|
608
|
+
timestamp_writes: None,
|
|
609
|
+
});
|
|
610
|
+
pass.set_pipeline(&self.sdf_bake_pipeline);
|
|
611
|
+
pass.set_bind_group(0, &bg, &[]);
|
|
612
|
+
pass.dispatch_workgroups(MESH_SDF_RES / 4, MESH_SDF_RES / 4, MESH_SDF_RES / 4);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Ticket 022 — schedule a readback against the freshly-baked
|
|
616
|
+
// texture so the next launch can skip the bake. We only do
|
|
617
|
+
// this when scene.rs computed a hash (it always does, but
|
|
618
|
+
// skip defensively); padded staging size is bound by
|
|
619
|
+
// wgpu's COPY_BYTES_PER_ROW alignment.
|
|
620
|
+
if let Some(hash) = mesh_hash {
|
|
621
|
+
let row_padded = ((MESH_SDF_RES * 4 + 255) & !255) as u64;
|
|
622
|
+
let staging = self.device.create_buffer(&wgpu::BufferDescriptor {
|
|
623
|
+
label: Some("sdf_cache_readback"),
|
|
624
|
+
size: row_padded * (MESH_SDF_RES as u64) * (MESH_SDF_RES as u64),
|
|
625
|
+
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
|
626
|
+
mapped_at_creation: false,
|
|
627
|
+
});
|
|
628
|
+
encoder.copy_texture_to_buffer(
|
|
629
|
+
wgpu::TexelCopyTextureInfo {
|
|
630
|
+
texture: &sdf_tex,
|
|
631
|
+
mip_level: 0,
|
|
632
|
+
origin: wgpu::Origin3d::ZERO,
|
|
633
|
+
aspect: wgpu::TextureAspect::All,
|
|
634
|
+
},
|
|
635
|
+
wgpu::TexelCopyBufferInfo {
|
|
636
|
+
buffer: &staging,
|
|
637
|
+
layout: wgpu::TexelCopyBufferLayout {
|
|
638
|
+
offset: 0,
|
|
639
|
+
bytes_per_row: Some(row_padded as u32),
|
|
640
|
+
rows_per_image: Some(MESH_SDF_RES),
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
wgpu::Extent3d {
|
|
644
|
+
width: MESH_SDF_RES,
|
|
645
|
+
height: MESH_SDF_RES,
|
|
646
|
+
depth_or_array_layers: MESH_SDF_RES,
|
|
647
|
+
},
|
|
648
|
+
);
|
|
649
|
+
self.sdf_cache_writes.push((hash, staging));
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|