MONKEY (r19 F4, lighting): per-frame light scan replaced by a room light index (Naxxramas stutter)
Lighting part of benilla-everwood 736e6a27 (monkey_room_index, global_light, static_gx shortlist). (cherry picked from commit 736e6a27, capture + physics hunks omitted)
This commit is contained in:
@@ -20,6 +20,10 @@ use crate::dev_state::DebugState;
|
||||
use crate::view::ViewDistance;
|
||||
use crate::view::WorldCamera;
|
||||
|
||||
#[path = "monkey_room_index.rs"]
|
||||
mod monkey_room_index;
|
||||
use monkey_room_index::{monkey_build_index, MONKEY_CLAIM_WORDS, MONKEY_TABLE_WORDS};
|
||||
|
||||
/// The shared light, std430-packed as `vec4<f32>` rows (all `vec4`, so std430 equals std140). Every
|
||||
/// shader that binds the buffer (`wow_model`, `terrain`, `liquid`, `wdl`, `wow_effect`,
|
||||
/// `static_gx`) mirrors this row order as a prefix; keep them in sync.
|
||||
@@ -322,19 +326,19 @@ pub const CLAIM_ENTRY_SHIFT: u32 = 17;
|
||||
/// three shaders plus the portrait booth and must never be resized, and only `static_gx` reads
|
||||
/// this. `static_gx::render` owns the buffer and the binding.
|
||||
#[derive(Resource, Clone, ExtractResource)]
|
||||
pub struct RoomClaimTable(pub Box<[u32; ROOM_CLAIM_STRIDE * MAX_POINT_LIGHTS]>);
|
||||
pub struct RoomClaimTable(pub Box<[u32; MONKEY_TABLE_WORDS]>);
|
||||
|
||||
/// MONKEY (room gate): the claim table's byte size — the one place `static_gx::render` sizes its
|
||||
/// GPU buffer from, so the table cannot grow here and leave the binding short (a bound storage
|
||||
/// buffer smaller than the shader's runtime-sized array fails validation at draw time, which
|
||||
/// vanishes every building).
|
||||
pub fn room_claim_bytes() -> u64 {
|
||||
(ROOM_CLAIM_STRIDE * MAX_POINT_LIGHTS * std::mem::size_of::<u32>()) as u64
|
||||
(MONKEY_TABLE_WORDS * std::mem::size_of::<u32>()) as u64
|
||||
}
|
||||
|
||||
impl Default for RoomClaimTable {
|
||||
fn default() -> Self {
|
||||
Self(Box::new([0; ROOM_CLAIM_STRIDE * MAX_POINT_LIGHTS]))
|
||||
Self(Box::new([0; MONKEY_TABLE_WORDS]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1659,9 +1663,20 @@ fn build_light_data(
|
||||
// Entries past the count are stale in the point table by design (the count row guards every
|
||||
// reader) — but the claim table is read at the SAME index, so a stale head there would gate a
|
||||
// live light with a dead building's identity. Clear the tail instead of trusting the count.
|
||||
for slot in claims.0[pts.len() * ROOM_CLAIM_STRIDE..].iter_mut() {
|
||||
for slot in claims.0[pts.len() * ROOM_CLAIM_STRIDE..MONKEY_CLAIM_WORDS].iter_mut() {
|
||||
*slot = 0;
|
||||
}
|
||||
// The appended room bitmap is a conservative shortlist; the shader still applies every
|
||||
// selected light's exact claim/fade predicate, in the original ascending light order.
|
||||
static MONKEY_ROOM_INDEX_ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
if *MONKEY_ROOM_INDEX_ENABLED
|
||||
.get_or_init(|| std::env::var("WOW_MONKEY_ROOM_INDEX").as_deref() != Ok("0"))
|
||||
{
|
||||
monkey_build_index(&mut claims.0[..], &fresh.points, pts.len());
|
||||
} else {
|
||||
// Zero header selects the unchanged full-scan shader path for same-build measurement.
|
||||
claims.0[MONKEY_CLAIM_WORDS..].fill(0);
|
||||
}
|
||||
// `WOW_POINTS_DUMP=1` prints the nearest 8 packed lights once a second; `=frame` every frame.
|
||||
// MONKEY (room diagnostics): `=all` prints every committed row once a second, including
|
||||
// lane/claim/fade data. Eight nearest-to-camera rows can omit the hearth under investigation.
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
//! MONKEY: invert packed room claims once per frame, preserving light order and exact gate weights.
|
||||
use super::{MAX_POINT_LIGHTS, ROOM_CLAIM_MAX, ROOM_CLAIM_STRIDE};
|
||||
|
||||
pub(super) const MONKEY_CLAIM_WORDS: usize = ROOM_CLAIM_STRIDE * MAX_POINT_LIGHTS;
|
||||
pub(super) const MONKEY_INDEX_SLOTS: usize = 4096;
|
||||
pub(super) const MONKEY_MASK_WORDS: usize = MAX_POINT_LIGHTS / 32;
|
||||
pub(super) const MONKEY_INDEX_STRIDE: usize = 2 + MONKEY_MASK_WORDS;
|
||||
pub(super) const MONKEY_UNGATED: usize = MONKEY_CLAIM_WORDS + 1;
|
||||
pub(super) const MONKEY_INDEX_START: usize = MONKEY_UNGATED + MONKEY_MASK_WORDS;
|
||||
pub(super) const MONKEY_TABLE_WORDS: usize =
|
||||
MONKEY_INDEX_START + MONKEY_INDEX_SLOTS * MONKEY_INDEX_STRIDE;
|
||||
|
||||
fn monkey_hash(instance: u32, group: u32) -> usize {
|
||||
((instance.wrapping_mul(0x9e37_79b9) ^ group.wrapping_mul(0x85eb_ca6b)) as usize)
|
||||
& (MONKEY_INDEX_SLOTS - 1)
|
||||
}
|
||||
|
||||
pub(super) fn monkey_build_index(table: &mut [u32], points: &[[f32; 4]], count: usize) {
|
||||
table[MONKEY_CLAIM_WORDS..].fill(0);
|
||||
table[MONKEY_CLAIM_WORDS] = 1;
|
||||
for i in 0..count {
|
||||
if points[2 * i + 1][3] < 0.5 {
|
||||
continue;
|
||||
}
|
||||
let head = i * ROOM_CLAIM_STRIDE;
|
||||
let instance = table[head];
|
||||
let n = (table[head + 1] as usize).min(ROOM_CLAIM_MAX);
|
||||
let bit = 1u32 << (i % 32);
|
||||
let word = i / 32;
|
||||
if n == 0 {
|
||||
table[MONKEY_UNGATED + word] |= bit;
|
||||
continue;
|
||||
}
|
||||
for claim in 0..n {
|
||||
let group = table[head + 2 + claim] & 0xffff;
|
||||
if group == 0 {
|
||||
continue;
|
||||
}
|
||||
let mut slot = monkey_hash(instance, group);
|
||||
loop {
|
||||
let at = MONKEY_INDEX_START + slot * MONKEY_INDEX_STRIDE;
|
||||
if table[at + 1] == 0 {
|
||||
table[at] = instance;
|
||||
table[at + 1] = group;
|
||||
}
|
||||
if table[at] == instance && table[at + 1] == group {
|
||||
table[at + 2 + word] |= bit;
|
||||
break;
|
||||
}
|
||||
slot = (slot + 1) & (MONKEY_INDEX_SLOTS - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
fn candidates(table: &[u32], instance: u32, group: u32, strict: bool) -> Vec<usize> {
|
||||
let mut slot = monkey_hash(instance, group);
|
||||
let at = loop {
|
||||
let at = MONKEY_INDEX_START + slot * MONKEY_INDEX_STRIDE;
|
||||
if table[at + 1] == 0 {
|
||||
break None;
|
||||
}
|
||||
if table[at] == instance && table[at + 1] == group {
|
||||
break Some(at);
|
||||
}
|
||||
slot = (slot + 1) & (MONKEY_INDEX_SLOTS - 1);
|
||||
};
|
||||
(0..MAX_POINT_LIGHTS)
|
||||
.filter(|i| {
|
||||
if group == 0 {
|
||||
return !strict;
|
||||
}
|
||||
let mask = at.map_or(0, |a| table[a + 2 + i / 32])
|
||||
| if strict {
|
||||
0
|
||||
} else {
|
||||
table[MONKEY_UNGATED + i / 32]
|
||||
};
|
||||
mask & (1 << (i % 32)) != 0
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
#[test]
|
||||
fn monkey_index_preserves_all_255_lights_gate_off_groups_and_sort_order() {
|
||||
let mut table = vec![0; MONKEY_TABLE_WORDS];
|
||||
let mut points = [[0.; 4]; 2 * MAX_POINT_LIGHTS];
|
||||
for i in 0..255 {
|
||||
points[2 * i + 1][3] = if i % 7 == 0 { 0. } else { 8. };
|
||||
let head = i * ROOM_CLAIM_STRIDE;
|
||||
if i % 9 != 0 {
|
||||
table[head] = 3 + (i % 4) as u32;
|
||||
table[head + 1] = ROOM_CLAIM_MAX as u32;
|
||||
for k in 0..ROOM_CLAIM_MAX {
|
||||
// Packed flags/portal entry weights must not affect the shortlist.
|
||||
table[head + 2 + k] = (1 + ((i + k) % 31) as u32) | (0xff << 17) | (1 << 16);
|
||||
}
|
||||
}
|
||||
}
|
||||
monkey_build_index(&mut table, &points, 255);
|
||||
for instance in 2..8 {
|
||||
for group in 1..34 {
|
||||
for strict in [false, true] {
|
||||
let old: Vec<_> = (0..255)
|
||||
.filter(|i| {
|
||||
let head = i * ROOM_CLAIM_STRIDE;
|
||||
points[2 * i + 1][3] >= 0.5
|
||||
&& ((table[head + 1] == 0 && !strict)
|
||||
|| (table[head] == instance
|
||||
&& (0..table[head + 1] as usize)
|
||||
.any(|k| table[head + 2 + k] & 0xffff == group)))
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(candidates(&table, instance, group, strict), old);
|
||||
}
|
||||
}
|
||||
}
|
||||
// A second frame with fewer lights retires every old rank and room.
|
||||
monkey_build_index(&mut table, &points, 1);
|
||||
assert!(candidates(&table, 3, 1, false).is_empty());
|
||||
}
|
||||
#[test]
|
||||
fn monkey_room_zero_ungated_and_soft_claims_keep_the_original_gate_authoritative() {
|
||||
let mut table = vec![0; MONKEY_TABLE_WORDS];
|
||||
let mut points = [[0.; 4]; 2 * MAX_POINT_LIGHTS];
|
||||
for i in 0..4 {
|
||||
points[2 * i + 1][3] = 8.;
|
||||
}
|
||||
// Rank 0 is ungated. Ranks 1/2 claim room 1, with different strict/fade flags.
|
||||
for i in 1..3 {
|
||||
let head = i * ROOM_CLAIM_STRIDE;
|
||||
table[head] = 3;
|
||||
table[head + 1] = 1;
|
||||
table[head + 2] = 1 | if i == 1 { (1 << 16) | (128 << 17) } else { 0 };
|
||||
table[head + 8 + 3] = 1024; // a four-yard portal fade
|
||||
}
|
||||
// Rank 3 belongs to a different building and must never enter room (3,1).
|
||||
table[3 * ROOM_CLAIM_STRIDE] = 4;
|
||||
table[3 * ROOM_CLAIM_STRIDE + 1] = 1;
|
||||
table[3 * ROOM_CLAIM_STRIDE + 2] = 1;
|
||||
monkey_build_index(&mut table, &points, 4);
|
||||
assert_eq!(candidates(&table, 3, 1, false), vec![0, 1, 2]);
|
||||
// Both claims remain conservative candidates even though strict rejects rank 2.
|
||||
assert_eq!(candidates(&table, 3, 1, true), vec![1, 2]);
|
||||
assert_eq!(candidates(&table, 3, 99, false), vec![0]);
|
||||
assert!(candidates(&table, 3, 99, true).is_empty());
|
||||
assert!(candidates(&table, 3, 0, true).is_empty());
|
||||
assert_eq!(
|
||||
candidates(&table, 3, 0, false)
|
||||
.into_iter()
|
||||
.filter(|i| *i < 4)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![0, 1, 2, 3]
|
||||
);
|
||||
// The index has neither changed strict eligibility nor the quantized portal fade.
|
||||
assert_eq!(table[ROOM_CLAIM_STRIDE + 2], 1 | (1 << 16) | (128 << 17));
|
||||
assert_eq!(table[ROOM_CLAIM_STRIDE + 8 + 3], 1024);
|
||||
}
|
||||
#[test]
|
||||
fn monkey_index_handles_maximum_claim_population_and_hash_collisions() {
|
||||
let mut table = vec![0; MONKEY_TABLE_WORDS];
|
||||
let mut points = [[0.; 4]; 2 * MAX_POINT_LIGHTS];
|
||||
for i in 0..255 {
|
||||
points[2 * i + 1][3] = 4.;
|
||||
table[i * ROOM_CLAIM_STRIDE] = 100 + i as u32;
|
||||
table[i * ROOM_CLAIM_STRIDE + 1] = 6;
|
||||
for k in 0..6 {
|
||||
table[i * ROOM_CLAIM_STRIDE + 2 + k] = 1 + k as u32;
|
||||
}
|
||||
}
|
||||
monkey_build_index(&mut table, &points, 255);
|
||||
for i in 0..255 {
|
||||
for group in 1..=6 {
|
||||
assert_eq!(candidates(&table, 100 + i as u32, group, true), vec![i]);
|
||||
}
|
||||
}
|
||||
assert!(candidates(&table, 999, 1, true).is_empty());
|
||||
}
|
||||
}
|
||||
@@ -1085,6 +1085,50 @@ fn gx_room_inst() -> u32 {
|
||||
fn gx_room_group(word: u32) -> u32 {
|
||||
return (recs[word & 0xffffu].w >> RECORD_ROOM_SHIFT) & RECORD_ROOM_MASK;
|
||||
}
|
||||
// MONKEY: the claim prefix is unchanged. The appended hash table shortlists lights by room;
|
||||
// flags and portal weights remain authoritative in interior_room_admits. Mirror monkey_room_index.rs.
|
||||
const MONKEY_CLAIM_WORDS: u32 = ROOM_CLAIM_STRIDE * 256u;
|
||||
const MONKEY_MASK_WORDS: u32 = 8u;
|
||||
const MONKEY_INDEX_SLOTS: u32 = 4096u;
|
||||
const MONKEY_INDEX_STRIDE: u32 = 2u + MONKEY_MASK_WORDS;
|
||||
const MONKEY_UNGATED: u32 = MONKEY_CLAIM_WORDS + 1u;
|
||||
const MONKEY_INDEX_START: u32 = MONKEY_UNGATED + MONKEY_MASK_WORDS;
|
||||
|
||||
fn monkey_room_index(room_inst: u32, room_group: u32) -> u32 {
|
||||
if (room_claims[MONKEY_CLAIM_WORDS] == 0u || room_group == 0u) {
|
||||
return 0u;
|
||||
}
|
||||
var slot = ((room_inst * 0x9e3779b9u) ^ (room_group * 0x85ebca6bu)) & (MONKEY_INDEX_SLOTS - 1u);
|
||||
for (var probe = 0u; probe < MONKEY_INDEX_SLOTS; probe = probe + 1u) {
|
||||
let at = MONKEY_INDEX_START + slot * MONKEY_INDEX_STRIDE;
|
||||
if (room_claims[at + 1u] == 0u) {
|
||||
return 0u;
|
||||
}
|
||||
if (room_claims[at] == room_inst && room_claims[at + 1u] == room_group) {
|
||||
return at;
|
||||
}
|
||||
slot = (slot + 1u) & (MONKEY_INDEX_SLOTS - 1u);
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
|
||||
fn monkey_room_mask(at: u32, room_group: u32, strict: bool, word: u32) -> u32 {
|
||||
// A zero header falls back to the original full table walk, including its exact lane gate.
|
||||
if (room_claims[MONKEY_CLAIM_WORDS] == 0u) {
|
||||
return 0xffffffffu;
|
||||
}
|
||||
if (room_group == 0u) {
|
||||
return select(0xffffffffu, 0u, strict);
|
||||
}
|
||||
var bits = 0u;
|
||||
if (!strict) {
|
||||
bits = room_claims[MONKEY_UNGATED + word];
|
||||
}
|
||||
if (at != 0u) {
|
||||
bits = bits | room_claims[at + 2u + word];
|
||||
}
|
||||
return bits;
|
||||
}
|
||||
// MONKEY (room gate): `room_inst`/`room_group` are the RECEIVING surface's room (see
|
||||
// `interior_room_admits`). `wow_model.wgsl`'s copy takes the same room arguments and stubs the
|
||||
// predicate to `true` — it has neither the claim binding nor a per-fragment room key, which is
|
||||
@@ -1101,7 +1145,24 @@ fn interior_room_light(
|
||||
let k_fill = wow_light.point_count.z;
|
||||
var direct = vec3<f32>(0.0);
|
||||
var fill = vec3<f32>(0.0);
|
||||
for (var i = 0u; i < count; i = i + 1u) {
|
||||
let monkey_at = monkey_room_index(room_inst, room_group);
|
||||
// Ascending words and least-significant bits preserve the original floating-point sum order.
|
||||
var monkey_word = 0u;
|
||||
var monkey_bits = monkey_room_mask(monkey_at, room_group, strict, monkey_word);
|
||||
loop {
|
||||
if (monkey_bits == 0u) {
|
||||
monkey_word = monkey_word + 1u;
|
||||
if (monkey_word >= MONKEY_MASK_WORDS || monkey_word * 32u >= count) {
|
||||
break;
|
||||
}
|
||||
monkey_bits = monkey_room_mask(monkey_at, room_group, strict, monkey_word);
|
||||
continue;
|
||||
}
|
||||
let i = monkey_word * 32u + firstTrailingBit(monkey_bits);
|
||||
monkey_bits = monkey_bits & (monkey_bits - 1u);
|
||||
if (i >= count) {
|
||||
break;
|
||||
}
|
||||
let color_lane = wow_light.points[2u * i + 1u];
|
||||
// MONKEY (light lanes): only a fixture that CLAIMS a room lights this room (keep in sync
|
||||
// with wow_model.wgsl). `.w` is 0 on every exterior source, so a campfire burning just
|
||||
|
||||
Reference in New Issue
Block a user