use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use bevy::asset::RenderAssetUsages;
use bevy::image::{Image, ImageSampler};
use bevy::prelude::*;
use bevy::render::render_resource::{
Extent3d, Texture, TextureDimension, TextureFormat, TextureUsages,
};
use bevy_render::{
Render, RenderApp, RenderSystems,
extract_resource::{ExtractResource, ExtractResourcePlugin},
render_asset::RenderAssets,
texture::GpuImage,
};
use crate::render::{NoesisRenderState, NoesisSet};
const SAMPLE_FORMAT: TextureFormat = TextureFormat::Rgba8UnormSrgb;
const RENDER_FORMAT: TextureFormat = TextureFormat::Rgba8Unorm;
const RENDER_VIEW_FORMATS: &[TextureFormat] = &[RENDER_FORMAT];
#[derive(Clone)]
struct BakeRequest {
target: AssetId<Image>,
xaml_uri: String,
size: UVec2,
fields: Vec<(String, String)>,
}
#[derive(Default)]
struct BakerState {
cache: HashMap<String, Handle<Image>>,
pending: Vec<BakeRequest>,
in_flight: usize,
want: HashSet<AssetId<Image>>,
resolved: HashMap<AssetId<Image>, Texture>,
}
#[derive(Resource, Clone, Default)]
pub struct NoesisLabelBaker {
inner: Arc<Mutex<BakerState>>,
}
impl NoesisLabelBaker {
pub fn bake_label(
&self,
content_key: impl Into<String>,
xaml_uri: impl Into<String>,
size: UVec2,
fields: Vec<(String, String)>,
images: &mut Assets<Image>,
) -> Handle<Image> {
let key = content_key.into();
let mut state = self.inner.lock().expect("NoesisLabelBaker poisoned");
if let Some(handle) = state.cache.get(&key) {
return handle.clone();
}
let handle = images.add(bake_target(size));
state.cache.insert(key, handle.clone());
state.want.insert(handle.id());
state.pending.push(BakeRequest {
target: handle.id(),
xaml_uri: xaml_uri.into(),
size,
fields,
});
handle
}
#[must_use]
pub fn pending_count(&self) -> usize {
let guard = self.inner.lock().expect("NoesisLabelBaker poisoned");
guard.pending.len() + guard.in_flight
}
}
impl ExtractResource for NoesisLabelBaker {
type Source = NoesisLabelBaker;
fn extract_resource(source: &Self::Source) -> Self {
source.clone()
}
}
fn bake_target(size: UVec2) -> Image {
let mut image = Image::new_uninit(
Extent3d {
width: size.x.max(1),
height: size.y.max(1),
depth_or_array_layers: 1,
},
TextureDimension::D2,
SAMPLE_FORMAT,
RenderAssetUsages::RENDER_WORLD,
);
image.texture_descriptor.usage =
TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING;
image.texture_descriptor.view_formats = RENDER_VIEW_FORMATS;
image.sampler = ImageSampler::linear();
image
}
#[allow(clippy::needless_pass_by_value)]
fn resolve_bake_textures(baker: Res<NoesisLabelBaker>, gpu_images: Res<RenderAssets<GpuImage>>) {
let mut guard = baker.inner.lock().expect("NoesisLabelBaker poisoned");
if guard.want.is_empty() {
return;
}
let ready: Vec<AssetId<Image>> = guard
.want
.iter()
.copied()
.filter(|id| gpu_images.get(*id).is_some())
.collect();
for id in ready {
if let Some(gpu) = gpu_images.get(id) {
guard.resolved.insert(id, gpu.texture.clone());
guard.want.remove(&id);
}
}
}
#[allow(clippy::needless_pass_by_value)]
fn bake_pending_labels(
baker: Option<Res<NoesisLabelBaker>>,
state: Option<NonSendMut<NoesisRenderState>>,
) {
let Some(mut state) = state else {
return;
};
let Some(baker) = baker else {
return;
};
let mut work: Vec<(BakeRequest, Texture)> = Vec::new();
{
let mut guard = baker.inner.lock().expect("NoesisLabelBaker poisoned");
if guard.pending.is_empty() {
return;
}
let mut keep = Vec::new();
for req in std::mem::take(&mut guard.pending) {
match guard.resolved.get(&req.target).cloned() {
Some(texture) => work.push((req, texture)),
None => keep.push(req),
}
}
guard.pending = keep;
guard.in_flight = work.len();
}
if work.is_empty() {
return;
}
let mut baked = Vec::new();
let mut requeue = Vec::new();
for (req, texture) in work {
let render_view = texture.create_view(&wgpu::TextureViewDescriptor {
label: Some("noesis label bake (render, Unorm)"),
format: Some(RENDER_FORMAT),
..Default::default()
});
if state.bake_into(&render_view, &req.xaml_uri, req.size, &req.fields) {
baked.push(req.target);
} else {
requeue.push(req);
}
}
let mut guard = baker.inner.lock().expect("NoesisLabelBaker poisoned");
for id in baked {
guard.resolved.remove(&id);
}
guard.pending.append(&mut requeue);
guard.in_flight = 0;
}
pub struct NoesisLabelBakerPlugin;
impl Plugin for NoesisLabelBakerPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<NoesisLabelBaker>()
.add_plugins(ExtractResourcePlugin::<NoesisLabelBaker>::default());
app.add_systems(PostUpdate, bake_pending_labels.in_set(NoesisSet::Apply));
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.add_systems(Render, resolve_bake_textures.in_set(RenderSystems::Prepare));
}
}
}