diff --git a/CLAUDE.md b/CLAUDE.md index 330b3ba..eae4246 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -82,7 +82,7 @@ native/ Rust implementations (one crate per platform) ios/ Metal + UIKit + Core Audio tvos/ Metal + UIKit + GCController windows/ DirectX 12 + Win32 + WASAPI - linux/ Vulkan/OpenGL + X11 + PulseAudio + linux/ Vulkan/OpenGL + X11 + ALSA android/ Vulkan/OpenGL ES + NativeActivity + AAudio web/ WebGPU/WebGL + Canvas + Web Audio API (WASM via wasm-pack) visionos/ Metal + UIKit-style shell (wgpu; iOS/tvOS-family port) diff --git a/README.md b/README.md index 38bcba0..aeb91b7 100644 --- a/README.md +++ b/README.md @@ -141,8 +141,8 @@ native/ Rust implementations macos/ Metal + AppKit + Core Audio ios/ Metal + UIKit + Core Audio tvos/ Metal + UIKit + GCController - windows/ DirectX 12 + Win32 + XAudio2 - linux/ Vulkan/OpenGL + X11/Wayland + PulseAudio + windows/ DirectX 12 + Win32 + WASAPI + linux/ Vulkan/OpenGL + X11 + ALSA android/ Vulkan/OpenGL ES + NativeActivity + AAudio web/ WebGPU/WebGL + Canvas + Web Audio (WASM) diff --git a/docs/perf/lumen-roadmap.md b/docs/perf/lumen-roadmap.md index 4dca877..de62d3a 100644 --- a/docs/perf/lumen-roadmap.md +++ b/docs/perf/lumen-roadmap.md @@ -38,12 +38,17 @@ deferred), so numbering skips directly from 013/014 to 016. ## Platform matrix -| Platform | SW path | HW path | Notes | +> **As-built correction (2026-07):** the "HW path" column below describes +> *capability*, not the default. The HW ray-query path is **opt-in** +> everywhere (`BLOOM_HW_GI=1` / `BLOOM_PT` / `--pt`); SW GI is the shipping +> default on every platform. See the as-built note under the table. + +| Platform | SW path | HW path (opt-in) | Notes | |---|---|---|---| -| macOS | yes (Phase 1a) | yes after 007-prep (Metal ray-query) | Unblocked by wgpu upgrade | -| iOS | yes | yes after 007-prep | Same | -| tvOS | yes | yes after 007-prep | Same | -| Windows | yes | yes (DXR) | Works today post-upgrade | +| macOS | yes (Phase 1a) | capable after 007-prep (Metal ray-query) | Unblocked by wgpu upgrade | +| iOS | yes | capable after 007-prep | Same | +| tvOS | yes | capable after 007-prep | Same | +| Windows | yes | capable (DXR) | Works post-upgrade; opt-in only | | Linux | yes | adapter-gated (Vulkan ray-query) | Runtime feature check; SW fallback | | Android | yes | adapter-gated | Most Android GPUs lack RT; SW expected in practice | | Web | yes | never | No WebGPU RT spec; SW-only permanently | diff --git a/docs/pt/pt-roadmap.md b/docs/pt/pt-roadmap.md index c200dcc..c3145ff 100644 --- a/docs/pt/pt-roadmap.md +++ b/docs/pt/pt-roadmap.md @@ -132,5 +132,7 @@ ray query by default (`BLOOM_FORCE_SW_GI` disables it). | PT-1 | Progressive megakernel, accumulation, FFI mode plumbing, shooter toggle | | PT-2 | Geometry megabuffer, interpolated hit attributes, texture binding array, GGX+MIS, emissive hits | | PT-3 | Realtime mode: temporal reprojection + à-trous, half-res option, perf gate | +| PT-3b | SVGF denoiser for realtime mode — landed 2026-07-14 (`PT-3b-svgf-denoiser.md`) | | PT-4 | ReSTIR DI (experimental flag) | | PT-5 | Settings/editor/gameplay integration, fallback matrix, reference-diff CI hook | +| PT-6/7/8 | Skinned meshes in the TLAS, real skinned motion vectors, golden-image correctness oracle — landed 2026-07-14 (`PT-6-7-8-skinned-tlas-motion-oracle.md`) | diff --git a/native/shared/src/audio/render.rs b/native/shared/src/audio/render.rs index a40f218..08e2375 100644 --- a/native/shared/src/audio/render.rs +++ b/native/shared/src/audio/render.rs @@ -335,6 +335,15 @@ impl Reverb { } } +/// Hard cap on simultaneously-playing voices. The render thread NEVER grows +/// `voices` past this — a heap reallocation on the audio thread is a glitch +/// (and a priority inversion against the malloc lock). At the cap, a new play +/// steals the quietest voice instead. `voices` is preallocated to this size. +const MAX_VOICES: usize = 64; +/// Same discipline for music tracks. Practically 1-2 ever play at once; the +/// cap only exists so the render thread can never reallocate. +const MAX_MUSIC: usize = 4; + pub struct AudioRenderer { rx: Consumer, voices: Vec, @@ -357,8 +366,8 @@ impl AudioRenderer { pub(super) fn new(rx: Consumer) -> Self { Self { rx, - voices: Vec::with_capacity(64), - music: Vec::with_capacity(4), + voices: Vec::with_capacity(MAX_VOICES), + music: Vec::with_capacity(MAX_MUSIC), master: 1.0, listener_pos: [0.0; 3], listener_forward: [0.0, 0.0, -1.0], @@ -380,6 +389,21 @@ impl AudioRenderer { sound_id, voice_id, data, volume, spatial, looping, ref_dist, max_dist, rolloff, pitch, bus, send, lowpass, } => { + if self.voices.len() >= MAX_VOICES { + // Voice steal: drop the quietest non-stopping voice (least + // audible) so the push below can never reallocate. O(n) over + // <= MAX_VOICES, no allocation. `swap_remove` is O(1) and + // voice order does not affect the mix. + let victim = self.voices.iter().enumerate() + .filter(|(_, v)| !v.stopping) + .min_by(|(_, a), (_, b)| { + a.volume.partial_cmp(&b.volume) + .unwrap_or(std::cmp::Ordering::Equal) + }) + .map(|(i, _)| i) + .unwrap_or(0); + self.voices.swap_remove(victim); + } self.voices.push(Voice { sound_id, voice_id, data, frame_pos: 0.0, volume, spatial, looping, @@ -419,6 +443,13 @@ impl AudioRenderer { ended: false, }, }; + if self.music.len() >= MAX_MUSIC { + // Evict the oldest track so the push can't reallocate. The + // evicted track's shared flag is cleared so its control-side + // handle reports stopped. + let old = self.music.remove(0); + old.shared.playing.store(false, Ordering::Relaxed); + } self.music.push(MusicVoice { music_id, samples, shared, volume, looping, consumed: 0 }); } Cmd::StopMusic { music_id } => { @@ -811,3 +842,51 @@ impl AudioRenderer { }); } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::audio::spsc; + + fn dummy_sound() -> Arc { + Arc::new(SoundData { samples: vec![0.0; 16], sample_rate: 44_100, channels: 1 }) + } + + fn play(id: u64, volume: f32) -> Cmd { + Cmd::PlaySound { + sound_id: id, voice_id: id, data: dummy_sound(), volume, + spatial: None, looping: false, + ref_dist: 1.0, max_dist: 0.0, rolloff: 1.0, pitch: 1.0, + bus: bus::SFX, send: 0.0, lowpass: 0.0, + } + } + + // RT-safety: the render thread must never grow `voices` past its + // preallocated capacity — a malloc on the audio thread is a glitch. + #[test] + fn voices_never_exceed_cap_or_reallocate() { + let (_tx, rx) = spsc::channel::(8); + let mut r = AudioRenderer::new(rx); + let cap0 = r.voices.capacity(); + for i in 0..(MAX_VOICES as u64 * 4) { + r.apply(play(i, 0.5 + (i % 7) as f32 * 0.01)); + assert!(r.voices.len() <= MAX_VOICES); + } + assert_eq!(r.voices.len(), MAX_VOICES); + assert_eq!(r.voices.capacity(), cap0, "voices reallocated on the audio thread"); + } + + // At the cap, a new play steals the QUIETEST voice, not an arbitrary one. + #[test] + fn voice_steal_drops_the_quietest() { + let (_tx, rx) = spsc::channel::(8); + let mut r = AudioRenderer::new(rx); + for i in 0..(MAX_VOICES as u64 - 1) { r.apply(play(i, 1.0)); } + r.apply(play(9999, 0.01)); // quiet marker, brings us to the cap + assert_eq!(r.voices.len(), MAX_VOICES); + r.apply(play(10_000, 1.0)); // must evict the quiet marker + assert!(!r.voices.iter().any(|v| v.sound_id == 9999), + "the quietest voice should have been stolen"); + assert_eq!(r.voices.len(), MAX_VOICES); + } +} diff --git a/native/shared/src/renderer/mod.rs b/native/shared/src/renderer/mod.rs index adefe7b..640a595 100644 --- a/native/shared/src/renderer/mod.rs +++ b/native/shared/src/renderer/mod.rs @@ -87,6 +87,7 @@ use types::{ Uniforms2D, Uniforms3D, DirLight, PointLight, LightingUniforms, DrawCall2D, DrawCall3D, FNV_OFFSET, fnv1a_bytes, }; +use types::*; // pass/compute uniform param structs (EN-052 split) @@ -121,607 +122,6 @@ use types::{ // clamp if fireflies appear (high-luminance pixels with few samples). // At 64 samples per mip we haven't seen them in the test HDRs. -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct PrefilterUniforms { - /// x = roughness (∈ [0, 1]), y = sample count, zw = mip resolution - params: [f32; 4], -} - -// ============================================================ -// Sky / equirectangular HDR background shader -// ============================================================ -// -// Renders a fullscreen triangle with z=1 (far plane) and samples the -// environment map by the world-space view direction reconstructed from -// inverse VP. Tone-maps with the same ACES curve the rest of the -// renderer uses so the background blends seamlessly with lit -// geometry. Always overwrites depth — the 3D opaque pass drawn after -// will occlude wherever it has actual geometry. - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct SkyUniforms { - /// Camera right vector × tan(fovy/2) × aspect — pre-scaled so the - /// fragment shader just multiplies by NDC.x to get the horizontal - /// offset from the forward direction. - /// `.w` = camera world position X (see below). - right: [f32; 4], - /// Camera up vector × tan(fovy/2). `.w` = camera world position Y. - up: [f32; 4], - /// Camera forward unit vector. `.w` = camera world position Z. - /// - /// The camera POSITION rides in the spare `.w` lanes of the three basis - /// vectors. The sky pass never needed it — a sky at infinity only cares - /// which way you are looking — but the cloud deck is anchored in world - /// space so that its shadow lands under it, and that makes the camera's - /// position load-bearing. Packing it here keeps the bind group unchanged. - forward: [f32; 4], - /// x = intensity multiplier, y = elapsed seconds (the cloud deck's clock); - /// zw padding. - intensity: [f32; 4], - /// Cloud deck: x = shadow strength, y = deck height (m), z = feature scale - /// (noise units per metre), w = drift speed (m/s). Same vec4 the world - /// materials get, so the sky and the ground cannot disagree. - cloud: [f32; 4], - /// xy = wind direction in the XZ plane — the clouds drift downwind, the - /// same way the grass is leaning. - wind: [f32; 4], -} - -// EN-005 Phase 2 — uniforms for the procedural sky path. -// -// `SkyViewParams` drives the sky-view LUT compute shader (recomputed -// when the sun moves). `SunUniforms` drives the per-frame sun-disk -// composite in the procedural sky fragment shader. - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct SkyViewParams { - /// xyz = sun direction (world space, unit), w = sun intensity scale. - sun: [f32; 4], - /// x = rayleigh density mult, y = mie density mult, z = ground albedo, w unused. - knobs: [f32; 4], -} - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct SunUniforms { - /// xyz = sun direction (unit), w = sun intensity. - sun: [f32; 4], - /// x = sun angular radius (rad), y = limb darkening (0..1), zw unused. - params: [f32; 4], -} - -// EN-005 V2 — aerial-perspective compute uniforms. -// -// Driven each frame from the renderer (camera + sun + atmosphere -// knobs). Layout must mirror `AerialParams` in -// AERIAL_PERSPECTIVE_SHADER_WGSL exactly. -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct AerialParams { - /// xyz = camera world position (engine units, metres assumed), - /// w = max distance the LUT covers (km). - cam_pos: [f32; 4], - /// World→clip inverse, used per-voxel to reconstruct view rays - /// from NDC. - inv_vp: [[f32; 4]; 4], - /// xyz = sun direction (unit), w = sun intensity scalar. - sun: [f32; 4], - /// x = rayleigh density mult, y = mie density mult, zw unused. - knobs: [f32; 4], -} - - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -pub(super) struct HizLinearizeParams { - /// xy = inv_size, z = proj[2][2], w = proj[3][2] - params: [f32; 4], - /// xy = mip-0 size, zw unused - size: [u32; 4], -} - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -pub(super) struct HizDownsampleParams { - /// xy = dst-mip size, zw unused - size: [u32; 4], -} - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -pub(super) struct SsaoParams { - /// xy = inv_size (1/half_w, 1/half_h), z = radius (world units), - /// w = strength - params: [f32; 4], - /// x = proj[0][0], y = proj[1][1], z = proj[2][0] (TAA jitter), - /// w = proj[2][1] (TAA jitter). Column-major: proj[col][row]. - proj_row01: [f32; 4], - /// x = proj[2][2], y = proj[3][2], z = 1/proj[0][0], w = 1/proj[1][1] - proj_z: [f32; 4], - /// Light direction in view space (xyz, w unused). For contact shadows. - light_dir_vs: [f32; 4], - /// x = half-res width, y = half-res height, z = frame phase - /// (`frame_index % 4`), w = "force-refresh" flag (non-zero on - /// first few frames, resize, or any host-side history invalidation). - size: [u32; 4], - /// x = temporal blend alpha (≈0.25 steady, 4-frame EMA). - /// y = per-frame Halton-5 rotation of the direction basis - /// (uncorrelated with TAA's Halton-2/3 pixel jitter). - /// zw unused. - temporal: [f32; 4], -} - -// ============================================================ -// SSAO Bilateral Blur post-process -// ============================================================ - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -pub(super) struct SsaoBlurParams { - /// xy = texel_size (of the half-res SSAO RT), z = depth_sigma, w = unused. - params: [f32; 4], -} - -// ============================================================ -// Depth of Field (DoF) post-process -// ============================================================ - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct DofParams { - /// x = focus_distance, y = aperture, z = max_blur_radius (UV), w = unused - params: [f32; 4], - /// Inverse projection matrix — used to linearize depth. - inv_proj: [[f32; 4]; 4], -} - -// ============================================================ -// Motion Blur post-process -// ============================================================ -// -// Reads the TAA/DoF output (color) and the per-pixel velocity buffer. -// For each pixel, samples 8 taps along the velocity direction with a -// tent (linear) weight, blending them into a directionally-blurred -// result. Default OFF — no perf cost when disabled. - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct MotionBlurParams { - /// x = strength, y = max_blur (UV), zw = unused. - params: [f32; 4], -} - -// ============================================================ -// Screen-Space Subsurface Scattering (SSS) post-process -// ============================================================ -// -// Single-pass 9-tap disc blur applied after the motion blur pass -// (pre-composite). Uses a chromatic diffusion profile where red -// scatters furthest (kernel width 1×), green 0.5×, blue 0.25×, -// simulating the spectral absorption of skin/wax/leaves. -// Depth-guided bilateral weighting prevents color bleeding across -// depth discontinuities (hard edges stay sharp). Default OFF. - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct SssParams { - /// x = strength, y = width, z = falloff, w = unused. - params: [f32; 4], -} - -// ============================================================ -// SSGI (Screen-Space Global Illumination) post-process -// ============================================================ - -// Ticket 007a: per-pass uniform params for the screen-probe SSGI chain. - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct ProbePlaceParams { - inv_view: [[f32; 4]; 4], - /// x = proj[0][0], y = proj[1][1], z = proj[2][0], w = proj[2][1] - proj_row01: [f32; 4], - /// x = half_w, y = half_h, z = grid_w, w = grid_h - size: [u32; 4], - /// x = frame_index, y = tile_size (16.0), zw unused - params: [f32; 4], -} - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct ProbeTraceParams { - view: [[f32; 4]; 4], - proj: [[f32; 4]; 4], - inv_view: [[f32; 4]; 4], - proj_row01: [f32; 4], - size: [u32; 4], - /// x = frame_index, y = intensity, z = max_march_t (world units), - /// w = firefly luma cap. - params: [f32; 4], - /// Ticket 007b HW path — sun direction in world space (xyz) + - /// intensity (w). Ignored by the SW-HiZ shader, consumed by HW + SDF. - sun_dir: [f32; 4], - /// Sun colour (xyz), reserved (w). Used for analytical NdotL at hit. - sun_color: [f32; 4], - /// Sky dome colour (xyz), reserved (w). Flat analytic sky for - /// up-facing hits where the ray hits a surface with an up normal. - sky_color: [f32; 4], - /// Ticket 014 V3 — clipmap origin (xyz) + full extent (w). Used - /// by the SDF sphere-trace variant to map world-space march - /// positions into clipmap sample UVs. - clipmap: [f32; 4], - /// Ticket 014 V6/V13 — WSRC cascade cubes. Each entry is - /// (origin xyz, extent w). Cascades are ordered near→far; miss - /// paths pick the smallest cascade whose cube contains the - /// ray-terminal position. `extent = 0.0` marks an unbaked - /// cascade (shader falls through to the next one). HW + Hi-Z - /// carry these for uniform-buffer size parity; Hi-Z ignores - /// them. - wsrc_cascades: [[f32; 4]; 3], -} - -/// PT-2 — fixed size of the kernel's texture binding array. Real texture -/// indices at or above this clamp to 0 (white); unused tail slots are -/// padded with the white texture so no PARTIALLY_BOUND feature is needed. -const PT_MAX_TEXTURES: usize = 256; - -/// Uniform for the path-tracing megakernel — WGSL mirror `PtParams` in -/// shaders/pt.rs. Field-for-field, 16-byte aligned, 672 bytes. -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct PtParamsCpu { - inv_vp: [[f32; 4]; 4], - /// PT-3 — previous frame's unjittered VP (transposed like inv_vp) - /// for temporal history reprojection in realtime mode. - prev_vp: [[f32; 4]; 4], - /// xyz camera world pos, w unused. - cam_pos: [f32; 4], - /// xyz unit vector toward the sun, w unused. - sun_dir: [f32; 4], - /// rgb premultiplied by intensity, w unused. - sun_color: [f32; 4], - /// rgb ambient-derived sky tint, w unused. - sky_color: [f32; 4], - /// x/y = TRACE grid dims (half in realtime mode), z=frame_index, - /// w=accum_count. - size: [u32; 4], - /// x=mode (1 progressive / 2 realtime), y=max_bounces, - /// z=point_light_count (≤16), w=debug view (BLOOM_PT_DEBUG). - cfg: [f32; 4], - /// PT-3 half-res: x/y = full G-buffer dims. z = 1 → hybrid sun - /// (shadow cascades instead of traced sun rays). w unused. - ext: [u32; 4], - /// Raster shadow cascade VPs, transposed at upload like inv_vp. - shadow_vps: [[[f32; 4]; 4]; 3], - /// 16 lights × (pos_range vec4, color_int vec4). - lights: [[f32; 4]; 32], -} - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct ProbeTemporalParams { - /// x = alpha (EMA), y = force_refresh (1→alpha=1), z = grid_w (f32), w = grid_h (f32) - params: [f32; 4], -} - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct ProbeResolveParams { - inv_view: [[f32; 4]; 4], - proj_row01: [f32; 4], - /// x = half_w, y = half_h, z = grid_w, w = grid_h - size: [u32; 4], - /// x = tile_size (16.0), y = intensity, zw unused - params: [f32; 4], -} - -/// On-GPU `ProbeHeader` layout (must match PROBE_HELPERS_WGSL's struct). -/// 32 bytes per probe. -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct ProbeHeaderCpu { - world_pos: [f32; 4], - normal: [f32; 4], -} - -/// Ticket 013 V3 — CardCaptureParams. ortho_vp + base_color + emissive. -/// 96 bytes; we allocate 128 for uniform-alignment headroom. -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct CardCaptureParams { - ortho_vp: [[f32; 4]; 4], - base_color: [f32; 4], // rgb = factor, w = has_base_texture (0/1) - emissive: [f32; 4], // rgb = emissive_factor, w = has_emissive_texture (0/1) -} - -/// Ticket 013 V2 — orthographic projection for the 6 signed axes. -/// `face_axis` encoding: -/// 0 → +X, 1 → -X, 2 → +Y, 3 → -Y, 4 → +Z, 5 → -Z. -/// For each face we pick the two orthogonal AABB axes and map them -/// to clip-space [-1, +1]. The ±pair for each axis differ only in -/// the sign of the "u" clip axis so that when the HW shader picks -/// axis N at hit and projects the hit into card UV, the UV lines up -/// with the mesh geometry as seen FROM that face. -fn build_card_ortho_v2(face_axis: u32, bmin: [f32; 3], bmax: [f32; 3]) -> [[f32; 4]; 4] { - let wx = (bmax[0] - bmin[0]).max(1e-4); - let wy = (bmax[1] - bmin[1]).max(1e-4); - let wz = (bmax[2] - bmin[2]).max(1e-4); - let cx = bmin[0] + bmax[0]; - let cy = bmin[1] + bmax[1]; - let cz = bmin[2] + bmax[2]; - match face_axis { - // +X → project onto YZ; clip.x = y, clip.y = z. - 0 => [ - [0.0, 0.0, 0.0, 0.0], - [2.0/wy, 0.0, 0.0, 0.0], - [0.0, 2.0/wz, 0.0, 0.0], - [-cy/wy, -cz/wz, 0.0, 1.0], - ], - // -X → flip u so the -X view is the mirror of +X. clip.x = -y, clip.y = z. - 1 => [ - [0.0, 0.0, 0.0, 0.0], - [-2.0/wy, 0.0, 0.0, 0.0], - [0.0, 2.0/wz, 0.0, 0.0], - [cy/wy, -cz/wz, 0.0, 1.0], - ], - // +Y → project onto XZ; clip.x = x, clip.y = z. - 2 => [ - [2.0/wx, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0], - [0.0, 2.0/wz, 0.0, 0.0], - [-cx/wx, -cz/wz, 0.0, 1.0], - ], - // -Y → flip u; clip.x = -x, clip.y = z. - 3 => [ - [-2.0/wx, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0], - [0.0, 2.0/wz, 0.0, 0.0], - [cx/wx, -cz/wz, 0.0, 1.0], - ], - // +Z → project onto XY; clip.x = x, clip.y = y. - 4 => [ - [2.0/wx, 0.0, 0.0, 0.0], - [0.0, 2.0/wy, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0], - [-cx/wx, -cy/wy, 0.0, 1.0], - ], - // -Z → flip u; clip.x = -x, clip.y = y. - _ => [ - [-2.0/wx, 0.0, 0.0, 0.0], - [0.0, 2.0/wy, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0], - [cx/wx, -cy/wy, 0.0, 1.0], - ], - } -} - -/// Ticket 014 — per-mesh SDF bake uniform. -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct SdfBakeParams { - aabb_min: [f32; 4], - aabb_max: [f32; 4], - /// x = triangle_count, y = sdf_resolution (32), zw unused - counts: [u32; 4], -} - -/// Fullscreen-lag fix — an in-flight amortized scene-SDF-clipmap bake. -/// A job bakes `SCENE_SDF_CLIPMAP_LAYERS_PER_FRAME` voxel Z-layers per -/// frame into the staging texture; when the last slice lands the staging -/// contents are copied over the live clipmap and the origin flips -/// atomically, so traces never observe a half-baked field. The bind -/// group keeps the job's transient buffers alive. -struct SdfClipmapBakeJob { - origin: [f32; 3], - aabb_min: [f32; 4], - aabb_max: [f32; 4], - uniform: wgpu::Buffer, - bind_group: wgpu::BindGroup, - next_z: u32, -} - -/// Ticket 014 V6 — uniform for `WSRC_BAKE_WGSL`. Analytic sun × shadow -/// + analytic sky computed per probe-octel. Shadow VPs + splits + -/// flags mirror CARD_LIGHT_WGSL so the shader can re-use the same -/// cascade-sampling helper. -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct WsrcBakeParams { - sun_dir: [f32; 4], - sun_color: [f32; 4], - sky_color: [f32; 4], - /// xyz = WSRC cube origin, w = full extent. - grid: [f32; 4], - /// Cascade 0..2 VPs. Only cascade 2 is actually sampled by V6 - /// (widest cascade covers the whole 120 m cube), but we carry - /// all three to keep the uniform layout identical to the card- - /// lighting params. - shadow_vps: [[[f32; 4]; 4]; 3], - /// xyz = view-space split distances (unused by V6 because it - /// always samples cascade 2), w = 0. - shadow_splits: [f32; 4], - /// x = shadow bias, y = shadows_enabled (0/1), zw unused. - flags: [f32; 4], - /// EN-023 — xyz = scene-average albedo (mean of card-instance flat - /// albedos) for the SW bake's ground-bounce term; w unused. The HW - /// bake ignores it (it traces real geometry). - ground_albedo: [f32; 4], -} - -/// Uniform struct for the card-lighting compute pass. Matches -/// CARD_LIGHT_WGSL. -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct CardLightParams { - sun_dir: [f32; 4], - sun_color: [f32; 4], - sky_color: [f32; 4], - /// [atlas_size, slot_size, slots_per_row, active_slot_count] - atlas_info: [u32; 4], - /// Shadow cascade VPs (3× mat4) — identity when shadows disabled. - shadow_vps: [[[f32; 4]; 4]; 3], - /// xyz = view-space split distances for cascades 0/1/2, w = 0. - shadow_splits: [f32; 4], - /// Camera view matrix — needed to convert card-texel world pos - /// into view-space Z for cascade selection. - view_matrix: [[f32; 4]; 4], - /// x = shadow bias, y = shadows_enabled (0/1), zw unused. - flags: [f32; 4], -} - -/// Ticket 013 V3 — per-slot metadata consumed by `card_light_pass`. -/// Baked at capture time; carries enough state for the lighting -/// shader to reconstruct each texel's world-space position and query -/// the shadow cascade at that point. 128 bytes per slot × 4096 slots -/// = 512 KB — fits comfortably. -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct CardSlotMetaCpu { - /// xyz = world-space card-face normal, w = signed axis (0..6 as f32). - normal_ws: [f32; 4], - /// Object-space AABB min (xyz) + padding (w). - aabb_min: [f32; 4], - /// Object-space AABB max (xyz) + padding (w). - aabb_max: [f32; 4], - /// Mesh's world transform. Multiplied into the object-space - /// card-plane position to land in world space for shadow lookup. - transform: [[f32; 4]; 4], -} - -/// Ticket 007b — per-TLAS-instance GI shading input. Indexed by the -/// hit's `instance_custom_data` in the HW trace shader. Layout must -/// match the `InstanceGIData` struct in SSGI_PROBE_TRACE_HW_WGSL. -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct InstanceGiDataCpu { - /// Flat albedo (per-mesh, pre-baked from material base-color). - /// Used as a fallback when `card_slot.w < 0` (no card captured). - albedo: [f32; 3], - /// Scalar emissive luminance — multiplied by `albedo` at the hit. - emissive_luma: f32, - /// Flat world-space mesh normal. Rough — averaged over vertex - /// normals at BLAS build time. Used when the card atlas is - /// unavailable; ticket 013's textured path still drives lighting - /// from this flat normal but multiplies the sampled albedo in. - normal_ws: [f32; 3], - _pad0: f32, - /// Ticket 013 — card slot for textured hit shading. - /// `card_slot.xy` = atlas slot coord (0..CARD_SLOTS_PER_ROW). - /// `card_slot.z` = dominant axis (0=X, 1=Y, 2=Z). - /// `card_slot.w` = flag (1.0 = card captured, 0.0 = no card → fall - /// back to `albedo` flat value). - card_slot: [f32; 4], - /// Object-space AABB min (xyz) + unused pad (w). The HW paths - /// transform hits into object space (hit.world_to_object) and - /// compare against THESE — do not world-ify them. - card_aabb_min: [f32; 4], - /// Object-space AABB max (xyz) + unused pad (w). - card_aabb_max: [f32; 4], - /// EN-023 — WORLD-space AABB min/max. The SDF trace has no - /// world_to_object (it marches a world-space clipmap), so its - /// broad-phase compares the world hit against these. With the old - /// object-space-only bounds, every transformed instance fell - /// through to the flat-gray analytic fallback — zero colored - /// bounce on non-RT adapters (round-2 audit F4). - world_aabb_min: [f32; 4], - world_aabb_max: [f32; 4], - /// PT-2 — geometry window into the PT megabuffers + texture id. - /// x = first vertex (Vertex3D-stride slot) in pt_geo_vertices, - /// y = first index in pt_geo_indices, z = index count, - /// w = albedo texture index (renderer texture store; 0 = white). - /// z == 0 marks "no geometry window" (kernel falls back to the - /// flat normal + card albedo, i.e. PT-1 behaviour). - geo: [u32; 4], - /// PT-2 — x = roughness, y = metalness, z/w unused. - mat_params: [f32; 4], -} - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct SsrTemporalParams { - /// x = blend_alpha (0.1), yzw unused - params: [f32; 4], -} - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct SsrParams { - inv_proj: [[f32; 4]; 4], - proj: [[f32; 4]; 4], - /// x=strength, y=max_dist, z=n_steps, w=frame index - params: [f32; 4], - /// EN-021 — view→world rotation (transpose of the view 3×3) for the - /// env-miss fallback's direction lookup. - inv_view_rot: [[f32; 4]; 4], - /// EN-021 — x = env max LOD, y = env intensity, zw unused. - params2: [f32; 4], -} - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct SceneComposeParams { - misc: [f32; 4], - inv_vp: [[f32; 4]; 4], - fog_color_density: [f32; 4], - fog_params: [f32; 4], - sun_shaft_uv_strength: [f32; 4], - sun_shaft_color: [f32; 4], -} - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct TaaParams { - /// x = blend factor (current-frame weight), yzw padding. - params: [f32; 4], - inv_vp: [[f32; 4]; 4], - prev_vp: [[f32; 4]; 4], -} - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct UpscaleParams { - /// x = mode (0 = bilinear, 1 = Catmull-Rom), yzw padding. - params: [f32; 4], -} - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct RcasParams { - /// x = sharpen strength (0 off, 1 max), yzw padding. - params: [f32; 4], -} - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct ExposureParams { - params: [f32; 4], -} - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct BloomParams { - /// xy = source texel size, z = filter radius (upsample), - /// w = HDR threshold (downsample-threshold variant). - params: [f32; 4], -} - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct CompositeParams { - /// x = tonemap kind (0 ACES / 1 AgX), y = auto-exposure toggle, - /// z = manual exposure, w = auto-exposure target key. - params: [f32; 4], - /// Filmic-look knobs — see WGSL comment. - /// x = chromatic-aberration strength, y = vignette strength, - /// z = vignette softness, w = grain strength. - filmic: [f32; 4], - /// x = grain seed (frame index, animates the noise), - /// y = sharpen strength, zw padding. - misc: [f32; 4], -} // ============================================================ // Cached model GPU data diff --git a/native/shared/src/renderer/types.rs b/native/shared/src/renderer/types.rs index ab3ab55..4b919d0 100644 --- a/native/shared/src/renderer/types.rs +++ b/native/shared/src/renderer/types.rs @@ -330,3 +330,612 @@ pub enum RenderMode { Mode2D, Mode3D, } + +// ============================================================ +// Pass / compute uniform params (moved out of mod.rs, EN-052) +// ============================================================ +// Pure POD uniform structs for the sky, GI, SSR, post, and PT +// passes plus the card-ortho helper. Fields are pub(super) so the +// renderer and its pass submodules can build them by literal. + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct PrefilterUniforms { + /// x = roughness (∈ [0, 1]), y = sample count, zw = mip resolution + pub(super) params: [f32; 4], +} + +// ============================================================ +// Sky / equirectangular HDR background shader +// ============================================================ +// +// Renders a fullscreen triangle with z=1 (far plane) and samples the +// environment map by the world-space view direction reconstructed from +// inverse VP. Tone-maps with the same ACES curve the rest of the +// renderer uses so the background blends seamlessly with lit +// geometry. Always overwrites depth — the 3D opaque pass drawn after +// will occlude wherever it has actual geometry. + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct SkyUniforms { + /// Camera right vector × tan(fovy/2) × aspect — pre-scaled so the + /// fragment shader just multiplies by NDC.x to get the horizontal + /// offset from the forward direction. + /// `.w` = camera world position X (see below). + pub(super) right: [f32; 4], + /// Camera up vector × tan(fovy/2). `.w` = camera world position Y. + pub(super) up: [f32; 4], + /// Camera forward unit vector. `.w` = camera world position Z. + /// + /// The camera POSITION rides in the spare `.w` lanes of the three basis + /// vectors. The sky pass never needed it — a sky at infinity only cares + /// which way you are looking — but the cloud deck is anchored in world + /// space so that its shadow lands under it, and that makes the camera's + /// position load-bearing. Packing it here keeps the bind group unchanged. + pub(super) forward: [f32; 4], + /// x = intensity multiplier, y = elapsed seconds (the cloud deck's clock); + /// zw padding. + pub(super) intensity: [f32; 4], + /// Cloud deck: x = shadow strength, y = deck height (m), z = feature scale + /// (noise units per metre), w = drift speed (m/s). Same vec4 the world + /// materials get, so the sky and the ground cannot disagree. + pub(super) cloud: [f32; 4], + /// xy = wind direction in the XZ plane — the clouds drift downwind, the + /// same way the grass is leaning. + pub(super) wind: [f32; 4], +} + +// EN-005 Phase 2 — uniforms for the procedural sky path. +// +// `SkyViewParams` drives the sky-view LUT compute shader (recomputed +// when the sun moves). `SunUniforms` drives the per-frame sun-disk +// composite in the procedural sky fragment shader. + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct SkyViewParams { + /// xyz = sun direction (world space, unit), w = sun intensity scale. + pub(super) sun: [f32; 4], + /// x = rayleigh density mult, y = mie density mult, z = ground albedo, w unused. + pub(super) knobs: [f32; 4], +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct SunUniforms { + /// xyz = sun direction (unit), w = sun intensity. + pub(super) sun: [f32; 4], + /// x = sun angular radius (rad), y = limb darkening (0..1), zw unused. + pub(super) params: [f32; 4], +} + +// EN-005 V2 — aerial-perspective compute uniforms. +// +// Driven each frame from the renderer (camera + sun + atmosphere +// knobs). Layout must mirror `AerialParams` in +// AERIAL_PERSPECTIVE_SHADER_WGSL exactly. +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct AerialParams { + /// xyz = camera world position (engine units, metres assumed), + /// w = max distance the LUT covers (km). + pub(super) cam_pos: [f32; 4], + /// World→clip inverse, used per-voxel to reconstruct view rays + /// from NDC. + pub(super) inv_vp: [[f32; 4]; 4], + /// xyz = sun direction (unit), w = sun intensity scalar. + pub(super) sun: [f32; 4], + /// x = rayleigh density mult, y = mie density mult, zw unused. + pub(super) knobs: [f32; 4], +} + + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct HizLinearizeParams { + /// xy = inv_size, z = proj[2][2], w = proj[3][2] + pub(super) params: [f32; 4], + /// xy = mip-0 size, zw unused + pub(super) size: [u32; 4], +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct HizDownsampleParams { + /// xy = dst-mip size, zw unused + pub(super) size: [u32; 4], +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct SsaoParams { + /// xy = inv_size (1/half_w, 1/half_h), z = radius (world units), + /// w = strength + pub(super) params: [f32; 4], + /// x = proj[0][0], y = proj[1][1], z = proj[2][0] (TAA jitter), + /// w = proj[2][1] (TAA jitter). Column-major: proj[col][row]. + pub(super) proj_row01: [f32; 4], + /// x = proj[2][2], y = proj[3][2], z = 1/proj[0][0], w = 1/proj[1][1] + pub(super) proj_z: [f32; 4], + /// Light direction in view space (xyz, w unused). For contact shadows. + pub(super) light_dir_vs: [f32; 4], + /// x = half-res width, y = half-res height, z = frame phase + /// (`frame_index % 4`), w = "force-refresh" flag (non-zero on + /// first few frames, resize, or any host-side history invalidation). + pub(super) size: [u32; 4], + /// x = temporal blend alpha (≈0.25 steady, 4-frame EMA). + /// y = per-frame Halton-5 rotation of the direction basis + /// (uncorrelated with TAA's Halton-2/3 pixel jitter). + /// zw unused. + pub(super) temporal: [f32; 4], +} + +// ============================================================ +// SSAO Bilateral Blur post-process +// ============================================================ + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct SsaoBlurParams { + /// xy = texel_size (of the half-res SSAO RT), z = depth_sigma, w = unused. + pub(super) params: [f32; 4], +} + +// ============================================================ +// Depth of Field (DoF) post-process +// ============================================================ + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct DofParams { + /// x = focus_distance, y = aperture, z = max_blur_radius (UV), w = unused + pub(super) params: [f32; 4], + /// Inverse projection matrix — used to linearize depth. + pub(super) inv_proj: [[f32; 4]; 4], +} + +// ============================================================ +// Motion Blur post-process +// ============================================================ +// +// Reads the TAA/DoF output (color) and the per-pixel velocity buffer. +// For each pixel, samples 8 taps along the velocity direction with a +// tent (linear) weight, blending them into a directionally-blurred +// result. Default OFF — no perf cost when disabled. + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct MotionBlurParams { + /// x = strength, y = max_blur (UV), zw = unused. + pub(super) params: [f32; 4], +} + +// ============================================================ +// Screen-Space Subsurface Scattering (SSS) post-process +// ============================================================ +// +// Single-pass 9-tap disc blur applied after the motion blur pass +// (pre-composite). Uses a chromatic diffusion profile where red +// scatters furthest (kernel width 1×), green 0.5×, blue 0.25×, +// simulating the spectral absorption of skin/wax/leaves. +// Depth-guided bilateral weighting prevents color bleeding across +// depth discontinuities (hard edges stay sharp). Default OFF. + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct SssParams { + /// x = strength, y = width, z = falloff, w = unused. + pub(super) params: [f32; 4], +} + +// ============================================================ +// SSGI (Screen-Space Global Illumination) post-process +// ============================================================ + +// Ticket 007a: per-pass uniform params for the screen-probe SSGI chain. + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct ProbePlaceParams { + pub(super) inv_view: [[f32; 4]; 4], + /// x = proj[0][0], y = proj[1][1], z = proj[2][0], w = proj[2][1] + pub(super) proj_row01: [f32; 4], + /// x = half_w, y = half_h, z = grid_w, w = grid_h + pub(super) size: [u32; 4], + /// x = frame_index, y = tile_size (16.0), zw unused + pub(super) params: [f32; 4], +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct ProbeTraceParams { + pub(super) view: [[f32; 4]; 4], + pub(super) proj: [[f32; 4]; 4], + pub(super) inv_view: [[f32; 4]; 4], + pub(super) proj_row01: [f32; 4], + pub(super) size: [u32; 4], + /// x = frame_index, y = intensity, z = max_march_t (world units), + /// w = firefly luma cap. + pub(super) params: [f32; 4], + /// Ticket 007b HW path — sun direction in world space (xyz) + + /// intensity (w). Ignored by the SW-HiZ shader, consumed by HW + SDF. + pub(super) sun_dir: [f32; 4], + /// Sun colour (xyz), reserved (w). Used for analytical NdotL at hit. + pub(super) sun_color: [f32; 4], + /// Sky dome colour (xyz), reserved (w). Flat analytic sky for + /// up-facing hits where the ray hits a surface with an up normal. + pub(super) sky_color: [f32; 4], + /// Ticket 014 V3 — clipmap origin (xyz) + full extent (w). Used + /// by the SDF sphere-trace variant to map world-space march + /// positions into clipmap sample UVs. + pub(super) clipmap: [f32; 4], + /// Ticket 014 V6/V13 — WSRC cascade cubes. Each entry is + /// (origin xyz, extent w). Cascades are ordered near→far; miss + /// paths pick the smallest cascade whose cube contains the + /// ray-terminal position. `extent = 0.0` marks an unbaked + /// cascade (shader falls through to the next one). HW + Hi-Z + /// carry these for uniform-buffer size parity; Hi-Z ignores + /// them. + pub(super) wsrc_cascades: [[f32; 4]; 3], +} + +/// PT-2 — fixed size of the kernel's texture binding array. Real texture +/// indices at or above this clamp to 0 (white); unused tail slots are +/// padded with the white texture so no PARTIALLY_BOUND feature is needed. +pub(super) const PT_MAX_TEXTURES: usize = 256; + +/// Uniform for the path-tracing megakernel — WGSL mirror `PtParams` in +/// shaders/pt.rs. Field-for-field, 16-byte aligned, 672 bytes. +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct PtParamsCpu { + pub(super) inv_vp: [[f32; 4]; 4], + /// PT-3 — previous frame's unjittered VP (transposed like inv_vp) + /// for temporal history reprojection in realtime mode. + pub(super) prev_vp: [[f32; 4]; 4], + /// xyz camera world pos, w unused. + pub(super) cam_pos: [f32; 4], + /// xyz unit vector toward the sun, w unused. + pub(super) sun_dir: [f32; 4], + /// rgb premultiplied by intensity, w unused. + pub(super) sun_color: [f32; 4], + /// rgb ambient-derived sky tint, w unused. + pub(super) sky_color: [f32; 4], + /// x/y = TRACE grid dims (half in realtime mode), z=frame_index, + /// w=accum_count. + pub(super) size: [u32; 4], + /// x=mode (1 progressive / 2 realtime), y=max_bounces, + /// z=point_light_count (≤16), w=debug view (BLOOM_PT_DEBUG). + pub(super) cfg: [f32; 4], + /// PT-3 half-res: x/y = full G-buffer dims. z = 1 → hybrid sun + /// (shadow cascades instead of traced sun rays). w unused. + pub(super) ext: [u32; 4], + /// Raster shadow cascade VPs, transposed at upload like inv_vp. + pub(super) shadow_vps: [[[f32; 4]; 4]; 3], + /// 16 lights × (pos_range vec4, color_int vec4). + pub(super) lights: [[f32; 4]; 32], +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct ProbeTemporalParams { + /// x = alpha (EMA), y = force_refresh (1→alpha=1), z = grid_w (f32), w = grid_h (f32) + pub(super) params: [f32; 4], +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct ProbeResolveParams { + pub(super) inv_view: [[f32; 4]; 4], + pub(super) proj_row01: [f32; 4], + /// x = half_w, y = half_h, z = grid_w, w = grid_h + pub(super) size: [u32; 4], + /// x = tile_size (16.0), y = intensity, zw unused + pub(super) params: [f32; 4], +} + +/// On-GPU `ProbeHeader` layout (must match PROBE_HELPERS_WGSL's struct). +/// 32 bytes per probe. +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct ProbeHeaderCpu { + pub(super) world_pos: [f32; 4], + pub(super) normal: [f32; 4], +} + +/// Ticket 013 V3 — CardCaptureParams. ortho_vp + base_color + emissive. +/// 96 bytes; we allocate 128 for uniform-alignment headroom. +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct CardCaptureParams { + pub(super) ortho_vp: [[f32; 4]; 4], + pub(super) base_color: [f32; 4], // rgb = factor, w = has_base_texture (0/1) + pub(super) emissive: [f32; 4], // rgb = emissive_factor, w = has_emissive_texture (0/1) +} + +/// Ticket 013 V2 — orthographic projection for the 6 signed axes. +/// `face_axis` encoding: +/// 0 → +X, 1 → -X, 2 → +Y, 3 → -Y, 4 → +Z, 5 → -Z. +/// For each face we pick the two orthogonal AABB axes and map them +/// to clip-space [-1, +1]. The ±pair for each axis differ only in +/// the sign of the "u" clip axis so that when the HW shader picks +/// axis N at hit and projects the hit into card UV, the UV lines up +/// with the mesh geometry as seen FROM that face. +pub(super) fn build_card_ortho_v2(face_axis: u32, bmin: [f32; 3], bmax: [f32; 3]) -> [[f32; 4]; 4] { + let wx = (bmax[0] - bmin[0]).max(1e-4); + let wy = (bmax[1] - bmin[1]).max(1e-4); + let wz = (bmax[2] - bmin[2]).max(1e-4); + let cx = bmin[0] + bmax[0]; + let cy = bmin[1] + bmax[1]; + let cz = bmin[2] + bmax[2]; + match face_axis { + // +X → project onto YZ; clip.x = y, clip.y = z. + 0 => [ + [0.0, 0.0, 0.0, 0.0], + [2.0/wy, 0.0, 0.0, 0.0], + [0.0, 2.0/wz, 0.0, 0.0], + [-cy/wy, -cz/wz, 0.0, 1.0], + ], + // -X → flip u so the -X view is the mirror of +X. clip.x = -y, clip.y = z. + 1 => [ + [0.0, 0.0, 0.0, 0.0], + [-2.0/wy, 0.0, 0.0, 0.0], + [0.0, 2.0/wz, 0.0, 0.0], + [cy/wy, -cz/wz, 0.0, 1.0], + ], + // +Y → project onto XZ; clip.x = x, clip.y = z. + 2 => [ + [2.0/wx, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0], + [0.0, 2.0/wz, 0.0, 0.0], + [-cx/wx, -cz/wz, 0.0, 1.0], + ], + // -Y → flip u; clip.x = -x, clip.y = z. + 3 => [ + [-2.0/wx, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0], + [0.0, 2.0/wz, 0.0, 0.0], + [cx/wx, -cz/wz, 0.0, 1.0], + ], + // +Z → project onto XY; clip.x = x, clip.y = y. + 4 => [ + [2.0/wx, 0.0, 0.0, 0.0], + [0.0, 2.0/wy, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0], + [-cx/wx, -cy/wy, 0.0, 1.0], + ], + // -Z → flip u; clip.x = -x, clip.y = y. + _ => [ + [-2.0/wx, 0.0, 0.0, 0.0], + [0.0, 2.0/wy, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0], + [cx/wx, -cy/wy, 0.0, 1.0], + ], + } +} + +/// Ticket 014 — per-mesh SDF bake uniform. +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct SdfBakeParams { + pub(super) aabb_min: [f32; 4], + pub(super) aabb_max: [f32; 4], + /// x = triangle_count, y = sdf_resolution (32), zw unused + pub(super) counts: [u32; 4], +} + +/// Fullscreen-lag fix — an in-flight amortized scene-SDF-clipmap bake. +/// A job bakes `SCENE_SDF_CLIPMAP_LAYERS_PER_FRAME` voxel Z-layers per +/// frame into the staging texture; when the last slice lands the staging +/// contents are copied over the live clipmap and the origin flips +/// atomically, so traces never observe a half-baked field. The bind +/// group keeps the job's transient buffers alive. +pub(super) struct SdfClipmapBakeJob { + pub(super) origin: [f32; 3], + pub(super) aabb_min: [f32; 4], + pub(super) aabb_max: [f32; 4], + pub(super) uniform: wgpu::Buffer, + pub(super) bind_group: wgpu::BindGroup, + pub(super) next_z: u32, +} + +/// Ticket 014 V6 — uniform for `WSRC_BAKE_WGSL`. Analytic sun × shadow +/// + analytic sky computed per probe-octel. Shadow VPs + splits + +/// flags mirror CARD_LIGHT_WGSL so the shader can re-use the same +/// cascade-sampling helper. +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct WsrcBakeParams { + pub(super) sun_dir: [f32; 4], + pub(super) sun_color: [f32; 4], + pub(super) sky_color: [f32; 4], + /// xyz = WSRC cube origin, w = full extent. + pub(super) grid: [f32; 4], + /// Cascade 0..2 VPs. Only cascade 2 is actually sampled by V6 + /// (widest cascade covers the whole 120 m cube), but we carry + /// all three to keep the uniform layout identical to the card- + /// lighting params. + pub(super) shadow_vps: [[[f32; 4]; 4]; 3], + /// xyz = view-space split distances (unused by V6 because it + /// always samples cascade 2), w = 0. + pub(super) shadow_splits: [f32; 4], + /// x = shadow bias, y = shadows_enabled (0/1), zw unused. + pub(super) flags: [f32; 4], + /// EN-023 — xyz = scene-average albedo (mean of card-instance flat + /// albedos) for the SW bake's ground-bounce term; w unused. The HW + /// bake ignores it (it traces real geometry). + pub(super) ground_albedo: [f32; 4], +} + +/// Uniform struct for the card-lighting compute pass. Matches +/// CARD_LIGHT_WGSL. +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct CardLightParams { + pub(super) sun_dir: [f32; 4], + pub(super) sun_color: [f32; 4], + pub(super) sky_color: [f32; 4], + /// [atlas_size, slot_size, slots_per_row, active_slot_count] + pub(super) atlas_info: [u32; 4], + /// Shadow cascade VPs (3× mat4) — identity when shadows disabled. + pub(super) shadow_vps: [[[f32; 4]; 4]; 3], + /// xyz = view-space split distances for cascades 0/1/2, w = 0. + pub(super) shadow_splits: [f32; 4], + /// Camera view matrix — needed to convert card-texel world pos + /// into view-space Z for cascade selection. + pub(super) view_matrix: [[f32; 4]; 4], + /// x = shadow bias, y = shadows_enabled (0/1), zw unused. + pub(super) flags: [f32; 4], +} + +/// Ticket 013 V3 — per-slot metadata consumed by `card_light_pass`. +/// Baked at capture time; carries enough state for the lighting +/// shader to reconstruct each texel's world-space position and query +/// the shadow cascade at that point. 128 bytes per slot × 4096 slots +/// = 512 KB — fits comfortably. +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct CardSlotMetaCpu { + /// xyz = world-space card-face normal, w = signed axis (0..6 as f32). + pub(super) normal_ws: [f32; 4], + /// Object-space AABB min (xyz) + padding (w). + pub(super) aabb_min: [f32; 4], + /// Object-space AABB max (xyz) + padding (w). + pub(super) aabb_max: [f32; 4], + /// Mesh's world transform. Multiplied into the object-space + /// card-plane position to land in world space for shadow lookup. + pub(super) transform: [[f32; 4]; 4], +} + +/// Ticket 007b — per-TLAS-instance GI shading input. Indexed by the +/// hit's `instance_custom_data` in the HW trace shader. Layout must +/// match the `InstanceGIData` struct in SSGI_PROBE_TRACE_HW_WGSL. +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct InstanceGiDataCpu { + /// Flat albedo (per-mesh, pre-baked from material base-color). + /// Used as a fallback when `card_slot.w < 0` (no card captured). + pub(super) albedo: [f32; 3], + /// Scalar emissive luminance — multiplied by `albedo` at the hit. + pub(super) emissive_luma: f32, + /// Flat world-space mesh normal. Rough — averaged over vertex + /// normals at BLAS build time. Used when the card atlas is + /// unavailable; ticket 013's textured path still drives lighting + /// from this flat normal but multiplies the sampled albedo in. + pub(super) normal_ws: [f32; 3], + pub(super) _pad0: f32, + /// Ticket 013 — card slot for textured hit shading. + /// `card_slot.xy` = atlas slot coord (0..CARD_SLOTS_PER_ROW). + /// `card_slot.z` = dominant axis (0=X, 1=Y, 2=Z). + /// `card_slot.w` = flag (1.0 = card captured, 0.0 = no card → fall + /// back to `albedo` flat value). + pub(super) card_slot: [f32; 4], + /// Object-space AABB min (xyz) + unused pad (w). The HW paths + /// transform hits into object space (hit.world_to_object) and + /// compare against THESE — do not world-ify them. + pub(super) card_aabb_min: [f32; 4], + /// Object-space AABB max (xyz) + unused pad (w). + pub(super) card_aabb_max: [f32; 4], + /// EN-023 — WORLD-space AABB min/max. The SDF trace has no + /// world_to_object (it marches a world-space clipmap), so its + /// broad-phase compares the world hit against these. With the old + /// object-space-only bounds, every transformed instance fell + /// through to the flat-gray analytic fallback — zero colored + /// bounce on non-RT adapters (round-2 audit F4). + pub(super) world_aabb_min: [f32; 4], + pub(super) world_aabb_max: [f32; 4], + /// PT-2 — geometry window into the PT megabuffers + texture id. + /// x = first vertex (Vertex3D-stride slot) in pt_geo_vertices, + /// y = first index in pt_geo_indices, z = index count, + /// w = albedo texture index (renderer texture store; 0 = white). + /// z == 0 marks "no geometry window" (kernel falls back to the + /// flat normal + card albedo, i.e. PT-1 behaviour). + pub(super) geo: [u32; 4], + /// PT-2 — x = roughness, y = metalness, z/w unused. + pub(super) mat_params: [f32; 4], +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct SsrTemporalParams { + /// x = blend_alpha (0.1), yzw unused + pub(super) params: [f32; 4], +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct SsrParams { + pub(super) inv_proj: [[f32; 4]; 4], + pub(super) proj: [[f32; 4]; 4], + /// x=strength, y=max_dist, z=n_steps, w=frame index + pub(super) params: [f32; 4], + /// EN-021 — view→world rotation (transpose of the view 3×3) for the + /// env-miss fallback's direction lookup. + pub(super) inv_view_rot: [[f32; 4]; 4], + /// EN-021 — x = env max LOD, y = env intensity, zw unused. + pub(super) params2: [f32; 4], +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct SceneComposeParams { + pub(super) misc: [f32; 4], + pub(super) inv_vp: [[f32; 4]; 4], + pub(super) fog_color_density: [f32; 4], + pub(super) fog_params: [f32; 4], + pub(super) sun_shaft_uv_strength: [f32; 4], + pub(super) sun_shaft_color: [f32; 4], +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct TaaParams { + /// x = blend factor (current-frame weight), yzw padding. + pub(super) params: [f32; 4], + pub(super) inv_vp: [[f32; 4]; 4], + pub(super) prev_vp: [[f32; 4]; 4], +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct UpscaleParams { + /// x = mode (0 = bilinear, 1 = Catmull-Rom), yzw padding. + pub(super) params: [f32; 4], +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct RcasParams { + /// x = sharpen strength (0 off, 1 max), yzw padding. + pub(super) params: [f32; 4], +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct ExposureParams { + pub(super) params: [f32; 4], +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct BloomParams { + /// xy = source texel size, z = filter radius (upsample), + /// w = HDR threshold (downsample-threshold variant). + pub(super) params: [f32; 4], +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub(super) struct CompositeParams { + /// x = tonemap kind (0 ACES / 1 AgX), y = auto-exposure toggle, + /// z = manual exposure, w = auto-exposure target key. + pub(super) params: [f32; 4], + /// Filmic-look knobs — see WGSL comment. + /// x = chromatic-aberration strength, y = vignette strength, + /// z = vignette softness, w = grain strength. + pub(super) filmic: [f32; 4], + /// x = grain seed (frame index, animates the noise), + /// y = sharpen strength, zw padding. + pub(super) misc: [f32; 4], +} diff --git a/native/watchos/src/ffi_stubs.rs b/native/watchos/src/ffi_stubs.rs index 13625ce..42edc47 100644 --- a/native/watchos/src/ffi_stubs.rs +++ b/native/watchos/src/ffi_stubs.rs @@ -17,6 +17,9 @@ } #[no_mangle] pub extern "C" fn bloom_set_env_clear_from_hdr(_p0: i64) { } +#[no_mangle] pub extern "C" fn bloom_is_key_repeated(_p0: f64) -> f64 { + 0.0 +} #[no_mangle] pub extern "C" fn bloom_get_mouse_x() -> f64 { 0.0 } diff --git a/native/web/src/input_ffi.rs b/native/web/src/input_ffi.rs index 9b16856..664add5 100644 --- a/native/web/src/input_ffi.rs +++ b/native/web/src/input_ffi.rs @@ -24,6 +24,11 @@ pub fn bloom_is_key_released(key: f64) -> f64 { if engine().input.is_key_released(key as usize) { 1.0 } else { 0.0 } } +#[wasm_bindgen] +pub fn bloom_is_key_repeated(key: f64) -> f64 { + if engine().input.is_key_repeated(key as usize) { 1.0 } else { 0.0 } +} + // ============================================================ // Input - Mouse // ============================================================ diff --git a/src/core/index.ts b/src/core/index.ts index 1fa8862..6793baa 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -95,7 +95,6 @@ declare function bloom_profiler_hist_count(): number; declare function bloom_profiler_hist_cpu_us(i: number): number; declare function bloom_profiler_hist_gpu_us(i: number): number; declare function bloom_splat_impulse(x: number, z: number, radius: number, strength: number): void; -declare function bloom_set_material_params(handle: number, paramsPtr: any, paramCount: number): void; declare function bloom_set_material_params_scratch(handle: number, paramCount: number): void; declare function bloom_mesh_scratch_reset(): void; declare function bloom_mesh_scratch_push_f32(v: number): void; diff --git a/src/models/index.ts b/src/models/index.ts index 4c07e6c..e6495d3 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -41,14 +41,12 @@ declare function bloom_submit_material_draw_instanced(material: number, meshHand declare function bloom_destroy_instance_buffer(handle: number): void; declare function bloom_create_planar_reflection(planeY: number, normalX: number, normalY: number, normalZ: number, resolution: number): number; declare function bloom_set_material_reflection_probe(material: number, probe: number): void; -declare function bloom_create_texture_array(dataPtr: any, dataLen: number, width: number, height: number, layerCount: number): number; -declare function bloom_create_texture_array_ex(dataPtr: any, dataLen: number, width: number, height: number, layerCount: number, format: number, mipLevels: number): number; declare function bloom_set_material_texture_array(material: number, slot: number, array: number): void; declare function bloom_set_material_shading_model(material: number, model: number): void; declare function bloom_set_material_probe_visible(material: number, visible: number): void; declare function bloom_set_material_foliage(material: number, transR: number, transG: number, transB: number, transAmount: number, wrapFactor: number): void; declare function bloom_compile_material_from_file(path: number, bucketKind: number): number; -declare function bloom_set_material_params(handle: number, paramsPtr: any, paramCount: number): void; +declare function bloom_set_material_params_scratch(handle: number, paramCount: number): void; declare function bloom_draw_material(material: number, meshHandle: number, meshIdx: number, x: number, y: number, z: number, scale: number, r: number, g: number, b: number, a: number): void; declare function bloom_load_model_animation(path: number): number; declare function bloom_instantiate_animation(src: number): number; @@ -579,7 +577,21 @@ export function createTextureArray( bytes: number[], dataLen: number, width: number, height: number, layerCount: number, ): number { - return bloom_create_texture_array(bytes as any, dataLen, width, height, layerCount); + // Routes through the all-f64 mesh scratch (like createTextureArrayFromTexels): + // the raw *const u8 FFI is uncallable from Perry (number[] into an i64 pointer + // param throws). `bytes` is RGBA8 back-to-back, so pack each 4 bytes into one + // u32 the scratch consumer expects. SRGB / 1 mip to match the V1 semantics. + bloom_mesh_scratch_reset(); + const texelCount = dataLen / 4; + for (let i = 0; i < texelCount; i = i + 1) { + const b = i * 4; + bloom_mesh_scratch_push_u32( + bytes[b] | (bytes[b + 1] << 8) | (bytes[b + 2] << 16) | (bytes[b + 3] << 24), + ); + } + return bloom_create_texture_array_scratch( + width, height, layerCount, TEX_ARRAY_FORMAT_SRGB, 1, + ); } /// EN-014 V2 — texture-array pixel format codes for `createTextureArrayEx`. @@ -610,7 +622,16 @@ export function createTextureArrayEx( width: number, height: number, layerCount: number, format: number, mipLevels: number, ): number { - return bloom_create_texture_array_ex(bytes as any, dataLen, width, height, layerCount, format, mipLevels); + // Same scratch reroute as createTextureArray, with explicit format + mips. + bloom_mesh_scratch_reset(); + const texelCount = dataLen / 4; + for (let i = 0; i < texelCount; i = i + 1) { + const b = i * 4; + bloom_mesh_scratch_push_u32( + bytes[b] | (bytes[b + 1] << 8) | (bytes[b + 2] << 16) | (bytes[b + 3] << 24), + ); + } + return bloom_create_texture_array_scratch(width, height, layerCount, format, mipLevels); } declare function bloom_create_texture_array_scratch( @@ -619,12 +640,13 @@ declare function bloom_create_texture_array_scratch( /// EN-049 — build a texture array from data you computed, not from files. /// -/// `createTextureArray`/`createTextureArrayEx` take a `*const u8`, which the -/// manifest must declare `i64` — and Perry cannot pass a `number[]` to an i64 -/// param ("Expected safe integer for native i64 parameter"). They are, from -/// Perry, uncallable. That is fine for ART, which has files to name -/// (`createTextureArrayFromFiles`), and useless for DATA: a terrain splat map is -/// computed at load out of the world file and there is no file to point at. +/// `createTextureArray`/`createTextureArrayEx` originally took a `*const u8`, +/// which the manifest must declare `i64` — and Perry cannot pass a `number[]` +/// to an i64 param ("Expected safe integer for native i64 parameter"). They now +/// reroute through this same scratch path internally (repacking their RGBA8 +/// bytes into packed u32s), so they are callable again; this entry point is the +/// direct one when your data is already packed u32-per-texel — e.g. a terrain +/// splat map computed at load out of the world file, with no file to point at. /// /// So the payload goes through the mesh scratch buffer, exactly as /// `updateSceneNodeGeometry` already does for vertices. `texels` is one PACKED @@ -772,13 +794,15 @@ export interface MaterialDesc { export function loadMaterial(desc: MaterialDesc): number { const handle = compileMaterialFromFile(desc.shader, desc.bucket); if (handle > 0 && desc.params) { - // Bind to a local first — Perry currently mishandles `.length` - // on an object field passed directly into an FFI call (the FFI - // sees count = 0). Re-binding to a local lets `.length` evaluate - // before the FFI call is laid out. + // Params go through the all-f64 mesh scratch, same as setMaterialParams: + // the raw pointer variant (bloom_set_material_params) is not in the FFI + // manifest, so Perry silently no-ops it, and it also hits the + // number[]-into-i64-pointer bug. The _scratch path avoids both. const p = desc.params; if (p.length > 0) { - bloom_set_material_params(handle, p as any, p.length); + bloom_mesh_scratch_reset(); + for (let i = 0; i < p.length; i++) bloom_mesh_scratch_push_f32(p[i]); + bloom_set_material_params_scratch(handle, p.length); } } return handle; diff --git a/tools/file-lines-baseline.json b/tools/file-lines-baseline.json index 8311a3f..de6fc9b 100644 --- a/tools/file-lines-baseline.json +++ b/tools/file-lines-baseline.json @@ -1,3 +1,3 @@ { - "native/shared/src/renderer/mod.rs": 13058 + "native/shared/src/renderer/mod.rs": 12556 }