feat(viewer): ISO / extracted-dir loading pipeline + native file dialog
Some checks failed
CI / Native — ubuntu-latest (push) Failing after 7m14s
CI / WASM — Web (push) Failing after 6m35s
CI / Formatting (push) Failing after 35s
CI / Native — macos-latest (push) Has been cancelled
CI / Native — windows-latest (push) Has been cancelled

WIP: add iso_loader.rs — bridges the async XisoReader / synchronous
GameAssets to Bevy's ECS via background threads + mpsc channels polled
per frame (no main-thread blocking); UI open-iso/open-dir/file-select
events → texture preview. Register IsoLoaderPlugin; wire asset_loader
and the egui UI. Deps: rfd (native file dialog, workspace + viewer),
futures, and bevy tonemapping_luts feature.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-10 07:18:45 +02:00
parent ce9fe08bec
commit d92c7ddaea
7 changed files with 1111 additions and 61 deletions

489
Cargo.lock generated
View File

@@ -276,6 +276,24 @@ dependencies = [
"libloading", "libloading",
] ]
[[package]]
name = "ashpd"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd884d7c72877a94102c3715f3b1cd09ff4fac28221add3e57cfbe25c236d093"
dependencies = [
"async-fs",
"async-net",
"enumflags2",
"futures-channel",
"futures-util",
"rand",
"serde",
"serde_repr",
"url",
"zbus",
]
[[package]] [[package]]
name = "assert_type_match" name = "assert_type_match"
version = "0.1.1" version = "0.1.1"
@@ -297,6 +315,18 @@ dependencies = [
"futures-core", "futures-core",
] ]
[[package]]
name = "async-broadcast"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
dependencies = [
"event-listener 5.4.1",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]] [[package]]
name = "async-channel" name = "async-channel"
version = "2.5.0" version = "2.5.0"
@@ -334,6 +364,24 @@ dependencies = [
"futures-lite", "futures-lite",
] ]
[[package]]
name = "async-io"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
dependencies = [
"autocfg",
"cfg-if",
"concurrent-queue",
"futures-io",
"futures-lite",
"parking",
"polling",
"rustix 1.1.4",
"slab",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "async-lock" name = "async-lock"
version = "3.4.2" version = "3.4.2"
@@ -345,6 +393,64 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "async-net"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7"
dependencies = [
"async-io",
"blocking",
"futures-lite",
]
[[package]]
name = "async-process"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
dependencies = [
"async-channel",
"async-io",
"async-lock",
"async-signal",
"async-task",
"blocking",
"cfg-if",
"event-listener 5.4.1",
"futures-lite",
"rustix 1.1.4",
]
[[package]]
name = "async-recursion"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "async-signal"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c"
dependencies = [
"async-io",
"async-lock",
"atomic-waker",
"cfg-if",
"futures-core",
"futures-io",
"rustix 1.1.4",
"signal-hook-registry",
"slab",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "async-task" name = "async-task"
version = "4.7.1" version = "4.7.1"
@@ -438,7 +544,7 @@ version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73fd901b3be016088c4dda2f628bda96b7cb578b9bc8ae684bbf30bec0a9483e" checksum = "73fd901b3be016088c4dda2f628bda96b7cb578b9bc8ae684bbf30bec0a9483e"
dependencies = [ dependencies = [
"async-broadcast", "async-broadcast 0.5.1",
"async-fs", "async-fs",
"async-lock", "async-lock",
"atomicow", "atomicow",
@@ -719,6 +825,8 @@ dependencies = [
"derive_more", "derive_more",
"futures-lite", "futures-lite",
"image", "image",
"ktx2",
"ruzstd",
"serde", "serde",
"wgpu", "wgpu",
] ]
@@ -952,6 +1060,7 @@ dependencies = [
"futures-lite", "futures-lite",
"image", "image",
"js-sys", "js-sys",
"ktx2",
"naga", "naga",
"naga_oil", "naga_oil",
"nonmax", "nonmax",
@@ -1711,7 +1820,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
dependencies = [ dependencies = [
"dispatch2", "dispatch2",
"nix", "nix 0.31.2",
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
@@ -1907,6 +2016,33 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "endi"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
[[package]]
name = "enumflags2"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
dependencies = [
"enumflags2_derive",
"serde",
]
[[package]]
name = "enumflags2_derive"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "epaint" name = "epaint"
version = "0.29.1" version = "0.29.1"
@@ -2389,6 +2525,12 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "hexasphere" name = "hexasphere"
version = "15.1.0" version = "15.1.0"
@@ -2688,6 +2830,15 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "ktx2"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87d65e08a9ec02e409d27a0139eaa6b9756b4d81fe7cde71f6941a83730ce838"
dependencies = [
"bitflags 1.3.2",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@@ -2773,7 +2924,7 @@ version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a" checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a"
dependencies = [ dependencies = [
"twox-hash", "twox-hash 2.1.2",
] ]
[[package]] [[package]]
@@ -2820,6 +2971,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "metal" name = "metal"
version = "0.29.0" version = "0.29.0"
@@ -2953,6 +3113,19 @@ dependencies = [
"jni-sys 0.3.1", "jni-sys 0.3.1",
] ]
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.11.0",
"cfg-if",
"cfg_aliases 0.2.1",
"libc",
"memoffset",
]
[[package]] [[package]]
name = "nix" name = "nix"
version = "0.31.2" version = "0.31.2"
@@ -3043,6 +3216,17 @@ dependencies = [
"malloc_buf", "malloc_buf",
] ]
[[package]]
name = "objc-foundation"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
dependencies = [
"block",
"objc",
"objc_id",
]
[[package]] [[package]]
name = "objc-sys" name = "objc-sys"
version = "0.3.5" version = "0.3.5"
@@ -3313,6 +3497,15 @@ dependencies = [
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
] ]
[[package]]
name = "objc_id"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
dependencies = [
"objc",
]
[[package]] [[package]]
name = "offset-allocator" name = "offset-allocator"
version = "0.2.0" version = "0.2.0"
@@ -3345,6 +3538,16 @@ dependencies = [
"libredox", "libredox",
] ]
[[package]]
name = "ordered-stream"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
dependencies = [
"futures-core",
"pin-project-lite",
]
[[package]] [[package]]
name = "owned_ttf_parser" name = "owned_ttf_parser"
version = "0.25.1" version = "0.25.1"
@@ -3487,6 +3690,12 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "pollster"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2"
[[package]] [[package]]
name = "pori" name = "pori"
version = "0.0.0" version = "0.0.0"
@@ -3754,6 +3963,29 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
[[package]]
name = "rfd"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25a73a7337fc24366edfca76ec521f51877b114e42dab584008209cca6719251"
dependencies = [
"ashpd",
"block",
"dispatch",
"js-sys",
"log",
"objc",
"objc-foundation",
"objc_id",
"pollster",
"raw-window-handle",
"urlencoding",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "ron" name = "ron"
version = "0.8.1" version = "0.8.1"
@@ -3813,6 +4045,15 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ruzstd"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fad02996bfc73da3e301efe90b1837be9ed8f4a462b6ed410aa35d00381de89f"
dependencies = [
"twox-hash 1.6.3",
]
[[package]] [[package]]
name = "same-file" name = "same-file"
version = "1.0.6" version = "1.0.6"
@@ -3911,6 +4152,28 @@ dependencies = [
"zmij", "zmij",
] ]
[[package]]
name = "serde_repr"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]] [[package]]
name = "sha3" name = "sha3"
version = "0.10.8" version = "0.10.8"
@@ -4095,6 +4358,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"binrw", "binrw",
"flate2",
"futures", "futures",
"serde", "serde",
"serde_json", "serde_json",
@@ -4110,6 +4374,8 @@ version = "0.1.0"
dependencies = [ dependencies = [
"bevy", "bevy",
"bevy_egui", "bevy_egui",
"futures",
"rfd",
"sylpheed-formats", "sylpheed-formats",
"thiserror 2.0.18", "thiserror 2.0.18",
"tracing", "tracing",
@@ -4149,6 +4415,19 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.3.4",
"once_cell",
"rustix 1.1.4",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "termcolor" name = "termcolor"
version = "1.4.1" version = "1.4.1"
@@ -4448,6 +4727,16 @@ version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
[[package]]
name = "twox-hash"
version = "1.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
dependencies = [
"cfg-if",
"static_assertions",
]
[[package]] [[package]]
name = "twox-hash" name = "twox-hash"
version = "2.1.2" version = "2.1.2"
@@ -4472,6 +4761,17 @@ version = "1.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c1ae7cc0fdb8b842d65d127cb981574b0d2b249b74d1c7a2986863dc134f71" checksum = "f8c1ae7cc0fdb8b842d65d127cb981574b0d2b249b74d1c7a2986863dc134f71"
[[package]]
name = "uds_windows"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset",
"tempfile",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.24" version = "1.0.24"
@@ -4512,8 +4812,15 @@ dependencies = [
"idna", "idna",
"percent-encoding", "percent-encoding",
"serde", "serde",
"serde_derive",
] ]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]] [[package]]
name = "utf8_iter" name = "utf8_iter"
version = "1.0.4" version = "1.0.4"
@@ -4982,6 +5289,15 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"
@@ -5018,6 +5334,21 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.52.6" version = "0.52.6"
@@ -5051,6 +5382,12 @@ dependencies = [
"windows_x86_64_msvc 0.53.1", "windows_x86_64_msvc 0.53.1",
] ]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.52.6" version = "0.52.6"
@@ -5063,6 +5400,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.52.6" version = "0.52.6"
@@ -5075,6 +5418,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.52.6" version = "0.52.6"
@@ -5099,6 +5448,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.52.6" version = "0.52.6"
@@ -5111,6 +5466,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.52.6" version = "0.52.6"
@@ -5123,6 +5484,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.52.6" version = "0.52.6"
@@ -5135,6 +5502,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.52.6" version = "0.52.6"
@@ -5267,6 +5640,16 @@ version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b"
[[package]]
name = "xdg-home"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "xdvdfs" name = "xdvdfs"
version = "0.8.3" version = "0.8.3"
@@ -5335,6 +5718,68 @@ dependencies = [
"synstructure", "synstructure",
] ]
[[package]]
name = "zbus"
version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725"
dependencies = [
"async-broadcast 0.7.2",
"async-executor",
"async-fs",
"async-io",
"async-lock",
"async-process",
"async-recursion",
"async-task",
"async-trait",
"blocking",
"enumflags2",
"event-listener 5.4.1",
"futures-core",
"futures-sink",
"futures-util",
"hex",
"nix 0.29.0",
"ordered-stream",
"rand",
"serde",
"serde_repr",
"sha1",
"static_assertions",
"tracing",
"uds_windows",
"windows-sys 0.52.0",
"xdg-home",
"zbus_macros",
"zbus_names",
"zvariant",
]
[[package]]
name = "zbus_macros"
version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.117",
"zvariant_utils",
]
[[package]]
name = "zbus_names"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c"
dependencies = [
"serde",
"static_assertions",
"zvariant",
]
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.47" version = "0.8.47"
@@ -5429,3 +5874,41 @@ checksum = "0b7a1c0af6e5d8d1363f4994b7a091ccf963d8b694f7da5b0b9cceb82da2c0a6"
dependencies = [ dependencies = [
"zune-core", "zune-core",
] ]
[[package]]
name = "zvariant"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe"
dependencies = [
"endi",
"enumflags2",
"serde",
"static_assertions",
"url",
"zvariant_derive",
]
[[package]]
name = "zvariant_derive"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.117",
"zvariant_utils",
]
[[package]]
name = "zvariant_utils"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]

