feat(viewer): ISO / extracted-dir loading pipeline + native file dialog
Some checks failed
Some checks failed
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:
489
Cargo.lock
generated
489
Cargo.lock
generated
@@ -276,6 +276,24 @@ dependencies = [
|
||||
"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]]
|
||||
name = "assert_type_match"
|
||||
version = "0.1.1"
|
||||
@@ -297,6 +315,18 @@ dependencies = [
|
||||
"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]]
|
||||
name = "async-channel"
|
||||
version = "2.5.0"
|
||||
@@ -334,6 +364,24 @@ dependencies = [
|
||||
"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]]
|
||||
name = "async-lock"
|
||||
version = "3.4.2"
|
||||
@@ -345,6 +393,64 @@ dependencies = [
|
||||
"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]]
|
||||
name = "async-task"
|
||||
version = "4.7.1"
|
||||
@@ -438,7 +544,7 @@ version = "0.15.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73fd901b3be016088c4dda2f628bda96b7cb578b9bc8ae684bbf30bec0a9483e"
|
||||
dependencies = [
|
||||
"async-broadcast",
|
||||
"async-broadcast 0.5.1",
|
||||
"async-fs",
|
||||
"async-lock",
|
||||
"atomicow",
|
||||
@@ -719,6 +825,8 @@ dependencies = [
|
||||
"derive_more",
|
||||
"futures-lite",
|
||||
"image",
|
||||
"ktx2",
|
||||
"ruzstd",
|
||||
"serde",
|
||||
"wgpu",
|
||||
]
|
||||
@@ -952,6 +1060,7 @@ dependencies = [
|
||||
"futures-lite",
|
||||
"image",
|
||||
"js-sys",
|
||||
"ktx2",
|
||||
"naga",
|
||||
"naga_oil",
|
||||
"nonmax",
|
||||
@@ -1711,7 +1820,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
|
||||
dependencies = [
|
||||
"dispatch2",
|
||||
"nix",
|
||||
"nix 0.31.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
@@ -1907,6 +2016,33 @@ dependencies = [
|
||||
"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]]
|
||||
name = "epaint"
|
||||
version = "0.29.1"
|
||||
@@ -2389,6 +2525,12 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hexasphere"
|
||||
version = "15.1.0"
|
||||
@@ -2688,6 +2830,15 @@ version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -2773,7 +2924,7 @@ version = "0.11.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a"
|
||||
dependencies = [
|
||||
"twox-hash",
|
||||
"twox-hash 2.1.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2820,6 +2971,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "metal"
|
||||
version = "0.29.0"
|
||||
@@ -2953,6 +3113,19 @@ dependencies = [
|
||||
"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]]
|
||||
name = "nix"
|
||||
version = "0.31.2"
|
||||
@@ -3043,6 +3216,17 @@ dependencies = [
|
||||
"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]]
|
||||
name = "objc-sys"
|
||||
version = "0.3.5"
|
||||
@@ -3313,6 +3497,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "offset-allocator"
|
||||
version = "0.2.0"
|
||||
@@ -3345,6 +3538,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "owned_ttf_parser"
|
||||
version = "0.25.1"
|
||||
@@ -3487,6 +3690,12 @@ dependencies = [
|
||||
"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]]
|
||||
name = "pori"
|
||||
version = "0.0.0"
|
||||
@@ -3754,6 +3963,29 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "ron"
|
||||
version = "0.8.1"
|
||||
@@ -3813,6 +4045,15 @@ version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
@@ -3911,6 +4152,28 @@ dependencies = [
|
||||
"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]]
|
||||
name = "sha3"
|
||||
version = "0.10.8"
|
||||
@@ -4095,6 +4358,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"binrw",
|
||||
"flate2",
|
||||
"futures",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4110,6 +4374,8 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"bevy",
|
||||
"bevy_egui",
|
||||
"futures",
|
||||
"rfd",
|
||||
"sylpheed-formats",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
@@ -4149,6 +4415,19 @@ dependencies = [
|
||||
"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]]
|
||||
name = "termcolor"
|
||||
version = "1.4.1"
|
||||
@@ -4448,6 +4727,16 @@ version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "twox-hash"
|
||||
version = "2.1.2"
|
||||
@@ -4472,6 +4761,17 @@ version = "1.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
@@ -4512,8 +4812,15 @@ dependencies = [
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
@@ -4982,6 +5289,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
@@ -5018,6 +5334,21 @@ dependencies = [
|
||||
"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]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
@@ -5051,6 +5382,12 @@ dependencies = [
|
||||
"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]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
@@ -5063,6 +5400,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
@@ -5075,6 +5418,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
@@ -5099,6 +5448,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
@@ -5111,6 +5466,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
@@ -5123,6 +5484,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
@@ -5135,6 +5502,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
@@ -5267,6 +5640,16 @@ version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "xdvdfs"
|
||||
version = "0.8.3"
|
||||
@@ -5335,6 +5718,68 @@ dependencies = [
|
||||
"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]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.47"
|
||||
@@ -5429,3 +5874,41 @@ checksum = "0b7a1c0af6e5d8d1363f4994b7a091ccf963d8b694f7da5b0b9cceb82da2c0a6"
|
||||
dependencies = [
|
||||
"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",
|
||||
]
|
||||
|
||||
@@ -39,6 +39,9 @@ anyhow = "1"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
# ── Native file dialog ────────────────────────────────────────────────────────
|
||||
rfd = "0.14"
|
||||
|
||||
# ── CLI ───────────────────────────────────────────────────────────────────────
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
|
||||
@@ -31,11 +31,14 @@ bevy = { workspace = true, features = [
|
||||
"bevy_winit",
|
||||
"multi_threaded",
|
||||
"png",
|
||||
# Required for TonyMcMapFace tonemapping (default Camera3d tonemapper)
|
||||
"tonemapping_luts",
|
||||
# Linux display backends
|
||||
"x11",
|
||||
"wayland",
|
||||
] }
|
||||
bevy_egui = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
@@ -46,3 +49,4 @@ dev = ["bevy/dynamic_linking"]
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tracing-subscriber = { workspace = true }
|
||||
rfd = { workspace = true }
|
||||
|
||||
@@ -12,10 +12,11 @@
|
||||
//! 4. Bevy uploads it to the GPU automatically
|
||||
|
||||
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::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};
|
||||
|
||||
// ── Plugin ────────────────────────────────────────────────────────────────────
|
||||
@@ -86,24 +87,32 @@ impl AssetLoader for Xpr2TextureLoader {
|
||||
|
||||
/// Convert a decoded `X360Texture` into a Bevy-compatible `Image`.
|
||||
///
|
||||
/// Maps Xbox 360 D3DFORMAT codes to `wgpu::TextureFormat` values.
|
||||
/// Bevy uses wgpu internally, so this mapping is direct.
|
||||
/// Constructs the `Image` struct directly rather than using `Image::new()`,
|
||||
/// because `Image::new()` calls `pixel_size()` which panics for BCn
|
||||
/// block-compressed formats.
|
||||
pub fn x360_texture_to_bevy_image(tex: X360Texture) -> Result<Image, Xpr2LoadError> {
|
||||
let bevy_format = x360_format_to_wgpu(&tex.format)?;
|
||||
let format = x360_format_to_wgpu(&tex.format)?;
|
||||
|
||||
Ok(Image::new(
|
||||
Extent3d {
|
||||
width: tex.width,
|
||||
height: tex.height,
|
||||
depth_or_array_layers: 1,
|
||||
Ok(Image {
|
||||
data: tex.data,
|
||||
texture_descriptor: TextureDescriptor {
|
||||
label: None,
|
||||
size: Extent3d {
|
||||
width: tex.width,
|
||||
height: tex.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format,
|
||||
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
},
|
||||
TextureDimension::D2,
|
||||
tex.data,
|
||||
bevy_format,
|
||||
// Make the texture available on both CPU and GPU
|
||||
// (RenderOnly would be more efficient once RE is complete)
|
||||
RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
|
||||
))
|
||||
sampler: ImageSampler::Default,
|
||||
texture_view_descriptor: None,
|
||||
asset_usage: RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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,
|
||||
// DXT5 / BC3
|
||||
X360TextureFormat::Dxt5 => TextureFormat::Bc3RgbaUnormSrgb,
|
||||
// DXN / BC5 — normal maps (RG, not sRGB)
|
||||
// DXN / BC5 — two-channel normal maps (RG, not sRGB)
|
||||
X360TextureFormat::Dxn => TextureFormat::Bc5RgUnorm,
|
||||
// Uncompressed ARGB — note Xbox 360 is BGRA order (big-endian)
|
||||
// We may need to swizzle R and B channels
|
||||
// DXT5A / BC4 — single-channel (gloss, specular, luminance maps)
|
||||
X360TextureFormat::Dxt5A => TextureFormat::Bc4RUnorm,
|
||||
// Uncompressed ARGB — Xbox 360 stores as BGRA in memory
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
458
crates/sylpheed-viewer/src/iso_loader.rs
Normal file
458
crates/sylpheed-viewer/src/iso_loader.rs
Normal 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;
|
||||
}
|
||||
@@ -20,6 +20,7 @@ use bevy_egui::EguiPlugin;
|
||||
|
||||
pub mod asset_loader;
|
||||
pub mod camera;
|
||||
pub mod iso_loader;
|
||||
pub mod ui;
|
||||
|
||||
// ── Application state ────────────────────────────────────────────────────────
|
||||
@@ -75,6 +76,7 @@ pub fn run() {
|
||||
|
||||
app.add_plugins(EguiPlugin);
|
||||
app.add_plugins(asset_loader::SylpheedAssetPlugin);
|
||||
app.add_plugins(iso_loader::IsoLoaderPlugin);
|
||||
app.add_plugins(camera::OrbitCameraPlugin);
|
||||
app.add_plugins(ui::ViewerUiPlugin);
|
||||
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
//! egui-based debug UI for the asset viewer.
|
||||
//!
|
||||
//! Provides panels for:
|
||||
//! - File browser (list extracted game files)
|
||||
//! - File browser (list ISO / extracted game files, filter by name)
|
||||
//! - 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)
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_egui::{egui, EguiContexts};
|
||||
|
||||
use crate::iso_loader::{
|
||||
FileInfo, FileSelected, IsoState, RequestOpenDir, RequestOpenIso, TexturePreview,
|
||||
IsoLoaderSystemSet,
|
||||
};
|
||||
use crate::{ViewerState, ViewMode};
|
||||
|
||||
pub struct ViewerUiPlugin;
|
||||
|
||||
impl Plugin for ViewerUiPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(Update, draw_viewer_ui);
|
||||
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 viewer: ResMut<ViewerState>,
|
||||
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();
|
||||
|
||||
@@ -39,26 +50,45 @@ fn draw_viewer_ui(
|
||||
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |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"))]
|
||||
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| {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
ui.separator();
|
||||
@@ -66,7 +96,7 @@ fn draw_viewer_ui(
|
||||
});
|
||||
|
||||
ui.menu_button("Help", |ui| {
|
||||
if ui.button("Controls...").clicked() {
|
||||
if ui.button("Controls…").clicked() {
|
||||
// TODO: controls popup
|
||||
}
|
||||
if ui.button("About").clicked() {
|
||||
@@ -93,29 +123,39 @@ fn draw_viewer_ui(
|
||||
|
||||
// File list
|
||||
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(
|
||||
egui::Color32::YELLOW,
|
||||
"⚠ No files loaded.\nExtract your ISO first:\n\
|
||||
xdvdfs unpack game.iso ./assets/",
|
||||
"No files loaded.\nUse File → Open ISO disc image\n\
|
||||
or File → Open extracted folder.",
|
||||
);
|
||||
} else {
|
||||
let filter = browser.filter.to_lowercase();
|
||||
// Collect owned strings so we can later mutate `browser.selected`
|
||||
let filtered: Vec<(usize, String)> = browser.files
|
||||
let filtered: Vec<(usize, String)> = browser
|
||||
.files
|
||||
.iter()
|
||||
.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()))
|
||||
.collect();
|
||||
|
||||
for (idx, file) in &filtered {
|
||||
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() {
|
||||
browser.selected = Some(*idx);
|
||||
info!("Selected file: {}", file);
|
||||
// TODO: load the selected file into the viewer
|
||||
file_selected_events.send(FileSelected(file.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,31 +165,84 @@ fn draw_viewer_ui(
|
||||
// ── Bottom panel: status bar ──────────────────────────────────────────
|
||||
egui::TopBottomPanel::bottom("status_bar").show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
if let Some(label) = &iso_state.source_label {
|
||||
ui.label(label);
|
||||
ui.separator();
|
||||
}
|
||||
match viewer.current_mode {
|
||||
ViewMode::Textures => ui.label("Mode: Texture Viewer"),
|
||||
ViewMode::Mesh => ui.label("Mode: Mesh Viewer"),
|
||||
ViewMode::Audio => ui.label("Mode: Audio Viewer"),
|
||||
ViewMode::Mesh => ui.label("Mode: Mesh Viewer"),
|
||||
ViewMode::Audio => ui.label("Mode: Audio Viewer"),
|
||||
};
|
||||
ui.separator();
|
||||
ui.label(format!("{} files loaded", browser.files.len()));
|
||||
ui.label(format!("{} files", browser.files.len()));
|
||||
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) ──────────
|
||||
if browser.selected.is_none() {
|
||||
// ── Central area ──────────────────────────────────────────────────────
|
||||
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| {
|
||||
ui.heading("Project Sylpheed: Arc of Deception");
|
||||
ui.label("Asset Viewer — Milestone 1");
|
||||
ui.separator();
|
||||
|
||||
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.label("2. Restart the viewer — files will appear in the left panel");
|
||||
ui.label("3. Click a .xpr file to preview the texture");
|
||||
ui.label("4. Use File → Open to select a different directory");
|
||||
ui.code(" File → Open extracted folder…");
|
||||
ui.separator();
|
||||
ui.label("Click a .xpr file in the left panel to preview the texture.");
|
||||
});
|
||||
|
||||
ui.collapsing("Reverse Engineering Notes", |ui| {
|
||||
|
||||
Reference in New Issue
Block a user