mod gpu;
#[cfg(test)]
mod gpu_tests;
pub(crate) mod model;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use bevy::asset::embedded_asset;
use bevy::prelude::*;
use bevy::render::render_resource::ShaderType;
use bevy::render::storage::ShaderBuffer;
use bevy::shader::load_shader_library;
use crate::celestial::WorldClock;
use crate::climate::Weather;
use crate::gpu::grid::zeroed_buffer;
use crate::hydrology::{HydroParams, HydrologyBoundary, HydrologySettings};
use crate::plugin::{ElementsSystems, ensure_gpu_core};
use crate::terrain::{TerrainGpu, TerrainState};
use model::{StepClock, can_transport};
#[derive(Resource, Clone, Debug, PartialEq, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DustSettings {
pub enabled: bool,
pub threshold_ms: f32,
pub flux_coefficient: f32,
pub saturation_length_m: f32,
pub talus_deg: f32,
pub avalanche_passes: u32,
pub step_hz: f32,
}
impl Default for DustSettings {
fn default() -> Self {
Self {
enabled: true,
threshold_ms: model::THRESHOLD_MS,
flux_coefficient: model::FLUX_COEFFICIENT,
saturation_length_m: model::SATURATION_LENGTH_M,
talus_deg: model::TALUS_DEG,
avalanche_passes: model::AVALANCHE_PASSES,
step_hz: model::STEP_HZ,
}
}
}
#[derive(Resource, Clone, Debug, Default, PartialEq)]
pub struct DustStats {
pub steps: u64,
pub bed_time_s: f64,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, ShaderType)]
pub(crate) struct AeolianParams {
origin: Vec2,
cell: f32,
pad0: f32,
size: UVec2,
pad1: UVec2,
wind: Vec4,
law: Vec4,
step: Vec4,
}
#[derive(Clone, Debug)]
pub(crate) struct AeolianBuffers {
pub cap: Handle<ShaderBuffer>,
pub q: [Handle<ShaderBuffer>; 2],
}
#[derive(Resource, Clone)]
pub(crate) struct DustGpu {
pub buffers: AeolianBuffers,
pub params: AeolianParams,
pub avalanche: HydroParams,
pub avalanche_passes: u32,
pub run: bool,
pub executed: Arc<AtomicU64>,
pub ready: Arc<AtomicBool>,
}
#[derive(Resource, Default)]
struct DustRuntime {
built_for: Option<AssetId<ShaderBuffer>>,
clock: StepClock,
scheduled: u64,
bed_time_s: f64,
}
pub struct DustPlugin;
impl Plugin for DustPlugin {
fn build(&self, app: &mut App) {
ensure_gpu_core(app);
load_shader_library!(app, "surface_wind.wesl");
embedded_asset!(app, "aeolian.wesl");
app.init_resource::<DustSettings>()
.init_resource::<DustStats>()
.init_resource::<DustRuntime>()
.add_systems(
Update,
(ensure_dust, run_dust)
.chain()
.in_set(ElementsSystems::Simulation),
);
gpu::plugin(app);
}
}
fn ensure_dust(
mut commands: Commands,
terrain: Option<Res<TerrainState>>,
gpu: Option<Res<DustGpu>>,
mut runtime: ResMut<DustRuntime>,
mut stats: ResMut<DustStats>,
mut buffers: ResMut<Assets<ShaderBuffer>>,
) {
let Some(terrain) = terrain else {
if gpu.is_some() {
commands.remove_resource::<DustGpu>();
*runtime = DustRuntime::default();
*stats = DustStats::default();
}
return;
};
if runtime.built_for == Some(terrain.water.id()) && gpu.is_some() {
return;
}
let cells = u64::from(terrain.grid.size.x) * u64::from(terrain.grid.size.y);
*runtime = DustRuntime {
built_for: Some(terrain.water.id()),
..DustRuntime::default()
};
*stats = DustStats::default();
commands.insert_resource(DustGpu {
buffers: AeolianBuffers {
cap: zeroed_buffer(&mut buffers, "elements_aeolian_cap", cells * 4),
q: [
zeroed_buffer(&mut buffers, "elements_aeolian_q_x", cells * 4),
zeroed_buffer(&mut buffers, "elements_aeolian_q_z", cells * 4),
],
},
params: AeolianParams::default(),
avalanche: HydroParams::default(),
avalanche_passes: 0,
run: false,
executed: Arc::new(AtomicU64::new(0)),
ready: Arc::new(AtomicBool::new(false)),
});
}
#[allow(clippy::too_many_arguments)]
fn run_dust(
settings: Res<DustSettings>,
weather: Res<Weather>,
clock: Res<WorldClock>,
time: Res<Time<Virtual>>,
hydrology: Option<Res<HydrologySettings>>,
terrain: Option<Res<TerrainState>>,
terrain_gpu: Option<Res<TerrainGpu>>,
gpu: Option<ResMut<DustGpu>>,
mut runtime: ResMut<DustRuntime>,
mut stats: ResMut<DustStats>,
) {
let (Some(terrain), Some(terrain_gpu), Some(mut gpu)) = (terrain, terrain_gpu, gpu) else {
return;
};
gpu.run = false;
stats.steps = gpu.executed.load(Ordering::Relaxed);
let ready = gpu.ready.load(Ordering::Relaxed)
&& terrain.is_live()
&& terrain_gpu.generated.load(Ordering::Relaxed);
let direction = weather.wind_direction.normalize_or_zero();
let wind = weather.wind_speed_ms;
if !ready
|| !settings.enabled
|| direction == Vec2::ZERO
|| !can_transport(wind, settings.threshold_ms)
{
runtime.clock.reset();
return;
}
let Some(real_s) = runtime.clock.advance(time.delta_secs(), settings.step_hz) else {
return;
};
let bed_s = real_s * clock.time_scale.clamp(0.0, model::MAX_TIME_SCALE);
if bed_s <= 0.0 {
return;
}
let grid = terrain.grid;
let closed = hydrology.is_some_and(|h| h.boundary == HydrologyBoundary::Closed);
gpu.params = AeolianParams {
origin: grid.origin,
cell: grid.cell_m,
pad0: 0.0,
size: grid.size,
pad1: UVec2::ZERO,
wind: Vec4::new(direction.x, direction.y, wind, f32::from(u8::from(closed))),
law: Vec4::new(settings.flux_coefficient, settings.threshold_ms, 0.0, 0.0),
step: Vec4::new(settings.saturation_length_m.max(1e-3), bed_s, 0.0, 0.0),
};
gpu.avalanche = HydroParams::avalanche(&grid, settings.talus_deg, 90.0, model::AVALANCHE_RATE);
gpu.avalanche_passes = settings.avalanche_passes;
gpu.run = true;
runtime.scheduled += 1;
runtime.bed_time_s += f64::from(bed_s);
stats.bed_time_s = runtime.bed_time_s;
terrain_gpu.dirty.store(true, Ordering::Relaxed);
}