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)
.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::{
ashlar::{AshlarConfig, AshlarGenerator},
asphalt::{AsphaltConfig, AsphaltGenerator},
bark::{BarkConfig, BarkGenerator},
brick::{BrickConfig, BrickGenerator},
cobblestone::{CobblestoneConfig, CobblestoneGenerator},
concrete::{ConcreteConfig, ConcreteGenerator},
corrugated::{CorrugatedConfig, CorrugatedGenerator},
encaustic::{EncausticConfig, EncausticGenerator},
generator::{
GeneratedHandles, TextureError, TextureGenerator, TextureMap, map_to_images,
map_to_images_card,
},
ground::{GroundConfig, GroundGenerator},
iron_grille::{IronGrilleConfig, IronGrilleGenerator},
leaf::{LeafConfig, LeafGenerator},
marble::{MarbleConfig, MarbleGenerator},
metal::{MetalConfig, MetalGenerator},
pavers::{PaversConfig, PaversGenerator},
plank::{PlankConfig, PlankGenerator},
rock::{RockConfig, RockGenerator},
shingle::{ShingleConfig, ShingleGenerator},
stained_glass::{StainedGlassConfig, StainedGlassGenerator},
stucco::{StuccoConfig, StuccoGenerator},
thatch::{ThatchConfig, ThatchGenerator},
twig::{TwigConfig, TwigGenerator},
wainscoting::{WainscotingConfig, WainscotingGenerator},
window::{WindowConfig, WindowGenerator},
};
#[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()).ok();
}
}),
None => {
if !flag.load(Ordering::Relaxed) {
tx.send(f()).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()).ok();
}
})
.detach();
PendingTexture {
rx: std::sync::Mutex::new(rx),
cancelled,
is_card,
}
}
impl PendingTexture {
pub fn bark(config: BarkConfig, width: u32, height: u32) -> Self {
let generator = BarkGenerator::new(config);
spawn_task(move || generator.generate(width, height), false)
}
pub fn rock(config: RockConfig, width: u32, height: u32) -> Self {
let generator = RockGenerator::new(config);
spawn_task(move || generator.generate(width, height), false)
}
pub fn ground(config: GroundConfig, width: u32, height: u32) -> Self {
let generator = GroundGenerator::new(config);
spawn_task(move || generator.generate(width, height), false)
}
pub fn leaf(config: LeafConfig, width: u32, height: u32) -> Self {
let generator = LeafGenerator::new(config);
spawn_task(move || generator.generate(width, height), true)
}
pub fn twig(config: TwigConfig, width: u32, height: u32) -> Self {
let generator = TwigGenerator::new(config);
spawn_task(move || generator.generate(width, height), true)
}
pub fn brick(config: BrickConfig, width: u32, height: u32) -> Self {
let generator = BrickGenerator::new(config);
spawn_task(move || generator.generate(width, height), false)
}
pub fn window(config: WindowConfig, width: u32, height: u32) -> Self {
let generator = WindowGenerator::new(config);
spawn_task(move || generator.generate(width, height), true)
}
pub fn plank(config: PlankConfig, width: u32, height: u32) -> Self {
let generator = PlankGenerator::new(config);
spawn_task(move || generator.generate(width, height), false)
}
pub fn shingle(config: ShingleConfig, width: u32, height: u32) -> Self {
let generator = ShingleGenerator::new(config);
spawn_task(move || generator.generate(width, height), false)
}
pub fn stucco(config: StuccoConfig, width: u32, height: u32) -> Self {
let generator = StuccoGenerator::new(config);
spawn_task(move || generator.generate(width, height), false)
}
pub fn concrete(config: ConcreteConfig, width: u32, height: u32) -> Self {
let generator = ConcreteGenerator::new(config);
spawn_task(move || generator.generate(width, height), false)
}
pub fn metal(config: MetalConfig, width: u32, height: u32) -> Self {
let generator = MetalGenerator::new(config);
spawn_task(move || generator.generate(width, height), false)
}
pub fn pavers(config: PaversConfig, width: u32, height: u32) -> Self {
let generator = PaversGenerator::new(config);
spawn_task(move || generator.generate(width, height), false)
}
pub fn ashlar(config: AshlarConfig, width: u32, height: u32) -> Self {
let generator = AshlarGenerator::new(config);
spawn_task(move || generator.generate(width, height), false)
}
pub fn cobblestone(config: CobblestoneConfig, width: u32, height: u32) -> Self {
let generator = CobblestoneGenerator::new(config);
spawn_task(move || generator.generate(width, height), false)
}
pub fn thatch(config: ThatchConfig, width: u32, height: u32) -> Self {
let generator = ThatchGenerator::new(config);
spawn_task(move || generator.generate(width, height), false)
}
pub fn marble(config: MarbleConfig, width: u32, height: u32) -> Self {
let generator = MarbleGenerator::new(config);
spawn_task(move || generator.generate(width, height), false)
}
pub fn corrugated(config: CorrugatedConfig, width: u32, height: u32) -> Self {
let generator = CorrugatedGenerator::new(config);
spawn_task(move || generator.generate(width, height), false)
}
pub fn asphalt(config: AsphaltConfig, width: u32, height: u32) -> Self {
let generator = AsphaltGenerator::new(config);
spawn_task(move || generator.generate(width, height), false)
}
pub fn wainscoting(config: WainscotingConfig, width: u32, height: u32) -> Self {
let generator = WainscotingGenerator::new(config);
spawn_task(move || generator.generate(width, height), false)
}
pub fn stained_glass(config: StainedGlassConfig, width: u32, height: u32) -> Self {
let generator = StainedGlassGenerator::new(config);
spawn_task(move || generator.generate(width, height), true)
}
pub fn iron_grille(config: IronGrilleConfig, width: u32, height: u32) -> Self {
let generator = IronGrilleGenerator::new(config);
spawn_task(move || generator.generate(width, height), true)
}
pub fn encaustic(config: EncausticConfig, width: u32, height: u32) -> Self {
let generator = EncausticGenerator::new(config);
spawn_task(move || generator.generate(width, height), false)
}
}
#[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;
#[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);
}
}