use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use bevy::asset::{AssetApp, AssetLoader, LoadContext, io::Reader};
use bevy::prelude::*;
use bevy_render::extract_resource::{ExtractResource, ExtractResourcePlugin};
use noesis_runtime::texture_provider::{ImageData, TextureInfo, TextureProvider};
#[derive(Asset, TypePath, Debug, Clone)]
pub struct ImageAsset {
pub width: u32,
pub height: u32,
pub bytes: Arc<Vec<u8>>,
}
#[derive(thiserror::Error, Debug)]
pub enum ImageLoadError {
#[error("io: {0}")]
Io(#[from] std::io::Error),
#[error("decode: {0}")]
Decode(String),
}
#[derive(Default, TypePath)]
pub struct ImageAssetLoader;
impl AssetLoader for ImageAssetLoader {
type Asset = ImageAsset;
type Settings = ();
type Error = ImageLoadError;
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &(),
_load_context: &mut LoadContext<'_>,
) -> Result<ImageAsset, ImageLoadError> {
let mut encoded = Vec::new();
reader.read_to_end(&mut encoded).await?;
let decoded = image::load_from_memory(&encoded)
.map_err(|e| ImageLoadError::Decode(e.to_string()))?
.to_rgba8();
let (width, height) = decoded.dimensions();
let mut raw = decoded.into_raw();
premultiply_alpha(&mut raw);
let bytes = Arc::new(raw);
Ok(ImageAsset {
width,
height,
bytes,
})
}
fn extensions(&self) -> &[&str] {
&["png", "jpg", "jpeg"]
}
}
#[inline]
fn premultiply_alpha(bytes: &mut [u8]) {
debug_assert_eq!(
bytes.len() % 4,
0,
"premultiply_alpha expects RGBA8: len = {}",
bytes.len()
);
for chunk in bytes.chunks_exact_mut(4) {
let a = u32::from(chunk[3]);
if a == 255 {
continue;
}
if a == 0 {
chunk[0] = 0;
chunk[1] = 0;
chunk[2] = 0;
continue;
}
chunk[0] = ((u32::from(chunk[0]) * a + 127) / 255) as u8;
chunk[1] = ((u32::from(chunk[1]) * a + 127) / 255) as u8;
chunk[2] = ((u32::from(chunk[2]) * a + 127) / 255) as u8;
}
}
#[derive(Resource, ExtractResource, Default, Clone)]
pub struct ImageRegistry {
pub(crate) entries: HashMap<String, RegisteredImage>,
}
#[derive(Clone)]
pub(crate) struct RegisteredImage {
pub width: u32,
pub height: u32,
pub bytes: Arc<Vec<u8>>,
}
impl ImageRegistry {
#[must_use]
pub fn get(&self, uri: &str) -> Option<(u32, u32, &Arc<Vec<u8>>)> {
self.entries
.get(uri)
.map(|img| (img.width, img.height, &img.bytes))
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn keys(&self) -> impl Iterator<Item = &str> {
self.entries.keys().map(String::as_str)
}
pub fn insert(&mut self, uri: impl Into<String>, width: u32, height: u32, bytes: Arc<Vec<u8>>) {
self.entries.insert(
uri.into(),
RegisteredImage {
width,
height,
bytes,
},
);
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn update_image_registry(
mut events: MessageReader<AssetEvent<ImageAsset>>,
assets: Res<Assets<ImageAsset>>,
asset_server: Res<AssetServer>,
mut registry: ResMut<ImageRegistry>,
) {
for event in events.read() {
match *event {
AssetEvent::Added { id } | AssetEvent::Modified { id } => {
let Some(path) = asset_server.get_path(id) else {
continue;
};
let Some(asset) = assets.get(id) else {
continue;
};
registry.entries.insert(
path.to_string(),
RegisteredImage {
width: asset.width,
height: asset.height,
bytes: Arc::clone(&asset.bytes),
},
);
}
AssetEvent::Removed { id } | AssetEvent::Unused { id } => {
let Some(path) = asset_server.get_path(id) else {
continue;
};
registry.entries.remove(&path.to_string());
}
AssetEvent::LoadedWithDependencies { .. } => {}
}
}
}
type ImageMapEntries = HashMap<String, RegisteredImage>;
#[derive(Clone, Default)]
pub struct SharedImageMap(pub(crate) Arc<Mutex<ImageMapEntries>>);
impl SharedImageMap {
pub fn sync_from(&self, registry: &ImageRegistry) {
let mut guard = self.0.lock().expect("SharedImageMap mutex poisoned");
guard.clone_from(®istry.entries);
}
}
pub struct BevyTextureProvider {
shared: SharedImageMap,
current: Option<Arc<Vec<u8>>>,
current_dims: (u32, u32),
}
impl BevyTextureProvider {
#[must_use]
pub fn from_shared(map: SharedImageMap) -> Self {
Self {
shared: map,
current: None,
current_dims: (0, 0),
}
}
}
impl TextureProvider for BevyTextureProvider {
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn info(&mut self, uri: &str) -> Option<TextureInfo> {
let guard = self.shared.0.lock().ok()?;
let img = guard.get(uri)?;
Some(TextureInfo::new(img.width, img.height))
}
fn load(&mut self, uri: &str) -> Option<ImageData<'_>> {
let (arc, w, h) = {
let guard = self.shared.0.lock().ok()?;
let img = guard.get(uri)?;
(Arc::clone(&img.bytes), img.width, img.height)
};
self.current = Some(arc);
self.current_dims = (w, h);
self.current.as_deref().map(|bytes| ImageData {
width: w,
height: h,
bytes,
})
}
}
pub struct ImageAssetPlugin;
impl Plugin for ImageAssetPlugin {
fn build(&self, app: &mut App) {
app.init_asset::<ImageAsset>()
.init_asset_loader::<ImageAssetLoader>()
.init_resource::<ImageRegistry>()
.add_systems(Update, update_image_registry)
.add_plugins(ExtractResourcePlugin::<ImageRegistry>::default());
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_image(w: u32, h: u32, fill: [u8; 4]) -> Arc<Vec<u8>> {
let mut bytes = Vec::with_capacity((w * h * 4) as usize);
for _ in 0..(w * h) {
bytes.extend_from_slice(&fill);
}
Arc::new(bytes)
}
#[test]
fn provider_info_returns_dimensions() {
let shared = SharedImageMap::default();
{
let mut guard = shared.0.lock().unwrap();
guard.insert(
"Images/BgTile.png".into(),
RegisteredImage {
width: 6,
height: 6,
bytes: make_image(6, 6, [10, 20, 30, 255]),
},
);
}
let mut provider = BevyTextureProvider::from_shared(shared);
let info = provider.info("Images/BgTile.png").unwrap();
assert_eq!(info.width, 6);
assert_eq!(info.height, 6);
assert!(provider.info("Images/Missing.png").is_none());
}
#[test]
fn premultiply_alpha_zero_collapses_to_transparent_black() {
let mut bytes = vec![200, 150, 100, 0];
premultiply_alpha(&mut bytes);
assert_eq!(bytes, vec![0, 0, 0, 0]);
}
#[test]
fn premultiply_alpha_full_preserves_bytes() {
let mut bytes = vec![10, 20, 30, 255, 200, 150, 100, 255];
premultiply_alpha(&mut bytes);
assert_eq!(bytes, vec![10, 20, 30, 255, 200, 150, 100, 255]);
}
#[test]
fn premultiply_alpha_half_alpha_halves_rgb() {
let mut bytes = vec![200, 200, 200, 128];
premultiply_alpha(&mut bytes);
assert_eq!(bytes, vec![100, 100, 100, 128]);
}
#[test]
fn premultiply_alpha_arbitrary_alpha_matches_rounded_formula() {
let mut bytes = vec![
255, 128, 0, 64, 100, 100, 100, 200, 255, 255, 255, 1, ];
premultiply_alpha(&mut bytes);
assert_eq!(bytes, vec![64, 32, 0, 64, 78, 78, 78, 200, 1, 1, 1, 1]);
}
#[test]
fn provider_load_returns_bytes_with_expected_layout() {
let shared = SharedImageMap::default();
{
let mut guard = shared.0.lock().unwrap();
guard.insert(
"Images/A.png".into(),
RegisteredImage {
width: 2,
height: 2,
bytes: make_image(2, 2, [1, 2, 3, 4]),
},
);
}
let mut provider = BevyTextureProvider::from_shared(shared);
let img = provider.load("Images/A.png").unwrap();
assert_eq!(img.width, 2);
assert_eq!(img.height, 2);
assert_eq!(img.bytes.len(), 16);
assert_eq!(&img.bytes[..4], &[1, 2, 3, 4]);
assert!(provider.load("Images/Missing.png").is_none());
}
}