pub const DEFAULT_POOL_THREADS: usize = 4;
#[derive(bevy::ecs::resource::Resource, Clone, Debug)]
pub struct AsyncTextureConfig {
pub pool_threads: usize,
}
impl Default for AsyncTextureConfig {
fn default() -> Self {
Self {
pool_threads: DEFAULT_POOL_THREADS,
}
}
}
fn resolve_pool_threads(cfg: &AsyncTextureConfig) -> usize {
if cfg.pool_threads == 0 {
std::thread::available_parallelism()
.map(|n| (n.get() / 2).max(1))
.unwrap_or(2)
} else {
cfg.pool_threads
}
}
static POOL_CONFIG: OnceLock<AsyncTextureConfig> = OnceLock::new();
static POOL: OnceLock<Option<rayon::ThreadPool>> = OnceLock::new();
#[derive(Debug)]
pub struct PoolConfigAlreadySet;
impl std::fmt::Display for PoolConfigAlreadySet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("AsyncTextureConfig has already been applied; new value ignored")
}
}
impl std::error::Error for PoolConfigAlreadySet {}
pub fn set_pool_config(cfg: AsyncTextureConfig) -> Result<(), PoolConfigAlreadySet> {
POOL_CONFIG.set(cfg).map_err(|_| PoolConfigAlreadySet)
}
fn build_pool(cfg: &AsyncTextureConfig) -> Option<rayon::ThreadPool> {
let n = resolve_pool_threads(cfg);
match rayon::ThreadPoolBuilder::new()
.num_threads(n)
.stack_size(8 * 1024 * 1024)
.thread_name(|i| format!("texture-gen-{i}"))
.build()
{
Ok(pool) => Some(pool),
Err(e) => {
bevy::log::warn!(
"bevy_symbios_texture: failed to build texture-gen thread pool ({e}); \
falling back to inline (synchronous) generation. Each PendingTexture \
will be produced on the spawning thread, blocking it for the duration \
of the generator."
);
None
}
}
}
fn gen_pool() -> Option<&'static rayon::ThreadPool> {
POOL.get_or_init(|| {
let cfg = POOL_CONFIG.get().cloned().unwrap_or_default();
build_pool(&cfg)
})
.as_ref()
}
use std::sync::{
Arc, OnceLock,
atomic::{AtomicBool, Ordering},
mpsc,
};
use bevy::{
asset::Assets,
ecs::{
component::Component,
entity::Entity,
system::{Commands, Query, ResMut},
},
image::Image,
};
use crate::generator::{
GeneratedHandles, TextureError, TextureGenerator, TextureMap, map_to_images, map_to_images_card,
};
#[derive(Component)]
pub struct PendingTexture {
pub(crate) rx: std::sync::Mutex<mpsc::Receiver<Result<TextureMap, TextureError>>>,
cancelled: Arc<AtomicBool>,
is_card: bool,
}
impl PendingTexture {
pub fn is_card(&self) -> bool {
self.is_card
}
}
impl Drop for PendingTexture {
fn drop(&mut self) {
self.cancelled.store(true, Ordering::Relaxed);
}
}
#[cfg(not(target_arch = "wasm32"))]
fn spawn_task<F>(f: F, is_card: bool) -> PendingTexture
where
F: FnOnce() -> Result<TextureMap, TextureError> + Send + 'static,
{
let cancelled = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&cancelled);
let (tx, rx) = mpsc::sync_channel(1);
match gen_pool() {
Some(pool) => pool.spawn(move || {
if !flag.load(Ordering::Relaxed) {
tx.send(f().map(TextureMap::with_mips)).ok();
}
}),
None => {
if !flag.load(Ordering::Relaxed) {
tx.send(f().map(TextureMap::with_mips)).ok();
}
}
}
PendingTexture {
rx: std::sync::Mutex::new(rx),
cancelled,
is_card,
}
}
#[cfg(target_arch = "wasm32")]
fn spawn_task<F>(f: F, is_card: bool) -> PendingTexture
where
F: FnOnce() -> Result<TextureMap, TextureError> + Send + 'static,
{
use bevy::tasks::AsyncComputeTaskPool;
let cancelled = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&cancelled);
let (tx, rx) = mpsc::sync_channel(1);
AsyncComputeTaskPool::get()
.spawn(async move {
if !flag.load(Ordering::Relaxed) {
tx.send(f().map(TextureMap::with_mips)).ok();
}
})
.detach();
PendingTexture {
rx: std::sync::Mutex::new(rx),
cancelled,
is_card,
}
}
macro_rules! define_pending_constructors {
($(($variant:ident, $module:ident, $config_ty:ty, $generator_ty:ty, $kind:ident)),* $(,)?) => {
impl PendingTexture {
$(define_pending_constructors!(@one $variant, $module, $config_ty, $generator_ty, $kind);)*
}
};
(@one $variant:ident, $module:ident, $config_ty:ty, $generator_ty:ty, Surface) => {
#[doc = concat!(
"Spawn a ", stringify!($variant),
" surface-texture generation task at `width \u{d7} height` texels.",
)]
pub fn $module(config: $config_ty, width: u32, height: u32) -> Self {
let generator = <$generator_ty>::new(config);
spawn_task(move || generator.generate(width, height), false)
}
};
(@one $variant:ident, $module:ident, $config_ty:ty, $generator_ty:ty, Card) => {
#[doc = concat!(
"Spawn a ", stringify!($variant),
" card-texture generation task at `width \u{d7} height` texels.",
)]
pub fn $module(config: $config_ty, width: u32, height: u32) -> Self {
let generator = <$generator_ty>::new(config);
spawn_task(move || generator.generate(width, height), true)
}
};
}
symbios_texture::for_each_generator!(define_pending_constructors);
#[derive(Component)]
pub struct TextureReady(pub GeneratedHandles);
pub fn poll_texture_tasks(
mut commands: Commands,
tasks: Query<
(Entity, &PendingTexture),
bevy::ecs::query::Without<crate::material::PatchMaterialTextures>,
>,
mut images: ResMut<Assets<Image>>,
) {
for (entity, pending) in &tasks {
let poll = pending
.rx
.lock()
.expect("texture thread poisoned")
.try_recv();
match poll {
Ok(Ok(map)) => {
let handles = if pending.is_card {
map_to_images_card(map, &mut images)
} else {
map_to_images(map, &mut images)
};
commands
.entity(entity)
.remove::<PendingTexture>()
.insert(TextureReady(handles));
}
Ok(Err(e)) => {
bevy::log::error!("Texture generation failed: {e}");
commands.entity(entity).remove::<PendingTexture>();
}
Err(mpsc::TryRecvError::Disconnected) => {
bevy::log::error!("Texture generation thread panicked");
commands.entity(entity).remove::<PendingTexture>();
}
Err(mpsc::TryRecvError::Empty) => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bark::{BarkConfig, BarkGenerator};
#[test]
fn auto_pool_threads_is_at_least_one() {
let cfg = AsyncTextureConfig { pool_threads: 0 };
assert!(resolve_pool_threads(&cfg) >= 1);
}
#[test]
fn explicit_pool_threads_is_passthrough() {
let cfg = AsyncTextureConfig { pool_threads: 7 };
assert_eq!(resolve_pool_threads(&cfg), 7);
}
#[test]
fn inline_fallback_runs_synchronously() {
let cancelled = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&cancelled);
let (tx, rx) = mpsc::sync_channel(1);
let generator = BarkGenerator::new(BarkConfig::default());
if !flag.load(Ordering::Relaxed) {
tx.send(generator.generate(8, 8)).ok();
}
let received = rx
.try_recv()
.expect("inline fallback should make the result immediately available");
let map = received.expect("8x8 generation must succeed");
assert_eq!(map.width, 8);
assert_eq!(map.height, 8);
}
}