View File

@@ -39,6 +39,9 @@ anyhow = "1"
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# ── Native file dialog ────────────────────────────────────────────────────────
rfd = "0.14"
# ── CLI ─────────────────────────────────────────────────────────────────────── # ── CLI ───────────────────────────────────────────────────────────────────────
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }

View File

@@ -31,11 +31,14 @@ bevy = { workspace = true, features = [
"bevy_winit", "bevy_winit",
"multi_threaded", "multi_threaded",
"png", "png",
# Required for TonyMcMapFace tonemapping (default Camera3d tonemapper)
"tonemapping_luts",
# Linux display backends # Linux display backends
"x11", "x11",
"wayland", "wayland",
] } ] }
bevy_egui = { workspace = true } bevy_egui = { workspace = true }
futures = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
@@ -46,3 +49,4 @@ dev = ["bevy/dynamic_linking"]
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tracing-subscriber = { workspace = true } tracing-subscriber = { workspace = true }
rfd = { workspace = true }

View File

@@ -12,10 +12,11 @@
//! 4. Bevy uploads it to the GPU automatically //! 4. Bevy uploads it to the GPU automatically
use bevy::asset::io::Reader; use bevy::asset::io::Reader;
use bevy::asset::{AssetLoader, AsyncReadExt, LoadContext}; use bevy::asset::{AssetLoader, LoadContext};
use bevy::image::ImageSampler;
use bevy::prelude::*; use bevy::prelude::*;
use bevy::render::render_asset::RenderAssetUsages; use bevy::render::render_asset::RenderAssetUsages;
use bevy::render::render_resource::{Extent3d, TextureDimension, TextureFormat}; use bevy::render::render_resource::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages};
use sylpheed_formats::texture::{X360Texture, X360TextureFormat}; use sylpheed_formats::texture::{X360Texture, X360TextureFormat};
// ── Plugin ──────────────────────────────────────────────────────────────────── // ── Plugin ────────────────────────────────────────────────────────────────────
@@ -86,24 +87,32 @@ impl AssetLoader for Xpr2TextureLoader {
/// Convert a decoded `X360Texture` into a Bevy-compatible `Image`. /// Convert a decoded `X360Texture` into a Bevy-compatible `Image`.
/// ///
/// Maps Xbox 360 D3DFORMAT codes to `wgpu::TextureFormat` values. /// Constructs the `Image` struct directly rather than using `Image::new()`,
/// Bevy uses wgpu internally, so this mapping is direct. /// 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> { pub fn x360_texture_to_bevy_image(tex: X360Texture) -> Result<Image, Xpr2LoadError> {
let bevy_format = x360_format_to_wgpu(&tex.format)?; let format = x360_format_to_wgpu(&tex.format)?;
Ok(Image::new( Ok(Image {
Extent3d { data: tex.data,
width: tex.width, texture_descriptor: TextureDescriptor {
height: tex.height, label: None,
depth_or_array_layers: 1, 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: &[],
}, },
TextureDimension::D2, sampler: ImageSampler::Default,
tex.data, texture_view_descriptor: None,
bevy_format, asset_usage: RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
// 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`. /// Map an `X360TextureFormat` to the corresponding `wgpu::TextureFormat`.
@@ -119,14 +128,12 @@ fn x360_format_to_wgpu(format: &X360TextureFormat) -> Result<TextureFormat, Xpr2
X360TextureFormat::Dxt3 => TextureFormat::Bc2RgbaUnormSrgb, X360TextureFormat::Dxt3 => TextureFormat::Bc2RgbaUnormSrgb,
// DXT5 / BC3 // DXT5 / BC3
X360TextureFormat::Dxt5 => TextureFormat::Bc3RgbaUnormSrgb, X360TextureFormat::Dxt5 => TextureFormat::Bc3RgbaUnormSrgb,
// DXN / BC5 — normal maps (RG, not sRGB) // DXN / BC5 — two-channel normal maps (RG, not sRGB)
X360TextureFormat::Dxn => TextureFormat::Bc5RgUnorm, X360TextureFormat::Dxn => TextureFormat::Bc5RgUnorm,
// Uncompressed ARGB — note Xbox 360 is BGRA order (big-endian) // DXT5A / BC4 — single-channel (gloss, specular, luminance maps)
// We may need to swizzle R and B channels X360TextureFormat::Dxt5A => TextureFormat::Bc4RUnorm,
// Uncompressed ARGB — Xbox 360 stores as BGRA in memory
X360TextureFormat::A8R8G8B8 | X360TextureFormat::X8R8G8B8 => { 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 TextureFormat::Bgra8UnormSrgb
} }
}) })

