#![forbid(unsafe_code)]
use std::sync::Arc;
#[cfg(feature = "ron")]
pub use assets::ScatterPlanAssetSaver;
pub use assets::{
ParentDef, SamplingDef, ScatterKindDef, ScatterLayerDef, ScatterPlanAsset,
ScatterPlanAssetLoader, SelectionStrategyDef,
};
use bevy::prelude::*;
use bevy::tasks::{block_on, AsyncComputeTaskPool, Task};
pub use events::{ChannelSink, ScatterBus, ScatterBusConfig, ScatterEventFilter, ScatterMessage};
use map_scatter::prelude::*;
use rand::rngs::StdRng;
use rand::SeedableRng;
pub use streaming::{
MapScatterStreamingPlugin, ScatterStreamChunk, ScatterStreamChunks, ScatterStreamPlaced,
ScatterStreamPlacement, ScatterStreamSettings,
};
pub use textures::{ImageTexture, ImageTextureError};
mod assets;
mod events;
mod streaming;
mod textures;
pub mod prelude {
pub use map_scatter::prelude::*;
#[cfg(feature = "ron")]
pub use crate::assets::ScatterPlanAssetSaver;
pub use crate::assets::{
ParentDef, SamplingDef, ScatterKindDef, ScatterLayerDef, ScatterPlanAsset,
ScatterPlanAssetLoader, SelectionStrategyDef,
};
pub use crate::events::{
ChannelSink, ScatterBus, ScatterBusConfig, ScatterEventFilter, ScatterMessage,
};
pub use crate::streaming::{
MapScatterStreamingPlugin, ScatterStreamChunk, ScatterStreamChunks, ScatterStreamPlaced,
ScatterStreamPlacement, ScatterStreamSettings,
};
pub use crate::textures::{ImageTexture, ImageTextureError};
pub use crate::{MapScatterPlugin, ScatterFinished, ScatterRequest, ScatterTextureRegistry};
}
pub struct MapScatterPlugin;
#[derive(Resource, Clone)]
pub struct ScatterTextureRegistry(
pub Arc<TextureRegistry>,
);
impl Default for ScatterTextureRegistry {
fn default() -> Self {
Self(Arc::new(TextureRegistry::new()))
}
}
#[derive(Resource, Clone)]
struct ScatterCache(pub Arc<FieldProgramCache>);
impl Default for ScatterCache {
fn default() -> Self {
Self(Arc::new(FieldProgramCache::new()))
}
}
#[non_exhaustive]
#[derive(EntityEvent)]
pub struct ScatterRequest {
pub entity: Entity,
pub plan: Handle<ScatterPlanAsset>,
pub config: RunConfig,
pub seed: u64,
}
impl ScatterRequest {
pub fn new(
entity: Entity,
plan: Handle<ScatterPlanAsset>,
config: RunConfig,
seed: u64,
) -> Self {
Self {
entity,
plan,
config,
seed,
}
}
}
#[derive(Component)]
struct ScatterJob {
pub task: Option<Task<RunResult>>,
}
#[non_exhaustive]
#[derive(EntityEvent, Clone)]
pub struct ScatterFinished {
pub entity: Entity,
pub result: RunResult,
}
impl Plugin for MapScatterPlugin {
fn build(&self, app: &mut App) {
app.add_message::<ScatterMessage>()
.register_type::<ScatterPlanAsset>()
.register_type::<ScatterLayerDef>()
.register_type::<ScatterKindDef>()
.register_type::<SelectionStrategyDef>()
.register_type::<ParentDef>()
.register_type::<SamplingDef>()
.register_type::<Handle<ScatterPlanAsset>>()
.init_asset::<ScatterPlanAsset>()
.init_asset_loader::<ScatterPlanAssetLoader>()
.init_resource::<ScatterBusConfig>()
.init_resource::<ScatterBus>()
.init_resource::<ScatterTextureRegistry>()
.init_resource::<ScatterCache>()
.add_systems(Update, poll_scatter_jobs)
.add_systems(Update, drain_scatter_messages)
.add_observer(spawn_scatter_job);
}
}
fn spawn_scatter_job(
request: On<ScatterRequest>,
mut commands: Commands,
bus: Res<ScatterBus>,
cache: Res<ScatterCache>,
textures: Res<ScatterTextureRegistry>,
assets: Res<Assets<ScatterPlanAsset>>,
) {
let pool = AsyncComputeTaskPool::get();
let tx = bus.sender().clone();
let filter = bus.filter().clone();
let entity = request.entity;
if let Err(err) = request.config.validate() {
warn!(
"ScatterRequest config invalid for {:?}: {}",
request.entity, err
);
return;
}
let Some(plan) = assets.get(&request.plan) else {
error!("ScatterPlanAsset not loaded yet: {:?}", request.plan);
return;
};
let plan = plan.into();
let config = request.config.clone();
let seed = request.seed;
let textures = textures.0.clone();
let cache = cache.0.clone();
let tx = tx.clone();
let filter = filter.clone();
let task = pool.spawn(async move {
let mut rng = StdRng::seed_from_u64(seed);
let mut sink = ChannelSink {
request: entity,
tx,
filter,
};
let Ok(mut runner) = ScatterRunner::try_new(config.clone(), &textures, cache.as_ref())
else {
warn!("Scatter runner failed to initialize for {:?}", entity);
return RunResult::new();
};
runner.run_with_events(&plan, &mut rng, &mut sink)
});
commands
.entity(request.entity)
.insert(ScatterJob { task: Some(task) });
}
fn poll_scatter_jobs(mut commands: Commands, mut job_query: Query<(Entity, &mut ScatterJob)>) {
for (entity, mut job) in job_query.iter_mut() {
let Some(task) = job.task.as_ref() else {
continue;
};
if !task.is_finished() {
continue;
}
let task = job.task.take().expect("finished scatter task should exist");
let result = block_on(task);
commands.entity(entity).remove::<ScatterJob>();
commands.trigger(ScatterFinished { entity, result });
}
}
fn drain_scatter_messages(bus: Res<ScatterBus>, mut messages: ResMut<Messages<ScatterMessage>>) {
while let Ok(message) = bus.receiver().try_recv() {
messages.write(message);
}
}
#[cfg(test)]
mod tests {
use std::any::TypeId;
use std::thread;
use std::time::{Duration, Instant};
use bevy::asset::{AssetPlugin, ReflectAsset, ReflectHandle};
use bevy::ecs::reflect::ReflectComponent;
use bevy::tasks::{TaskPool, TaskPoolBuilder};
use super::*;
#[derive(Resource, Default)]
struct ObservedFinished {
count: usize,
entity: Option<Entity>,
result: Option<RunResult>,
}
#[test]
fn plugins_register_bevy_facing_reflected_types() {
let mut app = App::new();
app.add_plugins(AssetPlugin::default())
.add_plugins((MapScatterPlugin, MapScatterStreamingPlugin));
let registry = app.world().resource::<AppTypeRegistry>().read();
assert_registered::<ScatterPlanAsset>(®istry);
assert_registered::<ScatterLayerDef>(®istry);
assert_registered::<ScatterKindDef>(®istry);
assert_registered::<SelectionStrategyDef>(®istry);
assert_registered::<ParentDef>(®istry);
assert_registered::<SamplingDef>(®istry);
assert_registered::<Handle<ScatterPlanAsset>>(®istry);
assert_registered::<ScatterStreamSettings>(®istry);
assert_registered::<ScatterStreamChunks>(®istry);
assert_registered::<ScatterStreamChunk>(®istry);
assert_registered::<ScatterStreamPlacement>(®istry);
let asset_registration = registry
.get(TypeId::of::<ScatterPlanAsset>())
.expect("ScatterPlanAsset should be registered");
assert!(asset_registration.data::<ReflectAsset>().is_some());
let handle_registration = registry
.get(TypeId::of::<Handle<ScatterPlanAsset>>())
.expect("Handle<ScatterPlanAsset> should be registered");
assert!(handle_registration.data::<ReflectHandle>().is_some());
for type_id in [
TypeId::of::<ScatterStreamSettings>(),
TypeId::of::<ScatterStreamChunks>(),
TypeId::of::<ScatterStreamChunk>(),
TypeId::of::<ScatterStreamPlacement>(),
] {
let registration = registry.get(type_id).expect("type should be registered");
assert!(registration.data::<ReflectComponent>().is_some());
}
}
#[test]
fn unfinished_scatter_job_remains_after_polling() {
let pool = test_pool();
let mut app = app_with_polling();
let task = pool.spawn(async { std::future::pending::<RunResult>().await });
let entity = app.world_mut().spawn(ScatterJob { task: Some(task) }).id();
app.update();
let job = app
.world()
.entity(entity)
.get::<ScatterJob>()
.expect("unfinished job should remain on the request entity");
assert!(job.task.is_some());
assert_eq!(app.world().resource::<ObservedFinished>().count, 0);
}
#[test]
fn finished_scatter_job_triggers_finished_event_and_removes_component() {
let pool = test_pool();
let mut app = app_with_polling();
let task = finished_task(&pool);
let entity = app.world_mut().spawn(ScatterJob { task: Some(task) }).id();
app.update();
assert!(app.world().entity(entity).get::<ScatterJob>().is_none());
let observed = app.world().resource::<ObservedFinished>();
assert_eq!(observed.count, 1);
assert_eq!(observed.entity, Some(entity));
assert!(observed.result.is_some());
}
#[test]
fn despawned_scatter_job_does_not_panic_or_emit_finished() {
let pool = test_pool();
let mut app = app_with_polling();
let task = pool.spawn(async { std::future::pending::<RunResult>().await });
let entity = app.world_mut().spawn(ScatterJob { task: Some(task) }).id();
app.world_mut().entity_mut(entity).despawn();
app.update();
assert!(app.world().get_entity(entity).is_err());
assert_eq!(app.world().resource::<ObservedFinished>().count, 0);
}
fn app_with_polling() -> App {
let mut app = App::new();
app.init_resource::<ObservedFinished>()
.add_systems(Update, poll_scatter_jobs)
.add_observer(record_finished);
app
}
fn record_finished(finished: On<ScatterFinished>, mut observed: ResMut<ObservedFinished>) {
observed.count += 1;
observed.entity = Some(finished.entity);
observed.result = Some(finished.result.clone());
}
fn test_pool() -> TaskPool {
TaskPoolBuilder::new().num_threads(1).build()
}
fn finished_task(pool: &TaskPool) -> Task<RunResult> {
let task = pool.spawn(async { RunResult::new() });
let timeout = Instant::now() + Duration::from_secs(5);
while !task.is_finished() {
assert!(
Instant::now() < timeout,
"test scatter task did not finish in time"
);
thread::yield_now();
}
task
}
fn assert_registered<T: 'static>(registry: &bevy::reflect::TypeRegistry) {
assert!(
registry.get(TypeId::of::<T>()).is_some(),
"{} should be registered",
std::any::type_name::<T>()
);
}
}