Decode level textures: 8-bit indices through a CSM1 palette
Adds texture.rs and makes the pipeline write gs_ram.bin, which it had been skipping. Nothing could be decoded before this: the CLUTs live there. A level texture is not pixels, it is palette indices -- width*height bytes at core_data[textures_base + data_offset], with colour coming from a 256-entry CLUT at gs_ram[palette * 0x100]. gs_ram is a byte range in the level's data extent header at offset 0x10, 518,656 bytes on level 0. Two PS2 conventions have to be honoured and neither is a design choice: within each 32-entry CLUT group the two middle 8-entry blocks are swapped (bits 3 and 4 of the index are exchanged, an involution), and alpha runs 0..=0x80 rather than 0..=255 with 0x80 fully opaque. The mip chain splits across two buffers: levels 0 and 1 are in the texture block with level 1 immediately after level 0, and levels 2 and 3 are resident in gs_ram at mipmap*0x100 and pad*0x100. Tfrag textures are square so every level halves evenly. Verification is two-layered, because every step here is a table lookup and a wrong CLUT remap still produces a well-formed image of the wrong thing. All 1,595 tfrag textures across all 27 levels decode; 96.8% are fully opaque and 96.3% use more than eight colours. On top of that, `gc-extract dump-texture` writes any texture or mip level as a PPM. Level 0 texture 0 comes back as a clean bevelled metal panel with rivets and specular highlights, and texture 74 as a vent grille whose punched windows are fully transparent. That tool is worth keeping: it is the cheapest way to tell a decode that is merely well-formed from one that is right. The hash table is regenerated for the 27 new gs_ram.bin files, now 5,186 files and 5,685,541,143 bytes.
This commit is contained in:
@@ -25,6 +25,7 @@ fn main() -> ExitCode {
|
||||
// Scratch helper used while reversing the level formats: decompress a
|
||||
// byte range of an already-unpacked file with the WAD decoder.
|
||||
Some("wad") => run_wad(&args[1..]),
|
||||
Some("dump-texture") => run_dump_texture(&args[1..]),
|
||||
Some("--help") | Some("-h") | Some("help") | None => {
|
||||
usage();
|
||||
ExitCode::SUCCESS
|
||||
@@ -65,6 +66,11 @@ fn usage() {
|
||||
verify <out-dir> <table.tsv>
|
||||
Check <out-dir> against a previously written table.
|
||||
|
||||
dump-texture <unpacked-dir> <level> <index> [mip] <out.ppm>
|
||||
Decode one tfrag texture to a binary PPM, so a decode can be eyeballed
|
||||
against the disc rather than trusted. Textures are 8-bit indexed; a wrong
|
||||
CLUT remap is obvious and a wrong alpha scale is not, so look at it.
|
||||
|
||||
You supply the disc. Nothing here is redistributed with the project."
|
||||
);
|
||||
}
|
||||
@@ -190,6 +196,124 @@ fn run_unpack(args: &[String]) -> ExitCode {
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode one tfrag texture of one level and write it as a binary PPM.
|
||||
///
|
||||
/// A visual check matters here: every step of the decode is a lookup, so a
|
||||
/// wrong CLUT remap, a wrong mip offset or a wrong pixel base still produces
|
||||
/// a well-formed image — just the wrong one.
|
||||
fn run_dump_texture(args: &[String]) -> ExitCode {
|
||||
if args.len() < 5 {
|
||||
eprintln!("usage: gc-extract dump-texture <unpacked-dir> <level> <index> [mip] <out.ppm>");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
let dir = std::path::Path::new(&args[0]).join(format!("level{}", args[1]));
|
||||
let index: usize = match args[2].parse() {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!("bad texture index: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
// The optional mip argument sits between the index and the output path.
|
||||
let (mip, out) = if args.len() >= 6 {
|
||||
match args[3].parse() {
|
||||
Ok(v) => (v, &args[5]),
|
||||
Err(e) => {
|
||||
eprintln!("bad mip level: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(0, &args[4])
|
||||
};
|
||||
|
||||
let core = match std::fs::read(dir.join("core_data.bin")) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
eprintln!("cannot read core_data.bin: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
let idx = match std::fs::read(dir.join("core_index.bin")) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
eprintln!("cannot read core_index.bin: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
let gs = match std::fs::read(dir.join("gs_ram.bin")) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
eprintln!("cannot read gs_ram.bin: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
let core_header = match gc_formats::level_core::LevelCoreHeader::parse(&idx) {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
eprintln!("core header: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
let table = match gc_formats::level_core::texture_table(&idx, core_header.tfrag_textures) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
eprintln!("texture table: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
let entry = match table.get(index) {
|
||||
Some(e) => *e,
|
||||
None => {
|
||||
eprintln!("texture {index} is outside the {} entry table", table.len());
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
let base = core_header.textures_base.max(0) as usize;
|
||||
let mips = match gc_formats::texture::mip_images(&core[base..], &gs, &entry) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
eprintln!("mip chain: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
let img = match mips.get(mip) {
|
||||
Some(m) => m,
|
||||
None => {
|
||||
eprintln!("mip {mip} is outside the {} level chain", mips.len());
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
let tex = match img.decode() {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
eprintln!("decode: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
// Binary PPM, P6, RGB over a magenta background so alpha is visible.
|
||||
let mut out_bytes = format!("P6\n{} {}\n255\n", tex.width, tex.height).into_bytes();
|
||||
for px in tex.rgba.chunks_exact(4) {
|
||||
let rgb = if px[3] == 0 {
|
||||
[255, 0, 255]
|
||||
} else {
|
||||
[px[0], px[1], px[2]]
|
||||
};
|
||||
out_bytes.extend_from_slice(&rgb);
|
||||
}
|
||||
if let Err(e) = std::fs::write(out, &out_bytes) {
|
||||
eprintln!("cannot write {out}: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
println!(
|
||||
"wrote {out}: {}x{} texture {index} mip {mip}, {} x {} pixels, {} colours",
|
||||
tex.width, tex.height, entry.width, entry.height, entry.palette
|
||||
);
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
fn run_wad(args: &[String]) -> ExitCode {
|
||||
if args.len() < 4 {
|
||||
eprintln!("usage: gc-extract wad <file> <offset> <size> <out>");
|
||||
|
||||
Reference in New Issue
Block a user