View File

@@ -0,0 +1,458 @@
//! ISO / extracted-directory loading pipeline for the viewer.
//!
//! Bridges the async `XisoReader` API (and synchronous `GameAssets`) with
//! Bevy's ECS via background threads and `std::sync::mpsc` channels polled
//! each frame — no blocking on the main thread.
//!
//! ## Data flow (native)
//!
//! ```text
//! UI click → Event → handle_open_iso / handle_open_dir / handle_file_selected
//! └─ std::thread ─→ mpsc::Sender<IsoLoaderMsg>
//! ↓ (next frame)
//! poll_loader_channel → FileBrowserState / PendingFileBytes
//! ↓
//! apply_loaded_texture → TexturePreview (Image + egui TextureId)
//! ↓
//! draw_viewer_ui → renders preview panel
//! ```
#[cfg(not(target_arch = "wasm32"))]
use std::sync::{mpsc, Mutex};
use std::path::PathBuf;
use bevy::prelude::*;
use bevy_egui::egui;
use sylpheed_formats::vfs::FileFormat;
use crate::ui::FileBrowserState;
// ── Events ────────────────────────────────────────────────────────────────────
/// Fired by the "Open ISO disc image…" menu item.
#[derive(Event, Default)]
pub struct RequestOpenIso;
/// Fired by the "Open extracted folder…" menu item.
#[derive(Event, Default)]
pub struct RequestOpenDir;
/// Fired when the user clicks a file in the browser panel.
#[derive(Event)]
pub struct FileSelected(pub String);
// ── Resources (platform-agnostic) ────────────────────────────────────────────
/// Which kind of source is currently open.
#[derive(Default, Clone)]
pub enum SourceKind {
#[default]
None,
#[cfg(not(target_arch = "wasm32"))]
Iso(PathBuf),
#[cfg(not(target_arch = "wasm32"))]
Directory(PathBuf),
}
/// State of the currently-open file source (ISO or extracted folder).
#[derive(Resource, Default)]
pub struct IsoState {
/// Short description shown in the status bar (filename or directory path).
pub source_label: Option<String>,
/// True while a background thread is loading.
pub loading: bool,
/// The most recent error message (cleared when a new operation starts).
pub error: Option<String>,
/// Which backend to use when reading individual files.
pub source_kind: SourceKind,
}
/// The texture currently shown in the central panel.
#[derive(Resource, Default)]
pub struct TexturePreview {
/// Bevy handle — kept alive so the GPU texture stays allocated.
pub handle: Option<Handle<Image>>,
/// egui texture ID registered via `EguiContexts::add_image`.
pub egui_id: Option<egui::TextureId>,
pub width: u32,
pub height: u32,
/// Human-readable format / dimension summary (or error message).
pub format_info: String,
}
/// Info shown for any selected file (texture or not).
#[derive(Resource, Default)]
pub struct FileInfo {
pub name: String,
pub size_bytes: usize,
pub detected_format: Option<FileFormat>,
}
// ── SystemSet label ───────────────────────────────────────────────────────────
/// `draw_viewer_ui` runs `.after(IsoLoaderSystemSet)` to see the frame's
/// final state before rendering.
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub struct IsoLoaderSystemSet;
// ── Native-only types ─────────────────────────────────────────────────────────
/// Messages sent from background threads back to the Bevy main thread.
#[cfg(not(target_arch = "wasm32"))]
enum IsoLoaderMsg {
FilesListed {
source: SourceKind,
label: String,
files: Vec<String>,
},
FileLoaded {
path: String,
bytes: Vec<u8>,
},
/// User dismissed the file dialog — not an error.
Cancelled,
Error(String),
}
/// Channel resource. `Receiver<T>` is `!Sync`, so we wrap it in `Mutex`
/// to satisfy Bevy's `Resource: Send + Sync` bound.
#[cfg(not(target_arch = "wasm32"))]
#[derive(Resource)]
struct IsoChannels {
sender: mpsc::Sender<IsoLoaderMsg>,
receiver: Mutex<mpsc::Receiver<IsoLoaderMsg>>,
}
#[cfg(not(target_arch = "wasm32"))]
impl Default for IsoChannels {
fn default() -> Self {
let (sender, receiver) = mpsc::channel();
Self {
sender,
receiver: Mutex::new(receiver),
}
}
}
/// One-frame staging buffer: `poll_loader_channel` deposits raw bytes here;
/// `apply_loaded_texture` (chained after) consumes them.
#[cfg(not(target_arch = "wasm32"))]
#[derive(Resource, Default)]
struct PendingFileBytes {
ready: bool,
path: String,
bytes: Vec<u8>,
}
// ── Plugin ────────────────────────────────────────────────────────────────────
pub struct IsoLoaderPlugin;
impl Plugin for IsoLoaderPlugin {
fn build(&self, app: &mut App) {
app.add_event::<RequestOpenIso>()
.add_event::<RequestOpenDir>()
.add_event::<FileSelected>()
.init_resource::<IsoState>()
.init_resource::<TexturePreview>()
.init_resource::<FileInfo>();
#[cfg(not(target_arch = "wasm32"))]
{
app.init_resource::<IsoChannels>()
.init_resource::<PendingFileBytes>()
.add_systems(
Update,
(
handle_open_iso,
handle_open_dir,
handle_file_selected,
poll_loader_channel,
apply_loaded_texture,
)
.chain()
.in_set(IsoLoaderSystemSet),
);
}
}
}
// ── Systems (native-only) ─────────────────────────────────────────────────────
/// Listens for `RequestOpenIso`, opens a native file-picker in a background
/// thread, then lists all files in the selected ISO.
#[cfg(not(target_arch = "wasm32"))]
fn handle_open_iso(
mut events: EventReader<RequestOpenIso>,
mut iso_state: ResMut<IsoState>,
channels: Res<IsoChannels>,
) {
let count = events.read().count();
if count == 0 || iso_state.loading {
return;
}
iso_state.loading = true;
iso_state.error = None;
let sender = channels.sender.clone();
std::thread::spawn(move || {
let path = rfd::FileDialog::new()
.set_title("Open Xbox 360 Disc Image")
.add_filter("Xbox 360 ISO", &["iso", "xiso"])
.pick_file();
let Some(path) = path else {
let _ = sender.send(IsoLoaderMsg::Cancelled);
return;
};
let label = path.display().to_string();
let iso_path = path.clone();
let result = futures::executor::block_on(async move {
let mut reader = sylpheed_formats::xiso::open_iso(&path)
.await
.map_err(|e| e.to_string())?;
let files = reader
.list_all_files()
.await
.map_err(|e| e.to_string())?;
Ok::<_, String>((iso_path, label, files))
});
match result {
Ok((iso_path, label, files)) => {
let _ = sender.send(IsoLoaderMsg::FilesListed {
source: SourceKind::Iso(iso_path),
label,
files,
});
}
Err(e) => {
let _ = sender.send(IsoLoaderMsg::Error(e));
}
}
});
}
/// Listens for `RequestOpenDir`, opens a folder picker, lists files via
/// `GameAssets` (synchronous).
#[cfg(not(target_arch = "wasm32"))]
fn handle_open_dir(
mut events: EventReader<RequestOpenDir>,
mut iso_state: ResMut<IsoState>,
channels: Res<IsoChannels>,
) {
let count = events.read().count();
if count == 0 || iso_state.loading {
return;
}
iso_state.loading = true;
iso_state.error = None;
let sender = channels.sender.clone();
std::thread::spawn(move || {
let path = rfd::FileDialog::new()
.set_title("Open Extracted Game Directory")
.pick_folder();
let Some(path) = path else {
let _ = sender.send(IsoLoaderMsg::Cancelled);
return;
};
let label = path.display().to_string();
let assets = sylpheed_formats::vfs::GameAssets::from_directory(&path);
match assets.list("") {
Ok(files) => {
let _ = sender.send(IsoLoaderMsg::FilesListed {
source: SourceKind::Directory(path),
label,
files,
});
}
Err(e) => {
let _ = sender.send(IsoLoaderMsg::Error(e.to_string()));
}
}
});
}
/// Listens for `FileSelected`, reads the file from the active source,
/// sends bytes back via the channel.
#[cfg(not(target_arch = "wasm32"))]
fn handle_file_selected(
mut events: EventReader<FileSelected>,
iso_state: Res<IsoState>,
channels: Res<IsoChannels>,
) {
for FileSelected(file_path) in events.read() {
let file_path = file_path.clone();
let sender = channels.sender.clone();
match &iso_state.source_kind {
SourceKind::Iso(iso_path) => {
let iso_path = iso_path.clone();
std::thread::spawn(move || {
let result = futures::executor::block_on(async {
let mut reader =
sylpheed_formats::xiso::open_iso(&iso_path)
.await
.map_err(|e| e.to_string())?;
let bytes = reader
.read_file(&file_path)
.await
.map_err(|e| e.to_string())?;
Ok::<_, String>((file_path, bytes))
});
match result {
Ok((path, bytes)) => {
let _ = sender
.send(IsoLoaderMsg::FileLoaded { path, bytes });
}
Err(e) => {
let _ = sender.send(IsoLoaderMsg::Error(e));
}
}
});
}
SourceKind::Directory(root) => {
let assets =
sylpheed_formats::vfs::GameAssets::from_directory(root);
match assets.read(&file_path) {
Ok(bytes) => {
let _ = sender.send(IsoLoaderMsg::FileLoaded {
path: file_path,
bytes,
});
}
Err(e) => {
let _ = sender.send(IsoLoaderMsg::Error(e.to_string()));
}
}
}
SourceKind::None => {}
}
}
}
/// Polls the mpsc channel, updating `IsoState`, `FileBrowserState`, and
/// `PendingFileBytes` as messages arrive.
#[cfg(not(target_arch = "wasm32"))]
fn poll_loader_channel(
channels: Res<IsoChannels>,
mut iso_state: ResMut<IsoState>,
mut browser: ResMut<FileBrowserState>,
mut pending: ResMut<PendingFileBytes>,
) {
let receiver = channels.receiver.lock().unwrap();
loop {
use std::sync::mpsc::TryRecvError;
match receiver.try_recv() {
Ok(IsoLoaderMsg::FilesListed {
source,
label,
files,
}) => {
iso_state.loading = false;
iso_state.error = None;
iso_state.source_label = Some(label);
iso_state.source_kind = source;
browser.files = files;
browser.selected = None;
browser.filter.clear();
info!("Loaded {} files", browser.files.len());
}
Ok(IsoLoaderMsg::FileLoaded { path, bytes }) => {
pending.path = path;
pending.bytes = bytes;
pending.ready = true;
}
Ok(IsoLoaderMsg::Cancelled) => {
iso_state.loading = false;
}
Ok(IsoLoaderMsg::Error(msg)) => {
error!("ISO loader: {}", msg);
iso_state.loading = false;
iso_state.error = Some(msg);
}
Err(TryRecvError::Empty) | Err(TryRecvError::Disconnected) => break,
}
}
}
/// Converts pending raw bytes into a Bevy `Image`, registers it with egui,
/// and populates `TexturePreview` / `FileInfo`.
#[cfg(not(target_arch = "wasm32"))]
fn apply_loaded_texture(
mut pending: ResMut<PendingFileBytes>,
mut preview: ResMut<TexturePreview>,
mut file_info: ResMut<FileInfo>,
mut images: ResMut<Assets<Image>>,
mut contexts: bevy_egui::EguiContexts,
) {
if !pending.ready {
return;
}
pending.ready = false;
let bytes = std::mem::take(&mut pending.bytes);
let path = std::mem::take(&mut pending.path);
let fmt = sylpheed_formats::vfs::identify_format(&bytes);
// Always free the previous texture first (GPU memory + egui slot).
if let Some(ref handle) = preview.handle {
contexts.remove_image(handle);
images.remove(handle.id());
}
*preview = TexturePreview::default();
// Populate FileInfo for the status / info panel.
file_info.name = path
.split('/')
.next_back()
.unwrap_or(&path)
.to_string();
file_info.size_bytes = bytes.len();
file_info.detected_format = Some(fmt);
if fmt != FileFormat::Xpr2Texture {
return; // Not a texture — FileInfo is sufficient
}
let tex = match sylpheed_formats::X360Texture::from_xpr2(&bytes) {
Ok(t) => t,
Err(e) => {
preview.format_info = format!("XPR2 parse failed: {e}");
return;
}
};
let w = tex.width;
let h = tex.height;
let mips = tex.mip_levels;
let fmt_str = format!("{:?} {}×{} {} mip(s)", tex.format, w, h, mips);
let image = match crate::asset_loader::x360_texture_to_bevy_image(tex) {
Ok(img) => img,
Err(e) => {
preview.format_info = format!("Bevy image conversion failed: {e}");
return;
}
};
let handle = images.add(image);
let egui_id = contexts.add_image(handle.clone_weak());
preview.handle = Some(handle);
preview.egui_id = Some(egui_id);
preview.width = w;
preview.height = h;
preview.format_info = fmt_str;
}

