feat: initialise workspace — Milestone 1 asset explorer
Three-crate Cargo workspace structured per PROJECT.md spec: - crates/sylpheed-formats — Xbox 360 format parsers (no Bevy) - crates/sylpheed-viewer — Bevy 0.15 asset viewer + egui UI - crates/sylpheed-cli — CLI tools (extract/list/sniff/texture) Milestone 1 features: - XISO disc image reading via xdvdfs 0.8 - XPR2 texture container parsing + Morton de-tiling - D3DFORMAT → wgpu TextureFormat mapping (DXT1/3/5, DXN, ARGB) - Custom Bevy AssetLoader for .xpr files - Orbit camera (LMB orbit, RMB pan, scroll zoom) - egui file browser + RE notes panel - CLI: extract / list / sniff / texture info / texture export - GitHub Actions CI (Linux, macOS, Windows, WASM) - Trunk WASM build config Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
154
crates/sylpheed-viewer/src/asset_loader.rs
Normal file
154
crates/sylpheed-viewer/src/asset_loader.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
//! 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, AsyncReadExt, LoadContext};
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::render_asset::RenderAssetUsages;
|
||||
use bevy::render::render_resource::{Extent3d, TextureDimension, TextureFormat};
|
||||
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::<Xpr2TextureLoader>();
|
||||
// TODO: app.init_asset_loader::<SylpheedMeshLoader>();
|
||||
// TODO: app.init_asset_loader::<SylpheedAudioLoader>();
|
||||
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<Self::Asset, Self::Error> {
|
||||
// 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`.
|
||||
///
|
||||
/// Maps Xbox 360 D3DFORMAT codes to `wgpu::TextureFormat` values.
|
||||
/// Bevy uses wgpu internally, so this mapping is direct.
|
||||
pub fn x360_texture_to_bevy_image(tex: X360Texture) -> Result<Image, Xpr2LoadError> {
|
||||
let bevy_format = x360_format_to_wgpu(&tex.format)?;
|
||||
|
||||
Ok(Image::new(
|
||||
Extent3d {
|
||||
width: tex.width,
|
||||
height: tex.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
TextureDimension::D2,
|
||||
tex.data,
|
||||
bevy_format,
|
||||
// Make the texture available on both CPU and GPU
|
||||
// (RenderOnly would be more efficient once RE is complete)
|
||||
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<TextureFormat, Xpr2LoadError> {
|
||||
Ok(match format {
|
||||
// DXT1 / BC1
|
||||
X360TextureFormat::Dxt1 => TextureFormat::Bc1RgbaUnormSrgb,
|
||||
// DXT3 / BC2
|
||||
X360TextureFormat::Dxt3 => TextureFormat::Bc2RgbaUnormSrgb,
|
||||
// DXT5 / BC3
|
||||
X360TextureFormat::Dxt5 => TextureFormat::Bc3RgbaUnormSrgb,
|
||||
// DXN / BC5 — normal maps (RG, not sRGB)
|
||||
X360TextureFormat::Dxn => TextureFormat::Bc5RgUnorm,
|
||||
// Uncompressed ARGB — note Xbox 360 is BGRA order (big-endian)
|
||||
// We may need to swizzle R and B channels
|
||||
X360TextureFormat::A8R8G8B8 | X360TextureFormat::X8R8G8B8 => {
|
||||
// Xbox 360 stores as ARGB big-endian (BGRA in memory)
|
||||
// wgpu uses Rgba8UnormSrgb — swizzle may be needed
|
||||
// TODO: verify channel order against actual game textures
|
||||
TextureFormat::Bgra8UnormSrgb
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ── 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<AssetServer>) {
|
||||
/// let handle: Handle<Image> = asset_server.load("textures/ship.dds");
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn note() {}
|
||||
}
|
||||
Reference in New Issue
Block a user