//! Custom Bevy asset loaders for Project Sylpheed's file formats. //! //! Bevy's asset system is extended via `AssetLoader` implementations. //! Each loader converts a game-specific binary format into a Bevy `Image` //! or `Mesh` that can be used directly in the scene. //! //! ## How Bevy asset loading works //! //! 1. You request an asset: `asset_server.load("textures/ship01.xpr")` //! 2. Bevy looks for an `AssetLoader` that handles `.xpr` files //! 3. Our loader reads the bytes, de-tiles the texture, returns a `Image` //! 4. Bevy uploads it to the GPU automatically use bevy::asset::io::Reader; use bevy::asset::{AssetLoader, LoadContext}; use bevy::image::ImageSampler; use bevy::prelude::*; use bevy::render::render_asset::RenderAssetUsages; use bevy::render::render_resource::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages}; use sylpheed_formats::texture::{X360Texture, X360TextureFormat}; // ── Plugin ──────────────────────────────────────────────────────────────────── /// Registers all custom asset loaders with Bevy. pub struct SylpheedAssetPlugin; impl Plugin for SylpheedAssetPlugin { fn build(&self, app: &mut App) { app.init_asset_loader::(); // TODO: app.init_asset_loader::(); // TODO: app.init_asset_loader::(); info!("Sylpheed asset loaders registered"); } } // ── XPR2 Texture Loader ─────────────────────────────────────────────────────── /// Loads Xbox 360 XPR2 texture files (`.xpr`) as Bevy `Image` assets. /// /// The loader: /// 1. Reads the raw XPR2 bytes /// 2. Parses the XPR2 header to find the texture descriptor /// 3. De-tiles the GPU memory layout using Morton-order de-interleaving /// 4. Returns a `bevy::render::texture::Image` with the correct `TextureFormat` #[derive(Default)] pub struct Xpr2TextureLoader; #[derive(Debug, thiserror::Error)] pub enum Xpr2LoadError { #[error("IO error: {0}")] Io(#[from] std::io::Error), #[error("Texture parse error: {0}")] Texture(#[from] sylpheed_formats::texture::TextureError), #[error("Unsupported texture format for Bevy upload")] UnsupportedForBevy, } impl AssetLoader for Xpr2TextureLoader { type Asset = Image; type Settings = (); type Error = Xpr2LoadError; async fn load( &self, reader: &mut dyn Reader, _settings: &Self::Settings, _load_context: &mut LoadContext<'_>, ) -> Result { // Read all bytes from the asset source let mut bytes = Vec::new(); reader.read_to_end(&mut bytes).await?; // Parse the XPR2 container and de-tile the texture let x360_tex = X360Texture::from_xpr2(&bytes)?; // Convert to Bevy's Image type x360_texture_to_bevy_image(x360_tex) } fn extensions(&self) -> &[&str] { // Register for both .xpr and .xpr2 extensions &["xpr", "xpr2"] } } // ── Conversion: X360Texture → bevy::Image ───────────────────────────────────── /// Convert a decoded `X360Texture` into a Bevy-compatible `Image`. /// /// Constructs the `Image` struct directly rather than using `Image::new()`, /// because `Image::new()` calls `pixel_size()` which panics for BCn /// block-compressed formats. pub fn x360_texture_to_bevy_image(tex: X360Texture) -> Result { let format = x360_format_to_wgpu(&tex.format)?; // Uncompressed k_8_8_8_8: after `from_xpr2`'s k8in32 endian swap the bytes // are in [A,R,G,B] order (verified against the retail Acheron backdrop). // wgpu has no ARGB format, so reorder to [R,G,B,A] and upload as Rgba8 // (see `x360_format_to_wgpu`). BCn data is already GPU-ready. let data = match tex.format { X360TextureFormat::A8R8G8B8 | X360TextureFormat::X8R8G8B8 => { let opaque = matches!(tex.format, X360TextureFormat::X8R8G8B8); let mut out = tex.data; for px in out.chunks_exact_mut(4) { let (a, r, g, b) = (px[0], px[1], px[2], px[3]); px[0] = r; px[1] = g; px[2] = b; px[3] = if opaque { 0xFF } else { a }; } out } _ => tex.data, }; Ok(Image { data, texture_descriptor: TextureDescriptor { label: None, size: Extent3d { width: tex.width, height: tex.height, depth_or_array_layers: 1, }, mip_level_count: 1, sample_count: 1, dimension: TextureDimension::D2, format, usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, view_formats: &[], }, sampler: ImageSampler::Default, texture_view_descriptor: None, asset_usage: RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD, }) } /// Map an `X360TextureFormat` to the corresponding `wgpu::TextureFormat`. /// /// Bevy fully supports BCn (DXT) block compression — the GPU handles /// decompression in hardware, so we pass DXT data directly without /// software decompression. fn x360_format_to_wgpu(format: &X360TextureFormat) -> Result { Ok(match format { // DXT1 / BC1 X360TextureFormat::Dxt1 => TextureFormat::Bc1RgbaUnormSrgb, // DXT3 / BC2 X360TextureFormat::Dxt3 => TextureFormat::Bc2RgbaUnormSrgb, // DXT5 / BC3 X360TextureFormat::Dxt5 => TextureFormat::Bc3RgbaUnormSrgb, // DXN / BC5 — two-channel normal maps (RG, not sRGB) X360TextureFormat::Dxn => TextureFormat::Bc5RgUnorm, // DXT5A / BC4 — single-channel (gloss, specular, luminance maps) X360TextureFormat::Dxt5A => TextureFormat::Bc4RUnorm, // Uncompressed — reordered to [R,G,B,A] by `x360_texture_to_bevy_image`. X360TextureFormat::A8R8G8B8 | X360TextureFormat::X8R8G8B8 => { TextureFormat::Rgba8UnormSrgb } }) } // ── Fallback: load a raw DDS file ───────────────────────────────────────────── /// Loads a standard DDS file (if you've pre-converted game textures using /// a tool like Noesis). This is the fast path for early Milestone 1 work /// before the XPR2 loader is fully validated. /// /// Bevy has built-in DDS support via the `dds` feature. /// Just use: asset_server.load("textures/ship01.dds") /// No custom loader needed for DDS! pub struct DdsTextureInfo; impl DdsTextureInfo { /// Use this path in Bevy for standard DDS files: /// ```rust,no_run /// # use bevy::prelude::*; /// # fn example(asset_server: Res) { /// let handle: Handle = asset_server.load("textures/ship.dds"); /// # } /// ``` pub fn note() {} }