View File

@@ -20,6 +20,7 @@ use bevy_egui::EguiPlugin;
pub mod asset_loader; pub mod asset_loader;
pub mod camera; pub mod camera;
pub mod iso_loader;
pub mod ui; pub mod ui;
// ── Application state ──────────────────────────────────────────────────────── // ── Application state ────────────────────────────────────────────────────────
@@ -75,6 +76,7 @@ pub fn run() {
app.add_plugins(EguiPlugin); app.add_plugins(EguiPlugin);
app.add_plugins(asset_loader::SylpheedAssetPlugin); app.add_plugins(asset_loader::SylpheedAssetPlugin);
app.add_plugins(iso_loader::IsoLoaderPlugin);
app.add_plugins(camera::OrbitCameraPlugin); app.add_plugins(camera::OrbitCameraPlugin);
app.add_plugins(ui::ViewerUiPlugin); app.add_plugins(ui::ViewerUiPlugin);

View File

@@ -1,21 +1,27 @@
//! egui-based debug UI for the asset viewer. //! egui-based debug UI for the asset viewer.
//! //!
//! Provides panels for: //! Provides panels for:
//! - File browser (list extracted game files) //! - File browser (list ISO / extracted game files, filter by name)
//! - Texture inspector (preview + format info) //! - Texture inspector (preview + format info)
//! - Mesh inspector (vertex/index counts, materials) //! - File info (size, detected format for non-texture files)
//! - RE notes (track discoveries during reverse engineering) //! - RE notes (track discoveries during reverse engineering)
use bevy::prelude::*; use bevy::prelude::*;
use bevy_egui::{egui, EguiContexts}; use bevy_egui::{egui, EguiContexts};
use crate::iso_loader::{
FileInfo, FileSelected, IsoState, RequestOpenDir, RequestOpenIso, TexturePreview,
IsoLoaderSystemSet,
};
use crate::{ViewerState, ViewMode}; use crate::{ViewerState, ViewMode};
pub struct ViewerUiPlugin; pub struct ViewerUiPlugin;
impl Plugin for ViewerUiPlugin { impl Plugin for ViewerUiPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_systems(Update, draw_viewer_ui);
app.insert_resource(FileBrowserState::default()); app.insert_resource(FileBrowserState::default());
// Run after the iso_loader chain so we see the frame's final state.
app.add_systems(Update, draw_viewer_ui.after(IsoLoaderSystemSet));
} }
} }
@@ -31,7 +37,12 @@ fn draw_viewer_ui(
mut contexts: EguiContexts, mut contexts: EguiContexts,
mut viewer: ResMut<ViewerState>, mut viewer: ResMut<ViewerState>,
mut browser: ResMut<FileBrowserState>, mut browser: ResMut<FileBrowserState>,
asset_server: Res<AssetServer>, iso_state: Res<IsoState>,
preview: Res<TexturePreview>,
file_info: Res<FileInfo>,
mut open_iso_events: EventWriter<RequestOpenIso>,
mut open_dir_events: EventWriter<RequestOpenDir>,
mut file_selected_events: EventWriter<FileSelected>,
) { ) {
let ctx = contexts.ctx_mut(); let ctx = contexts.ctx_mut();
@@ -39,26 +50,45 @@ fn draw_viewer_ui(
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
egui::menu::bar(ui, |ui| { egui::menu::bar(ui, |ui| {
ui.menu_button("File", |ui| { ui.menu_button("File", |ui| {
if ui.button("Open extracted directory...").clicked() {
// TODO: file dialog (rfd crate on native, not available on WASM)
info!("TODO: open directory dialog");
ui.close_menu();
}
ui.separator();
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
if ui.button("Quit").clicked() { {
std::process::exit(0); if ui.button("Open ISO disc image…").clicked() {
open_iso_events.send_default();
ui.close_menu();
}
if ui.button("Open extracted folder…").clicked() {
open_dir_events.send_default();
ui.close_menu();
}
ui.separator();
if ui.button("Quit").clicked() {
std::process::exit(0);
}
} }
#[cfg(target_arch = "wasm32")]
ui.colored_label(
egui::Color32::GRAY,
"File loading not available in browser",
);
}); });
ui.menu_button("View", |ui| { ui.menu_button("View", |ui| {
if ui.selectable_label(viewer.current_mode == ViewMode::Textures, "Textures").clicked() { if ui
.selectable_label(viewer.current_mode == ViewMode::Textures, "Textures")
.clicked()
{
viewer.current_mode = ViewMode::Textures; viewer.current_mode = ViewMode::Textures;
} }
if ui.selectable_label(viewer.current_mode == ViewMode::Mesh, "Meshes").clicked() { if ui
.selectable_label(viewer.current_mode == ViewMode::Mesh, "Meshes")
.clicked()
{
viewer.current_mode = ViewMode::Mesh; viewer.current_mode = ViewMode::Mesh;
} }
if ui.selectable_label(viewer.current_mode == ViewMode::Audio, "Audio").clicked() { if ui
.selectable_label(viewer.current_mode == ViewMode::Audio, "Audio")
.clicked()
{
viewer.current_mode = ViewMode::Audio; viewer.current_mode = ViewMode::Audio;
} }
ui.separator(); ui.separator();
@@ -66,7 +96,7 @@ fn draw_viewer_ui(
}); });
ui.menu_button("Help", |ui| { ui.menu_button("Help", |ui| {
if ui.button("Controls...").clicked() { if ui.button("Controls").clicked() {
// TODO: controls popup // TODO: controls popup
} }
if ui.button("About").clicked() { if ui.button("About").clicked() {
@@ -93,29 +123,39 @@ fn draw_viewer_ui(
// File list // File list
egui::ScrollArea::vertical().show(ui, |ui| { egui::ScrollArea::vertical().show(ui, |ui| {
if browser.files.is_empty() { if iso_state.loading {
ui.spinner();
ui.label("Loading…");
} else if let Some(err) = &iso_state.error {
ui.colored_label(
egui::Color32::RED,
format!("⚠ Error:\n{}", err),
);
} else if browser.files.is_empty() {
ui.colored_label( ui.colored_label(
egui::Color32::YELLOW, egui::Color32::YELLOW,
"No files loaded.\nExtract your ISO first:\n\ "No files loaded.\nUse File → Open ISO disc image\n\
xdvdfs unpack game.iso ./assets/", or File → Open extracted folder.",
); );
} else { } else {
let filter = browser.filter.to_lowercase(); let filter = browser.filter.to_lowercase();
// Collect owned strings so we can later mutate `browser.selected` let filtered: Vec<(usize, String)> = browser
let filtered: Vec<(usize, String)> = browser.files .files
.iter() .iter()
.enumerate() .enumerate()
.filter(|(_, f)| filter.is_empty() || f.to_lowercase().contains(&filter)) .filter(|(_, f)| {
filter.is_empty() || f.to_lowercase().contains(&filter)
})
.map(|(i, f)| (i, f.clone())) .map(|(i, f)| (i, f.clone()))
.collect(); .collect();
for (idx, file) in &filtered { for (idx, file) in &filtered {
let is_selected = browser.selected == Some(*idx); let is_selected = browser.selected == Some(*idx);
let label = egui::SelectableLabel::new(is_selected, file.as_str()); let label =
egui::SelectableLabel::new(is_selected, file.as_str());
if ui.add(label).clicked() { if ui.add(label).clicked() {
browser.selected = Some(*idx); browser.selected = Some(*idx);
info!("Selected file: {}", file); file_selected_events.send(FileSelected(file.clone()));
// TODO: load the selected file into the viewer
} }
} }
} }
@@ -125,31 +165,84 @@ fn draw_viewer_ui(
// ── Bottom panel: status bar ────────────────────────────────────────── // ── Bottom panel: status bar ──────────────────────────────────────────
egui::TopBottomPanel::bottom("status_bar").show(ctx, |ui| { egui::TopBottomPanel::bottom("status_bar").show(ctx, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
if let Some(label) = &iso_state.source_label {
ui.label(label);
ui.separator();
}
match viewer.current_mode { match viewer.current_mode {
ViewMode::Textures => ui.label("Mode: Texture Viewer"), ViewMode::Textures => ui.label("Mode: Texture Viewer"),
ViewMode::Mesh => ui.label("Mode: Mesh Viewer"), ViewMode::Mesh => ui.label("Mode: Mesh Viewer"),
ViewMode::Audio => ui.label("Mode: Audio Viewer"), ViewMode::Audio => ui.label("Mode: Audio Viewer"),
}; };
ui.separator(); ui.separator();
ui.label(format!("{} files loaded", browser.files.len())); ui.label(format!("{} files", browser.files.len()));
ui.separator(); ui.separator();
ui.label("LMB drag: orbit | RMB drag: pan | Scroll: zoom | R: reset"); ui.label("LMB: orbit | RMB: pan | Scroll: zoom | R: reset");
}); });
}); });
// ── Central area: RE notes (shown when no asset is selected) ────────── // ── Central area ──────────────────────────────────────────────────────
if browser.selected.is_none() { if browser.selected.is_some() {
egui::CentralPanel::default().show(ctx, |ui| {
if let Some(egui_id) = preview.egui_id {
// Texture preview
ui.heading(&file_info.name);
ui.label(&preview.format_info);
if !preview.format_info.is_empty() && preview.format_info.contains("failed") {
ui.colored_label(egui::Color32::YELLOW, &preview.format_info);
}
ui.separator();
// Scale to fit, preserving aspect ratio, never upscale
let avail = ui.available_size();
let scale = if preview.width == 0 || preview.height == 0 {
1.0_f32
} else {
(avail.x / preview.width as f32)
.min(avail.y / preview.height as f32)
.min(1.0)
};
let display_w = preview.width as f32 * scale;
let display_h = preview.height as f32 * scale;
ui.add(egui::Image::new(egui::load::SizedTexture::new(
egui_id,
[display_w, display_h],
)));
} else {
// Non-texture file info
ui.heading(&file_info.name);
if !preview.format_info.is_empty() {
ui.colored_label(egui::Color32::YELLOW, &preview.format_info);
}
if file_info.size_bytes > 0 {
ui.separator();
ui.label(format!("Size: {} bytes", file_info.size_bytes));
if let Some(fmt) = file_info.detected_format {
ui.label(format!("Detected format: {:?}", fmt));
}
} else if iso_state.loading {
ui.spinner();
ui.label("Loading file…");
}
}
});
} else {
// No file selected — show welcome / RE notes
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Project Sylpheed: Arc of Deception"); ui.heading("Project Sylpheed: Arc of Deception");
ui.label("Asset Viewer — Milestone 1"); ui.label("Asset Viewer — Milestone 1");
ui.separator(); ui.separator();
ui.collapsing("Getting Started", |ui| { ui.collapsing("Getting Started", |ui| {
ui.label("1. Dump your Project Sylpheed disc to an ISO"); ui.label("Option A — open an ISO directly:");
ui.code(" File → Open ISO disc image…");
ui.separator();
ui.label("Option B — extract first, then open folder:");
ui.code(" xdvdfs unpack sylpheed.iso ./assets/"); ui.code(" xdvdfs unpack sylpheed.iso ./assets/");
ui.label("2. Restart the viewer — files will appear in the left panel"); ui.code(" File → Open extracted folder…");
ui.label("3. Click a .xpr file to preview the texture"); ui.separator();
ui.label("4. Use File → Open to select a different directory"); ui.label("Click a .xpr file in the left panel to preview the texture.");
}); });
ui.collapsing("Reverse Engineering Notes", |ui| { ui.collapsing("Reverse Engineering Notes", |ui| {