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:
@@ -1,13 +1,3 @@
|
|||||||
# SHA-1 and size of every file gc-extract produces from the disc.
|
|
||||||
#
|
|
||||||
# Target: Ratchet & Clank: Going Commando (PS2), SCUS_972.68 v1.01, NTSC.
|
|
||||||
# Boot ELF SHA-1: bacda860cbd6723142e86db56f197e47e728970f
|
|
||||||
#
|
|
||||||
# Generated by:
|
|
||||||
# gc-extract unpack <disc.iso> <out-dir> --table crates/gc-extract/data/gc-files.tsv
|
|
||||||
#
|
|
||||||
# Hashes and sizes only. No game content is stored in this repository.
|
|
||||||
# Format: sha1<TAB>size_bytes<TAB>path_relative_to_out_dir
|
|
||||||
3e93b99123a5611ed8cd197437a341daa7ad140c 264449 IOPRP255.IMG
|
3e93b99123a5611ed8cd197437a341daa7ad140c 264449 IOPRP255.IMG
|
||||||
8414766368c23db6a3e6dfdb23582ca293c25151 408444 RC2.HDR
|
8414766368c23db6a3e6dfdb23582ca293c25151 408444 RC2.HDR
|
||||||
bacda860cbd6723142e86db56f197e47e728970f 2618684 SCUS_972.68
|
bacda860cbd6723142e86db56f197e47e728970f 2618684 SCUS_972.68
|
||||||
@@ -2503,24 +2493,28 @@ e1baca5f8178da95e7dc496305a36f984be542aa 15 hud/0780.bin
|
|||||||
89496a7b1cf67a2fc88ad99cc72fc46807c95dfe 31600 level0/core_index.bin
|
89496a7b1cf67a2fc88ad99cc72fc46807c95dfe 31600 level0/core_index.bin
|
||||||
4a8f9d6608b776a86fb15420acadea4c2b749a9f 13279232 level0/data.bin
|
4a8f9d6608b776a86fb15420acadea4c2b749a9f 13279232 level0/data.bin
|
||||||
133206d28cd25c39f1b6a760e345c0f0f409b7bf 659456 level0/gameplay.bin
|
133206d28cd25c39f1b6a760e345c0f0f409b7bf 659456 level0/gameplay.bin
|
||||||
|
acef5e387c8051229eecf70a64fd6daa077b34b3 518656 level0/gs_ram.bin
|
||||||
7ec9566aeae513fe272bc97899a4dbe15a2a74ff 1503232 level0/sound_bank.bin
|
7ec9566aeae513fe272bc97899a4dbe15a2a74ff 1503232 level0/sound_bank.bin
|
||||||
6b1a358c6eb6039d0df51fce061b58c274e8c99c 19023392 level1/core_data.bin
|
6b1a358c6eb6039d0df51fce061b58c274e8c99c 19023392 level1/core_data.bin
|
||||||
96b33cef1ac3c9bab5283ccf11bd86b0aa20ebcb 12165629 level1/core_data.wad
|
96b33cef1ac3c9bab5283ccf11bd86b0aa20ebcb 12165629 level1/core_data.wad
|
||||||
7cae5c7c2258f391ef72a8503183e36747214fbd 34016 level1/core_index.bin
|
7cae5c7c2258f391ef72a8503183e36747214fbd 34016 level1/core_index.bin
|
||||||
60a9070e5668ff8f5bf560318968811c1ee38c71 15769600 level1/data.bin
|
60a9070e5668ff8f5bf560318968811c1ee38c71 15769600 level1/data.bin
|
||||||
a07364ccac5fb6dc3d1fb1c859fca13b205e6a1f 811008 level1/gameplay.bin
|
a07364ccac5fb6dc3d1fb1c859fca13b205e6a1f 811008 level1/gameplay.bin
|
||||||
|
afc39fac19d1a66aba56c4509c55977229dcb3cf 569344 level1/gs_ram.bin
|
||||||
1318041a43acca46fb4f2348b2f4fa81bd810948 18432 level1/occlusion.bin
|
1318041a43acca46fb4f2348b2f4fa81bd810948 18432 level1/occlusion.bin
|
||||||
5d6ead57dd2adf83e75b53ef5d026682676d5010 1300480 level1/sound_bank.bin
|
5d6ead57dd2adf83e75b53ef5d026682676d5010 1300480 level1/sound_bank.bin
|
||||||
c7bbd091e77719876e53fe38cfb66ad68d0952d3 14427312 level10/core_data.bin
|
c7bbd091e77719876e53fe38cfb66ad68d0952d3 14427312 level10/core_data.bin
|
||||||
1c56f35c21dd429f52cdc701de3d3ab846f57918 8999864 level10/core_data.wad
|
1c56f35c21dd429f52cdc701de3d3ab846f57918 8999864 level10/core_data.wad
|
||||||
2c0073d2e78ff61beb69050087ff953ac1ccc65c 19696 level10/core_index.bin
|
2c0073d2e78ff61beb69050087ff953ac1ccc65c 19696 level10/core_index.bin
|
||||||
fe9e5169820bc9272aa5fcfbbabfc74e7b12d1ce 12498944 level10/data.bin
|
fe9e5169820bc9272aa5fcfbbabfc74e7b12d1ce 12498944 level10/data.bin
|
||||||
|
4036bc105fa429b97a725ad40bd4051168c886ad 367872 level10/gs_ram.bin
|
||||||
d3e48237b03649578f6962e36f1bc197c3a8af9f 1306624 level10/sound_bank.bin
|
d3e48237b03649578f6962e36f1bc197c3a8af9f 1306624 level10/sound_bank.bin
|
||||||
da253d8b4dc4f8fc482880c54cd832c53bfd4aa0 20717584 level11/core_data.bin
|
da253d8b4dc4f8fc482880c54cd832c53bfd4aa0 20717584 level11/core_data.bin
|
||||||
a0173c7577180d32e9bfed67464eecf3bc12c033 11414905 level11/core_data.wad
|
a0173c7577180d32e9bfed67464eecf3bc12c033 11414905 level11/core_data.wad
|
||||||
0e0d5289451a3bffa376992d0b6a288421a7a55d 36000 level11/core_index.bin
|
0e0d5289451a3bffa376992d0b6a288421a7a55d 36000 level11/core_index.bin
|
||||||
49116fede92a6846bbbb0c3951f3314cbc770f80 15378432 level11/data.bin
|
49116fede92a6846bbbb0c3951f3314cbc770f80 15378432 level11/data.bin
|
||||||
ff6811c08f3fe034bae54ab066e3051d4cbb39cc 1034240 level11/gameplay.bin
|
ff6811c08f3fe034bae54ab066e3051d4cbb39cc 1034240 level11/gameplay.bin
|
||||||
|
472e916feae6b88e5028e48e9d2e4d1a433a8ed7 538624 level11/gs_ram.bin
|
||||||
e91c93e8af747ce8c0802c3c96cd88acc126a5c3 51200 level11/occlusion.bin
|
e91c93e8af747ce8c0802c3c96cd88acc126a5c3 51200 level11/occlusion.bin
|
||||||
850f01ef9678ed0b0b6242c410ad7e57b935079f 991232 level11/sound_bank.bin
|
850f01ef9678ed0b0b6242c410ad7e57b935079f 991232 level11/sound_bank.bin
|
||||||
d13c8eb4f2ab552e330a119bf1e14d55f9921220 20995456 level12/core_data.bin
|
d13c8eb4f2ab552e330a119bf1e14d55f9921220 20995456 level12/core_data.bin
|
||||||
@@ -2528,48 +2522,56 @@ d13c8eb4f2ab552e330a119bf1e14d55f9921220 20995456 level12/core_data.bin
|
|||||||
7b3ab93650bd82117b2b8ff596f95799123ff6e8 34864 level12/core_index.bin
|
7b3ab93650bd82117b2b8ff596f95799123ff6e8 34864 level12/core_index.bin
|
||||||
a134bad90883ad15ff7b52d909da461a491d253b 15136768 level12/data.bin
|
a134bad90883ad15ff7b52d909da461a491d253b 15136768 level12/data.bin
|
||||||
b058a1f7858198be55a90bf70d81db1f8039e132 813056 level12/gameplay.bin
|
b058a1f7858198be55a90bf70d81db1f8039e132 813056 level12/gameplay.bin
|
||||||
|
cbccba141d2efe7b4165dd81a265532465106def 510464 level12/gs_ram.bin
|
||||||
709e185927a11b846e6b1d66c2fa770cf284ea65 1568768 level12/sound_bank.bin
|
709e185927a11b846e6b1d66c2fa770cf284ea65 1568768 level12/sound_bank.bin
|
||||||
74fd46bfcfcb97132c509d2147070327976faab4 21301168 level13/core_data.bin
|
74fd46bfcfcb97132c509d2147070327976faab4 21301168 level13/core_data.bin
|
||||||
7c4b8c7fd086de66e19bd440a5e63fea4729e0bb 12037495 level13/core_data.wad
|
7c4b8c7fd086de66e19bd440a5e63fea4729e0bb 12037495 level13/core_data.wad
|
||||||
c7a99c3252427f11a202a907df605b3d43baaae0 35744 level13/core_index.bin
|
c7a99c3252427f11a202a907df605b3d43baaae0 35744 level13/core_index.bin
|
||||||
29c09e0877c62ba4dee1325cf00c944f9ad78ceb 15702016 level13/data.bin
|
29c09e0877c62ba4dee1325cf00c944f9ad78ceb 15702016 level13/data.bin
|
||||||
ab5e7bd921f3973e3d15052838ec87676d35928a 854016 level13/gameplay.bin
|
ab5e7bd921f3973e3d15052838ec87676d35928a 854016 level13/gameplay.bin
|
||||||
|
0554fcecd5cbe57f3b90a97bd75edba4ea438752 555520 level13/gs_ram.bin
|
||||||
2793ed3153f9f8ace889b855dbfc53f29c94c0bc 1406976 level13/sound_bank.bin
|
2793ed3153f9f8ace889b855dbfc53f29c94c0bc 1406976 level13/sound_bank.bin
|
||||||
386e9a7cef58f23f66fbc21de42be234c25be0b4 19738512 level14/core_data.bin
|
386e9a7cef58f23f66fbc21de42be234c25be0b4 19738512 level14/core_data.bin
|
||||||
b34bf0528e2c753ce9fd65791e89ba6ec8491358 11217244 level14/core_data.wad
|
b34bf0528e2c753ce9fd65791e89ba6ec8491358 11217244 level14/core_data.wad
|
||||||
cc03cc08cc55f27d57bcdc6c9c435e28479a65e3 34848 level14/core_index.bin
|
cc03cc08cc55f27d57bcdc6c9c435e28479a65e3 34848 level14/core_index.bin
|
||||||
fb30c8c10944377af4a76e2804209be630dba993 14948352 level14/data.bin
|
fb30c8c10944377af4a76e2804209be630dba993 14948352 level14/data.bin
|
||||||
95157d87f0c5e76a38daead9e07d01281b65ebf0 692224 level14/gameplay.bin
|
95157d87f0c5e76a38daead9e07d01281b65ebf0 692224 level14/gameplay.bin
|
||||||
|
0d8c49b2b405df331f859a077a6dfbb6aa922b3d 582656 level14/gs_ram.bin
|
||||||
28a70e7880794f94a76e2b340373d1dc40484fee 1689600 level14/sound_bank.bin
|
28a70e7880794f94a76e2b340373d1dc40484fee 1689600 level14/sound_bank.bin
|
||||||
9e1a9269a59741f15608ee773192a4c4e3adadbb 14566720 level15/core_data.bin
|
9e1a9269a59741f15608ee773192a4c4e3adadbb 14566720 level15/core_data.bin
|
||||||
3c7f4291ff09cdfb0c1cd2e435fb4c039faf7572 8538458 level15/core_data.wad
|
3c7f4291ff09cdfb0c1cd2e435fb4c039faf7572 8538458 level15/core_data.wad
|
||||||
cda9e44cee8b5ac58f9abd97dfcdbbe14e76d490 21792 level15/core_index.bin
|
cda9e44cee8b5ac58f9abd97dfcdbbe14e76d490 21792 level15/core_index.bin
|
||||||
39e2c8e15375dae4c73f61a7be980dccf4a85aa9 12122112 level15/data.bin
|
39e2c8e15375dae4c73f61a7be980dccf4a85aa9 12122112 level15/data.bin
|
||||||
9b13d67b1276d25937cab21344fa8a6687cc0882 491520 level15/gameplay.bin
|
9b13d67b1276d25937cab21344fa8a6687cc0882 491520 level15/gameplay.bin
|
||||||
|
d1437c7d2106f6e9cdb54f66cea4f1a714dc8867 407552 level15/gs_ram.bin
|
||||||
f89c334b887f377f341fc9232d474affa89fc544 1476608 level15/sound_bank.bin
|
f89c334b887f377f341fc9232d474affa89fc544 1476608 level15/sound_bank.bin
|
||||||
49c2efb647efc0baaf919aaaa140f0a660725b3d 21374192 level16/core_data.bin
|
49c2efb647efc0baaf919aaaa140f0a660725b3d 21374192 level16/core_data.bin
|
||||||
9c9291257d21ac62dff3ac4090f9d2f9b6c5c3e8 12442996 level16/core_data.wad
|
9c9291257d21ac62dff3ac4090f9d2f9b6c5c3e8 12442996 level16/core_data.wad
|
||||||
5a57d24086355cb7fb2410ac9b5daff4bf2d63a5 34080 level16/core_index.bin
|
5a57d24086355cb7fb2410ac9b5daff4bf2d63a5 34080 level16/core_index.bin
|
||||||
8e58f79a35fbe78382065bdce0f53d2cc9f1ccf1 16091136 level16/data.bin
|
8e58f79a35fbe78382065bdce0f53d2cc9f1ccf1 16091136 level16/data.bin
|
||||||
af60ee961ab3c4508f6fc7f7e5db32d1bbe08769 595968 level16/gameplay.bin
|
af60ee961ab3c4508f6fc7f7e5db32d1bbe08769 595968 level16/gameplay.bin
|
||||||
|
9b85cc208f66ffbbdff60d979597e1e2c47bc593 577024 level16/gs_ram.bin
|
||||||
84723c80beedd7e55143a424a1b2f6255a200ab0 1654784 level16/sound_bank.bin
|
84723c80beedd7e55143a424a1b2f6255a200ab0 1654784 level16/sound_bank.bin
|
||||||
485e5343a6f146179b319fb7d76388eee6e21276 20920320 level17/core_data.bin
|
485e5343a6f146179b319fb7d76388eee6e21276 20920320 level17/core_data.bin
|
||||||
fc90583b7560be17115f57c8ecf6fc14ec2e59d8 10922651 level17/core_data.wad
|
fc90583b7560be17115f57c8ecf6fc14ec2e59d8 10922651 level17/core_data.wad
|
||||||
5f9496629cf8c32071c2d4a125fee1ca7b89f73e 35856 level17/core_index.bin
|
5f9496629cf8c32071c2d4a125fee1ca7b89f73e 35856 level17/core_index.bin
|
||||||
d7669eec9eb3cf36bf4f649333ae79864c1a7043 14575616 level17/data.bin
|
d7669eec9eb3cf36bf4f649333ae79864c1a7043 14575616 level17/data.bin
|
||||||
10a5f5e2862f7364a9bbe886dadb9ebca8ab0381 796672 level17/gameplay.bin
|
10a5f5e2862f7364a9bbe886dadb9ebca8ab0381 796672 level17/gameplay.bin
|
||||||
|
899d4e61f032af4c66419d3a286a25d1e97e5644 574464 level17/gs_ram.bin
|
||||||
37c76a37d9e3cbbed6dfc324b0b90d3498ecb0e7 1347584 level17/sound_bank.bin
|
37c76a37d9e3cbbed6dfc324b0b90d3498ecb0e7 1347584 level17/sound_bank.bin
|
||||||
f9240e2bd6406f206e155b09cc5c914b6574e41e 20268640 level18/core_data.bin
|
f9240e2bd6406f206e155b09cc5c914b6574e41e 20268640 level18/core_data.bin
|
||||||
f44add8374224a84070995ecb4b1be8cfb58aa9f 11721139 level18/core_data.wad
|
f44add8374224a84070995ecb4b1be8cfb58aa9f 11721139 level18/core_data.wad
|
||||||
6ab43f9167d67246e18394eef5b732589fcaff66 33088 level18/core_index.bin
|
6ab43f9167d67246e18394eef5b732589fcaff66 33088 level18/core_index.bin
|
||||||
d0f931677cb6589a564937c3d15c13344b923969 15501312 level18/data.bin
|
d0f931677cb6589a564937c3d15c13344b923969 15501312 level18/data.bin
|
||||||
2adc53f721fca95dd1012994d243e0704edd034c 675840 level18/gameplay.bin
|
2adc53f721fca95dd1012994d243e0704edd034c 675840 level18/gameplay.bin
|
||||||
|
7f6a823c4b47fdfb58e29237b680d03e946329db 571136 level18/gs_ram.bin
|
||||||
b26483cc29ecb8b6d2c71919ff7cd56b09522bc1 1492992 level18/sound_bank.bin
|
b26483cc29ecb8b6d2c71919ff7cd56b09522bc1 1492992 level18/sound_bank.bin
|
||||||
0918d824b72b892a24f46108bbc60f6e875236f6 20233504 level19/core_data.bin
|
0918d824b72b892a24f46108bbc60f6e875236f6 20233504 level19/core_data.bin
|
||||||
b79df8e074db7d3c75bc2b3b9c2a4de61e5dd9cd 11797811 level19/core_data.wad
|
b79df8e074db7d3c75bc2b3b9c2a4de61e5dd9cd 11797811 level19/core_data.wad
|
||||||
b0ea377a39a616dbd2b15224a9996925c7b28f4d 31536 level19/core_index.bin
|
b0ea377a39a616dbd2b15224a9996925c7b28f4d 31536 level19/core_index.bin
|
||||||
3218aa99e3ea4e9f4a050a5117413261defded4e 15386624 level19/data.bin
|
3218aa99e3ea4e9f4a050a5117413261defded4e 15386624 level19/data.bin
|
||||||
8700a93086450328f2693b553e1df447a79fcd83 825344 level19/gameplay.bin
|
8700a93086450328f2693b553e1df447a79fcd83 825344 level19/gameplay.bin
|
||||||
|
d5061a551eb354ddf7952c4bfd62767c6e074c3f 497920 level19/gs_ram.bin
|
||||||
877ca7fb8e80d810d6c37fb993795aa10b5b1033 47104 level19/occlusion.bin
|
877ca7fb8e80d810d6c37fb993795aa10b5b1033 47104 level19/occlusion.bin
|
||||||
8298f799c394542e912e41ff3321ee55c70e0c8f 1570816 level19/sound_bank.bin
|
8298f799c394542e912e41ff3321ee55c70e0c8f 1570816 level19/sound_bank.bin
|
||||||
6d56189ad74f04d366116b736ab12888154de833 20209408 level2/core_data.bin
|
6d56189ad74f04d366116b736ab12888154de833 20209408 level2/core_data.bin
|
||||||
@@ -2577,6 +2579,7 @@ f0eb5c033e84092748cf3457818b0d4c85da7a0a 11372886 level2/core_data.wad
|
|||||||
73411362c1cfa8faec733705d93e953f1381ef30 33808 level2/core_index.bin
|
73411362c1cfa8faec733705d93e953f1381ef30 33808 level2/core_index.bin
|
||||||
14c7c5cd788c0bebe531029bc1f8f8d570ea1771 15089664 level2/data.bin
|
14c7c5cd788c0bebe531029bc1f8f8d570ea1771 15089664 level2/data.bin
|
||||||
bfc9f6bc73edfa7deb46505f187c274dcf7b863a 790528 level2/gameplay.bin
|
bfc9f6bc73edfa7deb46505f187c274dcf7b863a 790528 level2/gameplay.bin
|
||||||
|
164f775e33d9325b3e4c5ce4cbf8c515ed311fd2 513024 level2/gs_ram.bin
|
||||||
01c7d4c7b0ecb44a91694ffde796ff941057191a 28672 level2/occlusion.bin
|
01c7d4c7b0ecb44a91694ffde796ff941057191a 28672 level2/occlusion.bin
|
||||||
bce56b3b899d72c5d89f612bd70447f066c10757 1118208 level2/sound_bank.bin
|
bce56b3b899d72c5d89f612bd70447f066c10757 1118208 level2/sound_bank.bin
|
||||||
daf162b58756e2661f3ec891c8736a1611b4dc52 19637472 level20/core_data.bin
|
daf162b58756e2661f3ec891c8736a1611b4dc52 19637472 level20/core_data.bin
|
||||||
@@ -2584,6 +2587,7 @@ daf162b58756e2661f3ec891c8736a1611b4dc52 19637472 level20/core_data.bin
|
|||||||
b12e8599d211addc85cbe138e58157ade380a575 37360 level20/core_index.bin
|
b12e8599d211addc85cbe138e58157ade380a575 37360 level20/core_index.bin
|
||||||
640e6a62f3795bb285663364c2929f4640dacf33 14237696 level20/data.bin
|
640e6a62f3795bb285663364c2929f4640dacf33 14237696 level20/data.bin
|
||||||
7368af71bc6c070fc6422fd69a354b2f3518ed2b 833536 level20/gameplay.bin
|
7368af71bc6c070fc6422fd69a354b2f3518ed2b 833536 level20/gameplay.bin
|
||||||
|
4c7325cfdde68fc9160ec9a2cd18da3d327dcd9a 534272 level20/gs_ram.bin
|
||||||
61ae37031d1df746bffb4a0bddeaee85558c894e 36864 level20/occlusion.bin
|
61ae37031d1df746bffb4a0bddeaee85558c894e 36864 level20/occlusion.bin
|
||||||
abce77e8841c0ed0fa006db81840b35ea4c75afd 1335296 level20/sound_bank.bin
|
abce77e8841c0ed0fa006db81840b35ea4c75afd 1335296 level20/sound_bank.bin
|
||||||
5e850ab42d2dc3e8fa1f9ae74d401be1658c9ba7 14483120 level21/core_data.bin
|
5e850ab42d2dc3e8fa1f9ae74d401be1658c9ba7 14483120 level21/core_data.bin
|
||||||
@@ -2591,46 +2595,54 @@ f10321014e7ccd61a65f294baefaf35875d6294b 8178784 level21/core_data.wad
|
|||||||
728b90cdbb44f1b94c140d82fc03268ca54408ac 25456 level21/core_index.bin
|
728b90cdbb44f1b94c140d82fc03268ca54408ac 25456 level21/core_index.bin
|
||||||
4a3ebd058fe62df17ed4d74fe190442ff2993474 11722752 level21/data.bin
|
4a3ebd058fe62df17ed4d74fe190442ff2993474 11722752 level21/data.bin
|
||||||
6c2b9faaef23c142a61316190852251404734c39 448512 level21/gameplay.bin
|
6c2b9faaef23c142a61316190852251404734c39 448512 level21/gameplay.bin
|
||||||
|
43a953d770cd4c5833eda5c1ecae26d7ce127be1 512256 level21/gs_ram.bin
|
||||||
8fbcb2360d5244e92bfd7c2ccd453eec44cd042b 1167360 level21/sound_bank.bin
|
8fbcb2360d5244e92bfd7c2ccd453eec44cd042b 1167360 level21/sound_bank.bin
|
||||||
7b0733a07f28e7d7cf5d8a19b1058a98a5563c9f 14288048 level22/core_data.bin
|
7b0733a07f28e7d7cf5d8a19b1058a98a5563c9f 14288048 level22/core_data.bin
|
||||||
34a2d1ac57f13cacdf46cd7d73f356951f87dd43 8186976 level22/core_data.wad
|
34a2d1ac57f13cacdf46cd7d73f356951f87dd43 8186976 level22/core_data.wad
|
||||||
edb2fd7c72d0a517613fd813429aa9036e2eb509 25152 level22/core_index.bin
|
edb2fd7c72d0a517613fd813429aa9036e2eb509 25152 level22/core_index.bin
|
||||||
3519cac29e6371802b40e3d8234ef7064abee336 11683840 level22/data.bin
|
3519cac29e6371802b40e3d8234ef7064abee336 11683840 level22/data.bin
|
||||||
a8760bb193082c5e85faeb5f799ab8585f366805 557056 level22/gameplay.bin
|
a8760bb193082c5e85faeb5f799ab8585f366805 557056 level22/gameplay.bin
|
||||||
|
9c3af51d0b3ad2b786a428a49a8ee9d30886d636 482048 level22/gs_ram.bin
|
||||||
dba3b2b5856efbd28a8aeaa15fbee6f4fb479895 1363968 level22/sound_bank.bin
|
dba3b2b5856efbd28a8aeaa15fbee6f4fb479895 1363968 level22/sound_bank.bin
|
||||||
c6b6f4087f4c9cd374a587b13777f9575953838a 12647664 level23/core_data.bin
|
c6b6f4087f4c9cd374a587b13777f9575953838a 12647664 level23/core_data.bin
|
||||||
dbdca886def506bd2e6fd189c0fc4ca8d2834b97 6900832 level23/core_data.wad
|
dbdca886def506bd2e6fd189c0fc4ca8d2834b97 6900832 level23/core_data.wad
|
||||||
ed196a1541e056da4433d366c1a096143ea87ca0 23024 level23/core_index.bin
|
ed196a1541e056da4433d366c1a096143ea87ca0 23024 level23/core_index.bin
|
||||||
9a26194ba7d5930279a62113b1377ffeb1294269 10414080 level23/data.bin
|
9a26194ba7d5930279a62113b1377ffeb1294269 10414080 level23/data.bin
|
||||||
336b3c864d1aa317003499e7d1d2d5a8cee13d58 505856 level23/gameplay.bin
|
336b3c864d1aa317003499e7d1d2d5a8cee13d58 505856 level23/gameplay.bin
|
||||||
|
bcc4a8508f14af07ee85dd5f06dff7a910d73b6a 411136 level23/gs_ram.bin
|
||||||
e254322f1d87d2650c21ec2c118546df1a56d9e8 1675264 level23/sound_bank.bin
|
e254322f1d87d2650c21ec2c118546df1a56d9e8 1675264 level23/sound_bank.bin
|
||||||
508808939296de54fee8928824bdcc6d37204d01 6386608 level24/core_data.bin
|
508808939296de54fee8928824bdcc6d37204d01 6386608 level24/core_data.bin
|
||||||
c44701fc0a229dc8071a30594231d5edfb6b4119 3805312 level24/core_data.wad
|
c44701fc0a229dc8071a30594231d5edfb6b4119 3805312 level24/core_data.wad
|
||||||
c526f84012360c24a07e4bbd6823dd18ce8b3181 16608 level24/core_index.bin
|
c526f84012360c24a07e4bbd6823dd18ce8b3181 16608 level24/core_index.bin
|
||||||
f38883faa65e74f67a44da4a55b9b476848d7cae 6981632 level24/data.bin
|
f38883faa65e74f67a44da4a55b9b476848d7cae 6981632 level24/data.bin
|
||||||
|
adf90875335b529183111edaeaa90138ba111dbe 308480 level24/gs_ram.bin
|
||||||
3bf88b1d1c455ab90bbacc3270a3f54c94b900ef 937984 level24/sound_bank.bin
|
3bf88b1d1c455ab90bbacc3270a3f54c94b900ef 937984 level24/sound_bank.bin
|
||||||
c81ea8c485748fccec6619bacc75afe8ce2f7e20 5898736 level25/core_data.bin
|
c81ea8c485748fccec6619bacc75afe8ce2f7e20 5898736 level25/core_data.bin
|
||||||
80d763434b9dda1224c446de67e4907cfbdcfbe7 3513706 level25/core_data.wad
|
80d763434b9dda1224c446de67e4907cfbdcfbe7 3513706 level25/core_data.wad
|
||||||
c493407fb493ff359e90d1bdfc1b379a2a6c540a 16592 level25/core_index.bin
|
c493407fb493ff359e90d1bdfc1b379a2a6c540a 16592 level25/core_index.bin
|
||||||
e57a615ccce4afd0a3de13f638ecaa19f88d0789 6852608 level25/data.bin
|
e57a615ccce4afd0a3de13f638ecaa19f88d0789 6852608 level25/data.bin
|
||||||
|
b54226627562788f722c2fdc314d2117ca6ed592 287488 level25/gs_ram.bin
|
||||||
e9d246c13a7198b42a71f081d767b9e9598d01a1 1370112 level25/sound_bank.bin
|
e9d246c13a7198b42a71f081d767b9e9598d01a1 1370112 level25/sound_bank.bin
|
||||||
948974c4fd10e7ca7ad1a362af9cb46aa6c81773 9530880 level26/core_data.bin
|
948974c4fd10e7ca7ad1a362af9cb46aa6c81773 9530880 level26/core_data.bin
|
||||||
6140efd9a7ebbcd81bfddf1ce0a5395ffb136d2e 5894651 level26/core_data.wad
|
6140efd9a7ebbcd81bfddf1ce0a5395ffb136d2e 5894651 level26/core_data.wad
|
||||||
d71c13701f26cd0a4bbcb9508fb0387ba54d2d62 20704 level26/core_index.bin
|
d71c13701f26cd0a4bbcb9508fb0387ba54d2d62 20704 level26/core_index.bin
|
||||||
4fdd7d88a5d3db63418c968f8a5f53ac370b86be 9224192 level26/data.bin
|
4fdd7d88a5d3db63418c968f8a5f53ac370b86be 9224192 level26/data.bin
|
||||||
0d63ad0bc2edcd958b1c7dbec303c256ed8850e1 679936 level26/gameplay.bin
|
0d63ad0bc2edcd958b1c7dbec303c256ed8850e1 679936 level26/gameplay.bin
|
||||||
|
abb3fb6a4175cf6b41eff1d7178d59ce7affb84f 378880 level26/gs_ram.bin
|
||||||
8f6a74b8008a9ebc9e92853d0b339a8e725c9992 1073152 level26/sound_bank.bin
|
8f6a74b8008a9ebc9e92853d0b339a8e725c9992 1073152 level26/sound_bank.bin
|
||||||
f5d3cd4480fe6a52a019d04401a360018b279b81 21148480 level3/core_data.bin
|
f5d3cd4480fe6a52a019d04401a360018b279b81 21148480 level3/core_data.bin
|
||||||
690209848c64647194548d5e81f329d471392a8e 11383592 level3/core_data.wad
|
690209848c64647194548d5e81f329d471392a8e 11383592 level3/core_data.wad
|
||||||
351819059a5b74d8e17f50ee87974e5c30504c3e 37568 level3/core_index.bin
|
351819059a5b74d8e17f50ee87974e5c30504c3e 37568 level3/core_index.bin
|
||||||
eb8366046b5643cc54d42d800c06bf66ccd4ef36 15157248 level3/data.bin
|
eb8366046b5643cc54d42d800c06bf66ccd4ef36 15157248 level3/data.bin
|
||||||
55cc4acecac6ba19232e6505fdc06a22b0633a45 749568 level3/gameplay.bin
|
55cc4acecac6ba19232e6505fdc06a22b0633a45 749568 level3/gameplay.bin
|
||||||
|
ed1f77cd35e2572adc51f4f43de030f31b8c6ed9 675840 level3/gs_ram.bin
|
||||||
d986825dfc2bed713eb9699504536346658aaa3f 1705984 level3/sound_bank.bin
|
d986825dfc2bed713eb9699504536346658aaa3f 1705984 level3/sound_bank.bin
|
||||||
f8c0aa3778b5bcb1995e516045b28ca6ac35502f 21107088 level4/core_data.bin
|
f8c0aa3778b5bcb1995e516045b28ca6ac35502f 21107088 level4/core_data.bin
|
||||||
ec75f324670188af581c4fceaa4cb285353f95b8 12171908 level4/core_data.wad
|
ec75f324670188af581c4fceaa4cb285353f95b8 12171908 level4/core_data.wad
|
||||||
cc4997acbdc0a6ac85d427227c04e6af6e34cd46 38080 level4/core_index.bin
|
cc4997acbdc0a6ac85d427227c04e6af6e34cd46 38080 level4/core_index.bin
|
||||||
67a114e12f840f4c01e3f96b48c504f93b839b03 15998976 level4/data.bin
|
67a114e12f840f4c01e3f96b48c504f93b839b03 15998976 level4/data.bin
|
||||||
eeee7ebf8d9980f9afc6d9c2bc2156bcca015ddd 960512 level4/gameplay.bin
|
eeee7ebf8d9980f9afc6d9c2bc2156bcca015ddd 960512 level4/gameplay.bin
|
||||||
|
5a9a603e46e2008df02e60dce2c66ae9368d9993 522496 level4/gs_ram.bin
|
||||||
fed269c2d706a2ba11cab965e090fd41d434055c 49152 level4/occlusion.bin
|
fed269c2d706a2ba11cab965e090fd41d434055c 49152 level4/occlusion.bin
|
||||||
bdd446767b1a84043d65ff968278cd2d5808705a 1236992 level4/sound_bank.bin
|
bdd446767b1a84043d65ff968278cd2d5808705a 1236992 level4/sound_bank.bin
|
||||||
cb07a102a92b887d06d9314c07ce1db50e14c1fb 16715440 level5/core_data.bin
|
cb07a102a92b887d06d9314c07ce1db50e14c1fb 16715440 level5/core_data.bin
|
||||||
@@ -2638,18 +2650,21 @@ e5cc08e39cb6883b391605bdcff8ba3bb8423699 11205994 level5/core_data.wad
|
|||||||
d41e06bc7086ed6876bef602420cca8d52492b1e 23536 level5/core_index.bin
|
d41e06bc7086ed6876bef602420cca8d52492b1e 23536 level5/core_index.bin
|
||||||
b241ea9d8fe18dd5bdbc8a4255ba8450a66b3c6a 14772224 level5/data.bin
|
b241ea9d8fe18dd5bdbc8a4255ba8450a66b3c6a 14772224 level5/data.bin
|
||||||
c71539e0f8b0e2d666d9382475539c7a590fee04 514048 level5/gameplay.bin
|
c71539e0f8b0e2d666d9382475539c7a590fee04 514048 level5/gameplay.bin
|
||||||
|
ad4318ac2d601a2b1b0b9b444325082e0fc50ffc 434176 level5/gs_ram.bin
|
||||||
5c2fd4470dd8c892af7686770af8cf3e2396373c 1390592 level5/sound_bank.bin
|
5c2fd4470dd8c892af7686770af8cf3e2396373c 1390592 level5/sound_bank.bin
|
||||||
8ea18c5a51e4ae07ca56d92d65ffde60f4bcce12 20935696 level6/core_data.bin
|
8ea18c5a51e4ae07ca56d92d65ffde60f4bcce12 20935696 level6/core_data.bin
|
||||||
f2962a73cfb15ab1bef23253d863d2433b264383 12810673 level6/core_data.wad
|
f2962a73cfb15ab1bef23253d863d2433b264383 12810673 level6/core_data.wad
|
||||||
29284ba33a151cb45df0b4d074fa7ccc9d2c0667 38080 level6/core_index.bin
|
29284ba33a151cb45df0b4d074fa7ccc9d2c0667 38080 level6/core_index.bin
|
||||||
ccb0a4e8c51be81fbcd8ad325ab8d521bbdb7604 16777216 level6/data.bin
|
ccb0a4e8c51be81fbcd8ad325ab8d521bbdb7604 16777216 level6/data.bin
|
||||||
b9ff3b4b8fb6308aac7f5ddba1c4b1099d484dd5 929792 level6/gameplay.bin
|
b9ff3b4b8fb6308aac7f5ddba1c4b1099d484dd5 929792 level6/gameplay.bin
|
||||||
|
c404dfa08cab9603e5deeeed9778e4d29b755d88 604160 level6/gs_ram.bin
|
||||||
82fcce964396eb508768933d092a557bc1b05c89 1669120 level6/sound_bank.bin
|
82fcce964396eb508768933d092a557bc1b05c89 1669120 level6/sound_bank.bin
|
||||||
6692290d78706a2b92cba541264ddea9a764e2f2 18209392 level7/core_data.bin
|
6692290d78706a2b92cba541264ddea9a764e2f2 18209392 level7/core_data.bin
|
||||||
f1cae801f1050c57258f93a7035148b3094ece03 10780010 level7/core_data.wad
|
f1cae801f1050c57258f93a7035148b3094ece03 10780010 level7/core_data.wad
|
||||||
79e3957d149ce3b00f713eb1757fec89d7db86c4 32736 level7/core_index.bin
|
79e3957d149ce3b00f713eb1757fec89d7db86c4 32736 level7/core_index.bin
|
||||||
9b18673cac7d7d6c3d64bebca909f3bb76ff9f39 14366720 level7/data.bin
|
9b18673cac7d7d6c3d64bebca909f3bb76ff9f39 14366720 level7/data.bin
|
||||||
bc8f542a56d4795c77774201903e25b01962c039 636928 level7/gameplay.bin
|
bc8f542a56d4795c77774201903e25b01962c039 636928 level7/gameplay.bin
|
||||||
|
9671dd66d0a20ef778bbd76f0e8517792646467c 580352 level7/gs_ram.bin
|
||||||
e02d1b0d2339d72ed5d07abfe105f41e2288f59f 16384 level7/occlusion.bin
|
e02d1b0d2339d72ed5d07abfe105f41e2288f59f 16384 level7/occlusion.bin
|
||||||
8e07c803b709100023c3b44c8842eec9b8f49898 1556480 level7/sound_bank.bin
|
8e07c803b709100023c3b44c8842eec9b8f49898 1556480 level7/sound_bank.bin
|
||||||
264a306c3a83b9137d9753b282e9c773cd386713 21086224 level8/core_data.bin
|
264a306c3a83b9137d9753b282e9c773cd386713 21086224 level8/core_data.bin
|
||||||
@@ -2657,6 +2672,7 @@ e02d1b0d2339d72ed5d07abfe105f41e2288f59f 16384 level7/occlusion.bin
|
|||||||
8d1b6c3a05e9b2140c056df3e538ed537b9d3a33 30624 level8/core_index.bin
|
8d1b6c3a05e9b2140c056df3e538ed537b9d3a33 30624 level8/core_index.bin
|
||||||
ee6f0ca2c15e1e0828d63547a8c19d2c45b8c0e2 13375488 level8/data.bin
|
ee6f0ca2c15e1e0828d63547a8c19d2c45b8c0e2 13375488 level8/data.bin
|
||||||
ff4c7dd1924165ddc7dbb9494a166eb3e140c95c 899072 level8/gameplay.bin
|
ff4c7dd1924165ddc7dbb9494a166eb3e140c95c 899072 level8/gameplay.bin
|
||||||
|
2d0d9a2d6092d0172bf50a3716d03b3631580e8b 541184 level8/gs_ram.bin
|
||||||
afeb717c151befdabfcf6297b0dbb1772ac13fdb 22528 level8/occlusion.bin
|
afeb717c151befdabfcf6297b0dbb1772ac13fdb 22528 level8/occlusion.bin
|
||||||
1f65f6b44e4e4a146aaac3d63b7f459657de0010 1593344 level8/sound_bank.bin
|
1f65f6b44e4e4a146aaac3d63b7f459657de0010 1593344 level8/sound_bank.bin
|
||||||
fd39207fd776cb3ba6b8d5f6d3b10d61610c5e49 21592016 level9/core_data.bin
|
fd39207fd776cb3ba6b8d5f6d3b10d61610c5e49 21592016 level9/core_data.bin
|
||||||
@@ -2664,6 +2680,7 @@ fd39207fd776cb3ba6b8d5f6d3b10d61610c5e49 21592016 level9/core_data.bin
|
|||||||
ffed974844aa0bef984bc33c86ff6757e0f9c5bc 34976 level9/core_index.bin
|
ffed974844aa0bef984bc33c86ff6757e0f9c5bc 34976 level9/core_index.bin
|
||||||
db530147f5d900e5a85294070a0a5e707c086213 16232448 level9/data.bin
|
db530147f5d900e5a85294070a0a5e707c086213 16232448 level9/data.bin
|
||||||
3839c295b0408460f50032ff166cac27c0bdf411 864256 level9/gameplay.bin
|
3839c295b0408460f50032ff166cac27c0bdf411 864256 level9/gameplay.bin
|
||||||
|
e117d8deddac4a05f917a218e7583859397ad4d9 568320 level9/gs_ram.bin
|
||||||
bc38b5983d8717ff4c7a35fac3cd1042ef7bd675 1697792 level9/sound_bank.bin
|
bc38b5983d8717ff4c7a35fac3cd1042ef7bd675 1697792 level9/sound_bank.bin
|
||||||
f666c702e75052034d72125db6bd5d6821ac8379 9 misc/0000.bin
|
f666c702e75052034d72125db6bd5d6821ac8379 9 misc/0000.bin
|
||||||
2af38bb5b2fe77af4ed2a6079ebe5857247082a5 367 misc/0001.bin
|
2af38bb5b2fe77af4ed2a6079ebe5857247082a5 367 misc/0001.bin
|
||||||
|
|||||||
|
@@ -25,6 +25,7 @@ fn main() -> ExitCode {
|
|||||||
// Scratch helper used while reversing the level formats: decompress a
|
// Scratch helper used while reversing the level formats: decompress a
|
||||||
// byte range of an already-unpacked file with the WAD decoder.
|
// byte range of an already-unpacked file with the WAD decoder.
|
||||||
Some("wad") => run_wad(&args[1..]),
|
Some("wad") => run_wad(&args[1..]),
|
||||||
|
Some("dump-texture") => run_dump_texture(&args[1..]),
|
||||||
Some("--help") | Some("-h") | Some("help") | None => {
|
Some("--help") | Some("-h") | Some("help") | None => {
|
||||||
usage();
|
usage();
|
||||||
ExitCode::SUCCESS
|
ExitCode::SUCCESS
|
||||||
@@ -65,6 +66,11 @@ fn usage() {
|
|||||||
verify <out-dir> <table.tsv>
|
verify <out-dir> <table.tsv>
|
||||||
Check <out-dir> against a previously written table.
|
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."
|
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 {
|
fn run_wad(args: &[String]) -> ExitCode {
|
||||||
if args.len() < 4 {
|
if args.len() < 4 {
|
||||||
eprintln!("usage: gc-extract wad <file> <offset> <size> <out>");
|
eprintln!("usage: gc-extract wad <file> <offset> <size> <out>");
|
||||||
|
|||||||
@@ -158,6 +158,19 @@ pub fn unpack(iso_path: &Path, out_dir: &Path) -> Result<Vec<Extracted>, String>
|
|||||||
);
|
);
|
||||||
parts += 1;
|
parts += 1;
|
||||||
}
|
}
|
||||||
|
// Graphics Synthesizer RAM holds the CLUTs the tfrag
|
||||||
|
// texture table's palette indices point into, so it
|
||||||
|
// is needed to decode any level texture.
|
||||||
|
let gs = data_header.part(LevelDataPart::GsRam);
|
||||||
|
if let Ok(bytes) = gs.slice(data_extent, data_extent.len()) {
|
||||||
|
write_file(
|
||||||
|
out_dir,
|
||||||
|
&format!("{stem}/gs_ram.bin"),
|
||||||
|
bytes,
|
||||||
|
&mut written,
|
||||||
|
);
|
||||||
|
parts += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => eprintln!(" {stem}: data extent header did not parse: {e}"),
|
Err(e) => eprintln!(" {stem}: data extent header did not parse: {e}"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -704,3 +704,106 @@ fn tfrag_ad_gifs_index_the_level_texture_table() {
|
|||||||
"{ad_gifs} ad-gifs over {patches} patches reference {textures_seen}/{textures_total} level textures"
|
"{ad_gifs} ad-gifs over {patches} patches reference {textures_seen}/{textures_total} level textures"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Level textures decode: every tfrag texture's base level must resolve to a
|
||||||
|
/// plausible RGBA image.
|
||||||
|
///
|
||||||
|
/// This is the join between the texture table, the texel block and GS RAM, so
|
||||||
|
/// it is where a wrong CLUT remap, a wrong alpha scale or a wrong pixel base
|
||||||
|
/// would show up. The check is deliberately about *shape*, not specific
|
||||||
|
/// colours: a decoded image is plausible when it is fully opaque, is not
|
||||||
|
/// uniformly one colour, and uses more than a handful of distinct entries.
|
||||||
|
#[test]
|
||||||
|
fn tfrag_textures_decode_to_plausible_images() {
|
||||||
|
require_disc!();
|
||||||
|
gc_formats::iso9660::IsoImage::open(std::path::Path::new(
|
||||||
|
&std::env::var("GC_DISC").expect("GC_DISC is set by require_disc!"),
|
||||||
|
))
|
||||||
|
.expect("ISO should open");
|
||||||
|
let data = std::env::var_os("GC_EXTRACTED")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
|
||||||
|
PathBuf::from(home).join("Documents/gc-extracted")
|
||||||
|
});
|
||||||
|
if !data.exists() {
|
||||||
|
eprintln!("skipped: unpack the disc first");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut decoded = 0usize;
|
||||||
|
let mut opaque = 0usize;
|
||||||
|
let mut multi_colour = 0usize;
|
||||||
|
let mut flat = 0usize;
|
||||||
|
|
||||||
|
for n in 0..27 {
|
||||||
|
let dir = data.join(format!("level{n}"));
|
||||||
|
let core = match std::fs::read(dir.join("core_data.bin")) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
let index = match std::fs::read(dir.join("core_index.bin")) {
|
||||||
|
Ok(i) => i,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
let gs = match std::fs::read(dir.join("gs_ram.bin")) {
|
||||||
|
Ok(g) => g,
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!("level{n}: no gs_ram.bin, skipping");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let ch = gc_formats::level_core::LevelCoreHeader::parse(&index).expect("core header");
|
||||||
|
let textures_at = ch.textures_base.max(0) as usize;
|
||||||
|
let texture_block = &core[textures_at..];
|
||||||
|
let table =
|
||||||
|
gc_formats::level_core::texture_table(&index, ch.tfrag_textures).expect("table");
|
||||||
|
|
||||||
|
for (i, e) in table.iter().enumerate() {
|
||||||
|
let img = gc_formats::texture::base_image(texture_block, &gs, e)
|
||||||
|
.unwrap_or_else(|err| panic!("level{n} texture {i}: {err}"));
|
||||||
|
let t = img.decode().unwrap();
|
||||||
|
let texels = (e.width as i32) * (e.height as i32);
|
||||||
|
assert_eq!(t.rgba.len(), (texels * 4) as usize);
|
||||||
|
assert_eq!(t.width, e.width as u32);
|
||||||
|
decoded += 1;
|
||||||
|
|
||||||
|
// Count distinct colours and how many texels are fully opaque.
|
||||||
|
let mut seen = std::collections::HashSet::new();
|
||||||
|
let mut op = 0usize;
|
||||||
|
for px in t.rgba.chunks_exact(4) {
|
||||||
|
seen.insert([px[0], px[1], px[2], px[3]]);
|
||||||
|
if px[3] == 0xff {
|
||||||
|
op += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if seen.len() > 8 {
|
||||||
|
multi_colour += 1;
|
||||||
|
} else {
|
||||||
|
flat += 1;
|
||||||
|
}
|
||||||
|
if op == texels as usize {
|
||||||
|
opaque += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(decoded > 1_500, "only {decoded} textures decoded");
|
||||||
|
// Level art is mostly opaque and has real detail. A flat, transparent or
|
||||||
|
// single-colour result almost always means a CLUT or offset mistake.
|
||||||
|
let opaque_pct = opaque as f64 / decoded as f64;
|
||||||
|
let detail_pct = multi_colour as f64 / decoded as f64;
|
||||||
|
assert!(
|
||||||
|
opaque_pct > 0.9,
|
||||||
|
"only {opaque:.1}% of textures are fully opaque"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
detail_pct > 0.9,
|
||||||
|
"only {detail_pct:.1} of textures have >8 colours"
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
"{decoded} textures decoded: {:.1}% fully opaque, {:.1}% with >8 colours ({flat} flat)",
|
||||||
|
opaque_pct * 100.0,
|
||||||
|
detail_pct * 100.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ pub mod iso9660;
|
|||||||
pub mod level_core;
|
pub mod level_core;
|
||||||
pub mod level_data;
|
pub mod level_data;
|
||||||
pub mod level_wad;
|
pub mod level_wad;
|
||||||
|
pub mod texture;
|
||||||
pub mod tfrag;
|
pub mod tfrag;
|
||||||
pub mod wad;
|
pub mod wad;
|
||||||
|
|||||||
@@ -0,0 +1,370 @@
|
|||||||
|
//! Level textures: 8-bit palette indices plus a 256-entry CLUT.
|
||||||
|
//!
|
||||||
|
//! Spec: `docs/formats/texture.md`.
|
||||||
|
//!
|
||||||
|
//! Every level texture is a rectangular grid of **palette indices**, not
|
||||||
|
//! pixels: `width * height` bytes at `core_data[textures_base + data_offset]`.
|
||||||
|
//! The colour comes from a CLUT of 256 RGBA entries (1024 bytes) in GS RAM at
|
||||||
|
//! `palette * 0x100`. Both halves are indexed by the same byte, so nothing
|
||||||
|
//! about the layout is inferred — it is a straight lookup.
|
||||||
|
//!
|
||||||
|
//! Two conventions have to be respected, and both are PS2 hardware details
|
||||||
|
//! rather than choices:
|
||||||
|
//!
|
||||||
|
//! * **CSM1 CLUT order.** Within each 32-entry group the two middle 8-entry
|
||||||
|
//! blocks are swapped. The remap is an involution, so the same function
|
||||||
|
//! converts either way.
|
||||||
|
//! * **Alpha runs 0..=0x80, not 0..=255.** `0x80` is fully opaque, and
|
||||||
|
//! anything above it clamps to opaque.
|
||||||
|
|
||||||
|
use crate::buf::{Error, ErrorKind, Result};
|
||||||
|
use crate::level_core::TextureEntry;
|
||||||
|
|
||||||
|
/// PS2 CSM1 CLUT order to linear: within each 32-entry group, swap the two
|
||||||
|
/// middle 8-entry blocks. An involution.
|
||||||
|
pub fn clut_index(i: u32) -> u32 {
|
||||||
|
if ((i >> 3) & 1) != ((i >> 4) & 1) {
|
||||||
|
i ^ 0x18
|
||||||
|
} else {
|
||||||
|
i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PS2 alpha 0..=0x80 to 0..=0xff. `0x80` is opaque; above that clamps.
|
||||||
|
pub fn scale_alpha(a: u8) -> u8 {
|
||||||
|
if a < 0x80 {
|
||||||
|
a * 2
|
||||||
|
} else {
|
||||||
|
0xff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A decoded RGBA8 image.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Texture {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
/// `width * height * 4` bytes, row-major from the top.
|
||||||
|
pub rgba: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One stored image: palette indices plus the CLUT, exactly as on disc.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct IndexedImage<'a> {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub indices: &'a [u8],
|
||||||
|
/// 1024 bytes, 256 RGBA entries in CSM1 order.
|
||||||
|
pub clut: &'a [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IndexedImage<'_> {
|
||||||
|
/// Look every index up in the CLUT.
|
||||||
|
pub fn decode(&self) -> Result<Texture> {
|
||||||
|
decode_indexed8(self.indices, self.width, self.height, self.clut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode `width * height` palette indices against a 256-entry CLUT.
|
||||||
|
pub fn decode_indexed8(indices: &[u8], width: u32, height: u32, clut: &[u8]) -> Result<Texture> {
|
||||||
|
let n = width as usize * height as usize;
|
||||||
|
if indices.len() < n {
|
||||||
|
return Err(Error {
|
||||||
|
offset: 0,
|
||||||
|
context: "indexed texture data too small",
|
||||||
|
kind: ErrorKind::OutOfBounds {
|
||||||
|
needed: n,
|
||||||
|
available: indices.len(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if clut.len() < 1024 {
|
||||||
|
return Err(Error {
|
||||||
|
offset: 0,
|
||||||
|
context: "texture palette too small",
|
||||||
|
kind: ErrorKind::OutOfBounds {
|
||||||
|
needed: 1024,
|
||||||
|
available: clut.len(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let mut pal = [[0u8; 4]; 256];
|
||||||
|
for (i, p) in pal.iter_mut().enumerate() {
|
||||||
|
let at = clut_index(i as u32) as usize * 4;
|
||||||
|
let e = &clut[at..at + 4];
|
||||||
|
*p = [e[0], e[1], e[2], scale_alpha(e[3])];
|
||||||
|
}
|
||||||
|
let mut rgba = Vec::with_capacity(n * 4);
|
||||||
|
for &ix in &indices[..n] {
|
||||||
|
rgba.extend_from_slice(&pal[ix as usize]);
|
||||||
|
}
|
||||||
|
Ok(Texture {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
rgba,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The base level of a tfrag texture: `width * height` indices at `data_offset`.
|
||||||
|
pub fn base_image<'a>(
|
||||||
|
textures: &'a [u8],
|
||||||
|
gs_ram: &'a [u8],
|
||||||
|
e: &TextureEntry,
|
||||||
|
) -> Result<IndexedImage<'a>> {
|
||||||
|
let (px, clut) = pixels_and_clut(textures, gs_ram, e)?;
|
||||||
|
let (w, h) = (e.width as usize, e.height as usize);
|
||||||
|
Ok(IndexedImage {
|
||||||
|
width: w as u32,
|
||||||
|
height: h as u32,
|
||||||
|
indices: px,
|
||||||
|
clut,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every mip level of a tfrag texture, base level first.
|
||||||
|
///
|
||||||
|
/// All tfrag textures are square, so the levels halve evenly. Levels 0 and 1
|
||||||
|
/// live in the texture block; levels 2 and 3 are resident in GS RAM and are
|
||||||
|
/// addressed by the entry's `mipmap` and `pad` fields.
|
||||||
|
pub fn mip_images<'a>(
|
||||||
|
textures: &'a [u8],
|
||||||
|
gs_ram: &'a [u8],
|
||||||
|
e: &TextureEntry,
|
||||||
|
) -> Result<Vec<IndexedImage<'a>>> {
|
||||||
|
let (px0, clut) = pixels_and_clut(textures, gs_ram, e)?;
|
||||||
|
let (w, h) = (e.width as usize, e.height as usize);
|
||||||
|
let levels = e.ty as usize;
|
||||||
|
if !(1..=4).contains(&levels) {
|
||||||
|
return Err(Error {
|
||||||
|
offset: 0,
|
||||||
|
context: "tfrag texture level count outside 1..=4",
|
||||||
|
kind: ErrorKind::InvalidLength("tfrag texture level count"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let sub = |data: &'a [u8], at: usize, need: usize, what: &'static str| -> Result<&'a [u8]> {
|
||||||
|
let end = at
|
||||||
|
.checked_add(need)
|
||||||
|
.filter(|&e| e <= data.len())
|
||||||
|
.ok_or(Error {
|
||||||
|
offset: at,
|
||||||
|
context: what,
|
||||||
|
kind: ErrorKind::OutOfBounds {
|
||||||
|
needed: need,
|
||||||
|
available: data.len().saturating_sub(at),
|
||||||
|
},
|
||||||
|
})?;
|
||||||
|
Ok(&data[at..end])
|
||||||
|
};
|
||||||
|
let mut out = Vec::with_capacity(levels);
|
||||||
|
for level in 0..levels {
|
||||||
|
let (lw, lh) = (w >> level, h >> level);
|
||||||
|
if lw == 0 || lh == 0 {
|
||||||
|
return Err(Error {
|
||||||
|
offset: 0,
|
||||||
|
context: "mip level smaller than one texel",
|
||||||
|
kind: ErrorKind::InvalidLength("mip level size"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let need = lw * lh;
|
||||||
|
let indices = match level {
|
||||||
|
0 => px0,
|
||||||
|
1 => sub(
|
||||||
|
textures,
|
||||||
|
e.data_offset as usize + w * h,
|
||||||
|
need,
|
||||||
|
"mip 1 pixels",
|
||||||
|
)?,
|
||||||
|
2 if e.mipmap >= 0 => sub(gs_ram, e.mipmap as usize * 0x100, need, "mip 2 pixels")?,
|
||||||
|
3 if e.pad >= 0 => sub(gs_ram, e.pad as usize * 0x100, need, "mip 3 pixels")?,
|
||||||
|
_ => {
|
||||||
|
return Err(Error {
|
||||||
|
offset: 0,
|
||||||
|
context: "mip level without a gs_ram block",
|
||||||
|
kind: ErrorKind::InvalidLength("mip level"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
out.push(IndexedImage {
|
||||||
|
width: lw as u32,
|
||||||
|
height: lh as u32,
|
||||||
|
indices,
|
||||||
|
clut,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode the whole mip chain.
|
||||||
|
pub fn decode_tfrag_mips(textures: &[u8], gs_ram: &[u8], e: &TextureEntry) -> Result<Vec<Texture>> {
|
||||||
|
mip_images(textures, gs_ram, e)?
|
||||||
|
.iter()
|
||||||
|
.map(IndexedImage::decode)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pixels_and_clut<'a>(
|
||||||
|
textures: &'a [u8],
|
||||||
|
gs_ram: &'a [u8],
|
||||||
|
e: &TextureEntry,
|
||||||
|
) -> Result<(&'a [u8], &'a [u8])> {
|
||||||
|
if e.width <= 0 || e.height <= 0 || e.data_offset < 0 || e.palette < 0 {
|
||||||
|
return Err(Error {
|
||||||
|
offset: 0,
|
||||||
|
context: "texture entry has no pixels",
|
||||||
|
kind: ErrorKind::Invalid("texture entry has no pixels"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let need = e.width as usize * e.height as usize;
|
||||||
|
let end = (e.data_offset as usize)
|
||||||
|
.checked_add(need)
|
||||||
|
.filter(|&x| x <= textures.len())
|
||||||
|
.ok_or(Error {
|
||||||
|
offset: e.data_offset as usize,
|
||||||
|
context: "texture pixels run past the texture block",
|
||||||
|
kind: ErrorKind::OutOfBounds {
|
||||||
|
needed: need,
|
||||||
|
available: textures.len().saturating_sub(e.data_offset.max(0) as usize),
|
||||||
|
},
|
||||||
|
})?;
|
||||||
|
let clut_at = e.palette as usize * 0x100;
|
||||||
|
let clut_end = clut_at
|
||||||
|
.checked_add(1024)
|
||||||
|
.filter(|&x| x <= gs_ram.len())
|
||||||
|
.ok_or(Error {
|
||||||
|
offset: clut_at,
|
||||||
|
context: "texture palette runs past gs_ram",
|
||||||
|
kind: ErrorKind::OutOfBounds {
|
||||||
|
needed: 1024,
|
||||||
|
available: gs_ram.len().saturating_sub(clut_at),
|
||||||
|
},
|
||||||
|
})?;
|
||||||
|
Ok((
|
||||||
|
&textures[e.data_offset as usize..end],
|
||||||
|
&gs_ram[clut_at..clut_end],
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clut_order_swaps_the_middle_blocks() {
|
||||||
|
// Bits 3 and 4 differ inside a 32-entry group, so the remap applies.
|
||||||
|
assert_eq!(clut_index(0x00), 0x00);
|
||||||
|
assert_eq!(clut_index(0x08), 0x10);
|
||||||
|
assert_eq!(clut_index(0x10), 0x08);
|
||||||
|
assert_eq!(clut_index(0x1f), 0x1f);
|
||||||
|
// Outside that pattern the index is unchanged.
|
||||||
|
assert_eq!(clut_index(0x07), 0x07);
|
||||||
|
assert_eq!(clut_index(0x18), 0x18);
|
||||||
|
// The remap is an involution, so it round-trips.
|
||||||
|
for i in 0..256u32 {
|
||||||
|
assert_eq!(clut_index(clut_index(i)), i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alpha_is_scaled_from_eight_bit_halves() {
|
||||||
|
assert_eq!(scale_alpha(0), 0);
|
||||||
|
assert_eq!(scale_alpha(0x40), 0x80);
|
||||||
|
assert_eq!(scale_alpha(0x7f), 0xfe);
|
||||||
|
assert_eq!(scale_alpha(0x80), 0xff);
|
||||||
|
assert_eq!(scale_alpha(0xff), 0xff);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A 2x1 image whose two texels select distinct CLUT entries.
|
||||||
|
fn fixture() -> (Vec<u8>, Vec<u8>) {
|
||||||
|
let mut clut = vec![0u8; 1024];
|
||||||
|
for i in 0..256u32 {
|
||||||
|
let at = clut_index(i) as usize * 4;
|
||||||
|
clut[at] = i as u8;
|
||||||
|
clut[at + 1] = 255 - i as u8;
|
||||||
|
clut[at + 2] = 7;
|
||||||
|
clut[at + 3] = 0x80;
|
||||||
|
}
|
||||||
|
(vec![3, 200], clut)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decodes_indices_through_the_clut() {
|
||||||
|
let (idx, clut) = fixture();
|
||||||
|
let t = decode_indexed8(&idx, 2, 1, &clut).unwrap();
|
||||||
|
assert_eq!(t.rgba.len(), 8);
|
||||||
|
// Entry 3 is red, entry 200 has a flipped red and green.
|
||||||
|
assert_eq!(&t.rgba[0..4], &[3, 252, 7, 0xff]);
|
||||||
|
assert_eq!(&t.rgba[4..8], &[200, 55, 7, 0xff]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn short_data_is_an_error_not_a_panic() {
|
||||||
|
let (_idx, clut) = fixture();
|
||||||
|
assert!(decode_indexed8(&[0, 1], 4, 4, &clut).is_err());
|
||||||
|
assert!(decode_indexed8(&[0; 16], 2, 2, &clut[..512]).is_err());
|
||||||
|
assert!(decode_indexed8(&[0; 3], 2, 1, &clut).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_square_entry_halves_through_its_mip_chain() {
|
||||||
|
// 64x64 base, mip 1 immediately after it, mips 2 and 3 in gs_ram.
|
||||||
|
let mut textures = vec![0u8; 64 * 64 * 2];
|
||||||
|
for (i, b) in textures.iter_mut().enumerate() {
|
||||||
|
*b = (i % 251) as u8;
|
||||||
|
}
|
||||||
|
let mut gs = vec![0u8; 0x4000];
|
||||||
|
for (i, b) in gs.iter_mut().enumerate().take(0x3000) {
|
||||||
|
*b = (i % 253) as u8;
|
||||||
|
}
|
||||||
|
let e = TextureEntry {
|
||||||
|
data_offset: 0,
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
ty: 4,
|
||||||
|
palette: 8,
|
||||||
|
mipmap: 16,
|
||||||
|
pad: 24,
|
||||||
|
};
|
||||||
|
let mips = mip_images(&textures, &gs, &e).unwrap();
|
||||||
|
assert_eq!(mips.len(), 4);
|
||||||
|
let sizes: Vec<(u32, u32)> = mips.iter().map(|m| (m.width, m.height)).collect();
|
||||||
|
assert_eq!(sizes, [(64, 64), (32, 32), (16, 16), (8, 8)]);
|
||||||
|
// Level 0 is the base block; level 1 follows it immediately.
|
||||||
|
assert_eq!(mips[0].indices, &textures[0..64 * 64]);
|
||||||
|
assert_eq!(mips[1].indices, &textures[64 * 64..64 * 64 + 32 * 32]);
|
||||||
|
// Levels 2 and 3 come from gs_ram at `mipmap * 0x100` and `pad * 0x100`.
|
||||||
|
assert_eq!(mips[2].indices, &gs[16 * 0x100..16 * 0x100 + 16 * 16]);
|
||||||
|
assert_eq!(mips[3].indices, &gs[24 * 0x100..24 * 0x100 + 8 * 8]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_absent_high_mip_is_an_error() {
|
||||||
|
let textures = vec![0u8; 64 * 64];
|
||||||
|
let gs = vec![0u8; 0x4000];
|
||||||
|
let e = TextureEntry {
|
||||||
|
data_offset: 0,
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
ty: 4,
|
||||||
|
palette: 8,
|
||||||
|
mipmap: 16,
|
||||||
|
pad: -1,
|
||||||
|
};
|
||||||
|
assert!(mip_images(&textures, &gs, &e).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_palette_past_gs_ram_is_an_error() {
|
||||||
|
let textures = vec![0u8; 64 * 64];
|
||||||
|
let gs = vec![0u8; 0x100];
|
||||||
|
let e = TextureEntry {
|
||||||
|
data_offset: 0,
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
ty: 3,
|
||||||
|
palette: 900,
|
||||||
|
mipmap: -1,
|
||||||
|
pad: -1,
|
||||||
|
};
|
||||||
|
assert!(base_image(&textures, &gs, &e).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -393,9 +393,52 @@ data have been misread independently of each other. Across all 27 levels:
|
|||||||
1429 of the 1595 level texture entries (90%). Each patch's light array is
|
1429 of the 1595 level texture entries (90%). Each patch's light array is
|
||||||
exactly `vert_count` records and its RGBA array exactly `rgba_size * 4`.
|
exactly `vert_count` records and its RGBA array exactly `rgba_size * 4`.
|
||||||
|
|
||||||
|
### Texels: 8-bit indexed images and CSM1 palettes
|
||||||
|
|
||||||
|
Settled. Adds `texture.rs` and makes the pipeline write `gs_ram.bin`.
|
||||||
|
|
||||||
|
A level texture is not pixels, it is **palette indices**: `width * height`
|
||||||
|
bytes at `core_data[textures_base + data_offset]`. The colour comes from a
|
||||||
|
256-entry CLUT (1024 bytes) in GS RAM at `palette * 0x100`. `gs_ram` is a byte
|
||||||
|
range in the level's data extent header at offset 0x10 — 518,656 bytes on level
|
||||||
|
0 — and the extractor was not writing it out, so nothing could be decoded
|
||||||
|
before this.
|
||||||
|
|
||||||
|
Two PS2 conventions have to be honoured, and neither is a design choice:
|
||||||
|
|
||||||
|
* **CSM1 CLUT order.** Within each 32-entry group the two middle 8-entry
|
||||||
|
blocks are swapped: bits 3 and 4 of the index are exchanged. The remap is an
|
||||||
|
involution.
|
||||||
|
* **Alpha runs 0..=0x80, not 0..=255.** `0x80` is fully opaque and anything
|
||||||
|
above it clamps.
|
||||||
|
|
||||||
|
The mip chain splits across two buffers. Levels 0 and 1 are in the texture
|
||||||
|
block, with level 1 immediately after level 0 at `data_offset + w*h`; 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
|
||||||
|
|
||||||
|
Numbers alone do not settle this — every step is a table lookup, so a wrong
|
||||||
|
CLUT remap still yields a well-formed image of the wrong thing. Two layers:
|
||||||
|
|
||||||
|
* **Shape.** All 1,595 tfrag textures across all 27 levels decode; 96.8% are
|
||||||
|
fully opaque and 96.3% use more than eight distinct colours. A flat,
|
||||||
|
transparent or single-colour result would point at a CLUT or offset mistake.
|
||||||
|
* **Eyes.** `gc-extract dump-texture` writes any tfrag texture, or any of its
|
||||||
|
mip levels, as a binary PPM. Decoded level 0 texture 0 is a clean bevelled
|
||||||
|
metal panel with rivets and specular highlights; texture 74 is a vent grille
|
||||||
|
whose punched windows come back fully transparent. Both confirm the CLUT
|
||||||
|
remap, the alpha scale and the pixel base at once, which no assertion in the
|
||||||
|
suite does.
|
||||||
|
|
||||||
|
That tool is worth keeping: it is the cheapest way to tell a decode that is
|
||||||
|
merely well-formed from one that is right.
|
||||||
|
|
||||||
### Still open
|
### Still open
|
||||||
|
|
||||||
The LOD switch and morph rules in `TfragProc`, the texel data itself, and the
|
The LOD switch and morph rules in `TfragProc`, and the GS register values the
|
||||||
GS register values the level load builds from the texture entry. A tfrag can
|
level load builds from the texture entry (TBP0/TBP1 and so on) for a renderer
|
||||||
now be turned into positioned, UV-mapped, textured triangles; what is missing
|
that emulates the GS rather than resolving addresses itself. A tfrag can now be
|
||||||
is choosing which LOD to draw and decoding pixels.
|
turned into positioned, UV-mapped, textured triangles; what is missing is
|
||||||
|
choosing which LOD to draw.
|
||||||
|
|||||||
+8
-1
@@ -14,7 +14,7 @@ early, while they are still cheap to retire.
|
|||||||
- [x] Repository hygiene in place (pre-commit guard, `.gitignore`, licence
|
- [x] Repository hygiene in place (pre-commit guard, `.gitignore`, licence
|
||||||
record)
|
record)
|
||||||
- [x] **Phase 1 complete.** The disc pipeline runs end to end and is verified
|
- [x] **Phase 1 complete.** The disc pipeline runs end to end and is verified
|
||||||
against a committed hash table: 5,135 files, 4,500,106,960 bytes, every
|
against a committed hash table: 5,186 files, 5,685,541,143 bytes, every
|
||||||
one of the 92 asset containers parsing.
|
one of the 92 asset containers parsing.
|
||||||
- [x] **Phase 2 in progress.** Level containers are parsed through their typed
|
- [x] **Phase 2 in progress.** Level containers are parsed through their typed
|
||||||
header, and the `data` extent is parsed as a container of byte ranges. The
|
header, and the `data` extent is parsed as a container of byte ranges. The
|
||||||
@@ -134,6 +134,13 @@ Phase 1 hands over the extents; this phase works out what each one contains.
|
|||||||
in. Per-vertex RGBA and light/normal records recovered from their fixed
|
in. Per-vertex RGBA and light/normal records recovered from their fixed
|
||||||
offsets. All 27 levels: 47,950 ad-gifs, every index in range, 90% of the
|
offsets. All 27 levels: 47,950 ad-gifs, every index in range, 90% of the
|
||||||
level texture tables referenced.
|
level texture tables referenced.
|
||||||
|
- [x] **Level textures decode.** 8-bit indexed texels plus a 256-entry CSM1
|
||||||
|
CLUT from `gs_ram`, honouring the PS2 CLUT block swap and the 0x80 alpha
|
||||||
|
scale. The pipeline now writes `gs_ram.bin`, which it had been skipping.
|
||||||
|
All 1,595 tfrag textures on all 27 levels decode; 96.8% fully opaque and
|
||||||
|
96.3% with real detail. Verified visually via the new
|
||||||
|
`gc-extract dump-texture`, because a wrong lookup still yields a
|
||||||
|
well-formed image of the wrong thing.
|
||||||
- [ ] tfrag LOD selection: the switch and morph rules in `TfragProc`.
|
- [ ] tfrag LOD selection: the switch and morph rules in `TfragProc`.
|
||||||
- [ ] `gameplay` — begins `WAD` + a per-level byte, and does *not* decode with
|
- [ ] `gameplay` — begins `WAD` + a per-level byte, and does *not* decode with
|
||||||
the WAD LZ77 decoder. All 27 levels fail identically, so the input is
|
the WAD LZ77 decoder. All 27 levels fail identically, so the input is
|
||||||
|
|||||||
Reference in New Issue
Block a user