Co-Authored-By: Kimi <noreply@moonshot.ai>
This commit is contained in:
@@ -13,13 +13,25 @@
|
||||
//! at a time, and applied a tick or two later. While a film is being made, everything is traced
|
||||
//! at once and under the same kind of budget (cells, never time), so the sound of a film does not
|
||||
//! depend on the speed of the machine. Results are cached by the cells (2 blocks) of the source
|
||||
//! and the listener, and dropped when a block changes in a section a path passed through.
|
||||
//! and the listener, and dropped when a block changes in a section a path passed through, or when
|
||||
//! the chunk underneath them arrives, is replaced by a reload or goes away. A trace that met a
|
||||
//! block nobody knows is marked so and is traced again once it is a bit old, so a wall that
|
||||
//! streams in is found without anything else happening; answers that come back possibly after a
|
||||
//! chunk moved are checked against that change and dropped when it was theirs.
|
||||
//!
|
||||
//! What the mixer is told is also scoped: a sound that leaves the acoustic range (beyond
|
||||
//! [`RANGE`], too quiet to hear, or a world that is gone for a moment) is explicitly given the
|
||||
//! plain numbers again — the air's absorption at its distance, no wall, no echoes, no reverb sends
|
||||
//! — so what was once traced for it is not remembered for it; when it comes back, the current
|
||||
//! geometry applies.
|
||||
//!
|
||||
//! Stage 2 adds echo taps for loud one-shots (`echo`): rays from the source and the listener find
|
||||
//! the walls, and each wall that is really there gives a copy of the sound delayed by the longer
|
||||
//! path. They are traced on the same worker, cached by the 1-block cells of the source and the
|
||||
//! listener, and handed to the mixer by [`Acoustics::take_echoes`]; only [`ECHO_VOICES`] sounds
|
||||
//! at a time get them.
|
||||
//! get them at once (the loudest that deserve them) — a sound that drops out of that set has its
|
||||
//! taps recalled, which the mixer fades out over about 50 ms, so the taps actually sounding may
|
||||
//! briefly reach another round of [`ECHO_VOICES`] while a rotation plays out.
|
||||
//!
|
||||
//! Stage 3 adds the reverb (`room`): rays from the listener (and from each echo source, in its
|
||||
//! echo trace) measure how enclosed, how large and how reflective the space is, which gives the
|
||||
@@ -39,7 +51,7 @@ pub use room::Room;
|
||||
use spr_occlusion::run_occlusion;
|
||||
|
||||
use crate::chunk::{Chunk, HEIGHT, MIN_Y};
|
||||
use crate::fast_hash::FxHashMap;
|
||||
use crate::fast_hash::{FxHashMap, FxHashSet};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
@@ -61,9 +73,17 @@ const JOB_CELLS: usize = 400_000;
|
||||
const JOB_SOURCES: usize = 32;
|
||||
/// Sources that get the nine paths; the rest get one.
|
||||
const FULL_PATHS: usize = 16;
|
||||
/// Ticks a trace that met unknown cells is kept before it is traced again: the snapshot then may
|
||||
/// miss chunks that are not there yet, and they arrive on their own. A section change always
|
||||
/// drops the trace at once; this only bounds how long a partial answer is mixed with.
|
||||
const UNKNOWN_TICKS: u64 = 40;
|
||||
/// Cached estimates.
|
||||
const CACHE: usize = 4096;
|
||||
/// Sounds that have echo taps at the same time (the loudest of those that deserve them).
|
||||
/// Sounds that have echo taps at the same time (the loudest of those that deserve them). This is
|
||||
/// the budget of the engine's work, not of the mixer's taps alone: a sound that leaves the set has
|
||||
/// its taps recalled, which the mixer fades out over about 50 ms (about five mixer buffers), so
|
||||
/// right after a full rotation up to twice this many taps are sounding, and within that fade the
|
||||
/// steady number is `ECHO_VOICES` again.
|
||||
pub const ECHO_VOICES: usize = 8;
|
||||
/// Echo traces one worker job holds.
|
||||
const ECHO_JOB: usize = 4;
|
||||
@@ -100,6 +120,9 @@ pub struct Snapshot {
|
||||
pub reflect: Arc<Vec<f32>>,
|
||||
pub epoch: u32,
|
||||
pub revision: u64,
|
||||
/// The residency of the columns it was made from (`ChunkMap::content_revision`): a reload,
|
||||
/// an unload or a new column means the ranks' contents may differ from these `Arc`s.
|
||||
pub chunk_rev: u64,
|
||||
pub tick: u64,
|
||||
pub center: [f64; 3],
|
||||
}
|
||||
@@ -177,6 +200,10 @@ pub struct Changes {
|
||||
pub epoch: u32,
|
||||
pub overflow: bool,
|
||||
pub sections: Vec<(i32, i32, i32)>,
|
||||
/// What the resident chunks held (`ChunkMap::content_revision`): when it moves, a worker job
|
||||
/// made from the snapshot before it applies to chunks that may be gone, and its answers are
|
||||
/// dropped. The `sections` of the same change still drop the individual cached estimates.
|
||||
pub residency: u64,
|
||||
}
|
||||
|
||||
type Cell = (i32, i32, i32);
|
||||
@@ -187,6 +214,11 @@ struct Entry {
|
||||
/// The box (in blocks) the paths lay in.
|
||||
min: [i32; 3],
|
||||
max: [i32; 3],
|
||||
/// The tick it was traced at, so a trace that met unknown cells (see
|
||||
/// [`UNKNOWN_TICKS`]) is not kept for ever.
|
||||
born: u64,
|
||||
/// The walk met blocks nobody knows (a chunk the snapshot is missing).
|
||||
unk: bool,
|
||||
used: u64,
|
||||
}
|
||||
impl Entry {
|
||||
@@ -206,12 +238,16 @@ struct EchoEntry {
|
||||
room: Room,
|
||||
min: [i32; 3],
|
||||
max: [i32; 3],
|
||||
born: u64,
|
||||
unk: bool,
|
||||
used: u64,
|
||||
}
|
||||
/// The space round the listener's block.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct RoomEntry {
|
||||
room: Room,
|
||||
born: u64,
|
||||
unk: bool,
|
||||
used: u64,
|
||||
}
|
||||
/// The box a room measure from `cell` looked in.
|
||||
@@ -270,7 +306,7 @@ struct Done {
|
||||
seq: u64,
|
||||
entries: Vec<(Key, Entry)>,
|
||||
echoes: Vec<(Key, EchoEntry, u64)>,
|
||||
rooms: Vec<(Cell, Room)>,
|
||||
rooms: Vec<(Cell, Room, bool)>,
|
||||
cells: usize,
|
||||
unknown: usize,
|
||||
}
|
||||
@@ -294,7 +330,15 @@ fn trace_echo(geometry: &dyn Geometry, key: Key, budget: &mut usize, clock: u64)
|
||||
let m = room::measure(geometry, centre(key.0), &mut room_budget);
|
||||
let us = start.elapsed().as_micros() as u64;
|
||||
(
|
||||
EchoEntry { taps: t.taps, room: m.room, min: t.min, max: t.max, used: clock },
|
||||
EchoEntry {
|
||||
taps: t.taps,
|
||||
room: m.room,
|
||||
min: t.min,
|
||||
max: t.max,
|
||||
born: clock,
|
||||
unk: t.unknown + m.unknown > 0,
|
||||
used: clock,
|
||||
},
|
||||
t.cells + m.cells,
|
||||
t.unknown + m.unknown,
|
||||
us,
|
||||
@@ -320,6 +364,8 @@ fn trace(geometry: &dyn Geometry, listener: [f64; 3], at: [f64; 3], paths: usize
|
||||
paths: paths as u8,
|
||||
min: [lo(0), lo(1), lo(2)],
|
||||
max: [hi(0), hi(1), hi(2)],
|
||||
born: 0,
|
||||
unk: unknown > 0,
|
||||
used: 0,
|
||||
},
|
||||
before - *budget,
|
||||
@@ -334,6 +380,12 @@ pub struct Acoustics {
|
||||
cache: HashMap<Key, Entry>,
|
||||
clock: u64,
|
||||
applied: HashMap<u32, Applied>,
|
||||
/// The sounds that were in the acoustic range at the last step (with the geometry present),
|
||||
/// so one that leaves it (beyond `RANGE`, inaudible, or the world gone) is told so plainly.
|
||||
scoped: FxHashSet<u32>,
|
||||
/// The residency (`Changes::residency`) the last step saw: worker jobs made before a change
|
||||
/// are dropped (`floor`) when it moves.
|
||||
residency: Option<u64>,
|
||||
log: std::collections::VecDeque<(u64, Cell)>,
|
||||
seq: u64,
|
||||
/// Jobs up to this number belong to a world that is gone.
|
||||
@@ -360,6 +412,8 @@ impl Acoustics {
|
||||
cache: HashMap::new(),
|
||||
clock: 0,
|
||||
applied: HashMap::new(),
|
||||
scoped: FxHashSet::default(),
|
||||
residency: None,
|
||||
log: Default::default(),
|
||||
seq: 0,
|
||||
floor: 0,
|
||||
@@ -381,6 +435,8 @@ impl Acoustics {
|
||||
pub fn reset(&mut self) {
|
||||
self.cache.clear();
|
||||
self.applied.clear();
|
||||
self.scoped.clear();
|
||||
self.residency = None;
|
||||
self.echo_cache.clear();
|
||||
self.echo_applied.clear();
|
||||
self.echo_pending.clear();
|
||||
@@ -427,7 +483,7 @@ impl Acoustics {
|
||||
let m = room::measure(&*job.geometry, centre(cell), &mut b);
|
||||
cells += m.cells;
|
||||
unknown += m.unknown;
|
||||
rooms.push((cell, m.room));
|
||||
rooms.push((cell, m.room, m.unknown > 0));
|
||||
}
|
||||
if done_tx.send(Done { seq: job.seq, entries, echoes, rooms, cells, unknown }).is_err() {
|
||||
break;
|
||||
@@ -453,6 +509,7 @@ impl Acoustics {
|
||||
self.floor = self.seq;
|
||||
self.in_flight = None;
|
||||
self.applied.clear();
|
||||
self.scoped.clear();
|
||||
return;
|
||||
}
|
||||
for &s in &changes.sections {
|
||||
@@ -473,6 +530,7 @@ impl Acoustics {
|
||||
|
||||
fn insert(&mut self, key: Key, mut entry: Entry) {
|
||||
entry.used = self.clock;
|
||||
entry.born = self.clock;
|
||||
self.cache.insert(key, entry);
|
||||
if self.cache.len() > CACHE {
|
||||
let mut ages: Vec<u64> = self.cache.values().map(|e| e.used).collect();
|
||||
@@ -484,6 +542,9 @@ impl Acoustics {
|
||||
|
||||
fn insert_echo(&mut self, key: Key, mut entry: EchoEntry) {
|
||||
entry.used = self.clock;
|
||||
if entry.born == 0 {
|
||||
entry.born = self.clock;
|
||||
}
|
||||
self.echo_cache.insert(key, entry);
|
||||
if self.echo_cache.len() > ECHO_CACHE {
|
||||
let mut ages: Vec<u64> = self.echo_cache.values().map(|e| e.used).collect();
|
||||
@@ -510,6 +571,14 @@ impl Acoustics {
|
||||
) -> Vec<Applied> {
|
||||
self.clock += 1;
|
||||
self.invalidate(changes);
|
||||
// The resident chunks changed: a job traced from the snapshot before them applies to
|
||||
// chunks that may be gone, so its answers are dropped with the old residency's answers.
|
||||
if self.residency != Some(changes.residency) {
|
||||
self.residency = Some(changes.residency);
|
||||
self.seq += 1;
|
||||
self.floor = self.seq;
|
||||
self.in_flight = None;
|
||||
}
|
||||
// The worker's results; one that began before a change in its box is not kept.
|
||||
if let Some((_, rx)) = &self.worker {
|
||||
let done: Vec<Done> = rx.try_iter().collect();
|
||||
@@ -535,13 +604,16 @@ impl Acoustics {
|
||||
self.insert_echo(key, e);
|
||||
}
|
||||
}
|
||||
for (cell, room) in d.rooms {
|
||||
for (cell, room, unknown) in d.rooms {
|
||||
let (lo, hi) = room_box(cell);
|
||||
self.stats.room_traces += 1;
|
||||
if self.log.iter().any(|(s, c)| *s >= d.seq && box_touches(lo, hi, *c)) {
|
||||
self.stats.dropped_late += 1;
|
||||
} else {
|
||||
self.room_cache.insert(cell, RoomEntry { room, used: self.clock });
|
||||
self.room_cache.insert(
|
||||
cell,
|
||||
RoomEntry { room, born: self.clock, unk: unknown, used: self.clock },
|
||||
);
|
||||
}
|
||||
}
|
||||
for (key, e) in d.entries {
|
||||
@@ -562,8 +634,11 @@ impl Acoustics {
|
||||
self.applied.retain(|id, _| sources.iter().any(|s| s.id == *id));
|
||||
self.sends_applied.retain(|id, _| sources.iter().any(|s| s.id == *id));
|
||||
self.echo_applied.retain(|id, _| sources.iter().any(|s| s.id == *id));
|
||||
// A sound that left the acoustic range no longer holds what it was once told: the mixer
|
||||
// gets the plain numbers for it (and no taps, no sends), instead of its last wall.
|
||||
let mut told = self.settle_scope(sources, &live, listener, geometry.is_none());
|
||||
let Some(geometry) = geometry else {
|
||||
return self.settle(&live, listener, None);
|
||||
return told;
|
||||
};
|
||||
// The loudest first; ties by id, so the order never depends on a hash.
|
||||
let mut order: Vec<usize> = (0..live.len()).collect();
|
||||
@@ -577,7 +652,9 @@ impl Acoustics {
|
||||
let want = if rank < FULL_PATHS { 9 } else { 1 };
|
||||
if let Some(e) = self.cache.get_mut(&key) {
|
||||
e.used = self.clock;
|
||||
if e.paths as usize >= want || e.paths == 9 {
|
||||
// A trace that met unknown cells is not kept for ever: a chunk may have arrived.
|
||||
let partial = e.unk && self.clock >= e.born + UNKNOWN_TICKS;
|
||||
if !partial && (e.paths as usize >= want || e.paths == 9) {
|
||||
self.stats.hits += 1;
|
||||
continue;
|
||||
}
|
||||
@@ -621,7 +698,8 @@ impl Acoustics {
|
||||
}
|
||||
}
|
||||
self.stats.cached = self.cache.len();
|
||||
self.settle(&live, listener, Some(lcell))
|
||||
told.extend(self.settle(&live, listener, Some(lcell)));
|
||||
told
|
||||
}
|
||||
|
||||
/// Gives the loudest echo sources their taps from the cache, traces the missing ones at once
|
||||
@@ -638,10 +716,22 @@ impl Acoustics {
|
||||
}
|
||||
if served >= ECHO_VOICES {
|
||||
self.stats.echo_capped += 1;
|
||||
// It was served before (its taps are in the mixer): they are recalled, so the
|
||||
// cap holds on the taps that sound, not only on these traced just now.
|
||||
self.recall_echo(s.id);
|
||||
continue;
|
||||
}
|
||||
served += 1;
|
||||
let key = (block_of(s.at), lcell);
|
||||
// A trace that met unknown cells is not kept for ever: a chunk may have arrived
|
||||
// under it, and it is traced again like a missing one would be.
|
||||
if self
|
||||
.echo_cache
|
||||
.get(&key)
|
||||
.is_some_and(|e| e.unk && self.clock >= e.born + UNKNOWN_TICKS)
|
||||
{
|
||||
self.echo_cache.remove(&key);
|
||||
}
|
||||
if let Some(e) = self.echo_cache.get_mut(&key) {
|
||||
e.used = self.clock;
|
||||
self.stats.echo_hits += 1;
|
||||
@@ -677,24 +767,38 @@ impl Acoustics {
|
||||
}
|
||||
|
||||
/// The space the listener is in: from the cache, measured at once (a film) or asked of the
|
||||
/// worker (the cell is returned). The last known space stays until the new one is.
|
||||
/// worker (the cell is returned). The last known space stays until the new one is. A measure
|
||||
/// that met unknown cells is not kept for ever: a chunk may have arrived under it.
|
||||
fn settle_room(&mut self, geometry: &dyn Geometry, listener: [f64; 3]) -> Vec<Cell> {
|
||||
let cell = block_of(listener);
|
||||
if let Some(e) = self.room_cache.get_mut(&cell) {
|
||||
e.used = self.clock;
|
||||
self.stats.room_hits += 1;
|
||||
self.room_now = Some(e.room);
|
||||
} else if self.sync {
|
||||
if !(e.unk && self.clock >= e.born + UNKNOWN_TICKS) {
|
||||
self.stats.room_hits += 1;
|
||||
self.room_now = Some(e.room);
|
||||
return self.room_cache_tidy();
|
||||
}
|
||||
self.room_cache.remove(&cell);
|
||||
}
|
||||
if self.sync {
|
||||
let mut budget = ROOM_CELLS;
|
||||
let m = room::measure(geometry, centre(cell), &mut budget);
|
||||
self.stats.room_traces += 1;
|
||||
self.stats.room_cells += m.cells as u64;
|
||||
self.stats.unknown_cells += m.unknown as u64;
|
||||
self.room_cache.insert(cell, RoomEntry { room: m.room, used: self.clock });
|
||||
self.room_cache.insert(
|
||||
cell,
|
||||
RoomEntry { room: m.room, born: self.clock, unk: m.unknown > 0, used: self.clock },
|
||||
);
|
||||
self.room_now = Some(m.room);
|
||||
} else {
|
||||
return vec![cell];
|
||||
}
|
||||
self.room_cache_tidy()
|
||||
}
|
||||
|
||||
/// Keeps the room cache bounded and carries the listener's space into the stats.
|
||||
fn room_cache_tidy(&mut self) -> Vec<Cell> {
|
||||
if self.room_cache.len() > ROOM_CACHE {
|
||||
let mut ages: Vec<u64> = self.room_cache.values().map(|e| e.used).collect();
|
||||
ages.sort_unstable();
|
||||
@@ -741,6 +845,76 @@ impl Acoustics {
|
||||
std::mem::take(&mut self.sends_pending)
|
||||
}
|
||||
|
||||
/// A sound that left the acoustic range is not traced any more, but what the mixer was told
|
||||
/// keeps playing: the sounds that just left the range (beyond [`RANGE`], too quiet to hear, or
|
||||
/// with the world gone for a step) are given the plain numbers again — the air's absorption
|
||||
/// and no wall — and their echo taps are recalled and their reverb sends to nothing. Nothing
|
||||
/// is kept for a sound the mixer never had numbers for.
|
||||
fn settle_scope(&mut self, sources: &[Source], live: &[Source], listener: [f64; 3], no_geometry: bool) -> Vec<Applied> {
|
||||
let mut out = Vec::new();
|
||||
let mut was = std::mem::take(&mut self.scoped);
|
||||
if no_geometry {
|
||||
// The world is gone for a step: nothing can be traced for the live ones either, so
|
||||
// the plain numbers stand for them until the world is read again.
|
||||
for s in live {
|
||||
if was.remove(&s.id) {
|
||||
let cutoff = AIR.powf(dist(s.at, listener) as f32);
|
||||
out.extend(self.unhold(s.id, 1.0, cutoff));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for s in live {
|
||||
was.remove(&s.id);
|
||||
}
|
||||
}
|
||||
// What is left are the sounds that left the range while the geometry was there: beyond
|
||||
// RANGE, or quieter than the ear hears. The air still takes the highs by the distance,
|
||||
// so the numbers continue the open air's numbers without the old wall.
|
||||
for s in sources {
|
||||
if !was.remove(&s.id) {
|
||||
continue;
|
||||
}
|
||||
let cutoff = AIR.powf(dist(s.at, listener) as f32);
|
||||
out.extend(self.unhold(s.id, 1.0, cutoff));
|
||||
}
|
||||
if !no_geometry {
|
||||
self.scoped.extend(live.iter().map(|s| s.id));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Puts one sound's acoustics back to the plain numbers. The mixer hears them only when they
|
||||
/// differ; the echo taps are recalled (they fade out there) and the reverb send goes to
|
||||
/// nothing (it fades in the mixer too). Returns the numbers, if the mixer has to hear them.
|
||||
fn unhold(&mut self, id: u32, gain: f32, cutoff: f32) -> Option<Applied> {
|
||||
let out = self
|
||||
.applied
|
||||
.get(&id)
|
||||
.is_some_and(|p| {
|
||||
(p.gain - gain).abs() >= 0.002 || (p.cutoff - cutoff).abs() >= 0.002 * p.cutoff.max(0.01)
|
||||
})
|
||||
.then_some(Applied { id, gain, cutoff });
|
||||
if let Some(now) = out {
|
||||
self.applied.insert(id, now);
|
||||
}
|
||||
self.recall_echo(id);
|
||||
if self.sends_applied.get(&id).is_some_and(|s| s.iter().any(|v| v > &0.0)) {
|
||||
self.sends_applied.insert(id, [0.0; 4]);
|
||||
self.sends_pending.push((id, [0.0; 4]));
|
||||
self.stats.sends += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The echo taps of one sound are recalled (the mixer fades them out, so a tap that comes
|
||||
/// back a moment later picks up where it left off). A sound that had none gains none.
|
||||
fn recall_echo(&mut self, id: u32) {
|
||||
if self.echo_applied.get(&id).is_some_and(|t| !t.is_empty()) {
|
||||
self.echo_applied.insert(id, Vec::new());
|
||||
self.echo_pending.push((id, Vec::new()));
|
||||
}
|
||||
}
|
||||
|
||||
/// The numbers for each live source from the cache; only the changed ones are returned.
|
||||
fn settle(&mut self, live: &[Source], listener: [f64; 3], lcell: Option<Cell>) -> Vec<Applied> {
|
||||
let lcell = lcell.unwrap_or_else(|| cell_of(listener));
|
||||
@@ -1033,4 +1207,276 @@ mod tests {
|
||||
}
|
||||
panic!("the worker did not answer");
|
||||
}
|
||||
|
||||
/// A wall appears at x = 40..41 once `arrived` is set; while it is not, its columns are
|
||||
/// unknown to the walk (a chunk that is not resident yet), and open ground otherwise.
|
||||
struct Arriving {
|
||||
arrived: std::sync::atomic::AtomicU32,
|
||||
wall: std::ops::Range<i32>,
|
||||
}
|
||||
impl Geometry for Arriving {
|
||||
fn walker(&self) -> Box<dyn FnMut(i32, i32, i32) -> Option<f32> + '_> {
|
||||
let arrived = self.arrived.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let wall = self.wall.clone();
|
||||
Box::new(move |x, _, _| match (arrived, wall.contains(&x)) {
|
||||
(0, true) => None,
|
||||
(1, true) => Some(1.0),
|
||||
_ => Some(0.0),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// (AirDef fork #110.) A wall streams in underneath a trace made while its columns were
|
||||
/// unknown: nothing invalidates the cache, but the unknown trace does not outlive it.
|
||||
#[test]
|
||||
fn an_unknown_trace_is_not_kept_and_is_traced_again_when_the_chunks_answer() {
|
||||
let concrete = Arc::new(Arriving { arrived: 0.into(), wall: 40..42 });
|
||||
let g: Arc<dyn Geometry> = concrete.clone();
|
||||
let mut a = Acoustics::new(true);
|
||||
let l = [0.5, 70.5, 0.5];
|
||||
let before = a.step(Some(&g), l, &[src(1, 90.5)], &Changes::default());
|
||||
assert!(before[0].cutoff > 0.5, "nothing is known yet: {before:?}");
|
||||
assert!(a.stats.unknown_cells > 0);
|
||||
concrete.arrived.store(1, std::sync::atomic::Ordering::Relaxed);
|
||||
// The partial answer is mixed with only for a bounded while.
|
||||
for _ in 1..UNKNOWN_TICKS {
|
||||
let out = a.step(Some(&g), l, &[src(1, 90.5)], &Changes::default());
|
||||
assert!(out.is_empty(), "coasting on the partial answer: {out:?}");
|
||||
}
|
||||
// Then it is traced again against the geometry that now knows the wall: muted.
|
||||
let after = a.step(Some(&g), l, &[src(1, 90.5)], &Changes::default());
|
||||
assert_eq!(after.len(), 1);
|
||||
assert!(after[0].cutoff < 0.01, "{after:?}");
|
||||
assert!(a.stats.traces >= 2);
|
||||
// And being out of partial answers, the cache answers again.
|
||||
assert!(a.step(Some(&g), l, &[src(1, 90.5)], &Changes::default()).is_empty());
|
||||
}
|
||||
|
||||
/// (AirDef fork #110.) The column is reloaded (a chunk packet replaces a loaded one) and
|
||||
/// then it goes away: the estimates traced through its sections are dropped both ways.
|
||||
#[test]
|
||||
fn a_reload_of_the_same_column_with_other_content_changes_the_sound() {
|
||||
let mut a = Acoustics::new(true);
|
||||
let open: Arc<dyn Geometry> = Arc::new(Arriving { arrived: 0.into(), wall: 0..0 });
|
||||
let l = [0.5, 70.5, 0.5];
|
||||
let first = a.step(Some(&open), l, &[src(1, 90.5)], &Changes::default());
|
||||
assert!(first[0].cutoff > 0.5, "{first:?}");
|
||||
// Reloaded with a wall in it: its section counts as changed, and the estimate traced
|
||||
// through it is not kept.
|
||||
let walled: Arc<dyn Geometry> = Arc::new(Arriving { arrived: 1.into(), wall: 40..42 });
|
||||
let reload = a.step(
|
||||
Some(&walled),
|
||||
l,
|
||||
&[src(1, 90.5)],
|
||||
&Changes { residency: 3, sections: vec![(2, 4, 0)], ..Default::default() },
|
||||
);
|
||||
assert!(reload[0].cutoff < 0.01, "{reload:?}");
|
||||
// Unloaded again: bright once more.
|
||||
let gone = a.step(
|
||||
Some(&open),
|
||||
l,
|
||||
&[src(1, 90.5)],
|
||||
&Changes { residency: 4, sections: vec![(2, 4, 0)], ..Default::default() },
|
||||
);
|
||||
assert!(gone[0].cutoff > 0.5, "{gone:?}");
|
||||
}
|
||||
|
||||
/// (AirDef fork #111.) A sound (or a listener) that walks out of the acoustic range gives
|
||||
/// up its old wall and takes the plain numbers of its new distance until it comes back.
|
||||
#[test]
|
||||
fn a_sound_that_leaves_the_range_lets_the_wall_go_and_comes_back_traced_anew() {
|
||||
let mut a = Acoustics::new(true);
|
||||
let wall = Wall::new(100..103, 1.0);
|
||||
let l = [0.5, 70.5, 0.5];
|
||||
let first = a.step(Some(&wall), l, &[src(1, 200.5)], &Changes::default());
|
||||
assert!(first[0].cutoff < 0.001, "{first:?}");
|
||||
// The same sound moves past the range: the wall it was behind stops applying.
|
||||
let far = a.step(Some(&wall), l, &[src(1, 700.5)], &Changes::default());
|
||||
assert_eq!(far.len(), 1);
|
||||
assert_eq!(far[0].gain, 1.0);
|
||||
assert!((far[0].cutoff - AIR.powf(700.0)).abs() < 1e-6, "{far:?}");
|
||||
// Already there: the mixer hears nothing new.
|
||||
assert!(a.step(Some(&wall), l, &[src(1, 700.5)], &Changes::default()).is_empty());
|
||||
// It walks back into the range and the actual wall applies again.
|
||||
let back = a.step(Some(&wall), l, &[src(1, 200.5)], &Changes::default());
|
||||
assert!(back[0].cutoff < 0.001 && (back[0].gain - 0.41).abs() < 0.01, "{back:?}");
|
||||
// A listener walking past the range falls back the same way.
|
||||
let away = a.step(Some(&wall), [800.5, 70.5, 0.5], &[src(1, 200.5)], &Changes::default());
|
||||
assert!((away[0].cutoff - AIR.powf(600.0)).abs() < 1e-6, "{away:?}");
|
||||
let here = a.step(Some(&wall), l, &[src(1, 200.5)], &Changes::default());
|
||||
assert!(here[0].cutoff < 0.001, "{here:?}");
|
||||
// One sound, one set of numbers: nothing piles up.
|
||||
assert_eq!(a.applied.len(), 1);
|
||||
}
|
||||
|
||||
/// (AirDef fork #111.) A sound the distance gain makes inaudible stops holding the wall.
|
||||
#[test]
|
||||
fn a_temporarily_inaudible_sound_goes_plain_and_gets_the_wall_back_when_loud() {
|
||||
let mut a = Acoustics::new(true);
|
||||
let wall = Wall::new(40..42, 1.0);
|
||||
let l = [0.5, 70.5, 0.5];
|
||||
a.step(Some(&wall), l, &[src(1, 90.5)], &Changes::default());
|
||||
let quiet = [Source { loud: 0.0001, ..src(1, 90.5) }];
|
||||
let plain = a.step(Some(&wall), l, &quiet, &Changes::default());
|
||||
assert_eq!(plain.len(), 1, "{plain:?}");
|
||||
assert_eq!(plain[0].gain, 1.0);
|
||||
assert!((plain[0].cutoff - AIR.powf(90.0)).abs() < 1e-6);
|
||||
let loud_again = a.step(Some(&wall), l, &[src(1, 90.5)], &Changes::default());
|
||||
assert!(loud_again[0].cutoff < 0.01, "{loud_again:?}");
|
||||
}
|
||||
|
||||
/// (AirDef fork #111.) A world that is gone for a step (no geometry to read) does not keep
|
||||
/// any sound's numbers beyond its last trace, and the real numbers apply when it returns.
|
||||
#[test]
|
||||
fn a_world_that_is_gone_for_a_step_lets_every_sound_go_until_it_can_be_read() {
|
||||
let mut a = Acoustics::new(true);
|
||||
let wall = Wall::new(100..103, 1.0);
|
||||
let l = [0.5, 70.5, 0.5];
|
||||
let muted = a.step(Some(&wall), l, &[src(1, 200.5)], &Changes::default());
|
||||
assert!(muted[0].cutoff < 0.001, "{muted:?}");
|
||||
// No geometry: the plain numbers, and nothing gets traced.
|
||||
let plain = a.step(None, l, &[src(1, 200.5)], &Changes::default());
|
||||
assert_eq!(plain.len(), 1);
|
||||
assert_eq!(plain[0].gain, 1.0);
|
||||
assert!((plain[0].cutoff - AIR.powf(200.0)).abs() < 1e-6);
|
||||
assert!(a.step(None, l, &[src(1, 200.5)], &Changes::default()).is_empty(), "no repeats");
|
||||
assert_eq!(a.stats.traces, 1, "nothing was traced without the geometry");
|
||||
// The world is back: the actual numbers apply again.
|
||||
let back = a.step(Some(&wall), l, &[src(1, 200.5)], &Changes::default());
|
||||
assert!(back[0].cutoff < 0.001 && (back[0].gain - 0.41).abs() < 0.01, "{back:?}");
|
||||
assert_eq!(a.applied.len(), 1);
|
||||
}
|
||||
|
||||
/// (AirDef fork #111.) An echo sound out of the range recalls its taps (an empty set, which
|
||||
/// the mixer fades out) and takes the current geometry's taps when it comes back.
|
||||
#[test]
|
||||
fn an_echo_sound_leaving_the_range_recalls_its_taps_and_takes_them_back() {
|
||||
let mut a = Acoustics::new(true);
|
||||
let g = gorge(true);
|
||||
a.step(Some(&g), LISTENER, &[shot(1, 0.5)], &Changes::default());
|
||||
assert_eq!(a.take_echoes().len(), 1, "served once");
|
||||
a.take_sends();
|
||||
assert_eq!(a.echo_applied.len(), 1);
|
||||
assert_eq!(a.echo_applied[&1].len(), 2);
|
||||
a.step(Some(&g), LISTENER, &[shot(1, 700.5)], &Changes::default());
|
||||
assert_eq!(a.take_echoes(), [(1, Vec::new())], "recalled");
|
||||
a.take_sends();
|
||||
// Nothing more while it stays out.
|
||||
a.step(Some(&g), LISTENER, &[shot(1, 700.5)], &Changes::default());
|
||||
assert!(a.take_echoes().is_empty() && a.take_sends().is_empty());
|
||||
// Back in range: the current taps apply again.
|
||||
a.step(Some(&g), LISTENER, &[shot(1, 0.5)], &Changes::default());
|
||||
assert_eq!(a.echo_applied[&1].len(), 2);
|
||||
assert_eq!(a.echo_applied.len(), 1, "one sound, one record");
|
||||
}
|
||||
|
||||
/// (AirDef fork #111.) A sound out of the range stops its reverb send explicitly.
|
||||
#[test]
|
||||
fn a_sound_leaving_the_range_sends_nothing_to_the_reverb_until_it_returns() {
|
||||
let mut a = Acoustics::new(true);
|
||||
let inside = [12.5, 64.5, 12.5];
|
||||
let source = Source { id: 1, at: [12.5, 64.5, 18.5], loud: 0.5, echo: true };
|
||||
a.step(Some(&hall()), inside, &[source], &Changes::default());
|
||||
assert!(!a.take_sends().is_empty(), "the hall sends");
|
||||
// Far away: the send goes to nothing explicitly (the mixer fades it out).
|
||||
let far = Source { at: [700.5, 64.5, 12.5], ..source };
|
||||
a.step(Some(&hall()), inside, &[far], &Changes::default());
|
||||
assert_eq!(a.take_sends(), [(1, [0.0; 4])]);
|
||||
// Already nothing: no repeats.
|
||||
assert!(a.step(Some(&hall()), inside, &[far], &Changes::default()).is_empty());
|
||||
// Back in the hall: its sends apply again.
|
||||
a.step(Some(&hall()), inside, &[source], &Changes::default());
|
||||
let again = a.take_sends();
|
||||
assert_eq!(again.len(), 1);
|
||||
assert!(again[0].1.iter().any(|v| *v > 0.0), "{again:?}");
|
||||
}
|
||||
|
||||
/// (AirDef fork #112.) Rotating the loudest ECHO_VOICES around: the voices that leave the
|
||||
/// budget have their taps recalled, so the budget holds on the taps that sound.
|
||||
#[test]
|
||||
fn the_echo_budget_recalls_the_taps_of_the_voices_that_leave_it() {
|
||||
let mut a = Acoustics::new(true);
|
||||
let rocks: Vec<Source> = (0..16).map(|i| shot(i, f64::from(i) * 1.5 + 0.5)).collect();
|
||||
a.step(Some(&gorge(true)), LISTENER, &rocks, &Changes::default());
|
||||
a.take_echoes();
|
||||
// Equal loudnesses: the first eight ids take the budget, each with taps.
|
||||
assert_eq!(a.echo_applied.len(), 8);
|
||||
assert_eq!(a.echo_applied[&0].len(), 2);
|
||||
// Now the even ids are louder: they take the budget, the odd ones' taps are recalled.
|
||||
let swapped: Vec<Source> = (0..16)
|
||||
.map(|i| Source { loud: if i % 2 == 0 { 0.9 } else { 0.1 }, ..shot(i, f64::from(i) * 1.5 + 0.5) })
|
||||
.collect();
|
||||
a.step(Some(&gorge(true)), LISTENER, &swapped, &Changes::default());
|
||||
let told = a.take_echoes();
|
||||
assert_eq!(
|
||||
told.iter().filter(|(_, t)| t.is_empty()).map(|(id, _)| *id).collect::<Vec<_>>(),
|
||||
[1, 3, 5, 7],
|
||||
"the recalled voices",
|
||||
);
|
||||
assert_eq!(
|
||||
told.iter().filter(|(_, t)| !t.is_empty()).map(|(id, _)| *id).collect::<Vec<_>>(),
|
||||
[8, 10, 12, 14],
|
||||
"the new voices",
|
||||
);
|
||||
// Exactly the cap holds on the voices that carry taps, through the rotation.
|
||||
assert_eq!(a.echo_applied.iter().filter(|(_, t)| !t.is_empty()).count(), ECHO_VOICES);
|
||||
assert_eq!(a.echo_applied.iter().filter(|(_, t)| t.is_empty()).count(), 4);
|
||||
// And rotating back does not pile anything up.
|
||||
a.step(Some(&gorge(true)), LISTENER, &rocks, &Changes::default());
|
||||
a.take_echoes();
|
||||
assert_eq!(a.echo_applied.iter().filter(|(_, t)| !t.is_empty()).count(), ECHO_VOICES);
|
||||
assert_eq!(a.echo_applied.len(), 12);
|
||||
assert_eq!(a.stats.echo_capped, 24);
|
||||
}
|
||||
|
||||
/// A geometry whose walk is slow, so that a worker job can be watched without a race.
|
||||
struct Slow(std::ops::Range<i32>);
|
||||
impl Geometry for Slow {
|
||||
fn walker(&self) -> Box<dyn FnMut(i32, i32, i32) -> Option<f32> + '_> {
|
||||
Box::new(move |x, _, _| {
|
||||
std::hint::black_box(
|
||||
(0..3_000).fold(1u64, |s, i| s.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(i)),
|
||||
);
|
||||
Some(if self.0.contains(&x) { 1.0 } else { 0.0 })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// (AirDef fork #110.) A worker job that traced the snapshot before the resident chunks
|
||||
/// changed is dropped as a late answer; the job made after the change applies.
|
||||
#[test]
|
||||
fn an_answer_made_from_the_old_residency_is_dropped_and_the_new_one_applies() {
|
||||
let mut a = Acoustics::new(false);
|
||||
let slow: Arc<dyn Geometry> = Arc::new(Slow(40..42));
|
||||
let l = [0.5, 70.5, 0.5];
|
||||
let s = [src(1, 90.5)];
|
||||
a.step(Some(&slow), l, &s, &Changes::default());
|
||||
// The one-path estimate goes to the worker for its nine paths; before the answer lands,
|
||||
// the resident columns change: its answer is dropped with the old residency's answers.
|
||||
a.step(Some(&slow), l, &s, &Changes { residency: 1, ..Default::default() });
|
||||
let after = Changes { residency: 7, ..Default::default() };
|
||||
for _ in 0..200 {
|
||||
a.step(None, l, &s, &after);
|
||||
if a.stats.jobs >= 1 && a.stats.dropped_late > 0 {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
assert!(a.stats.dropped_late >= 1, "the job was not dropped: {:?}", &a.stats);
|
||||
assert_eq!(a.cache.len(), 1, "only the game thread's own estimate is cached");
|
||||
assert_eq!(a.cache.values().next().unwrap().paths, 1, "the worker's refined answer never landed");
|
||||
// The next job, made after the change, is kept. Wait for its actual result rather than
|
||||
// for the number of unrelated echo/room jobs that happened to finish first.
|
||||
a.step(Some(&slow), l, &s, &after);
|
||||
for _ in 0..400 {
|
||||
a.step(None, l, &s, &after);
|
||||
if a.cache.values().next().is_some_and(|e| e.paths == 9) {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
assert!(a.stats.jobs >= 2);
|
||||
assert_eq!(a.cache.len(), 1, "the new residency's answer is cached");
|
||||
assert_eq!(a.cache.values().next().unwrap().paths, 9, "it refined to nine paths");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
//! Resident chunk membership, with a token for consumers that cache its keys.
|
||||
use crate::{chunk::Chunk, fast_hash::FxHashMap};
|
||||
//! Resident chunk membership, with a token for consumers that cache its keys and the columns
|
||||
//! whose residency changed (AirDef fork #110: the acoustics must hear that too).
|
||||
use crate::{chunk::Chunk, fast_hash::{FxHashMap, FxHashSet}};
|
||||
use std::{
|
||||
collections::hash_map::{Iter, ValuesMut},
|
||||
collections::VecDeque,
|
||||
ops::Deref,
|
||||
sync::{
|
||||
Arc,
|
||||
@@ -16,18 +18,31 @@ fn next_revision() -> u64 {
|
||||
NEXT_REVISION.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Columns kept in the changed log past this many of them: everything counts as changed, then.
|
||||
const CHANGE_CAP: usize = 256;
|
||||
/// Read access behaves like a map. Structural writes are explicit so edits to
|
||||
/// blocks or light don't invalidate consumers of the loaded chunk key set.
|
||||
/// Tokens are process-wide: replacing the entire map also invalidates caches.
|
||||
pub struct ChunkMap {
|
||||
columns: Columns,
|
||||
revision: u64,
|
||||
/// Bumped by every change of what the columns hold: an insertion (new or
|
||||
/// replacing a loaded one) and a removal. Edits through `get_mut` are
|
||||
/// block writes, which the world reports by itself.
|
||||
content: u64,
|
||||
/// The columns whose residency changed since `take_changed` last read,
|
||||
/// in the order they changed, up to [`CHANGE_CAP`] of them.
|
||||
changed: VecDeque<Key>,
|
||||
changed_dropped: bool,
|
||||
}
|
||||
impl Default for ChunkMap {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
columns: Columns::default(),
|
||||
revision: next_revision(),
|
||||
content: next_revision(),
|
||||
changed: VecDeque::default(),
|
||||
changed_dropped: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,17 +50,41 @@ impl ChunkMap {
|
||||
pub fn membership_revision(&self) -> u64 {
|
||||
self.revision
|
||||
}
|
||||
/// What the columns hold, not just which of them are resident: a consumer
|
||||
/// that keeps chunk contents behind `Arc`s (the acoustics' snapshot) must
|
||||
/// be made again when this moves.
|
||||
pub fn content_revision(&self) -> u64 {
|
||||
self.content
|
||||
}
|
||||
/// The columns whose residency changed since the last call, and whether
|
||||
/// changes were dropped (every column counts as changed, then).
|
||||
pub fn take_changed(&mut self) -> (FxHashSet<Key>, bool) {
|
||||
let dropped = std::mem::take(&mut self.changed_dropped);
|
||||
let changed = self.changed.drain(..).collect();
|
||||
(changed, dropped)
|
||||
}
|
||||
fn changed_column(&mut self, key: Key) {
|
||||
if self.changed.len() >= CHANGE_CAP {
|
||||
self.changed_dropped = true;
|
||||
self.changed.clear();
|
||||
}
|
||||
self.changed.push_back(key);
|
||||
}
|
||||
pub fn insert(&mut self, key: Key, chunk: Arc<Chunk>) -> Option<Arc<Chunk>> {
|
||||
let previous = self.columns.insert(key, chunk);
|
||||
self.content = next_revision();
|
||||
if previous.is_none() {
|
||||
self.revision = next_revision();
|
||||
}
|
||||
self.changed_column(key);
|
||||
previous
|
||||
}
|
||||
pub fn remove(&mut self, key: &Key) -> Option<Arc<Chunk>> {
|
||||
let previous = self.columns.remove(key);
|
||||
if previous.is_some() {
|
||||
self.revision = next_revision();
|
||||
self.content = next_revision();
|
||||
self.changed_column(*key);
|
||||
}
|
||||
previous
|
||||
}
|
||||
@@ -53,6 +92,9 @@ impl ChunkMap {
|
||||
if !self.columns.is_empty() {
|
||||
self.columns.clear();
|
||||
self.revision = next_revision();
|
||||
self.content = next_revision();
|
||||
self.changed_dropped = true;
|
||||
self.changed.clear();
|
||||
}
|
||||
}
|
||||
pub fn get_mut(&mut self, key: &Key) -> Option<&mut Arc<Chunk>> {
|
||||
@@ -122,4 +164,55 @@ mod tests {
|
||||
map = ChunkMap::default();
|
||||
assert_ne!(cleared, map.membership_revision());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insertions_replacements_and_removals_change_the_content_token_and_are_reported() {
|
||||
let mut map = ChunkMap::default();
|
||||
let chunk = Arc::new(Chunk::empty(0, 0));
|
||||
let (changed, dropped) = map.take_changed();
|
||||
assert!(changed.is_empty() && !dropped);
|
||||
// An insertion is reported once.
|
||||
map.insert((0, 0), chunk.clone());
|
||||
let content = map.content_revision();
|
||||
let membership = map.membership_revision();
|
||||
let had_it = map.take_changed();
|
||||
assert_eq!(had_it.0.len(), 1);
|
||||
assert!(had_it.0.contains(&(0, 0)));
|
||||
// A replacement (a reload with other content): the key set is the
|
||||
// same, what the column holds is not.
|
||||
let mut other = Chunk::empty(0, 0);
|
||||
other.set(0, 0, 0, 9);
|
||||
let reload = map.insert((0, 0), Arc::new(other)).is_some();
|
||||
assert!(reload);
|
||||
assert_eq!(membership, map.membership_revision(), "the keys did not move");
|
||||
assert_ne!(content, map.content_revision(), "the content did");
|
||||
let reloaded = map.take_changed();
|
||||
assert_eq!(reloaded.0.len(), 1);
|
||||
assert!(reloaded.0.contains(&(0, 0)));
|
||||
// A removal is reported; reads of an idle map report nothing.
|
||||
assert!(map.remove(&(0, 0)).is_some());
|
||||
let removed = map.take_changed();
|
||||
assert_eq!(removed.0.len(), 1);
|
||||
assert!(removed.0.contains(&(0, 0)));
|
||||
let (idle, dropped) = map.take_changed();
|
||||
assert!(idle.is_empty() && !dropped, "{dropped}");
|
||||
// Endless change stays bounded: the drop says everything counts.
|
||||
for x in 0..1_000 {
|
||||
map.insert((x % 5, 0), chunk.clone());
|
||||
}
|
||||
let (_, dropped) = map.take_changed();
|
||||
assert!(dropped);
|
||||
// Idle again; a clear of a loaded map says so.
|
||||
let (empty, dropped) = map.take_changed();
|
||||
assert!(empty.is_empty() && !dropped);
|
||||
let was = map.content_revision();
|
||||
map.clear();
|
||||
let (_, dropped) = map.take_changed();
|
||||
assert!(dropped, "a clear loses every column");
|
||||
assert_ne!(was, map.content_revision());
|
||||
// A clear of an idle map is idle.
|
||||
map.clear();
|
||||
let (empty, dropped) = map.take_changed();
|
||||
assert!(empty.is_empty() && !dropped);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,13 +60,16 @@ impl Game<'_> {
|
||||
(table, reflect)
|
||||
}
|
||||
|
||||
/// The snapshot of the chunks round the listener, made again when a block changed or the
|
||||
/// listener went far from where it was made.
|
||||
/// The snapshot of the chunks round the listener, made again when a block changed, the
|
||||
/// resident columns arrived, were replaced or went away (AirDef fork #110), or the listener
|
||||
/// went far from where it was made.
|
||||
fn acoustic_snapshot(&mut self) -> Arc<Snapshot> {
|
||||
let at = self.audio.listener;
|
||||
let chunk_rev = self.world.chunks.content_revision();
|
||||
if let Some(s) = &self.mods.acoustic.snapshot
|
||||
&& s.epoch == self.world.epoch
|
||||
&& s.revision == self.world.revision
|
||||
&& s.chunk_rev == chunk_rev
|
||||
&& (s.center[0] - at[0]).abs() < 96.0
|
||||
&& (s.center[2] - at[2]).abs() < 96.0
|
||||
{
|
||||
@@ -87,6 +90,7 @@ impl Game<'_> {
|
||||
reflect,
|
||||
epoch: self.world.epoch,
|
||||
revision: self.world.revision,
|
||||
chunk_rev,
|
||||
tick: self.ticks as u64,
|
||||
center: at,
|
||||
});
|
||||
@@ -101,6 +105,15 @@ impl Game<'_> {
|
||||
return;
|
||||
}
|
||||
self.world.sound_changes.watching = true;
|
||||
// The columns whose residency changed (a chunk arrived, was replaced by a reload or is
|
||||
// gone) count as changed sections for the acoustics (AirDef fork #110).
|
||||
let (columns, dropped) = self.world.chunks.take_changed();
|
||||
for (x, z) in columns {
|
||||
self.world.sound_changes.column(x, z);
|
||||
}
|
||||
if dropped {
|
||||
self.world.sound_changes.overflow();
|
||||
}
|
||||
if !self.audio.acoustics_wanted() {
|
||||
return;
|
||||
}
|
||||
@@ -109,6 +122,7 @@ impl Game<'_> {
|
||||
epoch: self.world.epoch,
|
||||
overflow,
|
||||
sections,
|
||||
residency: self.world.chunks.content_revision(),
|
||||
};
|
||||
let geometry: Arc<dyn Geometry> = self.acoustic_snapshot();
|
||||
self.audio.tick_acoustics(Some(&geometry), &changes);
|
||||
|
||||
Reference in New Issue
Block a user