`cargo clippy --workspace -- -D warnings` now exits 0. `cargo test --workspace` still reports 207 passed, 0 failed, 14 ignored across 30 suites — identical to runs 203 and 204, so none of this changed behaviour. The workspace total was 73, not the 48 run 204 reported. `-D warnings` turns a lint into a hard compile error, so `sylpheed-formats` failing stopped its dependents from ever being built: `sylpheed-viewer` (14) and `sylpheed-cli` (11) had never been linted by anyone. Clearing formats inbc79817is what made them visible. formats 43 -> 0 (bc79817) viewer 14 -> 0 cli 11 -> 0 export 5 -> 0 Collision surface, measured rather than assumed. Every viewer file carrying a lint is byte-identical on both `auto/frame-blend-draw-path` (495 commits) and `auto/port-p6-audio` (366). All eleven cli sites fall outside every hunk either branch touches. 68 of the 73 sites could not collide with anything. The five that can are all in `sylpheed-export`, and three of those are real: main.rs:278 `&out` -> `out`, inside frame-blend's hunk -278,12 main.rs:318 `&out` -> `out`, inside port-p6-audio's hunk -303,44 audio.rs:113 an added `#[allow]` in a file frame-blend DELETES Each is one line. Resolving the first two means taking the branch's version and re-applying a borrow removal; the third resolves to the deletion. Flagged here so neither branch owner meets them cold. Judgement calls, all stated at the site rather than suppressed globally: * Three `too_many_arguments` in the viewer are false positives. `draw_viewer_ui`, `poll_loader_channel` and `apply_pak` are Bevy systems — every parameter is a `Res`/`ResMut`/`EventWriter` the scheduler injects, so the count is the framework's dependency list and cannot be reduced without a `SystemParam` struct. * `cmd_screen_render` (cli, 8/7) is a plain function, so that one is real if mild; its arguments are the subcommand's flags. * Two `dead_code` fields in export are serde schema fields. They model what the on-disc JSON accepts; deleting them would quietly change that. * `iso_loader.rs` gains a `FrameRx` alias for the ffmpeg frame channel, which is what "very complex type" was asking for. A site-local `#[allow]` with a reason is a decision recorded where it applies: one lint, one function, and any new violation elsewhere still fails the build. That is not the shape PROTOCOL.md forbids. Closes #13 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01McNbzUeq1KRBWs4G6X2YVj
182 lines
7.1 KiB
Rust
182 lines
7.1 KiB
Rust
//! 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::<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`.
|
|
///
|
|
/// 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<Image, Xpr2LoadError> {
|
|
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.as_chunks_mut::<4>().0 {
|
|
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<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 — 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<AssetServer>) {
|
|
/// let handle: Handle<Image> = asset_server.load("textures/ship.dds");
|
|
/// # }
|
|
/// ```
|
|
pub fn note() {}
|
|
}
|