diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index a4966b51..b959da54 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -11,4 +11,5 @@ crates/j2k-transcode-cuda/ @frames-sg/j2k-maintainers
crates/j2k-metal-support/ @frames-sg/j2k-maintainers
crates/j2k-jpeg-metal/ @frames-sg/j2k-maintainers
crates/j2k-metal/ @frames-sg/j2k-maintainers
+crates/j2k-ml/ @frames-sg/j2k-maintainers
crates/j2k-transcode-metal/ @frames-sg/j2k-maintainers
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 365f160a..d84168d4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,24 @@
# Changelog
-This changelog tracks the current staged release line. Historical phase notes
+This changelog tracks the current release line. Historical phase notes
and stale roadmap entries have been removed from the public documentation set.
+## [0.7.1] - 2026-07-14
+
+- Adds CUDA classic JPEG 2000 Tier-1 code-block decoding with shared host/device
+ MQ and context tables, batched resident submission, bounded host accounting,
+ and explicit CUDA ABI and unsafe-boundary inventories.
+- Adds an opaque, zero-copy Metal resident-image contract with checked layout
+ and device identity, submission-owned input and output lifetimes, safe
+ decoder-to-encoder handoff, and explicit unsafe raw-buffer interop.
+- Improves Metal decode residency with dynamic pool ceilings and enforced
+ inflight limits, fixes irreversible inverse-MCT addends, fuses native inverse
+ MCT output handling, and uses a staged irreversible 9/7 IDWT implementation.
+- Adds hardened CUDA and Metal profiling/capture controls and documents their
+ benchmark-only environment variables.
+- Adds the unpublished experimental `j2k-ml` workspace crate with CPU and Metal
+ validation paths and an optional CUDA-Oxide kernel route.
+
## [0.7.0] - 2026-07-12
- Moves the test-only `corpus_validation`, `dct53_1d`, and `dct53_multilevel`
diff --git a/Cargo.lock b/Cargo.lock
index 413b7078..d8b78bc3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,12 +2,33 @@
# It is not intended for manual editing.
version = 4
+[[package]]
+name = "addr2line"
+version = "0.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
+dependencies = [
+ "gimli",
+]
+
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
[[package]]
name = "aho-corasick"
version = "1.1.4"
@@ -26,6 +47,21 @@ dependencies = [
"cc",
]
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "anes"
version = "0.1.6"
@@ -44,19 +80,118 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+[[package]]
+name = "arrayvec"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
+
+[[package]]
+name = "ash"
+version = "0.38.0+1.3.281"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f"
+dependencies = [
+ "libloading 0.8.9",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "atomic_float"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "628d228f918ac3b82fe590352cc719d30664a0c13ca3a60266fe02c7132d480a"
+
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+[[package]]
+name = "backtrace"
+version = "0.3.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-link",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bincode"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
+dependencies = [
+ "serde",
+ "unty",
+]
+
+[[package]]
+name = "bindgen"
+version = "0.71.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "itertools 0.13.0",
+ "log",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash 2.1.3",
+ "shlex 1.3.0",
+ "syn",
+]
+
[[package]]
name = "bit-set"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
- "bit-vec",
+ "bit-vec 0.8.0",
+]
+
+[[package]]
+name = "bit-set"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd"
+dependencies = [
+ "bit-vec 0.9.1",
]
[[package]]
@@ -65,1414 +200,5343 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+[[package]]
+name = "bit-vec"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51"
+
[[package]]
name = "bitflags"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
+dependencies = [
+ "serde_core",
+]
[[package]]
name = "block"
version = "0.1.6"
[[package]]
-name = "bytemuck"
-version = "1.25.0"
+name = "block-buffer"
+version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
[[package]]
-name = "byteorder-lite"
-version = "0.1.0"
+name = "block2"
+version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
+dependencies = [
+ "objc2",
+]
[[package]]
-name = "cast"
-version = "0.3.0"
+name = "bumpalo"
+version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
-name = "cc"
-version = "1.2.64"
+name = "burn-autodiff"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f"
+checksum = "3b93a80e43bfab909399444b29ce3894ff51f7e879720bfc75b6007ae6a01c3d"
dependencies = [
- "find-msvc-tools",
- "jobserver",
- "libc",
- "shlex",
+ "burn-backend",
+ "burn-std",
+ "derive-new",
+ "hashbrown 0.16.1",
+ "log",
+ "num-traits",
+ "parking_lot",
+ "portable-atomic",
+ "spin",
]
[[package]]
-name = "cfg-if"
-version = "1.0.4"
+name = "burn-backend"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+checksum = "64491519ac0867f550fa9c726cd443e5db94608c629050986cf2614c8c5ad39f"
+dependencies = [
+ "burn-std",
+ "bytemuck",
+ "cubecl",
+ "derive-new",
+ "enumset",
+ "hashbrown 0.16.1",
+ "num-traits",
+ "portable-atomic-util",
+ "rand 0.10.2",
+ "rand_distr",
+ "serde",
+ "spin",
+ "thiserror 2.0.18",
+]
[[package]]
-name = "ciborium"
-version = "0.2.2"
+name = "burn-core"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
+checksum = "5bde1e3b8e7e39b0aa71cd5905207ceea5c3478bec47bdb64acf72dcd0d6f978"
dependencies = [
- "ciborium-io",
- "ciborium-ll",
+ "ahash",
+ "bincode",
+ "burn-dataset",
+ "burn-derive",
+ "burn-std",
+ "burn-tensor",
+ "data-encoding",
+ "derive-new",
+ "flate2",
+ "half",
+ "hashbrown 0.16.1",
+ "log",
+ "num-traits",
+ "portable-atomic",
+ "portable-atomic-util",
+ "rand 0.10.2",
+ "regex",
+ "rmp-serde",
"serde",
+ "serde_json",
+ "spin",
+ "uuid",
]
[[package]]
-name = "ciborium-io"
-version = "0.2.2"
+name = "burn-cubecl"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
+checksum = "26efbec96ca3f691593c966fa90f98ef8a72867f62627904920b619ba3a9e6b6"
+dependencies = [
+ "burn-backend",
+ "burn-cubecl-fusion",
+ "burn-fusion",
+ "burn-ir",
+ "burn-std",
+ "cubecl",
+ "cubek",
+ "derive-new",
+ "futures-lite",
+ "log",
+ "serde",
+ "text_placeholder",
+]
[[package]]
-name = "ciborium-ll"
-version = "0.2.2"
+name = "burn-cubecl-fusion"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
+checksum = "6993d15b8ed588f55626e835ca768c1c3c7aa7a79d59beff4c987c40fc82d1f4"
dependencies = [
- "ciborium-io",
- "half",
+ "burn-backend",
+ "burn-fusion",
+ "burn-ir",
+ "burn-std",
+ "cubecl",
+ "cubek",
+ "derive-new",
+ "hashbrown 0.16.1",
+ "serde",
+ "spin",
]
[[package]]
-name = "clap"
-version = "4.6.1"
+name = "burn-cuda"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+checksum = "a6c66b49136fc11f59773054358f401fbec7fa0d69615f55ef22c29cf241c9b7"
dependencies = [
- "clap_builder",
+ "burn-backend",
+ "burn-cubecl",
+ "burn-fusion",
+ "cubecl",
]
[[package]]
-name = "clap_builder"
-version = "4.6.0"
+name = "burn-dataset"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+checksum = "ba469fba38357c8bb65458873ddfd82aa97534aec0b3bae93d4c44604fc63839"
dependencies = [
- "anstyle",
- "clap_lex",
+ "csv",
+ "derive-new",
+ "dirs",
+ "rand 0.10.2",
+ "rmp-serde",
+ "sanitize-filename",
+ "serde",
+ "serde_json",
+ "strum",
+ "tempfile",
+ "thiserror 2.0.18",
]
[[package]]
-name = "clap_lex"
-version = "1.1.0"
+name = "burn-derive"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+checksum = "dce7be43cdfc9903c43dbaf3f4821543e0497d79047d8fc9ee9084e448844446"
+dependencies = [
+ "derive-new",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
[[package]]
-name = "core-foundation"
-version = "0.10.1"
+name = "burn-flex"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+checksum = "0098bf6555c151517b4c98ca0e8ed1ab6a976e40f5f4e967c117b00eee21e192"
dependencies = [
- "core-foundation-sys",
- "libc",
+ "burn-backend",
+ "burn-ir",
+ "burn-std",
+ "bytemuck",
+ "gemm",
+ "half",
+ "libm",
+ "num-traits",
]
[[package]]
-name = "core-foundation-sys"
-version = "0.8.7"
+name = "burn-fusion"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+checksum = "95e58fcfd52a60cdf2f7da34fbebf0ed38f960fbd8675d384bacd5795cf2bdf2"
+dependencies = [
+ "burn-backend",
+ "burn-ir",
+ "burn-std",
+ "derive-new",
+ "hashbrown 0.16.1",
+ "log",
+ "serde",
+ "smallvec",
+ "spin",
+ "tracing",
+]
[[package]]
-name = "core-graphics-types"
-version = "0.2.0"
+name = "burn-ir"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
+checksum = "a81a55c32f35c2265d9e15ba2e796013d9780d5a49f9e21ea0ac4d29609dbf13"
dependencies = [
- "bitflags",
- "core-foundation",
- "libc",
+ "burn-backend",
+ "hashbrown 0.16.1",
+ "serde",
]
[[package]]
-name = "crc32fast"
-version = "1.5.0"
+name = "burn-ndarray"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+checksum = "dceb9692e292782bab7ad0259e8e8f5ee80ba2b0329a78f0ba589a26a9633dd6"
dependencies = [
- "cfg-if",
+ "atomic_float",
+ "burn-autodiff",
+ "burn-backend",
+ "burn-ir",
+ "burn-std",
+ "const-random",
+ "libm",
+ "macerator",
+ "matrixmultiply",
+ "ndarray",
+ "num-traits",
+ "paste",
+ "portable-atomic",
+ "portable-atomic-util",
+ "rand 0.10.2",
]
[[package]]
-name = "criterion"
-version = "0.8.2"
+name = "burn-std"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3"
+checksum = "81c7eb60eb2c316854af5b214fd55f59fc7b0e25dfde7db671f09cc49f269048"
dependencies = [
- "alloca",
- "anes",
- "cast",
- "ciborium",
- "clap",
- "criterion-plot",
- "itertools",
+ "bytemuck",
+ "bytes",
+ "cubecl",
+ "cubecl-common",
+ "cubecl-zspace",
+ "half",
"num-traits",
- "oorandom",
- "page_size",
- "regex",
"serde",
- "serde_json",
- "tinytemplate",
- "walkdir",
+ "smallvec",
+ "spin",
]
[[package]]
-name = "criterion-plot"
-version = "0.8.2"
+name = "burn-tensor"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea"
+checksum = "223ed3804bb9436e401fc57b87e44c86267070b870813520ed9f706c80c81442"
dependencies = [
- "cast",
- "itertools",
+ "burn-backend",
+ "burn-std",
+ "colored",
+ "derive-new",
+ "num-traits",
+ "serde",
]
[[package]]
-name = "crossbeam-deque"
-version = "0.8.6"
+name = "burn-wgpu"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+checksum = "6eb009254af15922cb37814f3b3b61043046193ba2fde97c3879a25fbd51a92e"
dependencies = [
- "crossbeam-epoch",
- "crossbeam-utils",
+ "burn-backend",
+ "burn-cubecl",
+ "burn-fusion",
+ "cubecl",
]
[[package]]
-name = "crossbeam-epoch"
-version = "0.9.20"
+name = "bytemuck"
+version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
+checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
dependencies = [
- "crossbeam-utils",
+ "bytemuck_derive",
]
[[package]]
-name = "crossbeam-utils"
-version = "0.8.21"
+name = "bytemuck_derive"
+version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
[[package]]
-name = "crunchy"
-version = "0.2.4"
+name = "byteorder"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
-name = "either"
-version = "1.16.0"
+name = "byteorder-lite"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
-name = "equivalent"
-version = "1.0.2"
+name = "bytes"
+version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
+dependencies = [
+ "portable-atomic",
+]
[[package]]
-name = "errno"
-version = "0.3.14"
+name = "caseless"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+checksum = "8b6fd507454086c8edfd769ca6ada439193cdb209c7681712ef6275cccbfe5d8"
dependencies = [
- "libc",
- "windows-sys",
+ "unicode-normalization",
]
[[package]]
-name = "fastrand"
-version = "2.4.1"
+name = "cast"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
-name = "fax"
-version = "0.2.7"
+name = "cc"
+version = "1.2.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a"
+checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f"
+dependencies = [
+ "find-msvc-tools",
+ "jobserver",
+ "libc",
+ "shlex 2.0.1",
+]
[[package]]
-name = "fdeflate"
-version = "0.3.7"
+name = "cexpr"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
- "simd-adler32",
+ "nom 7.1.3",
]
[[package]]
-name = "fearless_simd"
-version = "0.4.1"
+name = "cfg-if"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
-name = "find-msvc-tools"
-version = "0.1.9"
+name = "cfg_aliases"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
-name = "flate2"
-version = "1.1.9"
+name = "chacha20"
+version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
dependencies = [
- "crc32fast",
- "libz-sys",
- "miniz_oxide",
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "rand_core 0.10.1",
]
[[package]]
-name = "fnv"
-version = "1.0.7"
+name = "ciborium"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
+dependencies = [
+ "ciborium-io",
+ "ciborium-ll",
+ "serde",
+]
[[package]]
-name = "foldhash"
-version = "0.1.5"
+name = "ciborium-io"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
-name = "foreign-types"
-version = "0.5.0"
+name = "ciborium-ll"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
+checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
- "foreign-types-macros",
- "foreign-types-shared",
+ "ciborium-io",
+ "half",
]
[[package]]
-name = "foreign-types-macros"
-version = "0.2.3"
+name = "clang-sys"
+version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
+checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "glob",
+ "libc",
+ "libloading 0.8.9",
]
[[package]]
-name = "foreign-types-shared"
-version = "0.3.1"
+name = "clap"
+version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+dependencies = [
+ "clap_builder",
+]
[[package]]
-name = "getrandom"
-version = "0.3.4"
+name = "clap_builder"
+version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
- "cfg-if",
- "libc",
- "r-efi 5.3.0",
- "wasip2",
+ "anstyle",
+ "clap_lex",
]
[[package]]
-name = "getrandom"
-version = "0.4.2"
+name = "clap_lex"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
-dependencies = [
- "cfg-if",
- "libc",
- "r-efi 6.0.0",
- "wasip2",
- "wasip3",
-]
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
-name = "half"
-version = "2.7.1"
+name = "codespan-reporting"
+version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
+checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
dependencies = [
- "cfg-if",
- "crunchy",
- "zerocopy",
+ "serde",
+ "termcolor",
+ "unicode-width",
]
[[package]]
-name = "hashbrown"
-version = "0.15.5"
+name = "colored"
+version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
dependencies = [
- "foldhash",
+ "windows-sys 0.61.2",
]
[[package]]
-name = "hashbrown"
-version = "0.17.1"
+name = "comrak"
+version = "0.39.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+checksum = "2fefab951771fc3beeed0773ce66a4f7b706273fc6c4c95b08dd1615744abcf5"
+dependencies = [
+ "caseless",
+ "entities",
+ "memchr",
+ "slug",
+ "typed-arena",
+ "unicode_categories",
+]
[[package]]
-name = "heck"
-version = "0.5.0"
+name = "concurrent-queue"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
[[package]]
-name = "id-arena"
-version = "2.3.0"
+name = "const-random"
+version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
+dependencies = [
+ "const-random-macro",
+]
[[package]]
-name = "image"
-version = "0.25.10"
+name = "const-random-macro"
+version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
+checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
dependencies = [
- "bytemuck",
- "byteorder-lite",
- "moxcms",
- "num-traits",
- "png",
- "tiff",
- "zune-core",
- "zune-jpeg",
+ "getrandom 0.2.17",
+ "once_cell",
+ "tiny-keccak",
]
[[package]]
-name = "indexmap"
-version = "2.14.0"
+name = "constcat"
+version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
-dependencies = [
- "equivalent",
- "hashbrown 0.17.1",
- "serde",
- "serde_core",
-]
+checksum = "136d3e02915a2cea4d74caa8681e2d44b1c3254bdbf17d11d41d587ff858832c"
[[package]]
-name = "itertools"
-version = "0.13.0"
+name = "convert_case"
+version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
dependencies = [
- "either",
+ "unicode-segmentation",
]
[[package]]
-name = "itoa"
-version = "1.0.18"
+name = "convert_case"
+version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
-
-[[package]]
-name = "j2k"
-version = "0.7.0"
+checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
dependencies = [
- "criterion",
- "j2k-codec-math",
- "j2k-core",
- "j2k-native",
- "j2k-test-support",
- "j2k-types",
- "proptest",
- "thiserror",
+ "unicode-segmentation",
]
[[package]]
-name = "j2k-cli"
-version = "0.7.0"
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
- "j2k",
- "j2k-jpeg",
- "j2k-test-support",
- "j2k-transcode",
+ "core-foundation-sys",
+ "libc",
]
[[package]]
-name = "j2k-codec-math"
-version = "0.7.0"
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
-name = "j2k-compare"
+name = "core-graphics-types"
version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [
- "cc",
- "image",
- "j2k",
- "j2k-core",
- "j2k-native",
- "j2k-test-support",
- "openjpeg-sys",
+ "bitflags",
+ "core-foundation",
+ "libc",
]
[[package]]
-name = "j2k-core"
-version = "0.7.0"
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
- "thiserror",
+ "libc",
]
[[package]]
-name = "j2k-cuda"
-version = "0.7.0"
+name = "cpufeatures"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
- "criterion",
- "j2k",
- "j2k-core",
- "j2k-cuda-runtime",
- "j2k-native",
- "j2k-profile",
- "j2k-test-support",
- "thiserror",
+ "libc",
]
[[package]]
-name = "j2k-cuda-runtime"
-version = "0.7.0"
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
- "j2k-codec-math",
- "j2k-core",
- "j2k-test-support",
- "libloading",
- "thiserror",
+ "cfg-if",
]
[[package]]
-name = "j2k-jpeg"
-version = "0.7.0"
+name = "criterion"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3"
dependencies = [
- "criterion",
- "j2k-codec-math",
- "j2k-core",
- "j2k-profile",
- "j2k-test-support",
- "jpeg-decoder",
- "memchr",
- "proptest",
- "rayon",
- "thiserror",
- "zune-core",
- "zune-jpeg",
-]
-
-[[package]]
-name = "j2k-jpeg-cuda"
-version = "0.7.0"
+ "alloca",
+ "anes",
+ "cast",
+ "ciborium",
+ "clap",
+ "criterion-plot",
+ "itertools 0.13.0",
+ "num-traits",
+ "oorandom",
+ "page_size",
+ "regex",
+ "serde",
+ "serde_json",
+ "tinytemplate",
+ "walkdir",
+]
+
+[[package]]
+name = "criterion-plot"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea"
dependencies = [
- "criterion",
- "j2k-core",
- "j2k-cuda-runtime",
- "j2k-jpeg",
- "j2k-profile",
- "j2k-test-support",
- "thiserror",
+ "cast",
+ "itertools 0.13.0",
]
[[package]]
-name = "j2k-jpeg-metal"
-version = "0.7.0"
+name = "critical-section"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
- "criterion",
- "j2k-core",
- "j2k-jpeg",
- "j2k-metal-support",
- "j2k-profile",
- "j2k-test-support",
- "jpeg-decoder",
- "jpeg-encoder",
- "metal",
- "thiserror",
+ "crossbeam-epoch",
+ "crossbeam-utils",
]
[[package]]
-name = "j2k-metal"
-version = "0.7.0"
+name = "crossbeam-epoch"
+version = "0.9.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "csv"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938"
+dependencies = [
+ "csv-core",
+ "itoa",
+ "ryu",
+ "serde_core",
+]
+
+[[package]]
+name = "csv-core"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "cubecl"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd203fef6e359472e4cb8f6c0ef3eca062815a1edd560eb6d6c1d5c1397838d9"
+dependencies = [
+ "cubecl-core",
+ "cubecl-cpu",
+ "cubecl-cuda",
+ "cubecl-hip",
+ "cubecl-ir",
+ "cubecl-runtime",
+ "cubecl-std",
+ "cubecl-wgpu",
+ "half",
+]
+
+[[package]]
+name = "cubecl-common"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc956c2dcc993f16f748d03bfdbd900f75cab4d469f4e5d8924f8ee128e861ad"
+dependencies = [
+ "backtrace",
+ "bytemuck",
+ "bytes",
+ "cfg-if",
+ "cfg_aliases",
+ "ciborium",
+ "derive-new",
+ "derive_more",
+ "dirs",
+ "embassy-futures",
+ "embassy-time",
+ "float4",
+ "float8",
+ "futures-lite",
+ "half",
+ "hashbrown 0.16.1",
+ "log",
+ "num-traits",
+ "oneshot",
+ "parking_lot",
+ "portable-atomic",
+ "portable-atomic-util",
+ "rand 0.10.2",
+ "sanitize-filename",
+ "serde",
+ "serde_bytes",
+ "serde_json",
+ "spin",
+ "toml",
+ "tracing",
+ "tynm",
+ "wasm-bindgen-futures",
+ "web-time",
+ "xxhash-rust",
+]
+
+[[package]]
+name = "cubecl-core"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7522e7acf25d7848032c7b5270780f9f2abe84e13c7ed6345aa27ba70c312ca"
+dependencies = [
+ "bitflags",
+ "bytemuck",
+ "cubecl-common",
+ "cubecl-ir",
+ "cubecl-macros",
+ "cubecl-runtime",
+ "cubecl-zspace",
+ "derive-new",
+ "derive_more",
+ "enumset",
+ "float-ord",
+ "half",
+ "hashbrown 0.16.1",
+ "log",
+ "num-traits",
+ "paste",
+ "serde",
+ "serde_json",
+ "tracing",
+ "variadics_please",
+]
+
+[[package]]
+name = "cubecl-cpp"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "411af04828cbf4583ecd388579fb30cd7a644eec19a334bb8f0d9d08522e1458"
+dependencies = [
+ "bytemuck",
+ "cubecl-common",
+ "cubecl-core",
+ "cubecl-opt",
+ "cubecl-runtime",
+ "derive-new",
+ "half",
+ "itertools 0.14.0",
+ "log",
+]
+
+[[package]]
+name = "cubecl-cpu"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f572143f54e42a49ee0635a4ec36005f639e62be029e4f49890c808985981b35"
+dependencies = [
+ "bytemuck",
+ "cubecl-common",
+ "cubecl-core",
+ "cubecl-opt",
+ "cubecl-runtime",
+ "cubecl-std",
+ "derive-new",
+ "half",
+ "log",
+ "paste",
+ "serde",
+ "sysinfo",
+ "tracel-llvm",
+ "tracel-llvm-bundler",
+]
+
+[[package]]
+name = "cubecl-cuda"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6b0a69ff45688d322ad8e92c8bf645167b9ca490fa8fa087fc6adac8c5e46be"
+dependencies = [
+ "bytemuck",
+ "cubecl-common",
+ "cubecl-core",
+ "cubecl-cpp",
+ "cubecl-runtime",
+ "cudarc",
+ "derive-new",
+ "half",
+ "log",
+ "serde",
+ "tracing",
+]
+
+[[package]]
+name = "cubecl-hip"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c6b510a9348f06ecd56b32cf10eb6241639e927a87f5c09ec61cc7a7610d5a3"
+dependencies = [
+ "bytemuck",
+ "cubecl-common",
+ "cubecl-core",
+ "cubecl-cpp",
+ "cubecl-hip-sys",
+ "cubecl-runtime",
+ "derive-new",
+ "half",
+ "log",
+ "paste",
+ "serde",
+ "tracing",
+]
+
+[[package]]
+name = "cubecl-hip-sys"
+version = "7.2.5321100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58887c6d217859c2261a868a3a6b66aa8f44ea2f4bfa51d0dbe4dcb0d1f5768d"
dependencies = [
- "cc",
- "j2k",
- "j2k-codec-math",
- "j2k-core",
- "j2k-metal-support",
- "j2k-native",
- "j2k-profile",
- "j2k-test-support",
"libc",
- "metal",
- "rayon",
- "thiserror",
+ "regex",
]
[[package]]
-name = "j2k-metal-support"
-version = "0.7.0"
+name = "cubecl-ir"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d28a897a40c8ee6c57a8b8d76d8823ba2e97f4c2b83db9338f17a0752132d486"
dependencies = [
- "j2k-core",
- "j2k-test-support",
+ "cubecl-common",
+ "cubecl-macros-internal",
+ "derive-new",
+ "derive_more",
+ "enumset",
+ "float-ord",
+ "fnv",
+ "foldhash 0.2.0",
+ "half",
+ "hashbrown 0.16.1",
+ "num-traits",
+ "portable-atomic",
+ "serde",
+ "variadics_please",
+]
+
+[[package]]
+name = "cubecl-macros"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77aa6df563e8e6a0926d2e9eeacb968737940a0e1ae9be819f6a65a341006e12"
+dependencies = [
+ "cubecl-common",
+ "darling 0.23.0",
+ "derive-new",
+ "ident_case",
+ "inflections",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "cubecl-macros-internal"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a515651a5a91e25d87f71f311f80a088e6cf815798b9922560a97636da6b8740"
+dependencies = [
+ "darling 0.23.0",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "cubecl-opt"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a599c6c3efefdcee8636994c90e58ef696c7efbed2d18b5b5ad8d5f29923abb"
+dependencies = [
+ "cubecl-common",
+ "cubecl-core",
+ "cubecl-ir",
+ "float-ord",
"log",
- "metal",
+ "num",
+ "petgraph",
+ "smallvec",
+ "stable-vec",
+ "type-map",
+]
+
+[[package]]
+name = "cubecl-runtime"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b68491bf5b3e997ae36bdc4e63b4ccd6d2f0e86b3b596a5d7a48d2b9e92622a0"
+dependencies = [
+ "ahash",
+ "async-channel",
+ "bytemuck",
+ "cfg-if",
+ "cfg_aliases",
+ "cubecl-common",
+ "cubecl-ir",
+ "cubecl-zspace",
+ "derive-new",
+ "derive_more",
+ "dirs",
+ "enumset",
+ "hashbrown 0.16.1",
+ "log",
+ "md5",
+ "serde",
+ "serde_json",
+ "spin",
+ "thiserror 2.0.18",
+ "toml",
+ "tracing",
+ "wasm-bindgen-futures",
+ "web-time",
+]
+
+[[package]]
+name = "cubecl-std"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b391b584a4897683dd9fc0767f96337ac712b733126ce0f68a9ee8a2df073d2d"
+dependencies = [
+ "cubecl-common",
+ "cubecl-core",
+ "cubecl-runtime",
+ "half",
+ "num-traits",
+ "paste",
+ "serde",
+ "spin",
+ "variadics_please",
+]
+
+[[package]]
+name = "cubecl-wgpu"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d3663c6e1a86187172b2cee495b6b533e7d91a2cb37e26f421649e315fe4c3a"
+dependencies = [
+ "async-channel",
+ "bytemuck",
+ "cfg-if",
+ "cfg_aliases",
+ "cubecl-common",
+ "cubecl-core",
+ "cubecl-cpp",
+ "cubecl-ir",
+ "cubecl-runtime",
+ "derive-new",
+ "derive_more",
+ "half",
+ "hashbrown 0.16.1",
+ "log",
+ "sanitize-filename",
+ "tracing",
+ "wasm-bindgen-futures",
+ "wgpu",
+]
+
+[[package]]
+name = "cubecl-zspace"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04054d95698afab785a4dda9f949e297831dd9e4cc6d036a93e6603f88e88b07"
+dependencies = [
+ "derive-new",
+ "serde",
+ "smallvec",
+]
+
+[[package]]
+name = "cubek"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba62fb5043d5eb723017415eec57c541f41901beea984500e2e16b57d75fa717"
+dependencies = [
+ "cubecl",
+ "cubek-attention",
+ "cubek-convolution",
+ "cubek-fft",
+ "cubek-matmul",
+ "cubek-quant",
+ "cubek-random",
+ "cubek-reduce",
+ "cubek-std",
+]
+
+[[package]]
+name = "cubek-attention"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75ca5c363f05a9297b9d1c0e90959cc5f9fd90bc596e4e537f26f20602ec63a5"
+dependencies = [
+ "bytemuck",
+ "cubecl",
+ "cubecl-common",
+ "cubek-matmul",
+ "cubek-std",
+ "half",
+ "serde",
+]
+
+[[package]]
+name = "cubek-convolution"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70f2755a8eb9ea5509c17edac78e4a9dc4be89c433d8e2ee709703a93b96f749"
+dependencies = [
+ "bytemuck",
+ "cubecl",
+ "cubecl-common",
+ "cubek-matmul",
+ "cubek-std",
+ "derive-new",
+ "enumset",
+ "half",
+ "serde",
+]
+
+[[package]]
+name = "cubek-fft"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8dd7c2972363332dc4609067a2ccc21856308d98089e676d3f0c52747ae61a5b"
+dependencies = [
+ "cubecl",
+]
+
+[[package]]
+name = "cubek-matmul"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83a4cea5f0f439907dc953c7638a6204b3f055f1bcbd10db91dfc5faa030ac1c"
+dependencies = [
+ "bytemuck",
+ "cubecl",
+ "cubecl-common",
+ "cubek-std",
+ "half",
+ "serde",
+]
+
+[[package]]
+name = "cubek-quant"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "802ff1b5f19597a7b7cb308c7c69e9eef28b78d57dab7794c816bb8f7d3d1b85"
+dependencies = [
+ "cubecl",
+ "cubecl-common",
+ "half",
+ "serde",
+]
+
+[[package]]
+name = "cubek-random"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8dfecce959bdbeb3b355120e586d1ea9e45a0fc7f2ca354f4260a571952551be"
+dependencies = [
+ "cubecl",
+ "cubecl-common",
+ "half",
+ "num-traits",
+ "rand 0.10.2",
+ "serde",
+]
+
+[[package]]
+name = "cubek-reduce"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86758b84452b06e9121f25200da13b4909ecd6235cefb5f33f459bea24dac41a"
+dependencies = [
+ "cubecl",
+ "cubek-std",
+ "half",
+ "num-traits",
+ "serde",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "cubek-std"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a587d0a44b6f14f4c3711ac10717179b48adb59203b2b2bffe334876bf713187"
+dependencies = [
+ "cubecl",
+ "cubecl-common",
+ "half",
+]
+
+[[package]]
+name = "cudarc"
+version = "0.19.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42310153e06cf4cd532901f7096beb27504d681736a29ee90728ae4e2d93b2a8"
+dependencies = [
+ "libloading 0.9.0",
+]
+
+[[package]]
+name = "darling"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
+dependencies = [
+ "darling_core 0.20.11",
+ "darling_macro 0.20.11",
+]
+
+[[package]]
+name = "darling"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
+dependencies = [
+ "darling_core 0.21.3",
+ "darling_macro 0.21.3",
+]
+
+[[package]]
+name = "darling"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
+dependencies = [
+ "darling_core 0.23.0",
+ "darling_macro 0.23.0",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
+dependencies = [
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
+dependencies = [
+ "darling_core 0.20.11",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
+dependencies = [
+ "darling_core 0.21.3",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
+dependencies = [
+ "darling_core 0.23.0",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
+
+[[package]]
+name = "derive-new"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "derive_more"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
+dependencies = [
+ "convert_case 0.10.0",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn",
+ "unicode-xid",
+]
+
+[[package]]
+name = "deunicode"
+version = "1.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04"
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "dispatch2"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
+dependencies = [
+ "bitflags",
+ "objc2",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "dlib"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a"
+dependencies = [
+ "libloading 0.8.9",
+]
+
+[[package]]
+name = "document-features"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
+dependencies = [
+ "litrs",
+]
+
+[[package]]
+name = "dyn-stack"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c4713e43e2886ba72b8271aa66c93d722116acf7a75555cce11dcde84388fe8"
+dependencies = [
+ "bytemuck",
+ "dyn-stack-macros",
+]
+
+[[package]]
+name = "dyn-stack-macros"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1d926b4d407d372f141f93bb444696142c29d32962ccbd3531117cf3aa0bfa9"
+
+[[package]]
+name = "either"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
+
+[[package]]
+name = "embassy-futures"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc2d050bdc5c21e0862a89256ed8029ae6c290a93aecefc73084b3002cdebb01"
+
+[[package]]
+name = "embassy-time"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "592b0c143ec626e821d4d90da51a2bd91d559d6c442b7c74a47d368c9e23d97a"
+dependencies = [
+ "cfg-if",
+ "critical-section",
+ "document-features",
+ "embassy-time-driver",
+ "embedded-hal 0.2.7",
+ "embedded-hal 1.0.0",
+ "embedded-hal-async",
+ "futures-core",
+]
+
+[[package]]
+name = "embassy-time-driver"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ee71af1b3a0deaa53eaf2d39252f83504c853646e472400b763060389b9fcc9"
+dependencies = [
+ "document-features",
+]
+
+[[package]]
+name = "embedded-hal"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff"
+dependencies = [
+ "nb 0.1.3",
+ "void",
+]
+
+[[package]]
+name = "embedded-hal"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89"
+
+[[package]]
+name = "embedded-hal-async"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884"
+dependencies = [
+ "embedded-hal 1.0.0",
+]
+
+[[package]]
+name = "entities"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
+
+[[package]]
+name = "enum-as-inner"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "enumset"
+version = "1.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "839c4174b41e75c8f7306110b2c51996a293b8d1d850edd529011841d9fede7d"
+dependencies = [
+ "enumset_derive",
+ "serde",
+]
+
+[[package]]
+name = "enumset_derive"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bd536557b58c682b217b8fb199afdff47cd3eff260623f19e77074eb073d63a"
+dependencies = [
+ "darling 0.21.3",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "event-listener"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+
+[[package]]
+name = "fax"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a"
+
+[[package]]
+name = "fdeflate"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "fearless_simd"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693"
+
+[[package]]
+name = "filetime"
+version = "0.2.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
+dependencies = [
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "fixedbitset"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "crc32fast",
+ "libz-sys",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "float-ord"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d"
+
+[[package]]
+name = "float4"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a5404bf31d22893d61cf24d4dda149d8e6b2ff07601c3cb3be651031f61a4ed"
+
+[[package]]
+name = "float8"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2d1f04709a8ac06e8e8042875a3c466cc4832d3c1a18dbcb9dba3c6e83046bc"
+dependencies = [
+ "half",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
+name = "foreign-types"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
+dependencies = [
+ "foreign-types-macros",
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-macros"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-io"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+
+[[package]]
+name = "futures-lite"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "gemm"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa0673db364b12263d103b68337a68fbecc541d6f6b61ba72fe438654709eacb"
+dependencies = [
+ "dyn-stack",
+ "gemm-c32",
+ "gemm-c64",
+ "gemm-common",
+ "gemm-f16",
+ "gemm-f32",
+ "gemm-f64",
+ "num-complex",
+ "num-traits",
+ "paste",
+ "raw-cpuid",
+ "seq-macro",
+]
+
+[[package]]
+name = "gemm-c32"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "086936dbdcb99e37aad81d320f98f670e53c1e55a98bee70573e83f95beb128c"
+dependencies = [
+ "dyn-stack",
+ "gemm-common",
+ "num-complex",
+ "num-traits",
+ "paste",
+ "raw-cpuid",
+ "seq-macro",
+]
+
+[[package]]
+name = "gemm-c64"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20c8aeeeec425959bda4d9827664029ba1501a90a0d1e6228e48bef741db3a3f"
+dependencies = [
+ "dyn-stack",
+ "gemm-common",
+ "num-complex",
+ "num-traits",
+ "paste",
+ "raw-cpuid",
+ "seq-macro",
+]
+
+[[package]]
+name = "gemm-common"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88027625910cc9b1085aaaa1c4bc46bb3a36aad323452b33c25b5e4e7c8e2a3e"
+dependencies = [
+ "bytemuck",
+ "dyn-stack",
+ "half",
+ "libm",
+ "num-complex",
+ "num-traits",
+ "once_cell",
+ "paste",
+ "pulp",
+ "raw-cpuid",
+ "seq-macro",
+ "sysctl",
+]
+
+[[package]]
+name = "gemm-f16"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3df7a55202e6cd6739d82ae3399c8e0c7e1402859b30e4cb780e61525d9486e"
+dependencies = [
+ "dyn-stack",
+ "gemm-common",
+ "gemm-f32",
+ "half",
+ "num-complex",
+ "num-traits",
+ "paste",
+ "raw-cpuid",
+ "seq-macro",
+]
+
+[[package]]
+name = "gemm-f32"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02e0b8c9da1fbec6e3e3ab2ce6bc259ef18eb5f6f0d3e4edf54b75f9fd41a81c"
+dependencies = [
+ "dyn-stack",
+ "gemm-common",
+ "num-complex",
+ "num-traits",
+ "paste",
+ "raw-cpuid",
+ "seq-macro",
+]
+
+[[package]]
+name = "gemm-f64"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "056131e8f2a521bfab322f804ccd652520c79700d81209e9d9275bbdecaadc6a"
+dependencies = [
+ "dyn-stack",
+ "gemm-common",
+ "num-complex",
+ "num-traits",
+ "paste",
+ "raw-cpuid",
+ "seq-macro",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi 6.0.0",
+ "rand_core 0.10.1",
+ "wasip2",
+ "wasip3",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gimli"
+version = "0.32.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
+
+[[package]]
+name = "gl_generator"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d"
+dependencies = [
+ "khronos_api",
+ "log",
+ "xml-rs",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+
+[[package]]
+name = "glow"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5"
+dependencies = [
+ "js-sys",
+ "slotmap",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "glutin_wgl_sys"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e"
+dependencies = [
+ "gl_generator",
+]
+
+[[package]]
+name = "gpu-allocator"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795"
+dependencies = [
+ "ash",
+ "hashbrown 0.16.1",
+ "log",
+ "presser",
+ "thiserror 2.0.18",
+ "windows",
+]
+
+[[package]]
+name = "gpu-descriptor"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca"
+dependencies = [
+ "bitflags",
+ "gpu-descriptor-types",
+ "hashbrown 0.15.5",
+]
+
+[[package]]
+name = "gpu-descriptor-types"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "h2"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "half"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
+dependencies = [
+ "bytemuck",
+ "cfg-if",
+ "crunchy",
+ "num-traits",
+ "serde",
+ "zerocopy",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
+dependencies = [
+ "ahash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash 0.1.5",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash 0.2.0",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "hexf-parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
+
+[[package]]
+name = "http"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "hyper"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+ "webpki-roots",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+
+[[package]]
+name = "icu_properties"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+
+[[package]]
+name = "icu_provider"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "image"
+version = "0.25.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
+dependencies = [
+ "bytemuck",
+ "byteorder-lite",
+ "moxcms",
+ "num-traits",
+ "png",
+ "tiff",
+ "zune-core",
+ "zune-jpeg",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "inflections"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a"
+
+[[package]]
+name = "ipnet"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "j2k"
+version = "0.7.1"
+dependencies = [
+ "criterion",
+ "j2k-codec-math",
+ "j2k-core",
+ "j2k-native",
+ "j2k-test-support",
+ "j2k-types",
+ "proptest",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "j2k-cli"
+version = "0.7.1"
+dependencies = [
+ "j2k",
+ "j2k-jpeg",
+ "j2k-test-support",
+ "j2k-transcode",
+]
+
+[[package]]
+name = "j2k-codec-math"
+version = "0.7.1"
+
+[[package]]
+name = "j2k-compare"
+version = "0.2.0"
+dependencies = [
+ "cc",
+ "image",
+ "j2k",
+ "j2k-core",
+ "j2k-native",
+ "j2k-test-support",
+ "openjpeg-sys",
+]
+
+[[package]]
+name = "j2k-core"
+version = "0.7.1"
+dependencies = [
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "j2k-cuda"
+version = "0.7.1"
+dependencies = [
+ "criterion",
+ "j2k",
+ "j2k-core",
+ "j2k-cuda-runtime",
+ "j2k-native",
+ "j2k-profile",
+ "j2k-test-support",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "j2k-cuda-runtime"
+version = "0.7.1"
+dependencies = [
+ "j2k-codec-math",
+ "j2k-core",
+ "j2k-test-support",
+ "libloading 0.9.0",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "j2k-jpeg"
+version = "0.7.1"
+dependencies = [
+ "criterion",
+ "j2k-codec-math",
+ "j2k-core",
+ "j2k-profile",
+ "j2k-test-support",
+ "jpeg-decoder",
+ "memchr",
+ "proptest",
+ "rayon",
+ "thiserror 2.0.18",
+ "zune-core",
+ "zune-jpeg",
+]
+
+[[package]]
+name = "j2k-jpeg-cuda"
+version = "0.7.1"
+dependencies = [
+ "criterion",
+ "j2k-core",
+ "j2k-cuda-runtime",
+ "j2k-jpeg",
+ "j2k-profile",
+ "j2k-test-support",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "j2k-jpeg-metal"
+version = "0.7.1"
+dependencies = [
+ "criterion",
+ "j2k-core",
+ "j2k-jpeg",
+ "j2k-metal-support",
+ "j2k-profile",
+ "j2k-test-support",
+ "jpeg-decoder",
+ "jpeg-encoder",
+ "metal",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "j2k-metal"
+version = "0.7.1"
+dependencies = [
+ "cc",
+ "j2k",
+ "j2k-codec-math",
+ "j2k-core",
+ "j2k-metal-support",
+ "j2k-native",
+ "j2k-profile",
+ "j2k-test-support",
+ "libc",
+ "metal",
+ "rayon",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "j2k-metal-support"
+version = "0.7.1"
+dependencies = [
+ "j2k-core",
+ "j2k-test-support",
+ "log",
+ "metal",
+]
+
+[[package]]
+name = "j2k-ml"
+version = "0.7.1"
+dependencies = [
+ "burn-autodiff",
+ "burn-core",
+ "burn-cubecl",
+ "burn-cuda",
+ "burn-flex",
+ "burn-fusion",
+ "burn-ir",
+ "burn-ndarray",
+ "burn-wgpu",
+ "criterion",
+ "j2k",
+ "j2k-cuda",
+ "j2k-cuda-runtime",
+ "j2k-metal",
+ "j2k-test-support",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "j2k-native"
+version = "0.7.1"
+dependencies = [
+ "criterion",
+ "fearless_simd",
+ "j2k-codec-math",
+ "j2k-profile",
+ "j2k-types",
+ "libm",
+ "log",
+ "rayon",
+]
+
+[[package]]
+name = "j2k-profile"
+version = "0.7.1"
+
+[[package]]
+name = "j2k-test-support"
+version = "0.7.1"
+dependencies = [
+ "j2k-native",
+]
+
+[[package]]
+name = "j2k-tilecodec"
+version = "0.7.1"
+dependencies = [
+ "criterion",
+ "flate2",
+ "j2k-core",
+ "thiserror 2.0.18",
+ "weezl",
+ "zstd",
+]
+
+[[package]]
+name = "j2k-transcode"
+version = "0.7.1"
+dependencies = [
+ "criterion",
+ "j2k",
+ "j2k-codec-math",
+ "j2k-core",
+ "j2k-jpeg",
+ "j2k-native",
+ "j2k-profile",
+ "j2k-test-support",
+ "j2k-transcode-test-support",
+ "proptest",
+ "rayon",
+]
+
+[[package]]
+name = "j2k-transcode-cuda"
+version = "0.7.1"
+dependencies = [
+ "j2k-core",
+ "j2k-cuda-runtime",
+ "j2k-native",
+ "j2k-test-support",
+ "j2k-transcode",
+ "j2k-transcode-test-support",
+]
+
+[[package]]
+name = "j2k-transcode-metal"
+version = "0.7.1"
+dependencies = [
+ "criterion",
+ "j2k-codec-math",
+ "j2k-core",
+ "j2k-jpeg",
+ "j2k-metal",
+ "j2k-metal-support",
+ "j2k-native",
+ "j2k-profile",
+ "j2k-test-support",
+ "j2k-transcode",
+ "j2k-transcode-test-support",
+ "metal",
+ "rayon",
+]
+
+[[package]]
+name = "j2k-transcode-test-support"
+version = "0.7.1"
+dependencies = [
+ "j2k-native",
+ "j2k-transcode",
+ "j2k-types",
+]
+
+[[package]]
+name = "j2k-types"
+version = "0.7.1"
+
+[[package]]
+name = "jni-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
+dependencies = [
+ "jni-sys 0.4.1",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
+dependencies = [
+ "jni-sys-macros",
+]
+
+[[package]]
+name = "jni-sys-macros"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "jobserver"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
+dependencies = [
+ "getrandom 0.3.4",
+ "libc",
+]
+
+[[package]]
+name = "jpeg-decoder"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07"
+
+[[package]]
+name = "jpeg-encoder"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b0b36cbb4e6704f12f5b5d7b01dac593982c6550859ebd5a66fb15c9ea27fd5"
+
+[[package]]
+name = "js-sys"
+version = "0.3.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "khronos-egl"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76"
+dependencies = [
+ "libc",
+ "libloading 0.8.9",
+ "pkg-config",
+]
+
+[[package]]
+name = "khronos_api"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "libloading"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
+dependencies = [
+ "cfg-if",
+ "windows-link",
+]
+
+[[package]]
+name = "libloading"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60"
+dependencies = [
+ "cfg-if",
+ "windows-link",
+]
+
+[[package]]
+name = "liblzma"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45aec2360b3933207e27908049d8e4df4e476b58180afb1e56b2a4fb72efe4ba"
+dependencies = [
+ "liblzma-sys",
+ "num_cpus",
+]
+
+[[package]]
+name = "liblzma-sys"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a046c7f353ba30f810545151e04f63545833803f5b86ee3ddf1517247fe560a5"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "libm"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+
+[[package]]
+name = "libredox"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "libz-sys"
+version = "1.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9"
+dependencies = [
+ "cc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "litemap"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+
+[[package]]
+name = "litrs"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
+
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
+[[package]]
+name = "macerator"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "508a2f720538bb7e3ea3cb6d098615b107cb82ae387d77d906276905e9ec894b"
+dependencies = [
+ "bytemuck",
+ "cfg_aliases",
+ "half",
+ "macerator-macros",
+ "moddef",
+ "num-traits",
+ "paste",
+ "rustc_version",
+]
+
+[[package]]
+name = "macerator-macros"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5ce85961d618ce9794bdf822bfe96fe9dd341aa5b033b454f7a8d96e79b9b1"
+dependencies = [
+ "darling 0.20.11",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "malloc_buf"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "matrixmultiply"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7"
+dependencies = [
+ "autocfg",
+ "rawpointer",
+]
+
+[[package]]
+name = "md5"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c"
+
+[[package]]
+name = "memchr"
+version = "2.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
+
+[[package]]
+name = "metal"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15"
+dependencies = [
+ "bitflags",
+ "block",
+ "core-graphics-types",
+ "foreign-types",
+ "log",
+ "objc",
+ "paste",
+]
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "moddef"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a0b3262dc837d2513fe2ef31ff8461352ef932dcca31ba0c0abe33547cf6b9b"
+
+[[package]]
+name = "moxcms"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
+dependencies = [
+ "num-traits",
+ "pxfm",
+]
+
+[[package]]
+name = "naga"
+version = "29.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2bf919621e7975acb27d881bae2fb993e0d45c8e0446e85e6272971e00dc8df"
+dependencies = [
+ "arrayvec",
+ "bit-set 0.9.1",
+ "bitflags",
+ "cfg-if",
+ "cfg_aliases",
+ "codespan-reporting",
+ "half",
+ "hashbrown 0.16.1",
+ "hexf-parse",
+ "indexmap",
+ "libm",
+ "log",
+ "num-traits",
+ "once_cell",
+ "rustc-hash 1.1.0",
+ "spirv",
+ "thiserror 2.0.18",
+ "unicode-ident",
+]
+
+[[package]]
+name = "nb"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f"
+dependencies = [
+ "nb 1.1.0",
+]
+
+[[package]]
+name = "nb"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d"
+
+[[package]]
+name = "ndarray"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d"
+dependencies = [
+ "matrixmultiply",
+ "num-complex",
+ "num-integer",
+ "num-traits",
+ "portable-atomic",
+ "portable-atomic-util",
+ "rawpointer",
+]
+
+[[package]]
+name = "ndk-sys"
+version = "0.6.0+11769913"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
+dependencies = [
+ "jni-sys 0.3.1",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "nom"
+version = "8.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "ntapi"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "num"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
+dependencies = [
+ "num-bigint",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-complex"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
+dependencies = [
+ "bytemuck",
+ "num-traits",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+ "libm",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "objc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
+dependencies = [
+ "malloc_buf",
+]
+
+[[package]]
+name = "objc2"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
+dependencies = [
+ "objc2-encode",
+]
+
+[[package]]
+name = "objc2-core-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
+dependencies = [
+ "bitflags",
+ "dispatch2",
+ "objc2",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "objc2-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
+dependencies = [
+ "bitflags",
+ "objc2",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-io-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15"
+dependencies = [
+ "libc",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-metal"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
+dependencies = [
+ "bitflags",
+ "block2",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
+dependencies = [
+ "bitflags",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "objc2-metal",
+]
+
+[[package]]
+name = "object"
+version = "0.37.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "oneshot"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe21416a02c693fb9f980befcb230ecc70b0b3d1cc4abf88b9675c4c1457f0c"
+
+[[package]]
+name = "oorandom"
+version = "11.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
+
+[[package]]
+name = "openjpeg-sys"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92682cb92e7b01e2c020cf8ec12f3374558924bb24621df1112d8a7c93d419ef"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "ordered-float"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "page_size"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "petgraph"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
+dependencies = [
+ "fixedbitset",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "serde",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+
+[[package]]
+name = "png"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
+dependencies = [
+ "bitflags",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "portable-atomic"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "presser"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "profiling"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5"
+
+[[package]]
+name = "proptest"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
+dependencies = [
+ "bit-set 0.8.0",
+ "bit-vec 0.8.0",
+ "bitflags",
+ "num-traits",
+ "rand 0.9.4",
+ "rand_chacha",
+ "rand_xorshift",
+ "regex-syntax",
+ "rusty-fork",
+ "tempfile",
+ "unarray",
+]
+
+[[package]]
+name = "pulp"
+version = "0.22.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a"
+dependencies = [
+ "bytemuck",
+ "cfg-if",
+ "libm",
+ "num-complex",
+ "paste",
+ "pulp-wasm-simd-flag",
+ "raw-cpuid",
+ "reborrow",
+ "version_check",
+]
+
+[[package]]
+name = "pulp-wasm-simd-flag"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740"
+
+[[package]]
+name = "pxfm"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
+
+[[package]]
+name = "quick-error"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
+
+[[package]]
+name = "quick-error"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
+
+[[package]]
+name = "quinn"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash 2.1.3",
+ "rustls",
+ "socket2",
+ "thiserror 2.0.18",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
+dependencies = [
+ "bytes",
+ "getrandom 0.4.2",
+ "lru-slab",
+ "rand 0.10.2",
+ "rand_pcg",
+ "ring",
+ "rustc-hash 2.1.3",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror 2.0.18",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rand"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
+dependencies = [
+ "rand_chacha",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
+dependencies = [
+ "chacha20",
+ "getrandom 0.4.2",
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+
+[[package]]
+name = "rand_distr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d431c2703ccf129de4d45253c03f49ebb22b97d6ad79ee3ecfc7e3f4862c1d8"
+dependencies = [
+ "num-traits",
+ "rand 0.10.2",
+]
+
+[[package]]
+name = "rand_pcg"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
+dependencies = [
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "rand_xorshift"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
+dependencies = [
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "range-alloc"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08"
+
+[[package]]
+name = "raw-cpuid"
+version = "11.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "raw-window-handle"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
+
+[[package]]
+name = "raw-window-metal"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135"
+dependencies = [
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "objc2-quartz-core",
+]
+
+[[package]]
+name = "rawpointer"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
+
+[[package]]
+name = "rayon"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "reborrow"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430"
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
+dependencies = [
+ "getrandom 0.2.17",
+ "libredox",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "renderdoc-sys"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
+
+[[package]]
+name = "reqwest"
+version = "0.12.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "webpki-roots",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rmp"
+version = "0.8.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "rmp-serde"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"
+dependencies = [
+ "rmp",
+ "serde",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb"
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f"
+dependencies = [
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[package]]
+name = "rusty-fork"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2"
+dependencies = [
+ "fnv",
+ "quick-error 1.2.3",
+ "tempfile",
+ "wait-timeout",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "sanitize-filename"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc984f4f9ceb736a7bb755c3e3bd17dc56370af2600c9780dcc48c66453da34d"
+dependencies = [
+ "regex",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
+[[package]]
+name = "seq-macro"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_bytes"
+version = "0.11.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_yaml_ng"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "slotmap"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "slug"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724"
+dependencies = [
+ "deunicode",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "socket2"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "spin"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3"
+dependencies = [
+ "lock_api",
+ "portable-atomic",
+]
+
+[[package]]
+name = "spirv"
+version = "0.4.0+sdk-1.4.341.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "stable-vec"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dac7bc0f7d0d44329b200020effbc25a534d89fa142af95e3ddf76113412a5e"
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "strum"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "sysctl"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc"
+dependencies = [
+ "bitflags",
+ "byteorder",
+ "enum-as-inner",
+ "libc",
+ "thiserror 1.0.69",
+ "walkdir",
+]
+
+[[package]]
+name = "sysinfo"
+version = "0.38.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f"
+dependencies = [
+ "libc",
+ "memchr",
+ "ntapi",
+ "objc2-core-foundation",
+ "objc2-io-kit",
+ "windows",
+]
+
+[[package]]
+name = "tar"
+version = "0.4.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
+dependencies = [
+ "filetime",
+ "libc",
+ "xattr",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.2",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "text_placeholder"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd5008f74a09742486ef0047596cf35df2b914e2a8dca5727fcb6ba6842a766b"
+dependencies = [
+ "hashbrown 0.13.2",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl 2.0.18",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tiff"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
+dependencies = [
+ "fax",
+ "flate2",
+ "half",
+ "quick-error 2.0.1",
+ "weezl",
+ "zune-jpeg",
+]
+
+[[package]]
+name = "tiny-keccak"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
+dependencies = [
+ "crunchy",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinytemplate"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
+dependencies = [
+ "serde",
+ "serde_json",
]
[[package]]
-name = "j2k-native"
-version = "0.7.0"
+name = "tinyvec"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
dependencies = [
- "criterion",
- "fearless_simd",
- "j2k-codec-math",
- "j2k-profile",
- "j2k-types",
- "libm",
- "log",
- "rayon",
+ "tinyvec_macros",
]
[[package]]
-name = "j2k-profile"
-version = "0.7.0"
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
-name = "j2k-test-support"
-version = "0.7.0"
+name = "tokio"
+version = "1.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
dependencies = [
- "j2k-native",
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "windows-sys 0.61.2",
]
[[package]]
-name = "j2k-tilecodec"
-version = "0.7.0"
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
- "criterion",
- "flate2",
- "j2k-core",
- "thiserror",
- "weezl",
- "zstd",
+ "rustls",
+ "tokio",
]
[[package]]
-name = "j2k-transcode"
-version = "0.7.0"
+name = "tokio-util"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
dependencies = [
- "criterion",
- "j2k",
- "j2k-codec-math",
- "j2k-core",
- "j2k-jpeg",
- "j2k-native",
- "j2k-profile",
- "j2k-test-support",
- "j2k-transcode-test-support",
- "proptest",
- "rayon",
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
]
[[package]]
-name = "j2k-transcode-cuda"
-version = "0.7.0"
+name = "toml"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
dependencies = [
- "j2k-core",
- "j2k-cuda-runtime",
- "j2k-native",
- "j2k-test-support",
- "j2k-transcode",
- "j2k-transcode-test-support",
+ "indexmap",
+ "serde_core",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_parser",
+ "toml_writer",
+ "winnow",
]
[[package]]
-name = "j2k-transcode-metal"
-version = "0.7.0"
+name = "toml_datetime"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
dependencies = [
- "criterion",
- "j2k-codec-math",
- "j2k-core",
- "j2k-jpeg",
- "j2k-metal",
- "j2k-metal-support",
- "j2k-native",
- "j2k-profile",
- "j2k-test-support",
- "j2k-transcode",
- "j2k-transcode-test-support",
- "metal",
- "rayon",
+ "serde_core",
]
[[package]]
-name = "j2k-transcode-test-support"
-version = "0.7.0"
+name = "toml_parser"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
dependencies = [
- "j2k-native",
- "j2k-transcode",
- "j2k-types",
+ "winnow",
]
[[package]]
-name = "j2k-types"
-version = "0.7.0"
+name = "toml_writer"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
[[package]]
-name = "jobserver"
-version = "0.1.34"
+name = "tower"
+version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
dependencies = [
- "getrandom 0.3.4",
- "libc",
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
]
[[package]]
-name = "jpeg-decoder"
-version = "0.3.2"
+name = "tower-http"
+version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "url",
+]
[[package]]
-name = "jpeg-encoder"
-version = "0.7.0"
+name = "tower-layer"
+version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b0b36cbb4e6704f12f5b5d7b01dac593982c6550859ebd5a66fb15c9ea27fd5"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]]
-name = "leb128fmt"
-version = "0.1.0"
+name = "tower-service"
+version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
-name = "libc"
-version = "0.2.186"
+name = "tracel-llvm"
+version = "20.1.4-7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+checksum = "982535db9eb1a30ac0f2c50239a0eec3e5cf50993a88e92b04747bd2f4d365b2"
+dependencies = [
+ "tracel-mlir-rs",
+ "tracel-mlir-sys",
+]
[[package]]
-name = "libloading"
-version = "0.9.0"
+name = "tracel-llvm-bundler"
+version = "20.1.4-7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60"
+checksum = "3c75b8e477cb8d49d907afab029ca74d48459f5b88c27bdb4c6cd6acb5e61977"
dependencies = [
- "cfg-if",
- "windows-link",
+ "anyhow",
+ "bytes",
+ "constcat",
+ "dirs",
+ "liblzma",
+ "regex",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "sha2",
+ "tar",
+ "walkdir",
]
[[package]]
-name = "libm"
-version = "0.2.16"
+name = "tracel-mlir-rs"
+version = "20.1.4-7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+checksum = "77a478a35efd68d0ba73f747adfb7923b121c64e7f5be9cd8364ca1dcb772d5c"
+dependencies = [
+ "tracel-mlir-rs-macros",
+ "tracel-mlir-sys",
+]
[[package]]
-name = "libz-sys"
-version = "1.1.29"
+name = "tracel-mlir-rs-macros"
+version = "20.1.4-7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9"
+checksum = "7a94f36868c3b10b1825945223d99d106c73f4d249f063caa4651deeb9379344"
dependencies = [
- "cc",
- "pkg-config",
- "vcpkg",
+ "comrak",
+ "convert_case 0.8.0",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "syn",
+ "tracel-llvm-bundler",
+ "tracel-tblgen-rs",
+ "unindent",
]
[[package]]
-name = "linux-raw-sys"
-version = "0.12.1"
+name = "tracel-mlir-sys"
+version = "20.1.4-7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+checksum = "02f26d31af0c225a6d2e3d65d012fd6de848c9fc776897b152ee83b7d1bd15c4"
+dependencies = [
+ "tracel-llvm-bundler",
+]
[[package]]
-name = "log"
-version = "0.4.32"
+name = "tracel-tblgen-rs"
+version = "20.1.4-7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
+checksum = "00d2581070380418ccc33b500f3739e4d4869421fdb477fcea51ff97c6253a52"
+dependencies = [
+ "bindgen",
+ "cc",
+ "paste",
+ "thiserror 2.0.18",
+ "tracel-llvm-bundler",
+]
[[package]]
-name = "malloc_buf"
-version = "0.0.6"
+name = "tracing"
+version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
- "libc",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
]
[[package]]
-name = "memchr"
-version = "2.8.2"
+name = "tracing-attributes"
+version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
[[package]]
-name = "metal"
-version = "0.33.0"
+name = "tracing-core"
+version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
- "bitflags",
- "block",
- "core-graphics-types",
- "foreign-types",
- "log",
- "objc",
- "paste",
+ "once_cell",
]
[[package]]
-name = "miniz_oxide"
-version = "0.8.9"
+name = "try-lock"
+version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
-dependencies = [
- "adler2",
- "simd-adler32",
-]
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
-name = "moxcms"
-version = "0.8.1"
+name = "tynm"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
+checksum = "a21cdb0fc8f85c98b1ec812bc4cd69faf6c0fa2fc17d44ea3c2cdd38dc08e999"
dependencies = [
- "num-traits",
- "pxfm",
+ "nom 8.0.0",
]
[[package]]
-name = "num-traits"
-version = "0.2.19"
+name = "type-map"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90"
dependencies = [
- "autocfg",
+ "rustc-hash 2.1.3",
]
[[package]]
-name = "objc"
-version = "0.2.7"
+name = "typed-arena"
+version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
-dependencies = [
- "malloc_buf",
-]
+checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
[[package]]
-name = "once_cell"
-version = "1.21.4"
+name = "typenum"
+version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
-name = "oorandom"
-version = "11.1.5"
+name = "unarray"
+version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
+checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
[[package]]
-name = "openjpeg-sys"
-version = "1.0.12"
+name = "unicode-ident"
+version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92682cb92e7b01e2c020cf8ec12f3374558924bb24621df1112d8a7c93d419ef"
-dependencies = [
- "cc",
- "libc",
-]
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
-name = "page_size"
-version = "0.6.0"
+name = "unicode-normalization"
+version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
+checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
dependencies = [
- "libc",
- "winapi",
+ "tinyvec",
]
[[package]]
-name = "paste"
-version = "1.0.15"
+name = "unicode-segmentation"
+version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]]
-name = "pkg-config"
-version = "0.3.33"
+name = "unicode-width"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
-name = "png"
-version = "0.18.1"
+name = "unicode-xid"
+version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
-dependencies = [
- "bitflags",
- "crc32fast",
- "fdeflate",
- "flate2",
- "miniz_oxide",
-]
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
-name = "ppv-lite86"
-version = "0.2.21"
+name = "unicode_categories"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
-dependencies = [
- "zerocopy",
-]
+checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
+
+[[package]]
+name = "unindent"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
[[package]]
-name = "prettyplease"
-version = "0.2.37"
+name = "unsafe-libyaml"
+version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
-dependencies = [
- "proc-macro2",
- "syn",
-]
+checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
-name = "proc-macro2"
-version = "1.0.106"
+name = "untrusted"
+version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
-dependencies = [
- "unicode-ident",
-]
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
-name = "proptest"
-version = "1.11.0"
+name = "unty"
+version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
-dependencies = [
- "bit-set",
- "bit-vec",
- "bitflags",
- "num-traits",
- "rand",
- "rand_chacha",
- "rand_xorshift",
- "regex-syntax",
- "rusty-fork",
- "tempfile",
- "unarray",
-]
+checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
[[package]]
-name = "pxfm"
-version = "0.1.29"
+name = "url"
+version = "2.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
[[package]]
-name = "quick-error"
-version = "1.2.3"
+name = "utf8_iter"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
-name = "quick-error"
-version = "2.0.1"
+name = "uuid"
+version = "1.23.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
+checksum = "ea5fab0d6c3c01ae70085a09cb03d4c7a1d6314e2b3e075392783396d724ca0a"
[[package]]
-name = "quote"
-version = "1.0.45"
+name = "variadics_please"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+checksum = "41b6d82be61465f97d42bd1d15bf20f3b0a3a0905018f38f9d6f6962055b0b5c"
dependencies = [
"proc-macro2",
+ "quote",
+ "syn",
]
[[package]]
-name = "r-efi"
-version = "5.3.0"
+name = "vcpkg"
+version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
-name = "r-efi"
-version = "6.0.0"
+name = "version_check"
+version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
-name = "rand"
-version = "0.9.4"
+name = "void"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
-dependencies = [
- "rand_chacha",
- "rand_core",
-]
+checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
[[package]]
-name = "rand_chacha"
-version = "0.9.0"
+name = "wait-timeout"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
dependencies = [
- "ppv-lite86",
- "rand_core",
+ "libc",
]
[[package]]
-name = "rand_core"
-version = "0.9.5"
+name = "walkdir"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
- "getrandom 0.3.4",
+ "same-file",
+ "winapi-util",
]
[[package]]
-name = "rand_xorshift"
-version = "0.4.0"
+name = "want"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
dependencies = [
- "rand_core",
+ "try-lock",
]
[[package]]
-name = "rayon"
-version = "1.12.0"
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.4+wasi-0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
dependencies = [
- "either",
- "rayon-core",
+ "wit-bindgen 0.57.1",
]
[[package]]
-name = "rayon-core"
-version = "1.13.0"
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
- "crossbeam-deque",
- "crossbeam-utils",
+ "wit-bindgen 0.51.0",
]
[[package]]
-name = "regex"
-version = "1.12.4"
+name = "wasm-bindgen"
+version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
+checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
dependencies = [
- "aho-corasick",
- "memchr",
- "regex-automata",
- "regex-syntax",
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
]
[[package]]
-name = "regex-automata"
-version = "0.4.14"
+name = "wasm-bindgen-futures"
+version = "0.4.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d"
dependencies = [
- "aho-corasick",
- "memchr",
- "regex-syntax",
+ "js-sys",
+ "wasm-bindgen",
]
[[package]]
-name = "regex-syntax"
-version = "0.8.11"
+name = "wasm-bindgen-macro"
+version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
[[package]]
-name = "rustix"
-version = "1.1.4"
+name = "wasm-bindgen-macro-support"
+version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
dependencies = [
- "bitflags",
- "errno",
- "libc",
- "linux-raw-sys",
- "windows-sys",
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
]
[[package]]
-name = "rusty-fork"
-version = "0.3.1"
+name = "wasm-bindgen-shared"
+version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2"
+checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
dependencies = [
- "fnv",
- "quick-error 1.2.3",
- "tempfile",
- "wait-timeout",
+ "unicode-ident",
]
[[package]]
-name = "ryu"
-version = "1.0.23"
+name = "wasm-encoder"
+version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
[[package]]
-name = "same-file"
-version = "1.0.6"
+name = "wasm-metadata"
+version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
- "winapi-util",
+ "anyhow",
+ "indexmap",
+ "wasm-encoder",
+ "wasmparser",
]
[[package]]
-name = "semver"
-version = "1.0.28"
+name = "wasmparser"
+version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
[[package]]
-name = "serde"
-version = "1.0.228"
+name = "wayland-sys"
+version = "0.31.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be"
dependencies = [
- "serde_core",
- "serde_derive",
+ "dlib",
+ "log",
+ "once_cell",
+ "pkg-config",
]
[[package]]
-name = "serde_core"
-version = "1.0.228"
+name = "web-sys"
+version = "0.3.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
dependencies = [
- "serde_derive",
+ "js-sys",
+ "wasm-bindgen",
]
[[package]]
-name = "serde_derive"
-version = "1.0.228"
+name = "web-time"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "js-sys",
+ "wasm-bindgen",
]
[[package]]
-name = "serde_json"
-version = "1.0.150"
+name = "webpki-roots"
+version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf"
dependencies = [
- "itoa",
- "memchr",
- "serde",
- "serde_core",
- "zmij",
+ "rustls-pki-types",
]
[[package]]
-name = "serde_spanned"
-version = "1.1.1"
+name = "weezl"
+version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
+checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
+
+[[package]]
+name = "wgpu"
+version = "29.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76e8840e1ba2881d4cbb18d2147627a56af426ff064c0401eb0c8410c6325d07"
dependencies = [
- "serde_core",
+ "arrayvec",
+ "bitflags",
+ "bytemuck",
+ "cfg-if",
+ "cfg_aliases",
+ "document-features",
+ "hashbrown 0.16.1",
+ "js-sys",
+ "log",
+ "naga",
+ "parking_lot",
+ "portable-atomic",
+ "profiling",
+ "raw-window-handle",
+ "smallvec",
+ "static_assertions",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "wgpu-core",
+ "wgpu-hal",
+ "wgpu-types",
]
[[package]]
-name = "serde_yaml_ng"
-version = "0.10.0"
+name = "wgpu-core"
+version = "29.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f"
+checksum = "2f519832254e56965a9940c4af57dcb75f702b6f6fa4a0b172f685395843a4d7"
dependencies = [
+ "arrayvec",
+ "bit-set 0.9.1",
+ "bit-vec 0.9.1",
+ "bitflags",
+ "bytemuck",
+ "cfg_aliases",
+ "document-features",
+ "hashbrown 0.16.1",
"indexmap",
- "itoa",
- "ryu",
- "serde",
- "unsafe-libyaml",
+ "log",
+ "naga",
+ "once_cell",
+ "parking_lot",
+ "portable-atomic",
+ "profiling",
+ "raw-window-handle",
+ "rustc-hash 1.1.0",
+ "smallvec",
+ "thiserror 2.0.18",
+ "wgpu-core-deps-apple",
+ "wgpu-core-deps-emscripten",
+ "wgpu-core-deps-windows-linux-android",
+ "wgpu-hal",
+ "wgpu-naga-bridge",
+ "wgpu-types",
]
[[package]]
-name = "shlex"
-version = "2.0.1"
+name = "wgpu-core-deps-apple"
+version = "29.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+checksum = "f5e39e26c4c0e07589e67d18546cf79ff45383659fc72fca4dd293358a0347f3"
+dependencies = [
+ "wgpu-hal",
+]
[[package]]
-name = "simd-adler32"
-version = "0.3.9"
+name = "wgpu-core-deps-emscripten"
+version = "29.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
+checksum = "01e09be551dc939498bdd5f6b2c66e55ab275dad25825267a08605a80fc9f0af"
+dependencies = [
+ "wgpu-hal",
+]
[[package]]
-name = "syn"
-version = "2.0.117"
+name = "wgpu-core-deps-windows-linux-android"
+version = "29.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+checksum = "4e592c1bbef6ad047647ae6e666ebd8cee7a32bb4544d9700ec96cbf73230257"
dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
+ "wgpu-hal",
]
[[package]]
-name = "tempfile"
-version = "3.27.0"
+name = "wgpu-hal"
+version = "29.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+checksum = "97ace1c17727311c22a46e4e3faf56ea6de81af99dcc839bdfb54857b94d448d"
dependencies = [
- "fastrand",
- "getrandom 0.4.2",
+ "android_system_properties",
+ "arrayvec",
+ "ash",
+ "bit-set 0.9.1",
+ "bitflags",
+ "block2",
+ "bytemuck",
+ "cfg-if",
+ "cfg_aliases",
+ "glow",
+ "glutin_wgl_sys",
+ "gpu-allocator",
+ "gpu-descriptor",
+ "hashbrown 0.16.1",
+ "js-sys",
+ "khronos-egl",
+ "libc",
+ "libloading 0.8.9",
+ "log",
+ "naga",
+ "ndk-sys",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "objc2-metal",
+ "objc2-quartz-core",
"once_cell",
- "rustix",
- "windows-sys",
+ "ordered-float",
+ "parking_lot",
+ "portable-atomic",
+ "portable-atomic-util",
+ "profiling",
+ "range-alloc",
+ "raw-window-handle",
+ "raw-window-metal",
+ "renderdoc-sys",
+ "smallvec",
+ "thiserror 2.0.18",
+ "wasm-bindgen",
+ "wayland-sys",
+ "web-sys",
+ "wgpu-naga-bridge",
+ "wgpu-types",
+ "windows",
+ "windows-core",
+ "windows-result",
]
[[package]]
-name = "thiserror"
-version = "2.0.18"
+name = "wgpu-naga-bridge"
+version = "29.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+checksum = "95226013f547544b223281cd16a4fb549aa9dcb562adbda0faae4c73ffbbc161"
dependencies = [
- "thiserror-impl",
+ "naga",
+ "wgpu-types",
]
[[package]]
-name = "thiserror-impl"
-version = "2.0.18"
+name = "wgpu-types"
+version = "29.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+checksum = "84bf84cd9ca8ca45e2b223a3868f1adf9bfc0c66aeac212e76ee7e40fdadf8f5"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "bitflags",
+ "bytemuck",
+ "js-sys",
+ "log",
+ "raw-window-handle",
+ "web-sys",
]
[[package]]
-name = "tiff"
-version = "0.11.3"
+name = "winapi"
+version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
- "fax",
- "flate2",
- "half",
- "quick-error 2.0.1",
- "weezl",
- "zune-jpeg",
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
]
[[package]]
-name = "tinytemplate"
-version = "1.2.1"
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
-dependencies = [
- "serde",
- "serde_json",
-]
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
-name = "toml"
-version = "1.1.2+spec-1.1.0"
+name = "winapi-util"
+version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
- "indexmap",
- "serde_core",
- "serde_spanned",
- "toml_datetime",
- "toml_parser",
- "toml_writer",
- "winnow",
+ "windows-sys 0.61.2",
]
[[package]]
-name = "toml_datetime"
-version = "1.1.1+spec-1.1.0"
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
-dependencies = [
- "serde_core",
-]
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
-name = "toml_parser"
-version = "1.1.2+spec-1.1.0"
+name = "windows"
+version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
+checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
- "winnow",
+ "windows-collections",
+ "windows-core",
+ "windows-future",
+ "windows-numerics",
]
[[package]]
-name = "toml_writer"
-version = "1.1.1+spec-1.1.0"
+name = "windows-collections"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
+checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
+dependencies = [
+ "windows-core",
+]
[[package]]
-name = "unarray"
-version = "0.1.4"
+name = "windows-core"
+version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
[[package]]
-name = "unicode-ident"
-version = "1.0.24"
+name = "windows-future"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
+dependencies = [
+ "windows-core",
+ "windows-link",
+ "windows-threading",
+]
[[package]]
-name = "unicode-xid"
-version = "0.2.6"
+name = "windows-implement"
+version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
[[package]]
-name = "unsafe-libyaml"
-version = "0.2.11"
+name = "windows-interface"
+version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
[[package]]
-name = "vcpkg"
-version = "0.2.15"
+name = "windows-link"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
-name = "wait-timeout"
-version = "0.2.1"
+name = "windows-numerics"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
+checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
- "libc",
+ "windows-core",
+ "windows-link",
]
[[package]]
-name = "walkdir"
-version = "2.5.0"
+name = "windows-result"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
- "same-file",
- "winapi-util",
+ "windows-link",
]
[[package]]
-name = "wasip2"
-version = "1.0.4+wasi-0.2.12"
+name = "windows-strings"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
- "wit-bindgen 0.57.1",
+ "windows-link",
]
[[package]]
-name = "wasip3"
-version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+name = "windows-sys"
+version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
- "wit-bindgen 0.51.0",
+ "windows-targets",
]
[[package]]
-name = "wasm-encoder"
-version = "0.244.0"
+name = "windows-sys"
+version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
- "leb128fmt",
- "wasmparser",
+ "windows-link",
]
[[package]]
-name = "wasm-metadata"
-version = "0.244.0"
+name = "windows-targets"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
- "anyhow",
- "indexmap",
- "wasm-encoder",
- "wasmparser",
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
]
[[package]]
-name = "wasmparser"
-version = "0.244.0"
+name = "windows-threading"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
- "bitflags",
- "hashbrown 0.15.5",
- "indexmap",
- "semver",
+ "windows-link",
]
[[package]]
-name = "weezl"
-version = "0.1.12"
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
-name = "winapi"
-version = "0.3.9"
+name = "windows_aarch64_msvc"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
-dependencies = [
- "winapi-i686-pc-windows-gnu",
- "winapi-x86_64-pc-windows-gnu",
-]
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
-name = "winapi-i686-pc-windows-gnu"
-version = "0.4.0"
+name = "windows_i686_gnu"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
-name = "winapi-util"
-version = "0.1.11"
+name = "windows_i686_gnullvm"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
-dependencies = [
- "windows-sys",
-]
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
-name = "winapi-x86_64-pc-windows-gnu"
-version = "0.4.0"
+name = "windows_i686_msvc"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
-name = "windows-link"
-version = "0.2.1"
+name = "windows_x86_64_gnu"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
-name = "windows-sys"
-version = "0.61.2"
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
-dependencies = [
- "windows-link",
-]
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
@@ -1574,6 +5638,28 @@ dependencies = [
"wasmparser",
]
+[[package]]
+name = "writeable"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+
+[[package]]
+name = "xattr"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
+dependencies = [
+ "libc",
+ "rustix",
+]
+
+[[package]]
+name = "xml-rs"
+version = "0.8.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
+
[[package]]
name = "xtask"
version = "0.0.0"
@@ -1592,6 +5678,35 @@ dependencies = [
"toml",
]
+[[package]]
+name = "xxhash-rust"
+version = "0.8.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d93c89cdc2d3a63c3ec48ffe926931bdc069eafa8e4402fe6d8f790c9d1e576"
+
+[[package]]
+name = "yoke"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
[[package]]
name = "zerocopy"
version = "0.8.52"
@@ -1612,6 +5727,66 @@ dependencies = [
"syn",
]
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "zmij"
version = "1.0.21"
diff --git a/Cargo.toml b/Cargo.toml
index b17b3f45..aad9432c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,6 +12,7 @@ members = [
"crates/j2k-jpeg-metal",
"crates/j2k-metal",
"crates/j2k-metal-support",
+ "crates/j2k-ml",
"crates/j2k-native",
"crates/j2k-profile",
"crates/j2k-test-support",
@@ -27,7 +28,7 @@ members = [
exclude = ["third_party/block-0.1.6-patched"]
[workspace.package]
-version = "0.7.0"
+version = "0.7.1"
edition = "2021"
rust-version = "1.96"
license = "MIT OR Apache-2.0"
@@ -63,6 +64,15 @@ serde_json = "1"
serde_yaml_ng = "0.10"
syn = "2.0.117"
toml = "1.1.2"
+burn-core = { version = "0.21.0", default-features = false, features = ["dataset", "std"] }
+burn-autodiff = { version = "0.21.0", default-features = false, features = ["std"] }
+burn-flex = { version = "0.21.0", default-features = false, features = ["std"] }
+burn-ndarray = { version = "0.21.0", default-features = false, features = ["std"] }
+burn-wgpu = { version = "0.21.0", default-features = false, features = ["fusion", "metal", "std"] }
+burn-cuda = { version = "0.21.0", default-features = false, features = ["fusion", "std"] }
+burn-cubecl = { version = "0.21.0", default-features = false, features = ["fusion", "std"] }
+burn-fusion = { version = "0.21.0", default-features = false, features = ["std"] }
+burn-ir = { version = "0.21.0", default-features = false, features = ["std"] }
[patch.crates-io]
# `metal 0.33.0` still depends on `block 0.1.6`. The crates.io copy declares
diff --git a/README.md b/README.md
index 3112296c..8a10ce60 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
**Docs & guides:**
-**Release status:** `0.7.0` is published and security-supported. See the
+**Release status:** `0.7.1` is published and security-supported. See the
[release notes](CHANGELOG.md) and [release policy](docs/release.md).
**Safe public Rust APIs, audited unsafe boundaries, and vendor-independent JPEG 2000 / HTJ2K.**
@@ -36,7 +36,10 @@ JP2/JPH still-image correctness. The living support boundary is
The APIs are general codec APIs. Whole-slide imaging and DICOM tile workloads
are the main public examples and benchmark fixtures because they stress
large tiled images, strict color handling, and high-throughput GPU paths, but
-the decoder, encoder, and transcode crates are not WSI-only.
+the decoder, encoder, and transcode crates are not WSI-only. The
+[digital-pathology workflow audit](docs/digital-pathology-workflow-audit.md)
+defines the container, indexing, color, memory, and validation responsibilities
+that remain outside the codec layer.
## Why J2K exists
@@ -136,6 +139,7 @@ Use lower-level crates only when you need a specific integration point:
| JPEG-to-HTJ2K coefficient-domain transcode | `j2k-transcode` |
| CUDA adapters | `j2k-jpeg-cuda`, `j2k-cuda`, `j2k-transcode-cuda` |
| Metal adapters | `j2k-jpeg-metal`, `j2k-metal`, `j2k-transcode-metal` |
+| Experimental Burn 0.21 tensor decode integration | `j2k-ml` (unpublished) |
| Tile compression codecs | `j2k-tilecodec` |
| Command-line inspection and JPEG-to-HTJ2K smoke transcode | `j2k-cli` |
@@ -226,6 +230,8 @@ Reference files:
environment variables
- [docs/public-support.md](docs/public-support.md) - exact J2K Part 1,
HTJ2K Part 15, JP2/JPH, and out-of-scope support boundary
+- [docs/j2k-ml.md](docs/j2k-ml.md) - Burn tensor layouts, normalization,
+ batching, and accelerator route guarantees
- [docs/release.md](docs/release.md) - release and package validation policy
- [docs/stable-api-1.0.md](docs/stable-api-1.0.md) - stable API snapshot policy
- [CHANGELOG.md](CHANGELOG.md) - current release notes
diff --git a/SECURITY.md b/SECURITY.md
index 3e514dcf..8df6a5e2 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -4,8 +4,9 @@
| Version | Status |
| --- | --- |
-| `0.7.0` workspace | Staged under `Unreleased`; not yet published or security-supported as a release |
-| `0.6.x` | Latest published and security-supported line |
+| `0.7.1` workspace | Local pre-candidate under `Unreleased`; not published or security-supported as a release |
+| `0.7.0` | Latest published and security-supported release |
+| `0.6.x` | Supported for security fixes during the 0.7 transition |
| Earlier than `0.6` | Unsupported |
Security fixes are developed on the staged workspace line and backported to the
@@ -21,8 +22,8 @@ If that button is unavailable, do not put vulnerability details in a public
issue. Open a [minimal issue](https://github.com/frames-sg/j2k/issues/new)
asking the maintainers to provide a private contact, without naming the
affected code or including proof-of-concept details. A
-verified direct private channel must be published before the staged `0.7.0`
-release is approved.
+verified direct private channel must be published before any future release is
+approved.
The tag-publish preflight authenticates to GitHub and reads the repository's
private-vulnerability-reporting setting. Publication fails closed unless that
diff --git a/crates/j2k-cli/Cargo.toml b/crates/j2k-cli/Cargo.toml
index 9607f229..f1c17830 100644
--- a/crates/j2k-cli/Cargo.toml
+++ b/crates/j2k-cli/Cargo.toml
@@ -18,9 +18,9 @@ path = "src/main.rs"
doc = false
[dependencies]
-j2k = { path = "../j2k", version = "=0.7.0" }
-j2k-jpeg = { path = "../j2k-jpeg", version = "=0.7.0" }
-j2k-transcode = { path = "../j2k-transcode", version = "=0.7.0" }
+j2k = { path = "../j2k", version = "=0.7.1" }
+j2k-jpeg = { path = "../j2k-jpeg", version = "=0.7.1" }
+j2k-transcode = { path = "../j2k-transcode", version = "=0.7.1" }
[dev-dependencies]
j2k-test-support = { path = "../j2k-test-support" }
diff --git a/crates/j2k-codec-math/src/classic.rs b/crates/j2k-codec-math/src/classic.rs
new file mode 100644
index 00000000..bd575228
--- /dev/null
+++ b/crates/j2k-codec-math/src/classic.rs
@@ -0,0 +1,227 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+//! Shared JPEG 2000 classic Tier-1 probability and context tables.
+
+/// One JPEG 2000 MQ probability-state transition.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct ClassicMqState {
+ /// Probability estimate.
+ pub qe: u32,
+ /// Next state after an MPS event.
+ pub nmps: u8,
+ /// Next state after an LPS event.
+ pub nlps: u8,
+ /// Whether an LPS event switches the MPS bit.
+ pub switch: bool,
+}
+
+macro_rules! mq_states {
+ ($($qe:expr, $nmps:expr, $nlps:expr, $switch:expr),+ $(,)?) => {
+ [$(ClassicMqState { qe: $qe, nmps: $nmps, nlps: $nlps, switch: $switch }),+]
+ };
+}
+
+/// MQ values and state transitions from ITU-T T.800 Table C.2.
+#[rustfmt::skip]
+pub const MQ_STATES: [ClassicMqState; 47] = mq_states!(
+ 0x5601, 1, 1, true,
+ 0x3401, 2, 6, false,
+ 0x1801, 3, 9, false,
+ 0x0AC1, 4, 12, false,
+ 0x0521, 5, 29, false,
+ 0x0221, 38, 33, false,
+ 0x5601, 7, 6, true,
+ 0x5401, 8, 14, false,
+ 0x4801, 9, 14, false,
+ 0x3801, 10, 14, false,
+ 0x3001, 11, 17, false,
+ 0x2401, 12, 18, false,
+ 0x1C01, 13, 20, false,
+ 0x1601, 29, 21, false,
+ 0x5601, 15, 14, true,
+ 0x5401, 16, 14, false,
+ 0x5101, 17, 15, false,
+ 0x4801, 18, 16, false,
+ 0x3801, 19, 17, false,
+ 0x3401, 20, 18, false,
+ 0x3001, 21, 19, false,
+ 0x2801, 22, 19, false,
+ 0x2401, 23, 20, false,
+ 0x2201, 24, 21, false,
+ 0x1C01, 25, 22, false,
+ 0x1801, 26, 23, false,
+ 0x1601, 27, 24, false,
+ 0x1401, 28, 25, false,
+ 0x1201, 29, 26, false,
+ 0x1101, 30, 27, false,
+ 0x0AC1, 31, 28, false,
+ 0x09C1, 32, 29, false,
+ 0x08A1, 33, 30, false,
+ 0x0521, 34, 31, false,
+ 0x0441, 35, 32, false,
+ 0x02A1, 36, 33, false,
+ 0x0221, 37, 34, false,
+ 0x0141, 38, 35, false,
+ 0x0111, 39, 36, false,
+ 0x0085, 40, 37, false,
+ 0x0049, 41, 38, false,
+ 0x0025, 42, 39, false,
+ 0x0015, 43, 40, false,
+ 0x0009, 44, 41, false,
+ 0x0005, 45, 42, false,
+ 0x0001, 45, 43, false,
+ 0x5601, 46, 46, false,
+);
+
+const fn mq_qe_values() -> [u32; 47] {
+ let mut values = [0; 47];
+ let mut index = 0;
+ while index < values.len() {
+ values[index] = MQ_STATES[index].qe;
+ index += 1;
+ }
+ values
+}
+
+const fn packed_mq_transitions() -> [u32; 47] {
+ let mut values = [0; 47];
+ let mut index = 0;
+ while index < values.len() {
+ let state = MQ_STATES[index];
+ values[index] =
+ state.nmps as u32 | ((state.nlps as u32) << 8) | ((state.switch as u32) << 16);
+ index += 1;
+ }
+ values
+}
+
+/// Primitive MQ probability values for device compilers that cannot lower arrays of structs.
+pub const MQ_QE_VALUES: [u32; 47] = mq_qe_values();
+/// Packed MQ MPS/LPS transitions and switch flag for device lookup tables.
+pub const PACKED_MQ_TRANSITION_VALUES: [u32; 47] = packed_mq_transitions();
+
+/// Sign-coding context and XOR bit indexed by packed cardinal-neighbor state.
+#[rustfmt::skip]
+pub const SIGN_CONTEXT_LOOKUP: [(u8, u8); 256] = [
+ (9,0), (10,0), (10,1), (0,0), (12,0), (13,0), (11,0), (0,0), (12,1), (11,1),
+ (13,1), (0,0), (0,0), (0,0), (0,0), (0,0), (12,0), (13,0), (11,0), (0,0),
+ (12,0), (13,0), (11,0), (0,0), (9,0), (10,0), (10,1), (0,0), (0,0), (0,0),
+ (0,0), (0,0), (12,1), (11,1), (13,1), (0,0), (9,0), (10,0), (10,1), (0,0),
+ (12,1), (11,1), (13,1), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0),
+ (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0),
+ (0,0), (0,0), (0,0), (10,0), (10,0), (9,0), (0,0), (13,0), (13,0), (12,0),
+ (0,0), (11,1), (11,1), (12,1), (0,0), (0,0), (0,0), (0,0), (0,0), (13,0),
+ (13,0), (12,0), (0,0), (13,0), (13,0), (12,0), (0,0), (10,0), (10,0), (9,0),
+ (0,0), (0,0), (0,0), (0,0), (0,0), (11,1), (11,1), (12,1), (0,0), (10,0),
+ (10,0), (9,0), (0,0), (11,1), (11,1), (12,1), (0,0), (0,0), (0,0), (0,0),
+ (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0),
+ (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (10,1), (9,0), (10,1), (0,0),
+ (11,0), (12,0), (11,0), (0,0), (13,1), (12,1), (13,1), (0,0), (0,0), (0,0),
+ (0,0), (0,0), (11,0), (12,0), (11,0), (0,0), (11,0), (12,0), (11,0), (0,0),
+ (10,1), (9,0), (10,1), (0,0), (0,0), (0,0), (0,0), (0,0), (13,1), (12,1),
+ (13,1), (0,0), (10,1), (9,0), (10,1), (0,0), (13,1), (12,1), (13,1), (0,0),
+ (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0),
+ (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0),
+ (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0),
+ (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0),
+ (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0),
+ (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0),
+ (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0),
+ (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0),
+];
+
+const fn packed_sign_contexts() -> [u16; 256] {
+ let mut values = [0; 256];
+ let mut index = 0;
+ while index < values.len() {
+ let (context, xor) = SIGN_CONTEXT_LOOKUP[index];
+ values[index] = context as u16 | ((xor as u16) << 8);
+ index += 1;
+ }
+ values
+}
+
+/// Packed sign context in the low byte and XOR bit in the high byte.
+pub const PACKED_SIGN_CONTEXT_LOOKUP: [u16; 256] = packed_sign_contexts();
+
+/// Zero-coding contexts for LL and LH sub-bands.
+#[rustfmt::skip]
+pub const ZERO_CTX_LL_LH_LOOKUP: [u8; 256] = [
+ 0, 3, 1, 3, 5, 7, 6, 7, 1, 3, 2, 3, 6, 7, 6, 7, 5, 7, 6, 7, 8, 8, 8, 8, 6,
+ 7, 6, 7, 8, 8, 8, 8, 1, 3, 2, 3, 6, 7, 6, 7, 2, 3, 2, 3, 6, 7, 6, 7, 6, 7,
+ 6, 7, 8, 8, 8, 8, 6, 7, 6, 7, 8, 8, 8, 8, 3, 4, 3, 4, 7, 7, 7, 7, 3, 4, 3,
+ 4, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 7, 7, 7, 7, 8, 8, 8, 8, 3, 4, 3, 4,
+ 7, 7, 7, 7, 3, 4, 3, 4, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 7, 7, 7, 7, 8,
+ 8, 8, 8, 1, 3, 2, 3, 6, 7, 6, 7, 2, 3, 2, 3, 6, 7, 6, 7, 6, 7, 6, 7, 8, 8,
+ 8, 8, 6, 7, 6, 7, 8, 8, 8, 8, 2, 3, 2, 3, 6, 7, 6, 7, 2, 3, 2, 3, 6, 7, 6,
+ 7, 6, 7, 6, 7, 8, 8, 8, 8, 6, 7, 6, 7, 8, 8, 8, 8, 3, 4, 3, 4, 7, 7, 7, 7,
+ 3, 4, 3, 4, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 7, 7, 7, 7, 8, 8, 8, 8, 3,
+ 4, 3, 4, 7, 7, 7, 7, 3, 4, 3, 4, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 7, 7,
+ 7, 7, 8, 8, 8, 8,
+];
+
+/// Zero-coding contexts for HL sub-bands.
+#[rustfmt::skip]
+pub const ZERO_CTX_HL_LOOKUP: [u8; 256] = [
+ 0, 5, 1, 6, 3, 7, 3, 7, 1, 6, 2, 6, 3, 7, 3, 7, 3, 7, 3, 7, 4, 7, 4, 7, 3,
+ 7, 3, 7, 4, 7, 4, 7, 1, 6, 2, 6, 3, 7, 3, 7, 2, 6, 2, 6, 3, 7, 3, 7, 3, 7,
+ 3, 7, 4, 7, 4, 7, 3, 7, 3, 7, 4, 7, 4, 7, 5, 8, 6, 8, 7, 8, 7, 8, 6, 8, 6,
+ 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8, 6, 8, 6, 8,
+ 7, 8, 7, 8, 6, 8, 6, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7,
+ 8, 7, 8, 1, 6, 2, 6, 3, 7, 3, 7, 2, 6, 2, 6, 3, 7, 3, 7, 3, 7, 3, 7, 4, 7,
+ 4, 7, 3, 7, 3, 7, 4, 7, 4, 7, 2, 6, 2, 6, 3, 7, 3, 7, 2, 6, 2, 6, 3, 7, 3,
+ 7, 3, 7, 3, 7, 4, 7, 4, 7, 3, 7, 3, 7, 4, 7, 4, 7, 6, 8, 6, 8, 7, 8, 7, 8,
+ 6, 8, 6, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8, 6,
+ 8, 6, 8, 7, 8, 7, 8, 6, 8, 6, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8,
+ 7, 8, 7, 8, 7, 8,
+];
+
+/// Zero-coding contexts for HH sub-bands.
+#[rustfmt::skip]
+pub const ZERO_CTX_HH_LOOKUP: [u8; 256] = [
+ 0, 1, 3, 4, 1, 2, 4, 5, 3, 4, 6, 7, 4, 5, 7, 7, 1, 2, 4, 5, 2, 2, 5, 5, 4,
+ 5, 7, 7, 5, 5, 7, 7, 3, 4, 6, 7, 4, 5, 7, 7, 6, 7, 8, 8, 7, 7, 8, 8, 4, 5,
+ 7, 7, 5, 5, 7, 7, 7, 7, 8, 8, 7, 7, 8, 8, 1, 2, 4, 5, 2, 2, 5, 5, 4, 5, 7,
+ 7, 5, 5, 7, 7, 2, 2, 5, 5, 2, 2, 5, 5, 5, 5, 7, 7, 5, 5, 7, 7, 4, 5, 7, 7,
+ 5, 5, 7, 7, 7, 7, 8, 8, 7, 7, 8, 8, 5, 5, 7, 7, 5, 5, 7, 7, 7, 7, 8, 8, 7,
+ 7, 8, 8, 3, 4, 6, 7, 4, 5, 7, 7, 6, 7, 8, 8, 7, 7, 8, 8, 4, 5, 7, 7, 5, 5,
+ 7, 7, 7, 7, 8, 8, 7, 7, 8, 8, 6, 7, 8, 8, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8,
+ 8, 7, 7, 8, 8, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 4, 5, 7, 7, 5, 5, 7, 7,
+ 7, 7, 8, 8, 7, 7, 8, 8, 5, 5, 7, 7, 5, 5, 7, 7, 7, 7, 8, 8, 7, 7, 8, 8, 7,
+ 7, 8, 8, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 7, 7, 8, 8, 7, 7, 8, 8, 8, 8,
+ 8, 8, 8, 8, 8, 8,
+];
+
+#[cfg(test)]
+mod tests {
+ use core::hint::black_box;
+
+ use super::*;
+
+ #[test]
+ fn device_tables_match_their_structured_sources_at_runtime() {
+ let build_qe: fn() -> [u32; 47] = mq_qe_values;
+ let build_transitions: fn() -> [u32; 47] = packed_mq_transitions;
+ let build_sign_contexts: fn() -> [u16; 256] = packed_sign_contexts;
+
+ let qe = black_box(build_qe)();
+ let transitions = black_box(build_transitions)();
+ let sign_contexts = black_box(build_sign_contexts)();
+
+ for (index, state) in MQ_STATES.iter().copied().enumerate() {
+ assert_eq!(qe[index], state.qe);
+ assert_eq!(
+ transitions[index],
+ u32::from(state.nmps)
+ | (u32::from(state.nlps) << 8)
+ | (u32::from(state.switch) << 16)
+ );
+ }
+ for (index, (context, xor)) in SIGN_CONTEXT_LOOKUP.iter().copied().enumerate() {
+ assert_eq!(
+ sign_contexts[index],
+ u16::from(context) | (u16::from(xor) << 8)
+ );
+ }
+ }
+}
diff --git a/crates/j2k-codec-math/src/lib.rs b/crates/j2k-codec-math/src/lib.rs
index 394d3ad8..acbc3859 100644
--- a/crates/j2k-codec-math/src/lib.rs
+++ b/crates/j2k-codec-math/src/lib.rs
@@ -10,6 +10,8 @@
#![forbid(unsafe_code)]
#![warn(missing_docs)]
+/// JPEG 2000 classic Tier-1 probability and context tables.
+pub mod classic;
/// JPEG 2000 DWT constants.
pub mod dwt;
/// Generated backend source fragments derived from codec constants.
diff --git a/crates/j2k-compare/Cargo.toml b/crates/j2k-compare/Cargo.toml
index 93274629..f8e2ced5 100644
--- a/crates/j2k-compare/Cargo.toml
+++ b/crates/j2k-compare/Cargo.toml
@@ -13,10 +13,10 @@ publish = false
ignored = ["cc"]
[dependencies]
-j2k-core = { path = "../j2k-core", version = "=0.7.0" }
-j2k-native = { path = "../j2k-native", version = "=0.7.0" }
-j2k = { path = "../j2k", version = "=0.7.0" }
-j2k-test-support = { path = "../j2k-test-support", version = "=0.7.0" }
+j2k-core = { path = "../j2k-core", version = "=0.7.1" }
+j2k-native = { path = "../j2k-native", version = "=0.7.1" }
+j2k = { path = "../j2k", version = "=0.7.1" }
+j2k-test-support = { path = "../j2k-test-support", version = "=0.7.1" }
image = { workspace = true }
openjpeg-sys = { workspace = true }
diff --git a/crates/j2k-cuda-runtime/Cargo.toml b/crates/j2k-cuda-runtime/Cargo.toml
index 19d9c91d..6294bcdd 100644
--- a/crates/j2k-cuda-runtime/Cargo.toml
+++ b/crates/j2k-cuda-runtime/Cargo.toml
@@ -24,8 +24,10 @@ cuda-oxide = [
"cuda-oxide-copy-u8",
"cuda-oxide-j2k-encode",
"cuda-oxide-j2k-decode-store",
+ "cuda-oxide-j2k-classic-decode",
"cuda-oxide-j2k-dequantize",
"cuda-oxide-j2k-idwt",
+ "cuda-oxide-j2k-ml",
"cuda-oxide-htj2k-decode",
"cuda-oxide-htj2k-encode",
"cuda-oxide-transcode",
@@ -35,8 +37,10 @@ cuda-oxide = [
cuda-oxide-copy-u8 = []
cuda-oxide-j2k-encode = []
cuda-oxide-j2k-decode-store = []
+cuda-oxide-j2k-classic-decode = []
cuda-oxide-j2k-dequantize = []
cuda-oxide-j2k-idwt = []
+cuda-oxide-j2k-ml = []
cuda-oxide-htj2k-decode = []
cuda-oxide-htj2k-encode = []
cuda-oxide-transcode = []
@@ -44,8 +48,8 @@ cuda-oxide-jpeg-decode = []
cuda-oxide-jpeg-encode = []
[dependencies]
-j2k-codec-math = { path = "../j2k-codec-math", version = "=0.7.0" }
-j2k-core = { path = "../j2k-core", version = "=0.7.0" }
+j2k-codec-math = { path = "../j2k-codec-math", version = "=0.7.1" }
+j2k-core = { path = "../j2k-core", version = "=0.7.1" }
libloading = { workspace = true }
thiserror = { workspace = true }
diff --git a/crates/j2k-cuda-runtime/build.rs b/crates/j2k-cuda-runtime/build.rs
index b9050b1b..96c33993 100644
--- a/crates/j2k-cuda-runtime/build.rs
+++ b/crates/j2k-cuda-runtime/build.rs
@@ -10,8 +10,10 @@ const CUDA_OXIDE_FEATURE_ENV_VARS: &[&str] = &[
"CARGO_FEATURE_CUDA_OXIDE_COPY_U8",
"CARGO_FEATURE_CUDA_OXIDE_J2K_ENCODE",
"CARGO_FEATURE_CUDA_OXIDE_J2K_DECODE_STORE",
+ "CARGO_FEATURE_CUDA_OXIDE_J2K_CLASSIC_DECODE",
"CARGO_FEATURE_CUDA_OXIDE_J2K_DEQUANTIZE",
"CARGO_FEATURE_CUDA_OXIDE_J2K_IDWT",
+ "CARGO_FEATURE_CUDA_OXIDE_J2K_ML",
"CARGO_FEATURE_CUDA_OXIDE_HTJ2K_DECODE",
"CARGO_FEATURE_CUDA_OXIDE_HTJ2K_ENCODE",
"CARGO_FEATURE_CUDA_OXIDE_TRANSCODE",
@@ -72,6 +74,7 @@ fn main() -> Result<(), Box> {
fn emit_build_script_metadata() {
println!("cargo:rerun-if-changed=../j2k-codec-math/src/lib.rs");
+ println!("cargo:rerun-if-changed=../j2k-codec-math/src/classic.rs");
println!("cargo:rerun-if-changed=../j2k-codec-math/src/dwt.rs");
println!("cargo:rerun-if-changed=../j2k-codec-math/src/jpeg.rs");
println!("cargo:rerun-if-changed=../j2k-codec-math/src/mct.rs");
@@ -94,6 +97,11 @@ fn emit_build_script_metadata() {
println!("cargo:rerun-if-changed=src/cuda_oxide_j2k_decode_store/src/main.rs");
println!("cargo:rerun-if-changed=src/cuda_oxide_j2k_decode_store/simt/Cargo.toml.in");
println!("cargo:rerun-if-changed=src/cuda_oxide_j2k_decode_store/simt/src/main.rs");
+ println!("cargo:rerun-if-changed=src/cuda_oxide_j2k_classic_decode/Cargo.toml.in");
+ println!("cargo:rerun-if-changed=src/cuda_oxide_j2k_classic_decode/rust-toolchain.toml");
+ println!("cargo:rerun-if-changed=src/cuda_oxide_j2k_classic_decode/src/main.rs");
+ println!("cargo:rerun-if-changed=src/cuda_oxide_j2k_classic_decode/simt/Cargo.toml.in");
+ println!("cargo:rerun-if-changed=src/cuda_oxide_j2k_classic_decode/simt/src/main.rs");
println!("cargo:rerun-if-changed=src/cuda_oxide_j2k_dequantize/Cargo.toml.in");
println!("cargo:rerun-if-changed=src/cuda_oxide_j2k_dequantize/rust-toolchain.toml");
println!("cargo:rerun-if-changed=src/cuda_oxide_j2k_dequantize/src/main.rs");
@@ -104,6 +112,11 @@ fn emit_build_script_metadata() {
println!("cargo:rerun-if-changed=src/cuda_oxide_j2k_idwt/src/main.rs");
println!("cargo:rerun-if-changed=src/cuda_oxide_j2k_idwt/simt/Cargo.toml.in");
println!("cargo:rerun-if-changed=src/cuda_oxide_j2k_idwt/simt/src/main.rs");
+ println!("cargo:rerun-if-changed=src/cuda_oxide_j2k_ml/Cargo.toml.in");
+ println!("cargo:rerun-if-changed=src/cuda_oxide_j2k_ml/rust-toolchain.toml");
+ println!("cargo:rerun-if-changed=src/cuda_oxide_j2k_ml/src/main.rs");
+ println!("cargo:rerun-if-changed=src/cuda_oxide_j2k_ml/simt/Cargo.toml.in");
+ println!("cargo:rerun-if-changed=src/cuda_oxide_j2k_ml/simt/src/main.rs");
println!("cargo:rerun-if-changed=src/cuda_oxide_htj2k_decode/Cargo.toml.in");
println!("cargo:rerun-if-changed=src/cuda_oxide_htj2k_decode/rust-toolchain.toml");
println!("cargo:rerun-if-changed=src/cuda_oxide_htj2k_decode/src/main.rs");
@@ -140,8 +153,10 @@ fn emit_build_script_metadata() {
println!("cargo:rustc-check-cfg=cfg(j2k_cuda_oxide_copy_u8_built)");
println!("cargo:rustc-check-cfg=cfg(j2k_cuda_oxide_j2k_encode_built)");
println!("cargo:rustc-check-cfg=cfg(j2k_cuda_oxide_j2k_decode_store_built)");
+ println!("cargo:rustc-check-cfg=cfg(j2k_cuda_oxide_j2k_classic_decode_built)");
println!("cargo:rustc-check-cfg=cfg(j2k_cuda_oxide_j2k_dequantize_built)");
println!("cargo:rustc-check-cfg=cfg(j2k_cuda_oxide_j2k_idwt_built)");
+ println!("cargo:rustc-check-cfg=cfg(j2k_cuda_oxide_j2k_ml_built)");
println!("cargo:rustc-check-cfg=cfg(j2k_cuda_oxide_htj2k_decode_built)");
println!("cargo:rustc-check-cfg=cfg(j2k_cuda_oxide_htj2k_encode_built)");
println!("cargo:rustc-check-cfg=cfg(j2k_cuda_oxide_transcode_built)");
@@ -177,6 +192,12 @@ fn compile_cuda_oxide_feature_projects(context: &BuildContext<'_>) {
println!("cargo:rustc-cfg=j2k_cuda_oxide_j2k_decode_store_built");
}
+ if env::var_os("CARGO_FEATURE_CUDA_OXIDE_J2K_CLASSIC_DECODE").is_some()
+ && compile_cuda_oxide_j2k_classic_decode(context, require_all_cuda_oxide)
+ {
+ println!("cargo:rustc-cfg=j2k_cuda_oxide_j2k_classic_decode_built");
+ }
+
if env::var_os("CARGO_FEATURE_CUDA_OXIDE_J2K_DEQUANTIZE").is_some()
&& compile_cuda_oxide_j2k_dequantize(context, require_all_cuda_oxide)
{
@@ -189,6 +210,12 @@ fn compile_cuda_oxide_feature_projects(context: &BuildContext<'_>) {
println!("cargo:rustc-cfg=j2k_cuda_oxide_j2k_idwt_built");
}
+ if env::var_os("CARGO_FEATURE_CUDA_OXIDE_J2K_ML").is_some()
+ && compile_cuda_oxide_j2k_ml(context, require_all_cuda_oxide)
+ {
+ println!("cargo:rustc-cfg=j2k_cuda_oxide_j2k_ml_built");
+ }
+
if env::var_os("CARGO_FEATURE_CUDA_OXIDE_HTJ2K_DECODE").is_some()
&& compile_cuda_oxide_htj2k_decode(context, require_all_cuda_oxide)
{
@@ -288,6 +315,35 @@ fn compile_cuda_oxide_j2k_idwt(context: &BuildContext<'_>, require_cuda_oxide: b
)
}
+fn compile_cuda_oxide_j2k_classic_decode(
+ context: &BuildContext<'_>,
+ require_cuda_oxide: bool,
+) -> bool {
+ compile_cuda_oxide_project(
+ context,
+ CudaOxideProject {
+ source_dir: Path::new("src/cuda_oxide_j2k_classic_decode"),
+ output_name: "cuda_oxide_j2k_classic_decode.ptx",
+ artifact_name: "j2k_cuda_oxide_j2k_classic_decode.ptx",
+ display_name: "cuda-oxide classic J2K decode",
+ },
+ require_cuda_oxide,
+ )
+}
+
+fn compile_cuda_oxide_j2k_ml(context: &BuildContext<'_>, require_cuda_oxide: bool) -> bool {
+ compile_cuda_oxide_project(
+ context,
+ CudaOxideProject {
+ source_dir: Path::new("src/cuda_oxide_j2k_ml"),
+ output_name: "cuda_oxide_j2k_ml.ptx",
+ artifact_name: "j2k_cuda_oxide_j2k_ml.ptx",
+ display_name: "cuda-oxide j2k-ml",
+ },
+ require_cuda_oxide,
+ )
+}
+
fn compile_cuda_oxide_htj2k_decode(context: &BuildContext<'_>, require_cuda_oxide: bool) -> bool {
compile_cuda_oxide_project(
context,
diff --git a/crates/j2k-cuda-runtime/src/build_flags.rs b/crates/j2k-cuda-runtime/src/build_flags.rs
index 8187c77c..91e546d9 100644
--- a/crates/j2k-cuda-runtime/src/build_flags.rs
+++ b/crates/j2k-cuda-runtime/src/build_flags.rs
@@ -81,6 +81,13 @@ cuda_oxide_ptx_guard!(
"cuda-oxide J2K decode store",
j2k_cuda_oxide_j2k_decode_store_built
);
+cuda_oxide_ptx_guard!(
+ feature = "cuda-oxide-j2k-classic-decode",
+ ensure_cuda_oxide_j2k_classic_decode_ptx_built,
+ CUDA_OXIDE_J2K_CLASSIC_DECODE_PTX_BUILT,
+ "cuda-oxide classic J2K decode",
+ j2k_cuda_oxide_j2k_classic_decode_built
+);
cuda_oxide_ptx_guard!(
feature = "cuda-oxide-j2k-dequantize",
ensure_cuda_oxide_j2k_dequantize_ptx_built,
@@ -95,6 +102,13 @@ cuda_oxide_ptx_guard!(
"cuda-oxide J2K IDWT",
j2k_cuda_oxide_j2k_idwt_built
);
+cuda_oxide_ptx_guard!(
+ feature = "cuda-oxide-j2k-ml",
+ ensure_cuda_oxide_j2k_ml_ptx_built,
+ CUDA_OXIDE_J2K_ML_PTX_BUILT,
+ "cuda-oxide j2k-ml",
+ j2k_cuda_oxide_j2k_ml_built
+);
cuda_oxide_ptx_guard!(
feature = "cuda-oxide-transcode",
ensure_cuda_oxide_transcode_ptx_built,
diff --git a/crates/j2k-cuda-runtime/src/bytes.rs b/crates/j2k-cuda-runtime/src/bytes.rs
index f8a2698a..0b2f4e77 100644
--- a/crates/j2k-cuda-runtime/src/bytes.rs
+++ b/crates/j2k-cuda-runtime/src/bytes.rs
@@ -1,4 +1,7 @@
use crate::{
+ classic_decode::{
+ CudaClassicKernelJob, CudaClassicKernelSegment, CudaClassicKernelTables, CudaClassicStatus,
+ },
error::CudaError,
htj2k_decode::{
CudaHtj2kCleanupMultiKernelJob, CudaHtj2kCodeBlockKernelJob, CudaHtj2kDequantizeKernelJob,
@@ -79,6 +82,7 @@ gpu_ref_bytes! {
store_rgb16_job_as_bytes: CudaJ2kStoreRgb16Job;
store_rgb16_mct_job_as_bytes: CudaJ2kStoreRgb16MctJob;
idwt_job_as_bytes: CudaJ2kIdwtJob;
+ classic_tables_as_bytes: CudaClassicKernelTables;
}
gpu_slice_bytes! {
@@ -112,6 +116,8 @@ gpu_slice_bytes! {
htj2k_cleanup_multi_jobs_as_bytes: CudaHtj2kCleanupMultiKernelJob;
htj2k_dequantize_jobs_as_bytes: CudaHtj2kDequantizeKernelJob;
idwt_multi_jobs_as_bytes: CudaJ2kIdwtMultiKernelJob;
+ classic_jobs_as_bytes: CudaClassicKernelJob;
+ classic_segments_as_bytes: CudaClassicKernelSegment;
}
gpu_slice_bytes_mut! {
@@ -128,6 +134,7 @@ gpu_slice_bytes_mut! {
htj2k_encode_statuses_as_bytes_mut: CudaHtj2kEncodeStatus;
htj2k_packetization_statuses_as_bytes_mut: CudaHtj2kPacketizationStatus;
htj2k_statuses_as_bytes_mut: CudaHtj2kStatus;
+ classic_statuses_as_bytes_mut: CudaClassicStatus;
}
pub(crate) fn htj2k_encode_statuses_byte_len(count: usize) -> Result {
diff --git a/crates/j2k-cuda-runtime/src/bytes/abi.rs b/crates/j2k-cuda-runtime/src/bytes/abi.rs
index 009936d8..a5bca56e 100644
--- a/crates/j2k-cuda-runtime/src/bytes/abi.rs
+++ b/crates/j2k-cuda-runtime/src/bytes/abi.rs
@@ -1,6 +1,9 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use crate::{
+ classic_decode::{
+ CudaClassicKernelJob, CudaClassicKernelSegment, CudaClassicKernelTables, CudaClassicStatus,
+ },
htj2k_decode::{
CudaHtj2kCleanupMultiKernelJob, CudaHtj2kCodeBlockKernelJob, CudaHtj2kDequantizeKernelJob,
CudaHtj2kStatus,
@@ -287,6 +290,46 @@ impl_cuda_gpu_abi! {
reserved0: u32,
reserved1: u32,
},
+ CudaClassicKernelJob {
+ output_ptr: u64,
+ coded_offset: u32,
+ coded_len: u32,
+ segment_offset: u32,
+ segment_count: u32,
+ scratch_offset: u32,
+ width: u32,
+ height: u32,
+ output_stride: u32,
+ output_offset: u32,
+ missing_msbs: u32,
+ total_bitplanes: u32,
+ number_of_coding_passes: u32,
+ sub_band_type: u32,
+ style_flags: u32,
+ strict: u32,
+ dequantization_step: f32,
+ },
+ CudaClassicKernelSegment {
+ data_offset: u32,
+ data_length: u32,
+ start_coding_pass: u32,
+ end_coding_pass: u32,
+ use_arithmetic: u32,
+ },
+ CudaClassicKernelTables {
+ mq_qe: [u32; 47],
+ mq_transitions: [u32; 47],
+ sign_contexts: [u16; 256],
+ zero_contexts_ll_lh: [u8; 256],
+ zero_contexts_hl: [u8; 256],
+ zero_contexts_hh: [u8; 256],
+ },
+ CudaClassicStatus {
+ code: u32,
+ detail: u32,
+ reserved0: u32,
+ reserved1: u32,
+ },
CudaJ2kRect {
x0: u32,
y0: u32,
@@ -410,56 +453,4 @@ impl_cuda_gpu_abi! {
}
#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn explicit_tail_fields_preserve_cuda_host_abi_sizes_and_offsets() {
- assert_eq!(size_of::(), 40);
- assert_eq!(offset_of!(CudaJpegEntropyCheckpoint, reserved_tail), 36);
- assert_eq!(size_of::(), 64);
- assert_eq!(
- offset_of!(CudaHtj2kCleanupMultiKernelJob, reserved_tail),
- 60
- );
- assert_eq!(size_of::(), 40);
- assert_eq!(offset_of!(CudaHtj2kDequantizeKernelJob, reserved_tail), 36);
- assert_eq!(size_of::(), 128);
- assert_eq!(offset_of!(CudaJ2kIdwtMultiKernelJob, reserved_tail), 124);
- assert_eq!(size_of::(), 128);
- assert_eq!(offset_of!(CudaJ2kStoreRgb8MctBatchJob, reserved_tail), 124);
- }
-
- #[test]
- fn explicit_cuda_tail_fields_are_part_of_safe_byte_views() {
- let checkpoint = CudaJpegEntropyCheckpoint {
- mcu_index: 1,
- entropy_pos: 2,
- bit_acc: 3,
- bit_count: 4,
- y_prev_dc: 5,
- cb_prev_dc: 6,
- cr_prev_dc: 7,
- reserved: 8,
- reserved_tail: 0x4433_2211,
- };
- let checkpoint_bytes = ::as_bytes(&checkpoint);
- assert_eq!(checkpoint_bytes.len(), 40);
- assert_eq!(&checkpoint_bytes[36..40], &0x4433_2211u32.to_ne_bytes());
-
- let jobs = [CudaHtj2kDequantizeKernelJob {
- output_ptr: 1,
- width: 2,
- height: 3,
- output_stride: 4,
- output_offset: 5,
- num_bitplanes: 6,
- reserved: 0,
- dequantization_step: 1.0,
- reserved_tail: 0x8877_6655,
- }];
- let job_bytes = ::slice_as_bytes(&jobs);
- assert_eq!(job_bytes.len(), 40);
- assert_eq!(&job_bytes[36..40], &0x8877_6655u32.to_ne_bytes());
- }
-}
+mod tests;
diff --git a/crates/j2k-cuda-runtime/src/bytes/abi/tests.rs b/crates/j2k-cuda-runtime/src/bytes/abi/tests.rs
new file mode 100644
index 00000000..78efaef3
--- /dev/null
+++ b/crates/j2k-cuda-runtime/src/bytes/abi/tests.rs
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use super::*;
+
+#[test]
+fn explicit_tail_fields_preserve_cuda_host_abi_sizes_and_offsets() {
+ assert_eq!(size_of::(), 40);
+ assert_eq!(offset_of!(CudaJpegEntropyCheckpoint, reserved_tail), 36);
+ assert_eq!(size_of::(), 64);
+ assert_eq!(
+ offset_of!(CudaHtj2kCleanupMultiKernelJob, reserved_tail),
+ 60
+ );
+ assert_eq!(size_of::(), 40);
+ assert_eq!(offset_of!(CudaHtj2kDequantizeKernelJob, reserved_tail), 36);
+ assert_eq!(size_of::(), 72);
+ assert_eq!(offset_of!(CudaClassicKernelJob, dequantization_step), 68);
+ assert_eq!(size_of::(), 20);
+ assert_eq!(size_of::(), 1_656);
+ assert_eq!(offset_of!(CudaClassicKernelTables, mq_transitions), 188);
+ assert_eq!(offset_of!(CudaClassicKernelTables, sign_contexts), 376);
+ assert_eq!(
+ offset_of!(CudaClassicKernelTables, zero_contexts_ll_lh),
+ 888
+ );
+ assert_eq!(offset_of!(CudaClassicKernelTables, zero_contexts_hl), 1_144);
+ assert_eq!(offset_of!(CudaClassicKernelTables, zero_contexts_hh), 1_400);
+ assert_eq!(size_of::(), 16);
+ assert_eq!(size_of::(), 128);
+ assert_eq!(offset_of!(CudaJ2kIdwtMultiKernelJob, reserved_tail), 124);
+ assert_eq!(size_of::(), 128);
+ assert_eq!(offset_of!(CudaJ2kStoreRgb8MctBatchJob, reserved_tail), 124);
+}
+
+#[test]
+fn explicit_cuda_tail_fields_are_part_of_safe_byte_views() {
+ let checkpoint = CudaJpegEntropyCheckpoint {
+ mcu_index: 1,
+ entropy_pos: 2,
+ bit_acc: 3,
+ bit_count: 4,
+ y_prev_dc: 5,
+ cb_prev_dc: 6,
+ cr_prev_dc: 7,
+ reserved: 8,
+ reserved_tail: 0x4433_2211,
+ };
+ let checkpoint_bytes = ::as_bytes(&checkpoint);
+ assert_eq!(checkpoint_bytes.len(), 40);
+ assert_eq!(&checkpoint_bytes[36..40], &0x4433_2211u32.to_ne_bytes());
+
+ let jobs = [CudaHtj2kDequantizeKernelJob {
+ output_ptr: 1,
+ width: 2,
+ height: 3,
+ output_stride: 4,
+ output_offset: 5,
+ num_bitplanes: 6,
+ reserved: 0,
+ dequantization_step: 1.0,
+ reserved_tail: 0x8877_6655,
+ }];
+ let job_bytes = ::slice_as_bytes(&jobs);
+ assert_eq!(job_bytes.len(), 40);
+ assert_eq!(&job_bytes[36..40], &0x8877_6655u32.to_ne_bytes());
+}
diff --git a/crates/j2k-cuda-runtime/src/classic_decode.rs b/crates/j2k-cuda-runtime/src/classic_decode.rs
new file mode 100644
index 00000000..a7638d9c
--- /dev/null
+++ b/crates/j2k-cuda-runtime/src/classic_decode.rs
@@ -0,0 +1,749 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use crate::{
+ allocation::HostPhaseBudget,
+ bytes::{
+ classic_jobs_as_bytes, classic_segments_as_bytes, classic_statuses_as_bytes_mut,
+ classic_tables_as_bytes,
+ },
+ context::CudaContext,
+ error::{select_resource_release_error, CudaError},
+ execution::cuda_kernel_param,
+ htj2k_decode::output_regions::{
+ validate_disjoint_output_regions, Htj2kOutputRect, Htj2kOutputRegion,
+ },
+ htj2k_decode::CudaHtj2kDecodeResources,
+ kernels::{j2k_classic_codeblock_launch_geometry, CudaKernel},
+ memory::{pooled_device_buffer, CheckedDeviceBufferRanges, CudaBufferPool, CudaDeviceBuffer},
+};
+use j2k_codec_math::classic::{
+ MQ_QE_VALUES, PACKED_MQ_TRANSITION_VALUES, PACKED_SIGN_CONTEXT_LOOKUP, ZERO_CTX_HH_LOOKUP,
+ ZERO_CTX_HL_LOOKUP, ZERO_CTX_LL_LH_LOOKUP,
+};
+use std::time::Instant;
+
+const CLASSIC_KERNEL_NAME: &str = "j2k_decode_classic_codeblocks_multi";
+const MAX_CODEBLOCK_DIMENSION: u32 = 64;
+const MAX_BITPLANES: u32 = 31;
+const STYLE_TERMALL: u32 = 1 << 1;
+const STYLE_BYPASS: u32 = 1 << 4;
+const KNOWN_STYLE_FLAGS: u32 = 0x1f;
+
+/// One classic JPEG 2000 Tier-1 code-block decode job.
+#[doc(hidden)]
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct CudaClassicCodeBlockJob {
+ /// Byte offset of this code-block in the shared compressed payload.
+ pub payload_offset: u64,
+ /// Byte length of this code-block payload.
+ pub payload_len: u32,
+ /// First segment in the target's segment slice.
+ pub segment_start: u32,
+ /// Number of contiguous segment records.
+ pub segment_count: u32,
+ /// Code-block width.
+ pub width: u32,
+ /// Code-block height.
+ pub height: u32,
+ /// Output row stride in f32 coefficients.
+ pub output_stride: u32,
+ /// First output coefficient.
+ pub output_offset: u32,
+ /// Missing most-significant bitplanes.
+ pub missing_bitplanes: u32,
+ /// Total bitplanes in the sub-band.
+ pub total_bitplanes: u32,
+ /// Number of coding passes present.
+ pub number_of_coding_passes: u32,
+ /// JPEG 2000 sub-band tag: LL=0, HL=1, LH=2, HH=3.
+ pub sub_band_type: u32,
+ /// JPEG 2000 code-block style bits.
+ pub style_flags: u32,
+ /// Whether malformed entropy data is rejected.
+ pub strict: bool,
+ /// Fused coefficient dequantization multiplier.
+ pub dequantization_step: f32,
+}
+
+/// One bounded classic Tier-1 pass segment.
+#[doc(hidden)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct CudaClassicSegment {
+ /// Byte offset relative to the owning code-block payload.
+ pub data_offset: u32,
+ /// Segment byte length.
+ pub data_length: u32,
+ /// Inclusive first coding pass.
+ pub start_coding_pass: u32,
+ /// Exclusive final coding pass.
+ pub end_coding_pass: u32,
+ /// True for MQ arithmetic coding; false for raw bypass coding.
+ pub use_arithmetic: bool,
+}
+
+/// One device coefficient target and its classic Tier-1 work.
+#[doc(hidden)]
+#[derive(Clone, Copy, Debug)]
+pub struct CudaClassicDecodeTarget<'a> {
+ /// Device-resident f32 coefficient plane.
+ pub coefficients: &'a CudaDeviceBuffer,
+ /// Code-block jobs writing this plane.
+ pub jobs: &'a [CudaClassicCodeBlockJob],
+ /// Segment records referenced by the jobs.
+ pub segments: &'a [CudaClassicSegment],
+ /// Number of f32 words in the coefficient plane.
+ pub output_words: usize,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug)]
+pub(crate) struct CudaClassicKernelJob {
+ pub(crate) output_ptr: u64,
+ pub(crate) coded_offset: u32,
+ pub(crate) coded_len: u32,
+ pub(crate) segment_offset: u32,
+ pub(crate) segment_count: u32,
+ pub(crate) scratch_offset: u32,
+ pub(crate) width: u32,
+ pub(crate) height: u32,
+ pub(crate) output_stride: u32,
+ pub(crate) output_offset: u32,
+ pub(crate) missing_msbs: u32,
+ pub(crate) total_bitplanes: u32,
+ pub(crate) number_of_coding_passes: u32,
+ pub(crate) sub_band_type: u32,
+ pub(crate) style_flags: u32,
+ pub(crate) strict: u32,
+ pub(crate) dequantization_step: f32,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug)]
+pub(crate) struct CudaClassicKernelSegment {
+ pub(crate) data_offset: u32,
+ pub(crate) data_length: u32,
+ pub(crate) start_coding_pass: u32,
+ pub(crate) end_coding_pass: u32,
+ pub(crate) use_arithmetic: u32,
+}
+
+/// Status returned for one classic Tier-1 code-block.
+#[doc(hidden)]
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
+pub struct CudaClassicStatus {
+ /// Zero on success.
+ pub code: u32,
+ /// Kernel-defined failure detail.
+ pub detail: u32,
+ pub(crate) reserved0: u32,
+ pub(crate) reserved1: u32,
+}
+
+/// Timings for one resident classic Tier-1 decode dispatch.
+#[doc(hidden)]
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
+pub struct CudaClassicDecodeStageTimings {
+ /// Host-observed jobs and segments upload time, in microseconds.
+ pub job_upload_us: u128,
+ /// Host-observed lookup-table upload time, in microseconds.
+ pub table_upload_us: u128,
+ /// Classic Tier-1 CUDA kernel time, in microseconds.
+ pub kernel_us: u128,
+ /// Host-observed status download time, in microseconds.
+ pub status_d2h_us: u128,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub(crate) struct CudaClassicKernelTables {
+ pub(crate) mq_qe: [u32; 47],
+ pub(crate) mq_transitions: [u32; 47],
+ pub(crate) sign_contexts: [u16; 256],
+ pub(crate) zero_contexts_ll_lh: [u8; 256],
+ pub(crate) zero_contexts_hl: [u8; 256],
+ pub(crate) zero_contexts_hh: [u8; 256],
+}
+
+const CLASSIC_KERNEL_TABLES: CudaClassicKernelTables = CudaClassicKernelTables {
+ mq_qe: MQ_QE_VALUES,
+ mq_transitions: PACKED_MQ_TRANSITION_VALUES,
+ sign_contexts: PACKED_SIGN_CONTEXT_LOOKUP,
+ zero_contexts_ll_lh: ZERO_CTX_LL_LH_LOOKUP,
+ zero_contexts_hl: ZERO_CTX_HL_LOOKUP,
+ zero_contexts_hh: ZERO_CTX_HH_LOOKUP,
+};
+
+struct PreparedClassicDecode {
+ jobs: Vec,
+ segments: Vec,
+ scratch_words: usize,
+}
+
+impl CudaContext {
+ /// Allocate and clear one classic Tier-1 coefficient plane.
+ #[doc(hidden)]
+ pub fn allocate_classic_coefficients_with_pool(
+ &self,
+ output_words: usize,
+ pool: &CudaBufferPool,
+ ) -> Result {
+ if !pool.is_owned_by(self) {
+ return Err(invalid(
+ "classic coefficient pool must belong to the allocation context",
+ ));
+ }
+ let bytes = checked_bytes::(output_words)?;
+ let output = pool.take(bytes)?;
+ self.memset_d32(pooled_device_buffer(&output)?, 0, output_words)?;
+ self.synchronize()?;
+ Ok(output)
+ }
+
+ /// Decode classic Tier-1 code-blocks into one or more device coefficient planes.
+ #[doc(hidden)]
+ pub fn decode_classic_codeblocks_multi_with_resources_and_pool(
+ &self,
+ resources: &CudaHtj2kDecodeResources,
+ targets: &[CudaClassicDecodeTarget<'_>],
+ pool: &CudaBufferPool,
+ live_host_bytes: usize,
+ ) -> Result, CudaError> {
+ self.decode_classic_codeblocks_multi_with_resources_and_pool_timed(
+ resources,
+ targets,
+ pool,
+ live_host_bytes,
+ false,
+ )
+ .map(|(statuses, _)| statuses)
+ }
+
+ /// Decode classic Tier-1 code-blocks and return optional stage timings.
+ #[doc(hidden)]
+ pub fn decode_classic_codeblocks_multi_with_resources_and_pool_timed(
+ &self,
+ resources: &CudaHtj2kDecodeResources,
+ targets: &[CudaClassicDecodeTarget<'_>],
+ pool: &CudaBufferPool,
+ live_host_bytes: usize,
+ collect_stage_timings: bool,
+ ) -> Result<(Vec, CudaClassicDecodeStageTimings), CudaError> {
+ validate_classic_launch_owners(self, resources, targets, pool)?;
+ let mut host_budget =
+ HostPhaseBudget::with_live_bytes("CUDA classic Tier-1 launch owners", live_host_bytes)?;
+ let prepared = prepare_classic_decode(resources.payload_len, targets, &mut host_budget)?;
+ if prepared.jobs.is_empty() {
+ return Ok((Vec::new(), CudaClassicDecodeStageTimings::default()));
+ }
+ let payload = resources.payload.buffer()?;
+ let job_upload_start = collect_stage_timings.then(Instant::now);
+ let jobs = pool.upload_pinned(classic_jobs_as_bytes(&prepared.jobs))?;
+ let segments = pool.upload_pinned(classic_segments_as_bytes(&prepared.segments))?;
+ let job_upload_us = job_upload_start.map_or(0, |start| start.elapsed().as_micros());
+ let table_upload_start = collect_stage_timings.then(Instant::now);
+ let tables = pool.upload_pinned(classic_tables_as_bytes(&CLASSIC_KERNEL_TABLES))?;
+ let table_upload_us = table_upload_start.map_or(0, |start| start.elapsed().as_micros());
+ let statuses = pool.take(checked_bytes::(prepared.jobs.len())?)?;
+ let scratch = pool.take(checked_bytes::(prepared.scratch_words)?)?;
+
+ let mut payload_ptr = payload.device_ptr();
+ let mut jobs_ptr = pooled_device_buffer(&jobs)?.device_ptr();
+ let mut segments_ptr = pooled_device_buffer(&segments)?.device_ptr();
+ let mut tables_ptr = pooled_device_buffer(&tables)?.device_ptr();
+ let mut statuses_ptr = pooled_device_buffer(&statuses)?.device_ptr();
+ let mut scratch_ptr = pooled_device_buffer(&scratch)?.device_ptr();
+ let mut params = cuda_kernel_params!(
+ payload_ptr,
+ jobs_ptr,
+ segments_ptr,
+ tables_ptr,
+ statuses_ptr,
+ scratch_ptr
+ );
+ let geometry = j2k_classic_codeblock_launch_geometry(prepared.jobs.len()).ok_or(
+ CudaError::LengthTooLarge {
+ len: prepared.jobs.len(),
+ },
+ )?;
+ let function = self.inner.cuda_oxide_j2k_classic_decode_kernel_function(
+ CudaKernel::J2kClassicDecodeCodeblocksMulti,
+ )?;
+ let pool_reuse_guard = pool.defer_reuse()?;
+ let kernel_result = if collect_stage_timings {
+ self.time_default_stream_named_us("j2k.classic.decode.tier1.batch", || {
+ self.launch_kernel(function, geometry, &mut params)
+ })
+ .map(|((), elapsed_us)| elapsed_us)
+ } else {
+ self.with_nvtx_range("j2k.classic.decode.tier1.batch", || {
+ self.launch_kernel(function, geometry, &mut params)
+ })
+ .map(|()| 0)
+ };
+ let kernel_us = match kernel_result {
+ Ok(elapsed_us) => elapsed_us,
+ Err(error) => return pool_reuse_guard.synchronize_then_error(error),
+ };
+
+ let mut host_statuses =
+ host_budget.try_vec_filled(prepared.jobs.len(), CudaClassicStatus::default())?;
+ let status_d2h_start = collect_stage_timings.then(Instant::now);
+ if let Err(error) = statuses.copy_to_host(classic_statuses_as_bytes_mut(&mut host_statuses))
+ {
+ return pool_reuse_guard.release_after_recoverable_operation_error(error);
+ }
+ let status_d2h_us = status_d2h_start.map_or(0, |start| start.elapsed().as_micros());
+ let release_result = pool_reuse_guard.release();
+ let status_error = host_statuses
+ .iter()
+ .copied()
+ .enumerate()
+ .find(|(_, status)| status.code != 0)
+ .map(|(index, status)| CudaError::KernelStatus {
+ kernel: CLASSIC_KERNEL_NAME,
+ code: status.code,
+ detail: ((u32::try_from(index).unwrap_or(u32::MAX)) << 8) | (status.detail & 0xff),
+ });
+ match (status_error, release_result) {
+ (Some(primary), Err(release)) => Err(select_resource_release_error(primary, release)),
+ (Some(error), Ok(())) | (None, Err(error)) => Err(error),
+ (None, Ok(())) => Ok((
+ host_statuses,
+ CudaClassicDecodeStageTimings {
+ job_upload_us,
+ table_upload_us,
+ kernel_us,
+ status_d2h_us,
+ },
+ )),
+ }
+ }
+}
+
+fn validate_classic_launch_owners(
+ context: &CudaContext,
+ resources: &CudaHtj2kDecodeResources,
+ targets: &[CudaClassicDecodeTarget<'_>],
+ pool: &CudaBufferPool,
+) -> Result<(), CudaError> {
+ if !pool.is_owned_by(context) || !resources.is_owned_by(context)? {
+ return Err(invalid(
+ "classic decode resources, targets, and pool must belong to the launch context",
+ ));
+ }
+ let target_ranges = CheckedDeviceBufferRanges::from_same_context(
+ context,
+ targets
+ .iter()
+ .enumerate()
+ .map(|(index, target)| (index, target.coefficients)),
+ )?;
+ if target_ranges.first_self_overlap().is_some() {
+ return Err(invalid(
+ "classic decode target allocations must be pairwise disjoint",
+ ));
+ }
+ Ok(())
+}
+
+fn prepare_classic_decode(
+ payload_len: usize,
+ targets: &[CudaClassicDecodeTarget<'_>],
+ host_budget: &mut HostPhaseBudget,
+) -> Result {
+ let total_jobs = targets.iter().try_fold(0usize, |count, target| {
+ count
+ .checked_add(target.jobs.len())
+ .ok_or(CudaError::LengthTooLarge { len: usize::MAX })
+ })?;
+ let total_segments = targets.iter().try_fold(0usize, |count, target| {
+ count
+ .checked_add(target.segments.len())
+ .ok_or(CudaError::LengthTooLarge { len: usize::MAX })
+ })?;
+ let mut jobs = host_budget.try_vec_with_capacity(total_jobs)?;
+ let mut segments = host_budget.try_vec_with_capacity(total_segments)?;
+ let mut scratch_words = 0usize;
+
+ for target in targets {
+ if target.output_words > target.coefficients.byte_len() / std::mem::size_of::() {
+ return Err(invalid(
+ "classic coefficient target is smaller than output_words",
+ ));
+ }
+ for job in target.jobs {
+ validate_classic_job(payload_len, target.segments, target.output_words, job)?;
+ }
+ validate_target_output_regions(target, host_budget)?;
+ let mut expected_segment_start = 0u32;
+ for job in target.jobs {
+ if job.segment_start != expected_segment_start {
+ return Err(invalid(
+ "classic job segment ranges must form a contiguous partition",
+ ));
+ }
+ let segment_offset =
+ u32::try_from(segments.len()).map_err(|_| CudaError::LengthTooLarge {
+ len: segments.len(),
+ })?;
+ let coded_offset = u32::try_from(job.payload_offset)
+ .map_err(|_| CudaError::LengthTooLarge { len: payload_len })?;
+ let scratch_offset = u32::try_from(scratch_words)
+ .map_err(|_| CudaError::LengthTooLarge { len: scratch_words })?;
+ scratch_words = scratch_words
+ .checked_add((job.width as usize + 2) * (job.height as usize + 2))
+ .ok_or(CudaError::LengthTooLarge { len: usize::MAX })?;
+ jobs.push(CudaClassicKernelJob {
+ output_ptr: target.coefficients.device_ptr(),
+ coded_offset,
+ coded_len: job.payload_len,
+ segment_offset,
+ segment_count: job.segment_count,
+ scratch_offset,
+ width: job.width,
+ height: job.height,
+ output_stride: job.output_stride,
+ output_offset: job.output_offset,
+ missing_msbs: job.missing_bitplanes,
+ total_bitplanes: job.total_bitplanes,
+ number_of_coding_passes: job.number_of_coding_passes,
+ sub_band_type: job.sub_band_type,
+ style_flags: job.style_flags,
+ strict: u32::from(job.strict),
+ dequantization_step: job.dequantization_step,
+ });
+ let segment_end = job.segment_start.checked_add(job.segment_count).ok_or(
+ CudaError::LengthTooLarge {
+ len: target.segments.len(),
+ },
+ )?;
+ for segment in &target.segments[job.segment_start as usize..segment_end as usize] {
+ let absolute = job
+ .payload_offset
+ .checked_add(u64::from(segment.data_offset))
+ .and_then(|value| u32::try_from(value).ok())
+ .ok_or(CudaError::LengthTooLarge { len: payload_len })?;
+ segments.push(CudaClassicKernelSegment {
+ data_offset: absolute,
+ data_length: segment.data_length,
+ start_coding_pass: segment.start_coding_pass,
+ end_coding_pass: segment.end_coding_pass,
+ use_arithmetic: u32::from(segment.use_arithmetic),
+ });
+ }
+ expected_segment_start = segment_end;
+ }
+ if expected_segment_start as usize != target.segments.len() {
+ return Err(invalid(
+ "classic job segment ranges do not cover the target segment slice",
+ ));
+ }
+ }
+ Ok(PreparedClassicDecode {
+ jobs,
+ segments,
+ scratch_words,
+ })
+}
+
+fn validate_classic_job(
+ payload_len: usize,
+ segments: &[CudaClassicSegment],
+ output_words: usize,
+ job: &CudaClassicCodeBlockJob,
+) -> Result<(), CudaError> {
+ if !(1..=MAX_CODEBLOCK_DIMENSION).contains(&job.width)
+ || !(1..=MAX_CODEBLOCK_DIMENSION).contains(&job.height)
+ || !(1..=MAX_BITPLANES).contains(&job.total_bitplanes)
+ || job.missing_bitplanes >= job.total_bitplanes
+ || job.sub_band_type > 3
+ || job.style_flags & !KNOWN_STYLE_FLAGS != 0
+ {
+ return Err(invalid(
+ "classic code-block dimensions, bitplanes, or sub-band are invalid",
+ ));
+ }
+ let coded_bitplanes = job.total_bitplanes - job.missing_bitplanes;
+ if job.number_of_coding_passes > 1 + 3 * (coded_bitplanes - 1) {
+ return Err(invalid(
+ "classic code-block pass count exceeds its coded bitplanes",
+ ));
+ }
+ let payload_end = job
+ .payload_offset
+ .checked_add(u64::from(job.payload_len))
+ .ok_or(CudaError::LengthTooLarge { len: payload_len })?;
+ if payload_end > payload_len as u64 {
+ return Err(invalid("classic code-block payload range is out of bounds"));
+ }
+ let segment_end = (job.segment_start as usize)
+ .checked_add(job.segment_count as usize)
+ .ok_or(CudaError::LengthTooLarge {
+ len: segments.len(),
+ })?;
+ let job_segments = segments
+ .get(job.segment_start as usize..segment_end)
+ .ok_or_else(|| invalid("classic code-block segment range is out of bounds"))?;
+ let mut expected_pass = 0;
+ let mut expected_offset = 0;
+ for segment in job_segments {
+ if segment.start_coding_pass != expected_pass
+ || segment.end_coding_pass < segment.start_coding_pass
+ || segment.data_offset != expected_offset
+ {
+ return Err(invalid("classic code-block segments are not contiguous"));
+ }
+ let pass_count = segment.end_coding_pass - segment.start_coding_pass;
+ if job.style_flags & STYLE_TERMALL != 0 && pass_count > 1 {
+ return Err(invalid(
+ "classic TERMALL segments may cover at most one coding pass",
+ ));
+ }
+ for pass in segment.start_coding_pass..segment.end_coding_pass {
+ let expected_arithmetic =
+ job.style_flags & STYLE_BYPASS == 0 || pass <= 9 || pass.is_multiple_of(3);
+ if segment.use_arithmetic != expected_arithmetic {
+ return Err(invalid(
+ "classic segment coding mode contradicts BYPASS pass boundaries",
+ ));
+ }
+ }
+ expected_pass = segment.end_coding_pass;
+ expected_offset = segment
+ .data_offset
+ .checked_add(segment.data_length)
+ .ok_or(CudaError::LengthTooLarge { len: payload_len })?;
+ }
+ if expected_pass != job.number_of_coding_passes || expected_offset != job.payload_len {
+ return Err(invalid(
+ "classic code-block segments do not cover its passes and payload",
+ ));
+ }
+ if job.style_flags & (STYLE_TERMALL | STYLE_BYPASS) == 0 && job_segments.len() != 1 {
+ return Err(invalid(
+ "classic normal mode requires one arithmetic segment",
+ ));
+ }
+ let output_end = u64::from(job.output_offset)
+ .checked_add(u64::from(job.height - 1) * u64::from(job.output_stride))
+ .and_then(|value| value.checked_add(u64::from(job.width)))
+ .ok_or(CudaError::LengthTooLarge { len: output_words })?;
+ if job.output_stride < job.width || output_end > output_words as u64 {
+ return Err(invalid("classic code-block output range is out of bounds"));
+ }
+ Ok(())
+}
+
+fn validate_target_output_regions(
+ target: &CudaClassicDecodeTarget<'_>,
+ host_budget: &mut HostPhaseBudget,
+) -> Result<(), CudaError> {
+ let mut regions = host_budget.try_vec_with_capacity(target.jobs.len())?;
+ for job in target.jobs {
+ let stride = job.output_stride as usize;
+ let width = job.width as usize;
+ let height = job.height as usize;
+ let start = job.output_offset as usize;
+ if stride == 0 || width > stride {
+ return Err(invalid(
+ "classic output rows require a nonzero stride at least as wide as the block",
+ ));
+ }
+ let column_start = start % stride;
+ let column_end = column_start
+ .checked_add(width)
+ .ok_or(CudaError::LengthTooLarge {
+ len: target.output_words,
+ })?;
+ if column_end > stride {
+ return Err(invalid("classic output block crosses its row stride"));
+ }
+ let row_start = start / stride;
+ let row_end = row_start
+ .checked_add(height)
+ .ok_or(CudaError::LengthTooLarge {
+ len: target.output_words,
+ })?;
+ let end = start
+ .checked_add(
+ stride
+ .checked_mul(height - 1)
+ .ok_or(CudaError::LengthTooLarge {
+ len: target.output_words,
+ })?,
+ )
+ .and_then(|last_row| last_row.checked_add(width))
+ .ok_or(CudaError::LengthTooLarge {
+ len: target.output_words,
+ })?;
+ if end > target.output_words {
+ return Err(invalid("classic code-block output range is out of bounds"));
+ }
+ regions.push(Htj2kOutputRegion {
+ stride,
+ rect: Htj2kOutputRect {
+ row_start,
+ row_end,
+ column_start,
+ column_end,
+ },
+ linear_start: start,
+ linear_end: end,
+ });
+ }
+ validate_disjoint_output_regions(&mut regions, host_budget.live_bytes())
+}
+
+fn checked_bytes(count: usize) -> Result {
+ count
+ .checked_mul(std::mem::size_of::())
+ .ok_or(CudaError::LengthTooLarge { len: count })
+}
+
+fn invalid(message: &'static str) -> CudaError {
+ CudaError::InvalidArgument {
+ message: message.to_string(),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn max_job() -> CudaClassicCodeBlockJob {
+ CudaClassicCodeBlockJob {
+ payload_offset: 0,
+ payload_len: 7,
+ segment_start: 0,
+ segment_count: 1,
+ width: 64,
+ height: 64,
+ output_stride: 64,
+ output_offset: 0,
+ missing_bitplanes: 0,
+ total_bitplanes: 31,
+ number_of_coding_passes: 91,
+ sub_band_type: 3,
+ style_flags: 0,
+ strict: true,
+ dequantization_step: 1.0,
+ }
+ }
+
+ #[test]
+ fn classic_preflight_accepts_maximum_contract() {
+ let segments = [CudaClassicSegment {
+ data_offset: 0,
+ data_length: 7,
+ start_coding_pass: 0,
+ end_coding_pass: 91,
+ use_arithmetic: true,
+ }];
+ validate_classic_job(7, &segments, 64 * 64, &max_job())
+ .expect("maximum classic Tier-1 contract");
+ }
+
+ #[test]
+ fn classic_preflight_accepts_zero_length_prefix_segment() {
+ let mut job = max_job();
+ job.number_of_coding_passes = 1;
+ job.style_flags = STYLE_TERMALL;
+ job.segment_count = 2;
+ let segments = [
+ CudaClassicSegment {
+ data_offset: 0,
+ data_length: 0,
+ start_coding_pass: 0,
+ end_coding_pass: 0,
+ use_arithmetic: true,
+ },
+ CudaClassicSegment {
+ data_offset: 0,
+ data_length: 7,
+ start_coding_pass: 0,
+ end_coding_pass: 1,
+ use_arithmetic: true,
+ },
+ ];
+ validate_classic_job(7, &segments, 64 * 64, &job).expect("zero-length classic prefix");
+ }
+
+ #[test]
+ fn classic_preflight_rejects_noncontiguous_segments_and_output_overrun() {
+ let mut job = max_job();
+ let segments = [CudaClassicSegment {
+ data_offset: 1,
+ data_length: 6,
+ start_coding_pass: 0,
+ end_coding_pass: 91,
+ use_arithmetic: true,
+ }];
+ assert!(validate_classic_job(7, &segments, 64 * 64, &job).is_err());
+
+ job.output_offset = 1;
+ let segments = [CudaClassicSegment {
+ data_offset: 0,
+ data_length: 7,
+ start_coding_pass: 0,
+ end_coding_pass: 91,
+ use_arithmetic: true,
+ }];
+ assert!(validate_classic_job(7, &segments, 64 * 64, &job).is_err());
+
+ job.output_offset = 0;
+ job.payload_len = 8;
+ assert!(validate_classic_job(8, &segments, 64 * 64, &job).is_err());
+ }
+
+ #[cfg(target_pointer_width = "64")]
+ #[test]
+ fn classic_preflight_accepts_output_ranges_beyond_u32_words() {
+ let mut job = max_job();
+ job.width = 2;
+ job.height = 2;
+ job.output_stride = u32::MAX;
+ job.number_of_coding_passes = 1;
+ let segments = [CudaClassicSegment {
+ data_offset: 0,
+ data_length: 7,
+ start_coding_pass: 0,
+ end_coding_pass: 1,
+ use_arithmetic: true,
+ }];
+
+ validate_classic_job(
+ 7,
+ &segments,
+ usize::try_from(u64::from(u32::MAX) + 2).expect("64-bit output words"),
+ &job,
+ )
+ .expect("device output addressing must support the host-validated range");
+ }
+
+ #[test]
+ fn classic_runtime_validates_empty_work_and_times_only_status_copy() {
+ let source = include_str!("classic_decode.rs");
+ let method = source
+ .split("pub fn decode_classic_codeblocks_multi_with_resources_and_pool_timed")
+ .nth(1)
+ .expect("timed classic decode method");
+ let owner_validation = method
+ .find("validate_classic_launch_owners")
+ .expect("owner validation");
+ let prepare = method.find("prepare_classic_decode").expect("preparation");
+ let empty_return = method
+ .find("if prepared.jobs.is_empty()")
+ .expect("validated empty fast path");
+ assert!(owner_validation < empty_return && prepare < empty_return);
+
+ let status_copy = method.find("statuses.copy_to_host").expect("status copy");
+ let status_timing = method
+ .find("let status_d2h_us")
+ .expect("status timing result");
+ let pool_release = method
+ .find("let release_result = pool_reuse_guard.release()")
+ .expect("pool release");
+ assert!(status_copy < status_timing && status_timing < pool_release);
+ }
+}
diff --git a/crates/j2k-cuda-runtime/src/context.rs b/crates/j2k-cuda-runtime/src/context.rs
index a93f3d0c..408b3622 100644
--- a/crates/j2k-cuda-runtime/src/context.rs
+++ b/crates/j2k-cuda-runtime/src/context.rs
@@ -14,6 +14,7 @@ use crate::{
mod band_transfer;
mod compact;
+mod creation;
mod device;
mod host_budget;
mod inner;
@@ -22,6 +23,7 @@ mod kernel_dispatch;
mod lifecycle;
mod operations;
mod pinned_host;
+mod pointer;
mod resource_creation;
#[cfg(test)]
mod test_kernels;
@@ -55,7 +57,14 @@ impl CudaContext {
#[doc(hidden)]
#[must_use]
pub fn is_same_context(&self, other: &Self) -> bool {
- Arc::ptr_eq(&self.inner, &other.inner)
+ self.inner.context == other.inner.context
+ }
+
+ /// Device ordinal associated with this context.
+ #[doc(hidden)]
+ #[must_use]
+ pub fn device_ordinal(&self) -> usize {
+ self.inner.device_ordinal
}
/// Dequantize HTJ2K cleanup outputs using the metadata buffer already held
diff --git a/crates/j2k-cuda-runtime/src/context/creation.rs b/crates/j2k-cuda-runtime/src/context/creation.rs
new file mode 100644
index 00000000..3d91f42c
--- /dev/null
+++ b/crates/j2k-cuda-runtime/src/context/creation.rs
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use std::{
+ collections::HashMap,
+ sync::{Arc, Mutex},
+};
+
+use crate::{driver::Driver, error::CudaError, memory::PinnedUploadStagingPool};
+
+use super::{
+ host_budget::SharedCudaHostBudget,
+ inner::{ContextInner, ContextOwnership},
+ validate_resource_handle, ContextResourceLifecycle, CudaContext,
+};
+
+pub(super) fn create_context(
+ device_ordinal: usize,
+ retain_primary: bool,
+) -> Result {
+ let driver = Driver::load()?;
+ // SAFETY: cuInit is the CUDA Driver API process initializer.
+ driver.check("cuInit", unsafe { (driver.cu_init)(0) })?;
+
+ let mut count = 0;
+ // SAFETY: CUDA writes one integer device count to the provided pointer.
+ driver.check("cuDeviceGetCount", unsafe {
+ (driver.cu_device_get_count)(&raw mut count)
+ })?;
+ if count <= 0 {
+ return Err(CudaError::Unavailable {
+ message: "no CUDA devices reported by driver".to_string(),
+ });
+ }
+
+ let ordinal = i32::try_from(device_ordinal).map_err(|_| CudaError::InvalidArgument {
+ message: "CUDA device ordinal exceeds i32".to_string(),
+ })?;
+ if ordinal >= count {
+ return Err(CudaError::InvalidArgument {
+ message: format!(
+ "CUDA device ordinal {device_ordinal} is out of range for {count} devices"
+ ),
+ });
+ }
+ let mut device = 0;
+ // SAFETY: the ordinal was checked against cuDeviceGetCount above.
+ driver.check("cuDeviceGet", unsafe {
+ (driver.cu_device_get)(&raw mut device, ordinal)
+ })?;
+
+ let mut context = std::ptr::null_mut();
+ let ownership = if retain_primary {
+ // SAFETY: CUDA writes the retained primary context for a valid device.
+ driver.check("cuDevicePrimaryCtxRetain", unsafe {
+ (driver.cu_device_primary_ctx_retain)(&raw mut context, device)
+ })?;
+ ContextOwnership::RetainedPrimary { device }
+ } else {
+ // SAFETY: CUDA writes a newly-created context handle for a valid device.
+ driver.check("cuCtxCreate_v2", unsafe {
+ (driver.cu_ctx_create)(&raw mut context, 0, device)
+ })?;
+ ContextOwnership::Owned
+ };
+ if let Err(error) = validate_resource_handle(
+ context,
+ "CUDA returned a null context after successful creation",
+ ) {
+ match ownership {
+ ContextOwnership::Owned => {
+ // SAFETY: a successful context-create call is balanced on the
+ // validation failure path before ownership can escape.
+ let _ = unsafe { (driver.cu_ctx_destroy)(context) };
+ }
+ ContextOwnership::RetainedPrimary { device } => {
+ // SAFETY: this balances the successful primary-context retain
+ // when its returned handle fails validation.
+ let _ = unsafe { (driver.cu_device_primary_ctx_release)(device) };
+ }
+ }
+ return Err(error);
+ }
+
+ Ok(CudaContext {
+ inner: Arc::new(ContextInner {
+ driver,
+ context,
+ ownership,
+ device_ordinal,
+ modules: Mutex::new(HashMap::new()),
+ pinned_upload_operation: Mutex::new(()),
+ pinned_upload_staging: Mutex::new(PinnedUploadStagingPool::new()),
+ host_budget: SharedCudaHostBudget::new(),
+ resource_lifecycle: ContextResourceLifecycle::new(),
+ }),
+ })
+}
diff --git a/crates/j2k-cuda-runtime/src/context/device.rs b/crates/j2k-cuda-runtime/src/context/device.rs
index 4f808b4b..850110d8 100644
--- a/crates/j2k-cuda-runtime/src/context/device.rs
+++ b/crates/j2k-cuda-runtime/src/context/device.rs
@@ -1,60 +1,25 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
-use std::{
- collections::HashMap,
- sync::{Arc, Mutex},
-};
+use crate::error::CudaError;
-use crate::{driver::Driver, error::CudaError, memory::PinnedUploadStagingPool};
-
-use super::host_budget::SharedCudaHostBudget;
-use super::{inner::ContextInner, ContextResourceLifecycle, CudaContext};
+use super::{creation::create_context, CudaContext};
impl CudaContext {
/// Create a context for the system default CUDA device.
pub fn system_default() -> Result {
- let driver = Driver::load()?;
-
- // SAFETY: cuInit is the CUDA Driver API process initializer.
- driver.check("cuInit", unsafe { (driver.cu_init)(0) })?;
-
- let mut count = 0;
- // SAFETY: CUDA writes one integer device count to the provided pointer.
- driver.check("cuDeviceGetCount", unsafe {
- (driver.cu_device_get_count)(&raw mut count)
- })?;
- if count <= 0 {
- return Err(CudaError::Unavailable {
- message: "no CUDA devices reported by driver".to_string(),
- });
- }
-
- let mut device = 0;
- // SAFETY: device 0 is valid when count is greater than zero.
- driver.check("cuDeviceGet", unsafe {
- (driver.cu_device_get)(&raw mut device, 0)
- })?;
+ Self::create_owned(0)
+ }
- let mut context = std::ptr::null_mut();
- // SAFETY: CUDA writes a newly-created context handle for a valid device.
- driver.check("cuCtxCreate_v2", unsafe {
- (driver.cu_ctx_create)(&raw mut context, 0, device)
- })?;
- super::validate_resource_handle(
- context,
- "CUDA returned a null context after successful creation",
- )?;
+ /// Retain the CUDA primary context for a selected device ordinal.
+ ///
+ /// This is the interoperability constructor for runtimes such as `CubeCL`
+ /// that also use CUDA primary contexts. Dropping the final clone releases
+ /// the retain; it never destroys the primary context directly.
+ pub fn retain_primary(device_ordinal: usize) -> Result {
+ create_context(device_ordinal, true)
+ }
- Ok(Self {
- inner: Arc::new(ContextInner {
- driver,
- context,
- modules: Mutex::new(HashMap::new()),
- pinned_upload_operation: Mutex::new(()),
- pinned_upload_staging: Mutex::new(PinnedUploadStagingPool::new()),
- host_budget: SharedCudaHostBudget::new(),
- resource_lifecycle: ContextResourceLifecycle::new(),
- }),
- })
+ fn create_owned(device_ordinal: usize) -> Result {
+ create_context(device_ordinal, false)
}
}
diff --git a/crates/j2k-cuda-runtime/src/context/inner.rs b/crates/j2k-cuda-runtime/src/context/inner.rs
index 1de82ffa..a8dac092 100644
--- a/crates/j2k-cuda-runtime/src/context/inner.rs
+++ b/crates/j2k-cuda-runtime/src/context/inner.rs
@@ -10,9 +10,17 @@ use crate::{
use super::host_budget::SharedCudaHostBudget;
use super::{CompiledKernel, CompiledKernelKey, ContextResourceLifecycle};
+#[derive(Debug, Clone, Copy)]
+pub(crate) enum ContextOwnership {
+ Owned,
+ RetainedPrimary { device: crate::driver::CuDevice },
+}
+
pub(crate) struct ContextInner {
pub(crate) driver: Driver,
pub(crate) context: CuContext,
+ pub(crate) ownership: ContextOwnership,
+ pub(crate) device_ordinal: usize,
pub(crate) modules: Mutex>,
pub(crate) pinned_upload_operation: Mutex<()>,
pub(crate) pinned_upload_staging: Mutex,
@@ -46,10 +54,18 @@ impl Drop for ContextInner {
let _ = unsafe { (self.driver.cu_module_unload)(compiled.module) };
}
}
- // SAFETY: context was created by this ContextInner and cached
- // resources are either already released or left for context-wide
- // destruction after a failed/uncertain completion state.
- let _ = unsafe { (self.driver.cu_ctx_destroy)(self.context) };
+ match self.ownership {
+ ContextOwnership::Owned => {
+ // SAFETY: this ContextInner owns the context returned by
+ // cuCtxCreate_v2 and releases it exactly once.
+ let _ = unsafe { (self.driver.cu_ctx_destroy)(self.context) };
+ }
+ ContextOwnership::RetainedPrimary { device } => {
+ // SAFETY: this balances the successful primary-context
+ // retain used to construct this ContextInner.
+ let _ = unsafe { (self.driver.cu_device_primary_ctx_release)(device) };
+ }
+ }
self.host_budget.clear_pinned_after_context_drop();
}
}
diff --git a/crates/j2k-cuda-runtime/src/context/kernel_cache.rs b/crates/j2k-cuda-runtime/src/context/kernel_cache.rs
index fe7d0cfc..b07ac7b6 100644
--- a/crates/j2k-cuda-runtime/src/context/kernel_cache.rs
+++ b/crates/j2k-cuda-runtime/src/context/kernel_cache.rs
@@ -1,8 +1,8 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
-#[cfg(j2k_cuda_oxide_enabled)]
-use std::ffi::{c_char, c_void};
-
+use super::inner::ContextInner;
+#[cfg(any(test, j2k_cuda_oxide_enabled))]
+use super::validate_resource_handle;
#[cfg(j2k_cuda_oxide_enabled)]
use crate::allocation::host_allocation_error;
use crate::driver::{CuFunction, CuModule};
@@ -12,10 +12,8 @@ use crate::error::{select_resource_release_error, CudaError};
use crate::kernels;
#[cfg(j2k_cuda_oxide_enabled)]
use crate::kernels::CudaKernel;
-
-use super::inner::ContextInner;
-#[cfg(any(test, j2k_cuda_oxide_enabled))]
-use super::validate_resource_handle;
+#[cfg(j2k_cuda_oxide_enabled)]
+use std::ffi::{c_char, c_void};
#[cfg_attr(
not(j2k_cuda_oxide_enabled),
@@ -29,7 +27,6 @@ pub(crate) struct CompiledKernel {
pub(crate) module: CuModule,
pub(crate) function: CuFunction,
}
-
#[cfg(any(test, j2k_cuda_oxide_enabled))]
fn resolve_loaded_kernel_function(
module: CuModule,
@@ -50,15 +47,18 @@ fn resolve_loaded_kernel_function(
},
}
}
-
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(crate) enum CompiledKernelKey {
#[cfg(feature = "cuda-oxide-copy-u8")]
CudaOxideCopyU8,
+ #[cfg(feature = "cuda-oxide-j2k-ml")]
+ CudaOxideJ2kMl,
#[cfg(feature = "cuda-oxide-j2k-encode")]
CudaOxideJ2kEncode(CudaKernel),
#[cfg(feature = "cuda-oxide-j2k-decode-store")]
CudaOxideJ2kDecodeStore(CudaKernel),
+ #[cfg(feature = "cuda-oxide-j2k-classic-decode")]
+ CudaOxideJ2kClassicDecode(CudaKernel),
#[cfg(feature = "cuda-oxide-j2k-dequantize")]
CudaOxideJ2kDequantize(CudaKernel),
#[cfg(feature = "cuda-oxide-j2k-idwt")]
@@ -74,7 +74,6 @@ pub(crate) enum CompiledKernelKey {
#[cfg(feature = "cuda-oxide-jpeg-encode")]
CudaOxideJpegEncode(CudaKernel),
}
-
impl ContextInner {
#[cfg(j2k_cuda_oxide_enabled)]
pub(in crate::context) fn kernel_function_from_key(
@@ -84,10 +83,14 @@ impl ContextInner {
match key {
#[cfg(feature = "cuda-oxide-copy-u8")]
CompiledKernelKey::CudaOxideCopyU8 => {}
+ #[cfg(feature = "cuda-oxide-j2k-ml")]
+ CompiledKernelKey::CudaOxideJ2kMl => {}
#[cfg(feature = "cuda-oxide-j2k-encode")]
CompiledKernelKey::CudaOxideJ2kEncode(_) => {}
#[cfg(feature = "cuda-oxide-j2k-decode-store")]
CompiledKernelKey::CudaOxideJ2kDecodeStore(_) => {}
+ #[cfg(feature = "cuda-oxide-j2k-classic-decode")]
+ CompiledKernelKey::CudaOxideJ2kClassicDecode(_) => {}
#[cfg(feature = "cuda-oxide-j2k-dequantize")]
CompiledKernelKey::CudaOxideJ2kDequantize(_) => {}
#[cfg(feature = "cuda-oxide-j2k-idwt")]
@@ -132,10 +135,14 @@ impl CompiledKernelKey {
match self {
#[cfg(feature = "cuda-oxide-copy-u8")]
Self::CudaOxideCopyU8 => CudaKernel::CopyU8,
+ #[cfg(feature = "cuda-oxide-j2k-ml")]
+ Self::CudaOxideJ2kMl => CudaKernel::J2kMlConvert,
#[cfg(feature = "cuda-oxide-j2k-encode")]
Self::CudaOxideJ2kEncode(kernel) => kernel,
#[cfg(feature = "cuda-oxide-j2k-decode-store")]
Self::CudaOxideJ2kDecodeStore(kernel) => kernel,
+ #[cfg(feature = "cuda-oxide-j2k-classic-decode")]
+ Self::CudaOxideJ2kClassicDecode(kernel) => kernel,
#[cfg(feature = "cuda-oxide-j2k-dequantize")]
Self::CudaOxideJ2kDequantize(kernel) => kernel,
#[cfg(feature = "cuda-oxide-j2k-idwt")]
@@ -157,10 +164,14 @@ impl CompiledKernelKey {
match self {
#[cfg(feature = "cuda-oxide-copy-u8")]
Self::CudaOxideCopyU8 => kernels::cuda_oxide_copy_u8_ptx(),
+ #[cfg(feature = "cuda-oxide-j2k-ml")]
+ Self::CudaOxideJ2kMl => kernels::cuda_oxide_j2k_ml_ptx(),
#[cfg(feature = "cuda-oxide-j2k-encode")]
Self::CudaOxideJ2kEncode(_) => kernels::cuda_oxide_j2k_encode_ptx(),
#[cfg(feature = "cuda-oxide-j2k-decode-store")]
Self::CudaOxideJ2kDecodeStore(_) => kernels::cuda_oxide_j2k_decode_store_ptx(),
+ #[cfg(feature = "cuda-oxide-j2k-classic-decode")]
+ Self::CudaOxideJ2kClassicDecode(_) => kernels::cuda_oxide_j2k_classic_decode_ptx(),
#[cfg(feature = "cuda-oxide-j2k-dequantize")]
Self::CudaOxideJ2kDequantize(_) => kernels::cuda_oxide_j2k_dequantize_ptx(),
#[cfg(feature = "cuda-oxide-j2k-idwt")]
diff --git a/crates/j2k-cuda-runtime/src/context/kernel_dispatch.rs b/crates/j2k-cuda-runtime/src/context/kernel_dispatch.rs
index 9b9b7c49..e8f98ca4 100644
--- a/crates/j2k-cuda-runtime/src/context/kernel_dispatch.rs
+++ b/crates/j2k-cuda-runtime/src/context/kernel_dispatch.rs
@@ -14,6 +14,8 @@ use crate::build_flags::ensure_cuda_oxide_j2k_dequantize_ptx_built;
use crate::build_flags::ensure_cuda_oxide_j2k_encode_ptx_built;
#[cfg(feature = "cuda-oxide-j2k-idwt")]
use crate::build_flags::ensure_cuda_oxide_j2k_idwt_ptx_built;
+#[cfg(feature = "cuda-oxide-j2k-ml")]
+use crate::build_flags::ensure_cuda_oxide_j2k_ml_ptx_built;
#[cfg(feature = "cuda-oxide-jpeg-decode")]
use crate::build_flags::ensure_cuda_oxide_jpeg_decode_ptx_built;
#[cfg(feature = "cuda-oxide-jpeg-encode")]
@@ -26,7 +28,27 @@ use super::inner::ContextInner;
#[cfg(j2k_cuda_oxide_enabled)]
use super::kernel_cache::CompiledKernelKey;
+mod classic;
+
impl ContextInner {
+ #[cfg(feature = "cuda-oxide-j2k-ml")]
+ pub(crate) fn cuda_oxide_j2k_ml_kernel_function(&self) -> Result {
+ ensure_cuda_oxide_j2k_ml_ptx_built()?;
+ self.kernel_function_from_key(CompiledKernelKey::CudaOxideJ2kMl)
+ }
+
+ #[cfg(not(feature = "cuda-oxide-j2k-ml"))]
+ #[expect(
+ clippy::unused_self,
+ reason = "feature-disabled method preserves the enabled dispatch interface"
+ )]
+ pub(crate) fn cuda_oxide_j2k_ml_kernel_function(&self) -> Result {
+ Err(Self::cuda_oxide_feature_missing(
+ "j2k-ml",
+ "cuda-oxide-j2k-ml",
+ ))
+ }
+
#[cfg(feature = "cuda-oxide-copy-u8")]
pub(crate) fn cuda_oxide_copy_u8_kernel_function(&self) -> Result {
ensure_cuda_oxide_copy_u8_ptx_built()?;
diff --git a/crates/j2k-cuda-runtime/src/context/kernel_dispatch/classic.rs b/crates/j2k-cuda-runtime/src/context/kernel_dispatch/classic.rs
new file mode 100644
index 00000000..6731b8a2
--- /dev/null
+++ b/crates/j2k-cuda-runtime/src/context/kernel_dispatch/classic.rs
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use crate::{driver::CuFunction, error::CudaError, kernels::CudaKernel};
+
+#[cfg(feature = "cuda-oxide-j2k-classic-decode")]
+use super::CompiledKernelKey;
+use super::ContextInner;
+#[cfg(feature = "cuda-oxide-j2k-classic-decode")]
+use crate::build_flags::ensure_cuda_oxide_j2k_classic_decode_ptx_built;
+
+impl ContextInner {
+ #[cfg(feature = "cuda-oxide-j2k-classic-decode")]
+ pub(crate) fn cuda_oxide_j2k_classic_decode_kernel_function(
+ &self,
+ kernel: CudaKernel,
+ ) -> Result {
+ ensure_cuda_oxide_j2k_classic_decode_ptx_built()?;
+ if !kernel.is_j2k_classic_decode_stage() {
+ return Err(CudaError::InvalidArgument {
+ message: format!("kernel {kernel:?} is not a classic J2K decode stage"),
+ });
+ }
+ self.kernel_function_from_key(CompiledKernelKey::CudaOxideJ2kClassicDecode(kernel))
+ }
+
+ #[cfg(not(feature = "cuda-oxide-j2k-classic-decode"))]
+ pub(crate) fn cuda_oxide_j2k_classic_decode_kernel_function(
+ &self,
+ kernel: CudaKernel,
+ ) -> Result {
+ let _ = (self, kernel);
+ Err(Self::cuda_oxide_feature_missing(
+ "classic J2K decode",
+ "cuda-oxide-j2k-classic-decode",
+ ))
+ }
+}
diff --git a/crates/j2k-cuda-runtime/src/context/pointer.rs b/crates/j2k-cuda-runtime/src/context/pointer.rs
new file mode 100644
index 00000000..14d54f3c
--- /dev/null
+++ b/crates/j2k-cuda-runtime/src/context/pointer.rs
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use crate::error::CudaError;
+
+use super::ContextInner;
+
+impl ContextInner {
+ pub(crate) fn validate_pointer_context(
+ &self,
+ ptr: crate::driver::CuDevicePtr,
+ ) -> Result<(), CudaError> {
+ const CU_POINTER_ATTRIBUTE_CONTEXT: i32 = 1;
+ let mut pointer_context: crate::driver::CuContext = std::ptr::null_mut();
+ self.with_current_resource_operation(|| {
+ // SAFETY: CUDA writes one CUcontext value to pointer_context for
+ // the live device pointer supplied by the caller.
+ self.driver.check("cuPointerGetAttribute", unsafe {
+ (self.driver.cu_pointer_get_attribute)(
+ (&raw mut pointer_context).cast(),
+ CU_POINTER_ATTRIBUTE_CONTEXT,
+ ptr,
+ )
+ })
+ })?;
+ if pointer_context != self.context {
+ return Err(CudaError::InvalidArgument {
+ message: "external CUDA pointer belongs to a different context".to_string(),
+ });
+ }
+ Ok(())
+ }
+}
diff --git a/crates/j2k-cuda-runtime/src/context/structure_tests.rs b/crates/j2k-cuda-runtime/src/context/structure_tests.rs
index 7e3097b8..a37936d2 100644
--- a/crates/j2k-cuda-runtime/src/context/structure_tests.rs
+++ b/crates/j2k-cuda-runtime/src/context/structure_tests.rs
@@ -8,6 +8,7 @@ const MODULES: &[(&str, &str, usize)] = &[
75,
),
("context/compact.rs", include_str!("compact.rs"), 150),
+ ("context/creation.rs", include_str!("creation.rs"), 100),
("context/device.rs", include_str!("device.rs"), 80),
("context/inner.rs", include_str!("inner.rs"), 100),
(
@@ -22,6 +23,7 @@ const MODULES: &[(&str, &str, usize)] = &[
),
("context/lifecycle.rs", include_str!("lifecycle.rs"), 175),
("context/operations.rs", include_str!("operations.rs"), 100),
+ ("context/pointer.rs", include_str!("pointer.rs"), 50),
("context/pinned_host.rs", include_str!("pinned_host.rs"), 75),
(
"context/test_kernels.rs",
@@ -41,12 +43,14 @@ fn cuda_context_uses_focused_real_modules() {
for module in [
"mod band_transfer;",
"mod compact;",
+ "mod creation;",
"mod device;",
"mod inner;",
"mod kernel_cache;",
"mod kernel_dispatch;",
"mod lifecycle;",
"mod operations;",
+ "mod pointer;",
"mod pinned_host;",
] {
assert!(CONTEXT.contains(module), "context.rs must contain {module}");
diff --git a/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_classic_decode/Cargo.toml.in b/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_classic_decode/Cargo.toml.in
new file mode 100644
index 00000000..9acf3d25
--- /dev/null
+++ b/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_classic_decode/Cargo.toml.in
@@ -0,0 +1,15 @@
+[package]
+name = "j2k_cuda_oxide_j2k_classic_decode_host"
+version = "0.1.0"
+edition = "2024"
+
+[package.metadata.cuda-oxide.interop]
+kind = "j2k-classic-decode"
+
+[[package.metadata.cuda-oxide.device-crates]]
+manifest-path = "simt/Cargo.toml"
+ptx-dir = "ptx"
+artifact-name = "j2k_cuda_oxide_j2k_classic_decode"
+
+[workspace]
+resolver = "2"
diff --git a/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_classic_decode/rust-toolchain.toml b/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_classic_decode/rust-toolchain.toml
new file mode 100644
index 00000000..9f964011
--- /dev/null
+++ b/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_classic_decode/rust-toolchain.toml
@@ -0,0 +1,3 @@
+[toolchain]
+channel = "nightly-2026-04-03"
+components = ["rust-src", "rustc-dev", "rust-analyzer", "clippy", "llvm-tools"]
diff --git a/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_classic_decode/simt/Cargo.toml.in b/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_classic_decode/simt/Cargo.toml.in
new file mode 100644
index 00000000..2786b985
--- /dev/null
+++ b/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_classic_decode/simt/Cargo.toml.in
@@ -0,0 +1,13 @@
+[package]
+name = "j2k_cuda_oxide_j2k_classic_decode"
+version = "0.1.0"
+edition = "2024"
+
+[workspace]
+resolver = "2"
+
+[dependencies]
+cuda-core = { git = "https://github.com/NVlabs/cuda-oxide.git", rev = "a9f964a956f397dd0b3c8db88a3ca5824186c261" }
+cuda-device = { git = "https://github.com/NVlabs/cuda-oxide.git", rev = "a9f964a956f397dd0b3c8db88a3ca5824186c261" }
+cuda-host = { git = "https://github.com/NVlabs/cuda-oxide.git", rev = "a9f964a956f397dd0b3c8db88a3ca5824186c261" }
+j2k-codec-math = { path = "__J2K_CODEC_MATH_PATH__" }
diff --git a/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_classic_decode/simt/src/main.rs b/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_classic_decode/simt/src/main.rs
new file mode 100644
index 00000000..18320652
--- /dev/null
+++ b/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_classic_decode/simt/src/main.rs
@@ -0,0 +1,909 @@
+#![allow(
+ static_mut_refs,
+ reason = "CUDA shared-memory state is scoped to one classic Tier-1 codeblock"
+)]
+
+use cuda_device::{kernel, thread, SharedArray};
+use cuda_host::cuda_module;
+include!("../../../cuda_oxide_simt_prelude.rs");
+
+const MAX_PADDED_COEFFICIENTS: usize = 66 * 66;
+const CLASSIC_DECODE_THREADS: u32 = 32;
+const SIGNIFICANT_SHIFT: u8 = 7;
+const MAGNITUDE_REFINED_SHIFT: u8 = 6;
+const SIGN_SHIFT: u8 = 5;
+const ZERO_CODED_MASK: u8 = 0x1f;
+const STYLE_RESET_CONTEXTS: u32 = 1 << 0;
+const STYLE_VERTICALLY_CAUSAL: u32 = 1 << 2;
+const STYLE_SEGMENTATION_SYMBOLS: u32 = 1 << 3;
+const KNOWN_STYLE_FLAGS: u32 = 0x1f;
+const STATUS_OK: u32 = 0;
+const STATUS_FAILED: u32 = 1;
+const STATUS_UNSUPPORTED: u32 = 2;
+const RAW_READ_FAILED: u32 = u32::MAX;
+const MQ_QE_WORD_OFFSET: usize = 0;
+const MQ_TRANSITION_WORD_OFFSET: usize = 47;
+const SIGN_CONTEXT_HALFWORD_OFFSET: usize = 188;
+const ZERO_CONTEXT_LL_LH_BYTE_OFFSET: usize = 888;
+const ZERO_CONTEXT_HL_BYTE_OFFSET: usize = 1144;
+const ZERO_CONTEXT_HH_BYTE_OFFSET: usize = 1400;
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+struct ClassicJob {
+ output_ptr: u64,
+ coded_offset: u32,
+ coded_len: u32,
+ segment_offset: u32,
+ segment_count: u32,
+ scratch_offset: u32,
+ width: u32,
+ height: u32,
+ output_stride: u32,
+ output_offset: u32,
+ missing_msbs: u32,
+ total_bitplanes: u32,
+ number_of_coding_passes: u32,
+ sub_band_type: u32,
+ style_flags: u32,
+ strict: u32,
+ dequantization_step: f32,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+struct ClassicSegment {
+ data_offset: u32,
+ data_length: u32,
+ start_coding_pass: u32,
+ end_coding_pass: u32,
+ use_arithmetic: u32,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+struct ClassicStatus {
+ code: u32,
+ detail: u32,
+ reserved0: u32,
+ reserved1: u32,
+}
+
+struct ArithmeticDecoder {
+ data: *const u8,
+ data_len: u32,
+ c: u32,
+ a: u32,
+ base_pointer: u32,
+ shift_count: u32,
+}
+
+struct BypassDecoder {
+ data: *const u8,
+ data_len: u32,
+ bit_pos: u32,
+ strict: bool,
+}
+
+#[inline(always)]
+fn state_bit(states: *const u8, index: u32, shift: u8) -> u8 {
+ (simt_load(states, index as usize) >> shift) & 1
+}
+
+#[inline(always)]
+fn set_state_bit(states: *mut u8, index: u32, shift: u8, value: u8) {
+ let state = simt_load(states.cast_const(), index as usize);
+ simt_store(
+ states,
+ index as usize,
+ (state & !(1 << shift)) | ((value & 1) << shift),
+ );
+}
+
+#[inline(always)]
+fn coefficient_index(padded_width: u32, x: u32, y: u32) -> u32 {
+ x + y * padded_width
+}
+
+#[inline(always)]
+fn is_significant(states: *const u8, index: u32) -> bool {
+ state_bit(states, index, SIGNIFICANT_SHIFT) != 0
+}
+
+#[inline(always)]
+fn sign(states: *const u8, index: u32) -> u32 {
+ state_bit(states, index, SIGN_SHIFT) as u32
+}
+
+#[inline(always)]
+fn zero_coded(states: *const u8, index: u32, marker: u8) -> bool {
+ marker != 0 && (simt_load(states, index as usize) & ZERO_CODED_MASK) == marker
+}
+
+#[inline(always)]
+fn set_zero_coded(states: *mut u8, index: u32, marker: u8) {
+ let state = simt_load(states.cast_const(), index as usize);
+ simt_store(
+ states,
+ index as usize,
+ (state & !ZERO_CODED_MASK) | (marker & ZERO_CODED_MASK),
+ );
+}
+
+#[inline(always)]
+fn set_significant(states: *mut u8, index: u32) {
+ set_state_bit(states, index, SIGNIFICANT_SHIFT, 1);
+}
+
+#[inline(always)]
+fn is_magnitude_refined(states: *const u8, index: u32) -> bool {
+ state_bit(states, index, MAGNITUDE_REFINED_SHIFT) != 0
+}
+
+#[inline(always)]
+fn set_magnitude_refined(states: *mut u8, index: u32) {
+ set_state_bit(states, index, MAGNITUDE_REFINED_SHIFT, 1);
+}
+
+#[inline(always)]
+fn push_coefficient_bit(coefficients: *mut u32, index: u32, position: u32) {
+ let value = simt_load(coefficients.cast_const(), index as usize);
+ simt_store(coefficients, index as usize, value | (1 << position));
+}
+
+#[inline(always)]
+fn set_coefficient_sign(coefficients: *mut u32, index: u32, negative: u32) {
+ let value = simt_load(coefficients.cast_const(), index as usize) & 0x7fff_ffff;
+ simt_store(
+ coefficients,
+ index as usize,
+ if negative != 0 {
+ value | 0x8000_0000
+ } else {
+ value
+ },
+ );
+}
+
+#[inline(always)]
+fn neighbor_in_next_stripe(y: u32, height: u32) -> bool {
+ let real_y = y - 1;
+ real_y + 1 < height && ((real_y + 1) >> 2) > (real_y >> 2)
+}
+
+#[inline(always)]
+fn neighborhood(states: *const u8, padded_width: u32, x: u32, y: u32) -> u8 {
+ (is_significant(states, coefficient_index(padded_width, x, y + 1)) as u8)
+ | ((is_significant(states, coefficient_index(padded_width, x + 1, y + 1)) as u8)
+ << 1)
+ | ((is_significant(states, coefficient_index(padded_width, x + 1, y)) as u8) << 2)
+ | ((is_significant(states, coefficient_index(padded_width, x - 1, y + 1)) as u8)
+ << 3)
+ | ((is_significant(states, coefficient_index(padded_width, x - 1, y)) as u8) << 4)
+ | ((is_significant(states, coefficient_index(padded_width, x + 1, y - 1)) as u8)
+ << 5)
+ | ((is_significant(states, coefficient_index(padded_width, x, y - 1)) as u8) << 6)
+ | ((is_significant(states, coefficient_index(padded_width, x - 1, y - 1)) as u8)
+ << 7)
+}
+
+#[inline(always)]
+fn effective_neighborhood(
+ states: *const u8,
+ padded_width: u32,
+ x: u32,
+ y: u32,
+ height: u32,
+ style_flags: u32,
+) -> u8 {
+ let mut result = neighborhood(states, padded_width, x, y);
+ if style_flags & STYLE_VERTICALLY_CAUSAL != 0 && neighbor_in_next_stripe(y, height) {
+ result &= 0b1111_0100;
+ }
+ result
+}
+
+#[inline(always)]
+fn zero_context(tables: *const u8, neighbors: u8, sub_band_type: u32) -> u8 {
+ let offset = match sub_band_type {
+ 1 => ZERO_CONTEXT_HL_BYTE_OFFSET,
+ 3 => ZERO_CONTEXT_HH_BYTE_OFFSET,
+ _ => ZERO_CONTEXT_LL_LH_BYTE_OFFSET,
+ };
+ simt_load(tables, offset + neighbors as usize)
+}
+
+#[inline(always)]
+fn magnitude_context(
+ states: *const u8,
+ padded_width: u32,
+ x: u32,
+ y: u32,
+ height: u32,
+ style_flags: u32,
+) -> u8 {
+ let index = coefficient_index(padded_width, x, y);
+ if is_magnitude_refined(states, index) {
+ 16
+ } else {
+ 14 + effective_neighborhood(states, padded_width, x, y, height, style_flags).min(1)
+ }
+}
+
+#[inline(always)]
+fn sign_context(
+ tables: *const u8,
+ states: *const u8,
+ padded_width: u32,
+ x: u32,
+ y: u32,
+ height: u32,
+ style_flags: u32,
+) -> (u8, u8) {
+ let significances =
+ effective_neighborhood(states, padded_width, x, y, height, style_flags) & 0b0101_0101;
+ let top = sign(states, coefficient_index(padded_width, x, y - 1));
+ let left = sign(states, coefficient_index(padded_width, x - 1, y));
+ let right = sign(states, coefficient_index(padded_width, x + 1, y));
+ let bottom = if style_flags & STYLE_VERTICALLY_CAUSAL != 0
+ && neighbor_in_next_stripe(y, height)
+ {
+ 0
+ } else {
+ sign(states, coefficient_index(padded_width, x, y + 1))
+ };
+ let signs = ((top << 6) | (left << 4) | (right << 2) | bottom) as u8;
+ let negative = significances & signs;
+ let positive = significances & !signs;
+ let packed = simt_load(
+ tables.cast::(),
+ SIGN_CONTEXT_HALFWORD_OFFSET + ((negative << 1) | positive) as usize,
+ );
+ (packed as u8, (packed >> 8) as u8)
+}
+
+#[inline(always)]
+fn reset_contexts(contexts: *mut u8) {
+ let mut index = 0;
+ while index < 19 {
+ simt_store(contexts, index, 0);
+ index += 1;
+ }
+ simt_store(contexts, 0, 4);
+ simt_store(contexts, 17, 3);
+ simt_store(contexts, 18, 46);
+}
+
+#[inline(always)]
+fn current_byte(decoder: &ArithmeticDecoder) -> u8 {
+ if decoder.base_pointer < decoder.data_len {
+ simt_load(decoder.data, decoder.base_pointer as usize)
+ } else {
+ 0xff
+ }
+}
+
+#[inline(always)]
+fn next_byte(decoder: &ArithmeticDecoder) -> u8 {
+ if decoder.base_pointer + 1 < decoder.data_len {
+ simt_load(decoder.data, decoder.base_pointer as usize + 1)
+ } else {
+ 0xff
+ }
+}
+
+#[inline(always)]
+fn arithmetic_read_byte(decoder: &mut ArithmeticDecoder) {
+ if current_byte(decoder) == 0xff {
+ let next = next_byte(decoder);
+ if next > 0x8f {
+ decoder.shift_count = 8;
+ } else {
+ decoder.base_pointer += 1;
+ decoder.c = decoder
+ .c
+ .wrapping_add(0xfe00)
+ .wrapping_sub((current_byte(decoder) as u32) << 9);
+ decoder.shift_count = 7;
+ }
+ } else {
+ decoder.base_pointer += 1;
+ decoder.c = decoder
+ .c
+ .wrapping_add(0xff00)
+ .wrapping_sub((current_byte(decoder) as u32) << 8);
+ decoder.shift_count = 8;
+ }
+}
+
+#[inline(always)]
+fn arithmetic_initialize(decoder: &mut ArithmeticDecoder) {
+ decoder.c = ((current_byte(decoder) ^ 0xff) as u32) << 16;
+ arithmetic_read_byte(decoder);
+ decoder.c <<= 7;
+ decoder.shift_count -= 7;
+ decoder.a = 0x8000;
+}
+
+#[inline(always)]
+fn arithmetic_renormalize(decoder: &mut ArithmeticDecoder) {
+ while decoder.a & 0x8000 == 0 {
+ if decoder.shift_count == 0 {
+ arithmetic_read_byte(decoder);
+ }
+ decoder.a <<= 1;
+ decoder.c <<= 1;
+ decoder.shift_count -= 1;
+ }
+}
+
+#[inline(always)]
+fn arithmetic_decode_bit(
+ decoder: &mut ArithmeticDecoder,
+ contexts: *mut u8,
+ label: u8,
+ tables: *const u8,
+) -> u32 {
+ let mut context = simt_load(contexts.cast_const(), label as usize);
+ let state = (context & 0x7f) as usize;
+ let qe = simt_load(tables.cast::(), MQ_QE_WORD_OFFSET + state);
+ let transition = simt_load(
+ tables.cast::(),
+ MQ_TRANSITION_WORD_OFFSET + state,
+ );
+ let nmps = transition as u8;
+ let nlps = (transition >> 8) as u8;
+ let switch = transition & (1 << 16) != 0;
+ decoder.a -= qe;
+
+ let decoded;
+ if decoder.c >> 16 < decoder.a {
+ if decoder.a & 0x8000 != 0 {
+ return (context >> 7) as u32;
+ }
+ if decoder.a < qe {
+ decoded = ((context >> 7) ^ 1) as u32;
+ if switch {
+ context ^= 0x80;
+ }
+ context = (context & 0x80) | nlps;
+ } else {
+ decoded = (context >> 7) as u32;
+ context = (context & 0x80) | nmps;
+ }
+ } else {
+ decoder.c -= decoder.a << 16;
+ if decoder.a < qe {
+ decoder.a = qe;
+ decoded = (context >> 7) as u32;
+ context = (context & 0x80) | nmps;
+ } else {
+ decoder.a = qe;
+ decoded = ((context >> 7) ^ 1) as u32;
+ if switch {
+ context ^= 0x80;
+ }
+ context = (context & 0x80) | nlps;
+ }
+ }
+ simt_store(contexts, label as usize, context);
+ arithmetic_renormalize(decoder);
+ decoded
+}
+
+#[inline(always)]
+fn raw_read_bit(decoder: &mut BypassDecoder) -> u32 {
+ let byte_position = decoder.bit_pos / 8;
+ if byte_position >= decoder.data_len {
+ if decoder.strict {
+ return RAW_READ_FAILED;
+ }
+ decoder.bit_pos += 1;
+ return 1;
+ }
+ let bit_position = decoder.bit_pos % 8;
+ let byte = simt_load(decoder.data, byte_position as usize);
+ decoder.bit_pos += 1;
+ ((byte as u32) >> (7 - bit_position)) & 1
+}
+
+#[inline(always)]
+fn bypass_read_bit(decoder: &mut BypassDecoder) -> u32 {
+ let byte_position = decoder.bit_pos / 8;
+ let bit_position = decoder.bit_pos % 8;
+ let bit = raw_read_bit(decoder);
+ if bit == RAW_READ_FAILED {
+ return RAW_READ_FAILED;
+ }
+ if bit_position == 7
+ && byte_position < decoder.data_len
+ && simt_load(decoder.data, byte_position as usize) == 0xff
+ {
+ let stuffed = raw_read_bit(decoder);
+ if decoder.strict && stuffed != 0 {
+ return RAW_READ_FAILED;
+ }
+ }
+ bit
+}
+
+#[inline(always)]
+fn decode_sign_arithmetic(
+ decoder: &mut ArithmeticDecoder,
+ contexts: *mut u8,
+ tables: *const u8,
+ states: *mut u8,
+ coefficients: *mut u32,
+ padded_width: u32,
+ x: u32,
+ y: u32,
+ height: u32,
+ style_flags: u32,
+) {
+ let (label, xor) = sign_context(
+ tables,
+ states.cast_const(),
+ padded_width,
+ x,
+ y,
+ height,
+ style_flags,
+ );
+ let negative = arithmetic_decode_bit(decoder, contexts, label, tables) ^ xor as u32;
+ let index = coefficient_index(padded_width, x, y);
+ set_state_bit(states, index, SIGN_SHIFT, negative as u8);
+ set_coefficient_sign(coefficients, index, negative);
+ set_significant(states, index);
+}
+
+#[inline(always)]
+fn decode_sign_bypass(
+ decoder: &mut BypassDecoder,
+ states: *mut u8,
+ coefficients: *mut u32,
+ padded_width: u32,
+ x: u32,
+ y: u32,
+) -> bool {
+ let negative = bypass_read_bit(decoder);
+ if negative == RAW_READ_FAILED {
+ return false;
+ }
+ let index = coefficient_index(padded_width, x, y);
+ set_state_bit(states, index, SIGN_SHIFT, negative as u8);
+ set_coefficient_sign(coefficients, index, negative);
+ set_significant(states, index);
+ true
+}
+
+#[inline(always)]
+fn set_status(statuses: *mut ClassicStatus, index: u32, code: u32, detail: u32) {
+ simt_store(
+ statuses,
+ index as usize,
+ ClassicStatus {
+ code,
+ detail,
+ reserved0: 0,
+ reserved1: 0,
+ },
+ );
+}
+
+#[inline(always)]
+fn fail(statuses: *mut ClassicStatus, index: u32, code: u32, detail: u32) -> bool {
+ set_status(statuses, index, code, detail);
+ false
+}
+
+#[inline(always)]
+fn validate_job_header(
+ job: ClassicJob,
+ statuses: *mut ClassicStatus,
+ job_index: u32,
+) -> bool {
+ if job.width == 0
+ || job.height == 0
+ || job.width > 64
+ || job.height > 64
+ || job.output_stride < job.width
+ {
+ return fail(statuses, job_index, STATUS_UNSUPPORTED, 1);
+ }
+ if job.total_bitplanes == 0
+ || job.total_bitplanes > 31
+ || job.missing_msbs >= job.total_bitplanes
+ || job.sub_band_type > 3
+ || job.style_flags & !KNOWN_STYLE_FLAGS != 0
+ {
+ return fail(statuses, job_index, STATUS_UNSUPPORTED, 2);
+ }
+ let bitplanes = job.total_bitplanes - job.missing_msbs;
+ let max_passes = 1 + 3 * (bitplanes - 1);
+ if job.number_of_coding_passes > max_passes {
+ return fail(statuses, job_index, STATUS_UNSUPPORTED, 3);
+ }
+ if job.number_of_coding_passes != 0 && job.segment_count == 0 {
+ return fail(statuses, job_index, STATUS_UNSUPPORTED, 4);
+ }
+ true
+}
+
+#[inline(always)]
+fn decode_job(
+ job: ClassicJob,
+ coded_data: *const u8,
+ segments: *const ClassicSegment,
+ tables: *const u8,
+ coefficients: *mut u32,
+ contexts: *mut u8,
+ states: *mut u8,
+ statuses: *mut ClassicStatus,
+ job_index: u32,
+) -> bool {
+ let bitplanes = job.total_bitplanes - job.missing_msbs;
+ if job.number_of_coding_passes == 0 {
+ return true;
+ }
+
+ let padded_width = job.width + 2;
+ let coded_end = job.coded_offset as u64 + job.coded_len as u64;
+ reset_contexts(contexts);
+ let mut expected_pass = 0;
+ let mut expected_offset = job.coded_offset as u64;
+
+ let mut segment_index = 0;
+ while segment_index < job.segment_count {
+ let segment = simt_load(
+ segments,
+ (job.segment_offset + segment_index) as usize,
+ );
+ if segment.start_coding_pass != expected_pass
+ || segment.start_coding_pass > segment.end_coding_pass
+ || segment.end_coding_pass > job.number_of_coding_passes
+ || segment.data_offset as u64 != expected_offset
+ {
+ return fail(statuses, job_index, STATUS_UNSUPPORTED, 5);
+ }
+ let segment_end = segment.data_offset as u64 + segment.data_length as u64;
+ if segment.data_offset < job.coded_offset || segment_end > coded_end {
+ return fail(statuses, job_index, STATUS_UNSUPPORTED, 6);
+ }
+ expected_pass = segment.end_coding_pass;
+ expected_offset = segment_end;
+ if segment.start_coding_pass == segment.end_coding_pass {
+ segment_index += 1;
+ continue;
+ }
+
+ let segment_data = if segment.data_length == 0 {
+ coded_data
+ } else {
+ simt_mut_ptr_at(coded_data.cast_mut(), segment.data_offset as usize).cast_const()
+ };
+ let mut arithmetic = ArithmeticDecoder {
+ data: segment_data,
+ data_len: segment.data_length,
+ c: 0,
+ a: 0,
+ base_pointer: 0,
+ shift_count: 0,
+ };
+ let mut bypass = BypassDecoder {
+ data: segment_data,
+ data_len: segment.data_length,
+ bit_pos: 0,
+ strict: job.strict != 0,
+ };
+ let use_arithmetic = segment.use_arithmetic != 0;
+ if use_arithmetic {
+ arithmetic_initialize(&mut arithmetic);
+ }
+
+ let mut zero_epoch = ((segment.start_coding_pass + 2) / 3) as u8;
+ let mut coding_pass = segment.start_coding_pass;
+ while coding_pass < segment.end_coding_pass {
+ let current_bitplane = (coding_pass + 2) / 3;
+ let current_position = bitplanes - 1 - current_bitplane;
+ let pass_type = coding_pass % 3;
+ if pass_type == 0 && !use_arithmetic {
+ return fail(statuses, job_index, STATUS_UNSUPPORTED, 7);
+ }
+
+ let mut base_row = 0;
+ while base_row < job.height {
+ let stripe_end = (base_row + 4).min(job.height);
+ let mut x = 0;
+ while x < job.width {
+ let index_x = x + 1;
+ let mut index_y = base_row + 1;
+ while index_y < stripe_end + 1 {
+ let index = coefficient_index(padded_width, index_x, index_y);
+ if pass_type == 0 {
+ if !is_significant(states.cast_const(), index)
+ && !zero_coded(states.cast_const(), index, zero_epoch)
+ {
+ let run_length = (index_y - 1) % 4 == 0
+ && job.height - (index_y - 1) >= 4
+ && effective_neighborhood(
+ states.cast_const(),
+ padded_width,
+ index_x,
+ index_y,
+ job.height,
+ job.style_flags,
+ ) == 0
+ && effective_neighborhood(
+ states.cast_const(),
+ padded_width,
+ index_x,
+ index_y + 1,
+ job.height,
+ job.style_flags,
+ ) == 0
+ && effective_neighborhood(
+ states.cast_const(),
+ padded_width,
+ index_x,
+ index_y + 2,
+ job.height,
+ job.style_flags,
+ ) == 0
+ && effective_neighborhood(
+ states.cast_const(),
+ padded_width,
+ index_x,
+ index_y + 3,
+ job.height,
+ job.style_flags,
+ ) == 0;
+ let bit;
+ if run_length {
+ bit = arithmetic_decode_bit(&mut arithmetic, contexts, 17, tables);
+ if bit == 0 {
+ index_y += 4;
+ continue;
+ }
+ let first =
+ arithmetic_decode_bit(&mut arithmetic, contexts, 18, tables);
+ let second =
+ arithmetic_decode_bit(&mut arithmetic, contexts, 18, tables);
+ index_y += (first << 1) | second;
+ } else {
+ let label = zero_context(
+ tables,
+ effective_neighborhood(
+ states.cast_const(),
+ padded_width,
+ index_x,
+ index_y,
+ job.height,
+ job.style_flags,
+ ),
+ job.sub_band_type,
+ );
+ bit =
+ arithmetic_decode_bit(&mut arithmetic, contexts, label, tables);
+ }
+ if bit != 0 {
+ let actual =
+ coefficient_index(padded_width, index_x, index_y);
+ push_coefficient_bit(coefficients, actual, current_position);
+ decode_sign_arithmetic(
+ &mut arithmetic,
+ contexts,
+ tables,
+ states,
+ coefficients,
+ padded_width,
+ index_x,
+ index_y,
+ job.height,
+ job.style_flags,
+ );
+ }
+ }
+ } else if pass_type == 1 {
+ let neighbors = effective_neighborhood(
+ states.cast_const(),
+ padded_width,
+ index_x,
+ index_y,
+ job.height,
+ job.style_flags,
+ );
+ if !is_significant(states.cast_const(), index) && neighbors != 0 {
+ let label = zero_context(tables, neighbors, job.sub_band_type);
+ let bit = if use_arithmetic {
+ arithmetic_decode_bit(&mut arithmetic, contexts, label, tables)
+ } else {
+ let bit = bypass_read_bit(&mut bypass);
+ if bit == RAW_READ_FAILED {
+ return fail(statuses, job_index, STATUS_FAILED, 8);
+ }
+ bit
+ };
+ set_zero_coded(states, index, zero_epoch);
+ if bit != 0 {
+ push_coefficient_bit(coefficients, index, current_position);
+ if use_arithmetic {
+ decode_sign_arithmetic(
+ &mut arithmetic,
+ contexts,
+ tables,
+ states,
+ coefficients,
+ padded_width,
+ index_x,
+ index_y,
+ job.height,
+ job.style_flags,
+ );
+ } else if !decode_sign_bypass(
+ &mut bypass,
+ states,
+ coefficients,
+ padded_width,
+ index_x,
+ index_y,
+ ) {
+ return fail(statuses, job_index, STATUS_FAILED, 9);
+ }
+ }
+ }
+ } else if is_significant(states.cast_const(), index)
+ && !zero_coded(states.cast_const(), index, zero_epoch)
+ {
+ let label = magnitude_context(
+ states.cast_const(),
+ padded_width,
+ index_x,
+ index_y,
+ job.height,
+ job.style_flags,
+ );
+ let bit = if use_arithmetic {
+ arithmetic_decode_bit(&mut arithmetic, contexts, label, tables)
+ } else {
+ let bit = bypass_read_bit(&mut bypass);
+ if bit == RAW_READ_FAILED {
+ return fail(statuses, job_index, STATUS_FAILED, 10);
+ }
+ bit
+ };
+ if bit != 0 {
+ push_coefficient_bit(coefficients, index, current_position);
+ }
+ set_magnitude_refined(states, index);
+ }
+ index_y += 1;
+ }
+ x += 1;
+ }
+ base_row += 4;
+ }
+
+ if pass_type == 0 {
+ if job.style_flags & STYLE_SEGMENTATION_SYMBOLS != 0 {
+ let b0 = arithmetic_decode_bit(&mut arithmetic, contexts, 18, tables);
+ let b1 = arithmetic_decode_bit(&mut arithmetic, contexts, 18, tables);
+ let b2 = arithmetic_decode_bit(&mut arithmetic, contexts, 18, tables);
+ let b3 = arithmetic_decode_bit(&mut arithmetic, contexts, 18, tables);
+ let valid = b0 == 1 && b1 == 0 && b2 == 1 && b3 == 0;
+ if !valid && job.strict != 0 {
+ return fail(statuses, job_index, STATUS_FAILED, 11);
+ }
+ }
+ zero_epoch = zero_epoch.saturating_add(1).min(ZERO_CODED_MASK);
+ }
+ if job.style_flags & STYLE_RESET_CONTEXTS != 0 {
+ reset_contexts(contexts);
+ }
+ coding_pass += 1;
+ }
+ segment_index += 1;
+ }
+
+ if expected_pass != job.number_of_coding_passes || expected_offset != coded_end {
+ return fail(statuses, job_index, STATUS_UNSUPPORTED, 12);
+ }
+ true
+}
+
+#[cuda_module]
+mod kernels {
+ use super::*;
+
+ #[expect(
+ static_mut_refs,
+ reason = "CUDA block-shared state belongs exclusively to one launched codeblock"
+ )]
+ #[kernel]
+ pub unsafe fn j2k_decode_classic_codeblocks_multi(
+ coded_data: *const u8,
+ jobs: *const ClassicJob,
+ segments: *const ClassicSegment,
+ tables: *const u8,
+ statuses: *mut ClassicStatus,
+ coefficient_scratch: *mut u32,
+ ) {
+ static mut STATES: SharedArray = SharedArray::UNINIT;
+ static mut CONTEXTS: SharedArray = SharedArray::UNINIT;
+
+ let job_index = thread::blockIdx_x();
+ let lane = thread::threadIdx_x();
+ let job = simt_load(jobs, job_index as usize);
+ if lane == 0 {
+ set_status(statuses, job_index, STATUS_OK, 0);
+ validate_job_header(job, statuses, job_index);
+ }
+ thread::sync_threads();
+ if simt_load(statuses.cast_const(), job_index as usize).code != STATUS_OK {
+ return;
+ }
+
+ let padded_width = job.width + 2;
+ let coefficient_count = padded_width * (job.height + 2);
+ let coefficients =
+ simt_mut_ptr_at(coefficient_scratch, job.scratch_offset as usize);
+ let states = unsafe { STATES.as_mut_ptr() };
+ let contexts = unsafe { CONTEXTS.as_mut_ptr() };
+
+ let mut index = lane;
+ while index < coefficient_count {
+ simt_store(coefficients, index as usize, 0);
+ simt_store(states, index as usize, 0);
+ index += CLASSIC_DECODE_THREADS;
+ }
+ thread::sync_threads();
+
+ if lane == 0
+ && !decode_job(
+ job,
+ coded_data,
+ segments,
+ tables,
+ coefficients,
+ contexts,
+ states,
+ statuses,
+ job_index,
+ )
+ && simt_load(statuses.cast_const(), job_index as usize).code == STATUS_OK
+ {
+ set_status(statuses, job_index, STATUS_FAILED, 0);
+ }
+ thread::sync_threads();
+
+ if simt_load(statuses.cast_const(), job_index as usize).code != STATUS_OK {
+ return;
+ }
+ let output = job.output_ptr as usize as *mut f32;
+ let sample_count = job.width * job.height;
+ let mut sample = lane;
+ while sample < sample_count {
+ let x = sample % job.width;
+ let y = sample / job.width;
+ let packed = simt_load(
+ coefficients.cast_const(),
+ coefficient_index(padded_width, x + 1, y + 1) as usize,
+ );
+ let magnitude = (packed & 0x7fff_ffff) as i32;
+ let signed = if packed & 0x8000_0000 != 0 {
+ -magnitude
+ } else {
+ magnitude
+ };
+ simt_store(
+ output,
+ job.output_offset as usize
+ + y as usize * job.output_stride as usize
+ + x as usize,
+ signed as f32 * job.dequantization_step,
+ );
+ sample += CLASSIC_DECODE_THREADS;
+ }
+ }
+}
+
+fn main() {}
diff --git a/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_classic_decode/src/main.rs b/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_classic_decode/src/main.rs
new file mode 100644
index 00000000..f328e4d9
--- /dev/null
+++ b/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_classic_decode/src/main.rs
@@ -0,0 +1 @@
+fn main() {}
diff --git a/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_ml/Cargo.toml.in b/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_ml/Cargo.toml.in
new file mode 100644
index 00000000..2aff80c4
--- /dev/null
+++ b/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_ml/Cargo.toml.in
@@ -0,0 +1,15 @@
+[package]
+name = "j2k_cuda_oxide_j2k_ml_host"
+version = "0.1.0"
+edition = "2024"
+
+[package.metadata.cuda-oxide.interop]
+kind = "j2k-ml"
+
+[[package.metadata.cuda-oxide.device-crates]]
+manifest-path = "simt/Cargo.toml"
+ptx-dir = "ptx"
+artifact-name = "j2k_cuda_oxide_j2k_ml"
+
+[workspace]
+resolver = "2"
diff --git a/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_ml/rust-toolchain.toml b/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_ml/rust-toolchain.toml
new file mode 100644
index 00000000..9f964011
--- /dev/null
+++ b/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_ml/rust-toolchain.toml
@@ -0,0 +1,3 @@
+[toolchain]
+channel = "nightly-2026-04-03"
+components = ["rust-src", "rustc-dev", "rust-analyzer", "clippy", "llvm-tools"]
diff --git a/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_ml/simt/Cargo.toml.in b/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_ml/simt/Cargo.toml.in
new file mode 100644
index 00000000..5ec049ad
--- /dev/null
+++ b/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_ml/simt/Cargo.toml.in
@@ -0,0 +1,13 @@
+[package]
+name = "j2k_cuda_oxide_j2k_ml"
+version = "0.1.0"
+edition = "2024"
+
+[workspace]
+resolver = "2"
+
+[dependencies]
+cuda-core = { git = "https://github.com/NVlabs/cuda-oxide.git", rev = "a9f964a956f397dd0b3c8db88a3ca5824186c261" }
+cuda-device = { git = "https://github.com/NVlabs/cuda-oxide.git", rev = "a9f964a956f397dd0b3c8db88a3ca5824186c261" }
+cuda-host = { git = "https://github.com/NVlabs/cuda-oxide.git", rev = "a9f964a956f397dd0b3c8db88a3ca5824186c261" }
+j2k-codec-math = { path = "__J2K_CODEC_MATH_PATH__" }
diff --git a/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_ml/simt/src/main.rs b/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_ml/simt/src/main.rs
new file mode 100644
index 00000000..dc9392a4
--- /dev/null
+++ b/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_ml/simt/src/main.rs
@@ -0,0 +1,84 @@
+use cuda_device::{kernel, thread};
+use cuda_host::cuda_module;
+
+include!("../../../cuda_oxide_simt_prelude.rs");
+
+#[cuda_module]
+mod kernels {
+ use super::*;
+
+ #[kernel]
+ #[expect(
+ clippy::too_many_arguments,
+ reason = "the CUDA kernel ABI carries validated conversion and normalization scalars"
+ )]
+ pub unsafe fn j2k_ml_convert(
+ destination: *mut u8,
+ source: *const u8,
+ sample_count: u64,
+ channels: u32,
+ source_sample: u32,
+ output_kind: u32,
+ layout: u32,
+ destination_offset: u64,
+ normalization: u32,
+ mean0: f32,
+ mean1: f32,
+ mean2: f32,
+ mean3: f32,
+ std0: f32,
+ std1: f32,
+ std2: f32,
+ std3: f32,
+ ) {
+ let index = thread::index_1d().get();
+ if index >= sample_count as usize {
+ return;
+ }
+ let channel = index % channels as usize;
+ let pixel = index / channels as usize;
+ let pixels = sample_count as usize / channels as usize;
+ let output_index = destination_offset as usize
+ + if layout == 0 {
+ channel * pixels + pixel
+ } else {
+ index
+ };
+ let value_u16 = if source_sample == 1 {
+ simt_load(source, index) as u16
+ } else {
+ simt_load(source.cast::(), index)
+ };
+ if output_kind == 1 {
+ simt_store(destination, output_index, value_u16 as u8);
+ return;
+ }
+ if output_kind == 2 {
+ simt_store(destination.cast::(), output_index, value_u16);
+ return;
+ }
+ let denominator = if source_sample == 1 { 255.0 } else { 65_535.0 };
+ let mut value = value_u16 as f32;
+ if normalization >= 2 {
+ value /= denominator;
+ }
+ if normalization == 3 {
+ let mean = match channel {
+ 0 => mean0,
+ 1 => mean1,
+ 2 => mean2,
+ _ => mean3,
+ };
+ let std = match channel {
+ 0 => std0,
+ 1 => std1,
+ 2 => std2,
+ _ => std3,
+ };
+ value = (value - mean) / std;
+ }
+ simt_store(destination.cast::(), output_index, value);
+ }
+}
+
+fn main() {}
diff --git a/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_ml/src/main.rs b/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_ml/src/main.rs
new file mode 100644
index 00000000..f328e4d9
--- /dev/null
+++ b/crates/j2k-cuda-runtime/src/cuda_oxide_j2k_ml/src/main.rs
@@ -0,0 +1 @@
+fn main() {}
diff --git a/crates/j2k-cuda-runtime/src/driver.rs b/crates/j2k-cuda-runtime/src/driver.rs
index 1d4129ce..b8eb0957 100644
--- a/crates/j2k-cuda-runtime/src/driver.rs
+++ b/crates/j2k-cuda-runtime/src/driver.rs
@@ -33,8 +33,16 @@ pub(crate) type CuCtxCreate = unsafe extern "C" fn(*mut CuContext, c_uint, CuDev
pub(crate) type CuCtxDestroy = unsafe extern "C" fn(CuContext) -> CuResult;
+pub(crate) type CuDevicePrimaryCtxRetain =
+ unsafe extern "C" fn(*mut CuContext, CuDevice) -> CuResult;
+
+pub(crate) type CuDevicePrimaryCtxRelease = unsafe extern "C" fn(CuDevice) -> CuResult;
+
pub(crate) type CuCtxSetCurrent = unsafe extern "C" fn(CuContext) -> CuResult;
+pub(crate) type CuPointerGetAttribute =
+ unsafe extern "C" fn(*mut c_void, c_int, CuDevicePtr) -> CuResult;
+
pub(crate) type CuMemAlloc = unsafe extern "C" fn(*mut CuDevicePtr, usize) -> CuResult;
pub(crate) type CuMemFree = unsafe extern "C" fn(CuDevicePtr) -> CuResult;
@@ -105,7 +113,10 @@ pub(crate) struct Driver {
pub(crate) cu_device_get: CuDeviceGet,
pub(crate) cu_ctx_create: CuCtxCreate,
pub(crate) cu_ctx_destroy: CuCtxDestroy,
+ pub(crate) cu_device_primary_ctx_retain: CuDevicePrimaryCtxRetain,
+ pub(crate) cu_device_primary_ctx_release: CuDevicePrimaryCtxRelease,
pub(crate) cu_ctx_set_current: CuCtxSetCurrent,
+ pub(crate) cu_pointer_get_attribute: CuPointerGetAttribute,
pub(crate) cu_mem_alloc: CuMemAlloc,
pub(crate) cu_mem_free: CuMemFree,
pub(crate) cu_mem_host_alloc: CuMemHostAlloc,
@@ -174,7 +185,13 @@ impl Driver {
cu_device_get: load_symbol(&library, b"cuDeviceGet\0")?,
cu_ctx_create: load_symbol(&library, b"cuCtxCreate_v2\0")?,
cu_ctx_destroy: load_symbol(&library, b"cuCtxDestroy_v2\0")?,
+ cu_device_primary_ctx_retain: load_symbol(&library, b"cuDevicePrimaryCtxRetain\0")?,
+ cu_device_primary_ctx_release: load_symbol(
+ &library,
+ b"cuDevicePrimaryCtxRelease_v2\0",
+ )?,
cu_ctx_set_current: load_symbol(&library, b"cuCtxSetCurrent\0")?,
+ cu_pointer_get_attribute: load_symbol(&library, b"cuPointerGetAttribute\0")?,
cu_mem_alloc: load_symbol(&library, b"cuMemAlloc_v2\0")?,
cu_mem_free: load_symbol(&library, b"cuMemFree_v2\0")?,
cu_mem_host_alloc: load_symbol(&library, b"cuMemHostAlloc\0")?,
diff --git a/crates/j2k-cuda-runtime/src/htj2k_decode.rs b/crates/j2k-cuda-runtime/src/htj2k_decode.rs
index 87ad89fe..a277c240 100644
--- a/crates/j2k-cuda-runtime/src/htj2k_decode.rs
+++ b/crates/j2k-cuda-runtime/src/htj2k_decode.rs
@@ -4,7 +4,7 @@ mod api;
mod completion;
mod context_validation;
mod launch;
-mod output_regions;
+pub(crate) mod output_regions;
mod planning;
mod queued;
mod status;
diff --git a/crates/j2k-cuda-runtime/src/htj2k_decode/api.rs b/crates/j2k-cuda-runtime/src/htj2k_decode/api.rs
index 12cdabca..405018f1 100644
--- a/crates/j2k-cuda-runtime/src/htj2k_decode/api.rs
+++ b/crates/j2k-cuda-runtime/src/htj2k_decode/api.rs
@@ -79,7 +79,21 @@ impl CudaContext {
Ok(CudaHtj2kDecodeResources {
payload: CudaHtj2kDecodePayload::Owned(self.upload_pinned(payload)?),
payload_len: payload.len(),
- tables: tables.clone(),
+ tables: Some(tables.clone()),
+ })
+ }
+
+ /// Upload a classic-only J2K payload without HTJ2K lookup tables.
+ #[doc(hidden)]
+ pub fn upload_j2k_decode_payload(
+ &self,
+ payload: &[u8],
+ ) -> Result {
+ self.inner.set_current()?;
+ Ok(CudaHtj2kDecodeResources {
+ payload: CudaHtj2kDecodePayload::Owned(self.upload_pinned(payload)?),
+ payload_len: payload.len(),
+ tables: None,
})
}
@@ -101,7 +115,27 @@ impl CudaContext {
Ok(CudaHtj2kDecodeResources {
payload: CudaHtj2kDecodePayload::Pooled(pool.upload_pinned(payload)?),
payload_len: payload.len(),
- tables: tables.clone(),
+ tables: Some(tables.clone()),
+ })
+ }
+
+ /// Upload a classic-only J2K payload into a pooled buffer without HTJ2K lookup tables.
+ #[doc(hidden)]
+ pub fn upload_j2k_decode_payload_with_pool(
+ &self,
+ payload: &[u8],
+ pool: &CudaBufferPool,
+ ) -> Result {
+ if !pool.is_owned_by(self) {
+ return Err(CudaError::InvalidArgument {
+ message: "J2K decode payload pool must belong to the upload context".to_string(),
+ });
+ }
+ self.inner.set_current()?;
+ Ok(CudaHtj2kDecodeResources {
+ payload: CudaHtj2kDecodePayload::Pooled(pool.upload_pinned(payload)?),
+ payload_len: payload.len(),
+ tables: None,
})
}
diff --git a/crates/j2k-cuda-runtime/src/htj2k_decode/completion.rs b/crates/j2k-cuda-runtime/src/htj2k_decode/completion.rs
index 6bd9ade1..32e01473 100644
--- a/crates/j2k-cuda-runtime/src/htj2k_decode/completion.rs
+++ b/crates/j2k-cuda-runtime/src/htj2k_decode/completion.rs
@@ -103,7 +103,7 @@ impl CudaContext {
}
self.inner.set_current()?;
let (decode_kernel, decode_kernel_name) = htj2k_decode_multi_kernel_for_jobs(&kernel_jobs);
- let tables = htj2k_decode_kernel_tables(resources);
+ let tables = htj2k_decode_kernel_tables(resources)?;
let mut host_budget = HostPhaseBudget::with_live_bytes(
"CUDA queued HTJ2K cleanup metadata",
@@ -168,7 +168,7 @@ impl CudaContext {
host_budget.try_vec_filled(kernel_jobs.len(), CudaHtj2kStatus::default())?;
let jobs_buffer = pool.upload(htj2k_cleanup_multi_jobs_as_bytes(kernel_jobs))?;
let status_buffer = pool.take(htj2k_statuses_byte_len(kernel_jobs.len())?)?;
- let tables = htj2k_decode_kernel_tables(resources);
+ let tables = htj2k_decode_kernel_tables(resources)?;
let payload_buffer = resources.payload.buffer()?;
let jobs_device_buffer = pooled_device_buffer(&jobs_buffer)?;
let status_device_buffer = pooled_device_buffer(&status_buffer)?;
@@ -202,13 +202,13 @@ impl CudaContext {
}
// A successful synchronous device-to-host copy completes the
// preceding default-stream cleanup launch.
+ let status_d2h_us = status_d2h_start.map_or(0, |start| start.elapsed().as_micros());
let release_result = pending_pool_reuse
.take()
.ok_or_else(|| CudaError::StatePoisoned {
message: "HTJ2K cleanup pool guard disappeared before release".to_string(),
})?
.release();
- let status_d2h_us = status_d2h_start.map_or(0, |start| start.elapsed().as_micros());
let execution = select_status_release_result(
CudaExecutionStats {
kernel_dispatches: 1,
@@ -469,7 +469,7 @@ impl CudaContext {
job_count: usize,
collect_stage_timings: bool,
) -> Result {
- let tables = htj2k_decode_kernel_tables(resources);
+ let tables = htj2k_decode_kernel_tables(resources)?;
if collect_stage_timings {
let ((), ht_cleanup_us) =
self.time_default_stream_named_us("j2k.htj2k.decode.cleanup", || {
@@ -504,3 +504,21 @@ impl CudaContext {
Ok(0)
}
}
+
+#[cfg(test)]
+mod tests {
+ #[test]
+ fn ht_status_timing_excludes_pool_release() {
+ let source = include_str!("completion.rs");
+ let status_copy = source
+ .find("status_buffer.copy_to_host")
+ .expect("HT status copy");
+ let status_timing = source
+ .find("let status_d2h_us")
+ .expect("HT status timing result");
+ let pool_release = source
+ .find("let release_result = pending_pool_reuse")
+ .expect("HT pool release");
+ assert!(status_copy < status_timing && status_timing < pool_release);
+ }
+}
diff --git a/crates/j2k-cuda-runtime/src/htj2k_decode/context_validation.rs b/crates/j2k-cuda-runtime/src/htj2k_decode/context_validation.rs
index 1d57f5b6..0ac3c619 100644
--- a/crates/j2k-cuda-runtime/src/htj2k_decode/context_validation.rs
+++ b/crates/j2k-cuda-runtime/src/htj2k_decode/context_validation.rs
@@ -27,8 +27,12 @@ impl CudaHtj2kDecodeTableResources {
}
impl CudaHtj2kDecodeResources {
- pub(super) fn is_owned_by(&self, context: &CudaContext) -> Result {
- Ok(self.payload.is_owned_by(context)? && self.tables.is_owned_by(context))
+ pub(crate) fn is_owned_by(&self, context: &CudaContext) -> Result {
+ Ok(self.payload.is_owned_by(context)?
+ && self
+ .tables
+ .as_ref()
+ .is_none_or(|tables| tables.is_owned_by(context)))
}
}
diff --git a/crates/j2k-cuda-runtime/src/htj2k_decode/output_regions.rs b/crates/j2k-cuda-runtime/src/htj2k_decode/output_regions.rs
index 56f80af6..fdd7dc6a 100644
--- a/crates/j2k-cuda-runtime/src/htj2k_decode/output_regions.rs
+++ b/crates/j2k-cuda-runtime/src/htj2k_decode/output_regions.rs
@@ -2,7 +2,7 @@
use super::CudaHtj2kCodeBlockJob;
use crate::{allocation::HostPhaseBudget, error::CudaError};
-use sweep::{validate_disjoint_output_regions, Htj2kOutputRect, Htj2kOutputRegion};
+pub(crate) use sweep::{validate_disjoint_output_regions, Htj2kOutputRect, Htj2kOutputRegion};
mod sweep;
diff --git a/crates/j2k-cuda-runtime/src/htj2k_decode/output_regions/sweep.rs b/crates/j2k-cuda-runtime/src/htj2k_decode/output_regions/sweep.rs
index acb6d96a..794272de 100644
--- a/crates/j2k-cuda-runtime/src/htj2k_decode/output_regions/sweep.rs
+++ b/crates/j2k-cuda-runtime/src/htj2k_decode/output_regions/sweep.rs
@@ -11,19 +11,19 @@ mod cross_stride;
use self::cross_stride::validate_cross_stride_spans;
#[derive(Clone, Copy)]
-pub(super) struct Htj2kOutputRect {
- pub(super) row_start: usize,
- pub(super) row_end: usize,
- pub(super) column_start: usize,
- pub(super) column_end: usize,
+pub(crate) struct Htj2kOutputRect {
+ pub(crate) row_start: usize,
+ pub(crate) row_end: usize,
+ pub(crate) column_start: usize,
+ pub(crate) column_end: usize,
}
#[derive(Clone, Copy)]
-pub(super) struct Htj2kOutputRegion {
- pub(super) stride: usize,
- pub(super) rect: Htj2kOutputRect,
- pub(super) linear_start: usize,
- pub(super) linear_end: usize,
+pub(crate) struct Htj2kOutputRegion {
+ pub(crate) stride: usize,
+ pub(crate) rect: Htj2kOutputRect,
+ pub(crate) linear_start: usize,
+ pub(crate) linear_end: usize,
}
fn validate_same_stride_rects(
@@ -79,7 +79,7 @@ fn validate_same_stride_rects(
Ok(())
}
-pub(super) fn validate_disjoint_output_regions(
+pub(crate) fn validate_disjoint_output_regions(
regions: &mut [Htj2kOutputRegion],
live_region_bytes: usize,
) -> Result<(), CudaError> {
diff --git a/crates/j2k-cuda-runtime/src/htj2k_decode/types.rs b/crates/j2k-cuda-runtime/src/htj2k_decode/types.rs
index 6eacb553..d2f0eb38 100644
--- a/crates/j2k-cuda-runtime/src/htj2k_decode/types.rs
+++ b/crates/j2k-cuda-runtime/src/htj2k_decode/types.rs
@@ -269,13 +269,13 @@ pub(crate) struct CudaHtj2kDecodeTableResourceInner {
pub(crate) uvlc_table1: CudaDeviceBuffer,
}
-/// Device-resident HTJ2K decode payload plus shared lookup tables reused across sub-band dispatches.
+/// Device-resident J2K decode payload with optional HTJ2K lookup tables.
#[doc(hidden)]
#[derive(Debug)]
pub struct CudaHtj2kDecodeResources {
pub(crate) payload: CudaHtj2kDecodePayload,
pub(crate) payload_len: usize,
- pub(crate) tables: CudaHtj2kDecodeTableResources,
+ pub(crate) tables: Option,
}
#[derive(Debug)]
@@ -320,7 +320,7 @@ pub(super) struct ValidatedHtj2kKernelJobs {
}
impl CudaHtj2kDecodePayload {
- pub(super) fn buffer(&self) -> Result<&CudaDeviceBuffer, CudaError> {
+ pub(crate) fn buffer(&self) -> Result<&CudaDeviceBuffer, CudaError> {
match self {
Self::Owned(buffer) => Ok(buffer),
Self::Pooled(buffer) => pooled_device_buffer(buffer),
@@ -330,13 +330,19 @@ impl CudaHtj2kDecodePayload {
pub(super) fn htj2k_decode_kernel_tables(
resources: &CudaHtj2kDecodeResources,
-) -> Htj2kDecodeKernelTables<'_> {
- Htj2kDecodeKernelTables {
- vlc_table0: &resources.tables.inner.vlc_table0,
- vlc_table1: &resources.tables.inner.vlc_table1,
- uvlc_table0: &resources.tables.inner.uvlc_table0,
- uvlc_table1: &resources.tables.inner.uvlc_table1,
- }
+) -> Result, CudaError> {
+ let tables = resources
+ .tables
+ .as_ref()
+ .ok_or_else(|| CudaError::InvalidArgument {
+ message: "HTJ2K decode requires resident lookup tables".to_string(),
+ })?;
+ Ok(Htj2kDecodeKernelTables {
+ vlc_table0: &tables.inner.vlc_table0,
+ vlc_table1: &tables.inner.vlc_table1,
+ uvlc_table0: &tables.inner.uvlc_table0,
+ uvlc_table1: &tables.inner.uvlc_table1,
+ })
}
pub(crate) const HTJ2K_STATUS_OK: u32 = 0;
diff --git a/crates/j2k-cuda-runtime/src/kernels.rs b/crates/j2k-cuda-runtime/src/kernels.rs
index e7afd38e..165a31ce 100644
--- a/crates/j2k-cuda-runtime/src/kernels.rs
+++ b/crates/j2k-cuda-runtime/src/kernels.rs
@@ -16,6 +16,14 @@ pub(crate) enum CudaKernel {
)
)]
CopyU8,
+ #[cfg_attr(
+ not(feature = "cuda-oxide-j2k-ml"),
+ expect(
+ dead_code,
+ reason = "variant is used only by the j2k-ml kernel feature"
+ )
+ )]
+ J2kMlConvert,
J2kDeinterleaveToF32,
J2kDeinterleaveStridedToF32,
J2kForwardRct,
@@ -30,6 +38,7 @@ pub(crate) enum CudaKernel {
Htj2kDecodeCodeblocksMulti,
Htj2kDecodeCodeblocksMultiCleanupOnly,
Htj2kDecodeCodeblocksMultiCleanupDequantize,
+ J2kClassicDecodeCodeblocksMulti,
J2kDequantizeHtj2kCodeblocks,
J2kDequantizeHtj2kCodeblocksMulti,
J2kDequantizeHtj2kCleanupJobsMulti,
@@ -225,6 +234,17 @@ impl CudaKernel {
)
}
+ #[cfg_attr(
+ not(feature = "cuda-oxide-j2k-classic-decode"),
+ expect(
+ dead_code,
+ reason = "classifier is used only by the classic J2K decode feature"
+ )
+ )]
+ pub(crate) fn is_j2k_classic_decode_stage(self) -> bool {
+ matches!(self, Self::J2kClassicDecodeCodeblocksMulti)
+ }
+
pub(crate) fn is_htj2k_encode_codeblock_stage(self) -> bool {
matches!(
self,
@@ -353,6 +373,7 @@ impl CudaKernel {
pub(crate) fn entrypoint(self) -> &'static [u8] {
match self {
Self::CopyU8 => b"j2k_copy_u8\0",
+ Self::J2kMlConvert => b"j2k_ml_convert\0",
Self::J2kDeinterleaveToF32 => b"j2k_deinterleave_to_f32\0",
Self::J2kDeinterleaveStridedToF32 => b"j2k_deinterleave_strided_to_f32\0",
Self::J2kForwardRct => b"j2k_forward_rct\0",
@@ -371,6 +392,7 @@ impl CudaKernel {
Self::Htj2kDecodeCodeblocksMultiCleanupDequantize => {
b"j2k_htj2k_decode_codeblocks_multi_cleanup_dequantize\0"
}
+ Self::J2kClassicDecodeCodeblocksMulti => b"j2k_decode_classic_codeblocks_multi\0",
Self::J2kDequantizeHtj2kCodeblocks => b"j2k_dequantize_htj2k_codeblocks\0",
Self::J2kDequantizeHtj2kCodeblocksMulti => b"j2k_dequantize_htj2k_codeblocks_multi\0",
Self::J2kDequantizeHtj2kCleanupJobsMulti => {
@@ -456,9 +478,17 @@ const CUDA_OXIDE_COPY_U8_PTX: &[u8] =
#[cfg(feature = "cuda-oxide-j2k-encode")]
const CUDA_OXIDE_J2K_ENCODE_PTX: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/cuda_oxide_j2k_encode.ptx"));
+#[cfg(feature = "cuda-oxide-j2k-ml")]
+const CUDA_OXIDE_J2K_ML_PTX: &[u8] =
+ include_bytes!(concat!(env!("OUT_DIR"), "/cuda_oxide_j2k_ml.ptx"));
#[cfg(feature = "cuda-oxide-j2k-decode-store")]
const CUDA_OXIDE_J2K_DECODE_STORE_PTX: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/cuda_oxide_j2k_decode_store.ptx"));
+#[cfg(feature = "cuda-oxide-j2k-classic-decode")]
+const CUDA_OXIDE_J2K_CLASSIC_DECODE_PTX: &[u8] = include_bytes!(concat!(
+ env!("OUT_DIR"),
+ "/cuda_oxide_j2k_classic_decode.ptx"
+));
#[cfg(feature = "cuda-oxide-j2k-dequantize")]
const CUDA_OXIDE_J2K_DEQUANTIZE_PTX: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/cuda_oxide_j2k_dequantize.ptx"));
@@ -482,6 +512,7 @@ const CUDA_OXIDE_JPEG_ENCODE_PTX: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/cuda_oxide_jpeg_encode.ptx"));
const HTJ2K_DECODE_CODEBLOCK_THREADS: usize = 32;
const HTJ2K_DECODE_CODEBLOCK_THREADS_CUDA: c_uint = 32;
+const CLASSIC_DECODE_CODEBLOCK_THREADS_CUDA: c_uint = 32;
const HTJ2K_DECODE_PACKED_BLOCK_MIN_JOBS: usize = 2_048;
const HTJ2K_ENCODE_CODEBLOCK_THREADS_CUDA: c_uint = 128;
@@ -567,6 +598,13 @@ pub(crate) fn htj2k_codeblock_sample_launch_geometry(
CudaLaunchGeometry::new((jobs, 1, 1), (COPY_U8_THREADS_CUDA, 1, 1))
}
+pub(crate) fn j2k_classic_codeblock_launch_geometry(
+ job_count: usize,
+) -> Option {
+ let jobs = c_uint::try_from(job_count).ok()?;
+ CudaLaunchGeometry::new((jobs, 1, 1), (CLASSIC_DECODE_CODEBLOCK_THREADS_CUDA, 1, 1))
+}
+
pub(crate) fn j2k_store_batch_launch_geometry(
max_pixels: usize,
job_count: usize,
@@ -614,6 +652,11 @@ pub(crate) fn cuda_oxide_copy_u8_ptx() -> &'static [u8] {
CUDA_OXIDE_COPY_U8_PTX
}
+#[cfg(feature = "cuda-oxide-j2k-ml")]
+pub(crate) fn cuda_oxide_j2k_ml_ptx() -> &'static [u8] {
+ CUDA_OXIDE_J2K_ML_PTX
+}
+
#[cfg(feature = "cuda-oxide-j2k-encode")]
pub(crate) fn cuda_oxide_j2k_encode_ptx() -> &'static [u8] {
CUDA_OXIDE_J2K_ENCODE_PTX
@@ -624,6 +667,11 @@ pub(crate) fn cuda_oxide_j2k_decode_store_ptx() -> &'static [u8] {
CUDA_OXIDE_J2K_DECODE_STORE_PTX
}
+#[cfg(feature = "cuda-oxide-j2k-classic-decode")]
+pub(crate) fn cuda_oxide_j2k_classic_decode_ptx() -> &'static [u8] {
+ CUDA_OXIDE_J2K_CLASSIC_DECODE_PTX
+}
+
#[cfg(feature = "cuda-oxide-j2k-dequantize")]
pub(crate) fn cuda_oxide_j2k_dequantize_ptx() -> &'static [u8] {
CUDA_OXIDE_J2K_DEQUANTIZE_PTX
@@ -827,6 +875,54 @@ mod tests {
assert_eq!(large_geometry.block(), (32, 1, 1));
}
+ #[test]
+ fn classic_decode_geometry_and_device_stride_share_a_classic_owned_constant() {
+ let geometry = j2k_classic_codeblock_launch_geometry(3).expect("classic geometry");
+ assert_eq!(geometry.grid(), (3, 1, 1));
+ assert_eq!(geometry.block(), (32, 1, 1));
+
+ let host = include_str!("kernels.rs");
+ let geometry_source = host
+ .split("pub(crate) fn j2k_classic_codeblock_launch_geometry")
+ .nth(1)
+ .expect("classic geometry source")
+ .split('}')
+ .next()
+ .expect("classic geometry body");
+ assert!(geometry_source.contains("CLASSIC_DECODE_CODEBLOCK_THREADS_CUDA"));
+ assert!(!geometry_source.contains("HTJ2K_DECODE_CODEBLOCK_THREADS_CUDA"));
+
+ let device = include_str!("cuda_oxide_j2k_classic_decode/simt/src/main.rs");
+ let entrypoint = device
+ .split("pub unsafe fn j2k_decode_classic_codeblocks_multi")
+ .nth(1)
+ .expect("classic device entrypoint");
+ assert!(device.contains("const CLASSIC_DECODE_THREADS: u32 = 32;"));
+ assert!(entrypoint.contains("index += CLASSIC_DECODE_THREADS"));
+ assert!(entrypoint.contains("sample += CLASSIC_DECODE_THREADS"));
+ assert!(!entrypoint.contains("index += 32"));
+ assert!(!entrypoint.contains("sample += 32"));
+ }
+
+ #[test]
+ fn classic_device_validates_job_before_using_job_dimensions() {
+ let device = include_str!("cuda_oxide_j2k_classic_decode/simt/src/main.rs");
+ let entrypoint = device
+ .split("pub unsafe fn j2k_decode_classic_codeblocks_multi")
+ .nth(1)
+ .expect("classic device entrypoint");
+ let validation = entrypoint
+ .find("validate_job_header(")
+ .expect("early device job validation");
+ let coefficient_count = entrypoint
+ .find("let coefficient_count")
+ .expect("coefficient count");
+ assert!(validation < coefficient_count);
+ assert!(device.contains("segment.end_coding_pass > job.number_of_coding_passes"));
+ assert!(entrypoint.contains("job.output_offset as usize"));
+ assert!(entrypoint.contains("y as usize * job.output_stride as usize"));
+ }
+
#[test]
fn htj2k_encode_geometry_uses_cooperative_threads_per_codeblock() {
let geometry = htj2k_encode_codeblock_launch_geometry(327).expect("geometry");
diff --git a/crates/j2k-cuda-runtime/src/lib.rs b/crates/j2k-cuda-runtime/src/lib.rs
index 2c82e0f5..533c812d 100644
--- a/crates/j2k-cuda-runtime/src/lib.rs
+++ b/crates/j2k-cuda-runtime/src/lib.rs
@@ -54,6 +54,7 @@ macro_rules! impl_cuda_htj2k_encoded_status_accessors {
mod allocation;
mod build_flags;
mod bytes;
+mod classic_decode;
mod context;
mod driver;
mod error;
@@ -66,11 +67,16 @@ mod j2k_encode;
mod jpeg;
mod kernels;
mod memory;
+mod ml;
#[cfg(test)]
mod tests;
mod transcode;
pub use build_flags::transcode_kernels_built;
+pub use classic_decode::{
+ CudaClassicCodeBlockJob, CudaClassicDecodeStageTimings, CudaClassicDecodeTarget,
+ CudaClassicSegment, CudaClassicStatus,
+};
#[cfg(test)]
pub(crate) use context::CudaKernelName;
pub use context::{
@@ -122,10 +128,11 @@ pub use jpeg::{
pub use memory::{
CudaBufferPool, CudaBufferPoolDiagnostics, CudaBufferPoolLimits, CudaBufferPoolTakeTrace,
CudaDeviceBuffer, CudaDeviceBufferRange, CudaDeviceBufferView, CudaDeviceBufferViewMut,
- CudaPinnedUploadOperationGuard, CudaPinnedUploadStagingCheckout,
- CudaPinnedUploadStagingPoolDiagnostics, CudaPinnedUploadStagingPoolLimits,
- CudaPooledDeviceBuffer,
+ CudaExternalDeviceBufferViewMut, CudaPinnedUploadOperationGuard,
+ CudaPinnedUploadStagingCheckout, CudaPinnedUploadStagingPoolDiagnostics,
+ CudaPinnedUploadStagingPoolLimits, CudaPooledDeviceBuffer,
};
+pub use ml::{CudaJ2kMlKernelConfig, CudaJ2kMlLayout, CudaJ2kMlNormalization, CudaJ2kMlSample};
pub use transcode::{
CudaDwt97BatchGeometry, CudaDwt97BatchWithPoolRequest, CudaHtj2k97CodeblockBands,
CudaHtj2k97CodeblockBatchWithPoolRequest, CudaHtj2k97DeviceCodeblockBands,
diff --git a/crates/j2k-cuda-runtime/src/memory.rs b/crates/j2k-cuda-runtime/src/memory.rs
index 5028bfdf..04eb6b83 100644
--- a/crates/j2k-cuda-runtime/src/memory.rs
+++ b/crates/j2k-cuda-runtime/src/memory.rs
@@ -235,6 +235,87 @@ pub struct CudaDeviceBufferViewMut<'a, T> {
pub(crate) _marker: std::marker::PhantomData<&'a mut T>,
}
+/// Lifetime-bound mutable view of CUDA memory owned by another runtime.
+///
+/// This value never frees the allocation. Its lifetime is tied to an exclusive
+/// borrow of the external runtime's managed-resource guard.
+#[doc(hidden)]
+#[derive(Debug)]
+pub struct CudaExternalDeviceBufferViewMut<'a> {
+ context: CudaContext,
+ ptr: CuDevicePtr,
+ len: usize,
+ _exclusive: std::marker::PhantomData<&'a mut ()>,
+}
+
+impl<'a> CudaExternalDeviceBufferViewMut<'a> {
+ /// Construct a non-owning external device-buffer view.
+ ///
+ /// # Safety
+ ///
+ /// `ptr..ptr+len` must be a live CUDA allocation range represented by
+ /// `_managed_owner`. The exclusive owner borrow must exclude every
+ /// overlapping host or device mutation for this view's lifetime. The
+ /// allocation must remain valid and must not be freed by the caller until
+ /// the view is dropped.
+ pub unsafe fn from_raw_parts(
+ context: &CudaContext,
+ ptr: u64,
+ len: usize,
+ required_alignment: usize,
+ _managed_owner: &'a mut Owner,
+ ) -> Result {
+ if len == 0 {
+ return Err(CudaError::InvalidArgument {
+ message: "external CUDA buffer must not be empty".to_string(),
+ });
+ }
+ if ptr == 0 {
+ return Err(CudaError::InvalidArgument {
+ message: "external CUDA buffer pointer must not be null".to_string(),
+ });
+ }
+ if required_alignment == 0 || !required_alignment.is_power_of_two() {
+ return Err(CudaError::InvalidArgument {
+ message: "external CUDA buffer alignment must be a nonzero power of two"
+ .to_string(),
+ });
+ }
+ if !ptr.is_multiple_of(required_alignment as u64) {
+ return Err(CudaError::InvalidArgument {
+ message: format!(
+ "external CUDA buffer pointer {ptr:#x} is not aligned to {required_alignment} bytes"
+ ),
+ });
+ }
+ let len_u64 = u64::try_from(len).map_err(|_| CudaError::LengthTooLarge { len })?;
+ ptr.checked_add(len_u64)
+ .ok_or(CudaError::LengthTooLarge { len })?;
+ context.inner.validate_pointer_context(ptr)?;
+ Ok(Self {
+ context: context.clone(),
+ ptr,
+ len,
+ _exclusive: std::marker::PhantomData,
+ })
+ }
+
+ /// Context that owns the external allocation.
+ pub fn context(&self) -> &CudaContext {
+ &self.context
+ }
+
+ /// Raw device pointer.
+ pub fn device_ptr(&self) -> u64 {
+ self.ptr
+ }
+
+ /// External allocation range length in bytes.
+ pub fn byte_len(&self) -> usize {
+ self.len
+ }
+}
+
impl CudaDeviceBufferViewMut<'_, T> {
/// Raw CUDA device pointer value for kernel argument binding.
pub fn device_ptr(&self) -> u64 {
diff --git a/crates/j2k-cuda-runtime/src/ml.rs b/crates/j2k-cuda-runtime/src/ml.rs
new file mode 100644
index 00000000..f724d54a
--- /dev/null
+++ b/crates/j2k-cuda-runtime/src/ml.rs
@@ -0,0 +1,229 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use crate::{
+ driver::CuDevicePtr, execution::cuda_kernel_param, kernels::copy_u8_launch_geometry,
+ CudaContext, CudaError, CudaExternalDeviceBufferViewMut,
+};
+
+/// Integer sample representation read from a resident J2K surface.
+#[doc(hidden)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum CudaJ2kMlSample {
+ /// Unsigned 8-bit sample.
+ U8,
+ /// Unsigned 16-bit sample.
+ U16,
+}
+
+impl CudaJ2kMlSample {
+ const fn byte_width(self) -> usize {
+ match self {
+ Self::U8 => 1,
+ Self::U16 => 2,
+ }
+ }
+
+ const fn flag(self) -> u32 {
+ match self {
+ Self::U8 => 1,
+ Self::U16 => 2,
+ }
+ }
+}
+
+/// Destination layout understood by the low-level j2k-ml CUDA kernel.
+#[doc(hidden)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum CudaJ2kMlLayout {
+ /// Channels-first output.
+ ChannelsFirst,
+ /// Channels-last output.
+ ChannelsLast,
+}
+
+/// Fused floating-point normalization understood by the CUDA kernel.
+#[doc(hidden)]
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum CudaJ2kMlNormalization {
+ /// Integer output with no floating-point conversion.
+ Integer,
+ /// Cast integer samples to F32 without scaling.
+ Raw,
+ /// Scale into `0..=1`.
+ Unit,
+ /// Unit-scale and apply per-channel mean and standard deviation.
+ MeanStd {
+ /// Up to four channel means.
+ mean: [f32; 4],
+ /// Up to four channel standard deviations.
+ std: [f32; 4],
+ },
+}
+
+/// Validated launch configuration for one resident surface.
+#[doc(hidden)]
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct CudaJ2kMlKernelConfig {
+ /// Image width.
+ pub width: u32,
+ /// Image height.
+ pub height: u32,
+ /// Channel count (1, 3, or 4).
+ pub channels: u32,
+ /// Source and integer-output sample representation.
+ pub sample: CudaJ2kMlSample,
+ /// Destination layout.
+ pub layout: CudaJ2kMlLayout,
+ /// Destination element offset for this batch item.
+ pub destination_offset_elements: usize,
+ /// Fused conversion and normalization.
+ pub normalization: CudaJ2kMlNormalization,
+}
+
+impl CudaContext {
+ /// Convert one resident interleaved J2K surface into externally owned CUDA memory.
+ #[doc(hidden)]
+ pub fn j2k_ml_convert_into_external(
+ &self,
+ source_ptr: u64,
+ source_byte_len: usize,
+ destination: &mut CudaExternalDeviceBufferViewMut<'_>,
+ config: CudaJ2kMlKernelConfig,
+ ) -> Result<(), CudaError> {
+ if !self.is_same_context(destination.context()) {
+ return Err(CudaError::InvalidArgument {
+ message: "j2k-ml destination belongs to a different CUDA context".to_string(),
+ });
+ }
+ if config.width == 0 || config.height == 0 {
+ return Err(CudaError::InvalidArgument {
+ message: "j2k-ml CUDA dimensions must be nonzero".to_string(),
+ });
+ }
+ if !matches!(config.channels, 1 | 3 | 4) {
+ return Err(CudaError::InvalidArgument {
+ message: "j2k-ml CUDA channels must be 1, 3, or 4".to_string(),
+ });
+ }
+ if source_ptr == 0 {
+ return Err(CudaError::InvalidArgument {
+ message: "j2k-ml CUDA source pointer must not be null".to_string(),
+ });
+ }
+ let sample_count = usize::try_from(config.width)
+ .ok()
+ .and_then(|width| width.checked_mul(config.height as usize))
+ .and_then(|pixels| pixels.checked_mul(config.channels as usize))
+ .ok_or(CudaError::LengthTooLarge {
+ len: source_byte_len,
+ })?;
+ let source_required = sample_count
+ .checked_mul(config.sample.byte_width())
+ .ok_or(CudaError::LengthTooLarge { len: sample_count })?;
+ if source_required > source_byte_len {
+ return Err(CudaError::OutputTooSmall {
+ required: source_required,
+ have: source_byte_len,
+ });
+ }
+ if !source_ptr.is_multiple_of(config.sample.byte_width() as u64) {
+ return Err(CudaError::InvalidArgument {
+ message: "j2k-ml CUDA source pointer is misaligned".to_string(),
+ });
+ }
+ self.inner.validate_pointer_context(source_ptr)?;
+
+ let output_width = match config.normalization {
+ CudaJ2kMlNormalization::Integer => config.sample.byte_width(),
+ CudaJ2kMlNormalization::Raw
+ | CudaJ2kMlNormalization::Unit
+ | CudaJ2kMlNormalization::MeanStd { .. } => 4,
+ };
+ let destination_end = config
+ .destination_offset_elements
+ .checked_add(sample_count)
+ .and_then(|elements| elements.checked_mul(output_width))
+ .ok_or(CudaError::LengthTooLarge { len: sample_count })?;
+ if destination_end > destination.byte_len() {
+ return Err(CudaError::OutputTooSmall {
+ required: destination_end,
+ have: destination.byte_len(),
+ });
+ }
+ validate_normalization(config.normalization, config.channels as usize)?;
+ let geometry = copy_u8_launch_geometry(sample_count)
+ .ok_or(CudaError::LengthTooLarge { len: sample_count })?;
+ let function = self.inner.cuda_oxide_j2k_ml_kernel_function()?;
+
+ let mut destination_ptr: CuDevicePtr = destination.device_ptr();
+ let mut source_ptr: CuDevicePtr = source_ptr;
+ let mut sample_count = sample_count as u64;
+ let mut channels = config.channels;
+ let mut source_sample = config.sample.flag();
+ let mut output_kind = match config.normalization {
+ CudaJ2kMlNormalization::Integer => config.sample.flag(),
+ _ => 4,
+ };
+ let mut layout = match config.layout {
+ CudaJ2kMlLayout::ChannelsFirst => 0u32,
+ CudaJ2kMlLayout::ChannelsLast => 1u32,
+ };
+ let mut destination_offset = config.destination_offset_elements as u64;
+ let (mut normalization, mean, std) = normalization_args(config.normalization);
+ let [mut mean0, mut mean1, mut mean2, mut mean3] = mean;
+ let [mut std0, mut std1, mut std2, mut std3] = std;
+ let mut params = cuda_kernel_params!(
+ destination_ptr,
+ source_ptr,
+ sample_count,
+ channels,
+ source_sample,
+ output_kind,
+ layout,
+ destination_offset,
+ normalization,
+ mean0,
+ mean1,
+ mean2,
+ mean3,
+ std0,
+ std1,
+ std2,
+ std3,
+ );
+ self.launch_kernel(function, geometry, &mut params)
+ }
+}
+
+fn validate_normalization(
+ normalization: CudaJ2kMlNormalization,
+ channels: usize,
+) -> Result<(), CudaError> {
+ let CudaJ2kMlNormalization::MeanStd { mean, std } = normalization else {
+ return Ok(());
+ };
+ if mean[..channels]
+ .iter()
+ .chain(&std[..channels])
+ .any(|value| !value.is_finite())
+ {
+ return Err(CudaError::InvalidArgument {
+ message: "j2k-ml CUDA normalization values must be finite".to_string(),
+ });
+ }
+ if std[..channels].contains(&0.0) {
+ return Err(CudaError::InvalidArgument {
+ message: "j2k-ml CUDA standard deviations must be nonzero".to_string(),
+ });
+ }
+ Ok(())
+}
+
+fn normalization_args(normalization: CudaJ2kMlNormalization) -> (u32, [f32; 4], [f32; 4]) {
+ match normalization {
+ CudaJ2kMlNormalization::Integer => (0, [0.0; 4], [1.0; 4]),
+ CudaJ2kMlNormalization::Raw => (1, [0.0; 4], [1.0; 4]),
+ CudaJ2kMlNormalization::Unit => (2, [0.0; 4], [1.0; 4]),
+ CudaJ2kMlNormalization::MeanStd { mean, std } => (3, mean, std),
+ }
+}
diff --git a/crates/j2k-cuda-runtime/src/tests.rs b/crates/j2k-cuda-runtime/src/tests.rs
index 5e2bfb02..9d9dca08 100644
--- a/crates/j2k-cuda-runtime/src/tests.rs
+++ b/crates/j2k-cuda-runtime/src/tests.rs
@@ -3,15 +3,17 @@ use super::{
idwt_batch_kernel_mode, idwt_batch_trace_row, idwt_batch_uses_cooperative_53,
jpeg_entropy_overflow_count, pool_fit_buffer_index_by_len, validate_dct_block_grid,
CudaContext, CudaDwt97BatchGeometry, CudaError, CudaExecutionStats,
- CudaHtj2k97CodeblockBatchWithPoolRequest, CudaHtj2kCleanupMultiKernelJob,
- CudaHtj2kCleanupTarget, CudaHtj2kCodeBlockJob, CudaHtj2kDecodeTables,
- CudaHtj2kDequantizeTarget, CudaHtj2kEncodeCodeBlockJob, CudaHtj2kEncodeCodeBlockRegionJob,
- CudaHtj2kEncodeResidentTarget, CudaHtj2kEncodeTables, CudaJ2kIdwtBatchKernelMode,
- CudaJ2kIdwtJob, CudaJ2kIdwtMultiKernelJob, CudaJ2kIdwtTarget, CudaJ2kQuantizeJob,
- CudaJ2kQuantizeSubbandRegionJob, CudaJ2kRect, CudaJpegChunkedEntropyConfig,
+ CudaExternalDeviceBufferViewMut, CudaHtj2k97CodeblockBatchWithPoolRequest,
+ CudaHtj2kCleanupMultiKernelJob, CudaHtj2kCleanupTarget, CudaHtj2kCodeBlockJob,
+ CudaHtj2kDecodeTables, CudaHtj2kDequantizeTarget, CudaHtj2kEncodeCodeBlockJob,
+ CudaHtj2kEncodeCodeBlockRegionJob, CudaHtj2kEncodeResidentTarget, CudaHtj2kEncodeTables,
+ CudaJ2kIdwtBatchKernelMode, CudaJ2kIdwtJob, CudaJ2kIdwtMultiKernelJob, CudaJ2kIdwtTarget,
+ CudaJ2kQuantizeJob, CudaJ2kQuantizeSubbandRegionJob, CudaJ2kRect, CudaJpegChunkedEntropyConfig,
CudaJpegChunkedEntropyPlan, CudaJpegChunkedEntropyReport, CudaJpegEntropyOverflowState,
CudaJpegEntropySyncState, CudaJpegHuffmanTable, CudaKernelName, CudaQueuedHtj2kCleanup,
};
+#[cfg(feature = "cuda-oxide-j2k-ml")]
+use super::{CudaJ2kMlKernelConfig, CudaJ2kMlLayout, CudaJ2kMlNormalization, CudaJ2kMlSample};
fn cuda_runtime_gate() -> bool {
j2k_test_support::cuda_runtime_gate(module_path!())
@@ -31,6 +33,102 @@ fn cuda_context_identity_distinguishes_clones_from_independent_contexts_when_req
assert!(!context.is_same_context(&independent));
}
+#[test]
+fn retained_primary_context_identity_and_release_are_balanced_when_required() {
+ if !cuda_runtime_gate() {
+ return;
+ }
+
+ let first = CudaContext::retain_primary(0).expect("retain primary context");
+ let second = CudaContext::retain_primary(0).expect("retain primary context again");
+ let owned = CudaContext::system_default().expect("independent owned context");
+
+ assert!(first.is_same_context(&second));
+ assert!(!first.is_same_context(&owned));
+ assert_eq!(first.device_ordinal(), 0);
+
+ drop(first);
+ drop(second);
+ let retained_again = CudaContext::retain_primary(0).expect("retain primary after release");
+ assert_eq!(retained_again.device_ordinal(), 0);
+}
+
+#[test]
+fn external_cuda_view_rejects_foreign_context_and_never_owns_memory_when_required() {
+ if !cuda_runtime_gate() {
+ return;
+ }
+
+ let context = CudaContext::system_default().expect("CUDA context");
+ let foreign = CudaContext::system_default().expect("foreign CUDA context");
+ let mut allocation = context.allocate(16).expect("device allocation");
+ let ptr = allocation.device_ptr();
+ let len = allocation.byte_len();
+
+ // SAFETY: `allocation` owns the live range and is exclusively borrowed by
+ // the view for the duration of this scope.
+ let view = unsafe {
+ CudaExternalDeviceBufferViewMut::from_raw_parts(&context, ptr, len, 4, &mut allocation)
+ }
+ .expect("external view");
+ assert_eq!(view.device_ptr(), ptr);
+ assert_eq!(view.byte_len(), 16);
+ drop(view);
+
+ // The external view has no ownership: the original allocation remains
+ // live and is still responsible for freeing its memory.
+ assert_eq!(allocation.device_ptr(), ptr);
+ assert_eq!(allocation.byte_len(), 16);
+
+ // SAFETY: the allocation remains live and exclusively borrowed, but the
+ // deliberately foreign context must reject its pointer identity.
+ let error = unsafe {
+ CudaExternalDeviceBufferViewMut::from_raw_parts(&foreign, ptr, len, 4, &mut allocation)
+ }
+ .expect_err("foreign context must fail");
+ assert!(matches!(error, CudaError::InvalidArgument { .. }));
+}
+
+#[cfg(feature = "cuda-oxide-j2k-ml")]
+#[test]
+fn j2k_ml_external_destination_checks_batch_offsets_before_launch_when_required() {
+ if !cuda_runtime_gate() {
+ return;
+ }
+
+ let context = CudaContext::system_default().expect("CUDA context");
+ let source = context.upload(&[1, 2, 3, 4]).expect("source upload");
+ let mut allocation = context.allocate(4).expect("destination allocation");
+ let ptr = allocation.device_ptr();
+ let len = allocation.byte_len();
+ // SAFETY: `allocation` owns this live four-byte range and the view holds
+ // its exclusive borrow until validation returns.
+ let mut destination = unsafe {
+ CudaExternalDeviceBufferViewMut::from_raw_parts(&context, ptr, len, 1, &mut allocation)
+ }
+ .expect("external view");
+
+ let error = context
+ .j2k_ml_convert_into_external(
+ source.device_ptr(),
+ source.byte_len(),
+ &mut destination,
+ CudaJ2kMlKernelConfig {
+ width: 2,
+ height: 2,
+ channels: 1,
+ sample: CudaJ2kMlSample::U8,
+ layout: CudaJ2kMlLayout::ChannelsFirst,
+ destination_offset_elements: 1,
+ normalization: CudaJ2kMlNormalization::Integer,
+ },
+ )
+ .expect_err("offset must exceed destination bounds");
+ assert!(matches!(error, CudaError::OutputTooSmall { .. }));
+ drop(destination);
+ assert_eq!(allocation.device_ptr(), ptr);
+}
+
#[cfg(all(feature = "cuda-oxide-transcode", j2k_cuda_oxide_transcode_built))]
fn cuda_transcode_kernel_gate() -> bool {
if super::transcode_kernels_built() {
@@ -1973,8 +2071,12 @@ fn htj2k_decode_table_resources_feed_multiple_payload_uploads_when_required() {
.expect("second payload resources");
assert!(std::sync::Arc::ptr_eq(
- &first_resources.tables.inner,
- &second_resources.tables.inner
+ &first_resources.tables.as_ref().expect("first tables").inner,
+ &second_resources
+ .tables
+ .as_ref()
+ .expect("second tables")
+ .inner
));
assert_eq!(first_resources.payload_len, 2);
assert_eq!(second_resources.payload_len, 3);
diff --git a/crates/j2k-cuda/Cargo.toml b/crates/j2k-cuda/Cargo.toml
index 8601283c..11032c9e 100644
--- a/crates/j2k-cuda/Cargo.toml
+++ b/crates/j2k-cuda/Cargo.toml
@@ -25,6 +25,7 @@ cuda-runtime = [
"j2k-cuda-runtime/cuda-oxide-copy-u8",
"j2k-cuda-runtime/cuda-oxide-j2k-encode",
"j2k-cuda-runtime/cuda-oxide-j2k-decode-store",
+ "j2k-cuda-runtime/cuda-oxide-j2k-classic-decode",
"j2k-cuda-runtime/cuda-oxide-j2k-dequantize",
"j2k-cuda-runtime/cuda-oxide-j2k-idwt",
"j2k-cuda-runtime/cuda-oxide-htj2k-decode",
@@ -33,11 +34,11 @@ cuda-runtime = [
cuda-profiling = ["cuda-runtime", "j2k-cuda-runtime/cuda-profiling"]
[dependencies]
-j2k-core = { path = "../j2k-core", version = "=0.7.0" }
-j2k-cuda-runtime = { path = "../j2k-cuda-runtime", version = "=0.7.0", optional = true }
-j2k = { path = "../j2k", version = "=0.7.0" }
-j2k-native = { path = "../j2k-native", version = "=0.7.0" }
-j2k-profile = { path = "../j2k-profile", version = "=0.7.0" }
+j2k-core = { path = "../j2k-core", version = "=0.7.1" }
+j2k-cuda-runtime = { path = "../j2k-cuda-runtime", version = "=0.7.1", optional = true }
+j2k = { path = "../j2k", version = "=0.7.1" }
+j2k-native = { path = "../j2k-native", version = "=0.7.1" }
+j2k-profile = { path = "../j2k-profile", version = "=0.7.1" }
thiserror = { workspace = true }
[dev-dependencies]
diff --git a/crates/j2k-cuda/benches/htj2k_decode.rs b/crates/j2k-cuda/benches/htj2k_decode.rs
index 81d917f6..e619362d 100644
--- a/crates/j2k-cuda/benches/htj2k_decode.rs
+++ b/crates/j2k-cuda/benches/htj2k_decode.rs
@@ -13,7 +13,7 @@ use j2k_core::{
ImageDecode, ImageDecodeSubmit, PixelFormat, Rect, TileBatchDecodeManyDevice,
};
use j2k_cuda::{Codec, CudaSession, J2kDecoder, SurfaceResidency};
-use j2k_native::{encode_htj2k, EncodeOptions};
+use j2k_native::{encode, encode_htj2k, EncodeOptions};
use j2k_test_support::{
canonicalize_manifest_row_path, fnv1a64_hex, manifest_column, manifest_field,
manifest_optional_value, optional_manifest_column,
@@ -59,9 +59,14 @@ struct ExternalDecodeCases {
fn bench_htj2k_decode(c: &mut Criterion) {
let corpus = all_decode_cases();
- emit_input_metadata(&corpus);
+ let classic = classic_decode_case_if_enabled();
+ emit_input_metadata(&corpus, classic.as_ref());
let scale = Downscale::Half;
+ if let Some(classic) = classic.as_ref() {
+ bench_classic_full_tile(c, classic);
+ bench_classic_tile_batch(c, classic);
+ }
bench_full_tile(c, &corpus.cases);
bench_roi(c, &corpus.cases);
bench_scaled(c, &corpus.cases, scale);
@@ -70,6 +75,119 @@ fn bench_htj2k_decode(c: &mut Criterion) {
bench_mixed_external_tile_batch(c, &corpus.cases);
}
+fn classic_decode_case_if_enabled() -> Option {
+ if !include_generated_decode_cases() || !enabled_decode_cases().contains(&"rgb8") {
+ return None;
+ }
+ Some(decode_case(
+ "classic_rgb8",
+ "generated_classic",
+ classic_rgb8_fixture(TILE_DIM, TILE_DIM),
+ PixelFormat::Rgb8,
+ (TILE_DIM, TILE_DIM),
+ ))
+}
+
+fn bench_classic_full_tile(c: &mut Criterion, case: &DecodeBenchCase) {
+ let mut group = c.benchmark_group("j2k_cuda_classic_full_tile_decode");
+ group.bench_function("cpu_rgb8", |b| {
+ b.iter(|| {
+ let mut decoder = J2kDecoder::new(std::hint::black_box(case.fixture.as_slice()))
+ .expect("classic decoder");
+ let mut output = vec![0; TILE_DIM as usize * TILE_DIM as usize * 3];
+ decoder
+ .decode_into(&mut output, TILE_DIM as usize * 3, PixelFormat::Rgb8)
+ .expect("CPU classic decode");
+ std::hint::black_box(output)
+ });
+ });
+ if case.cuda_available {
+ let mut session = warm_cuda_session(|session| {
+ let mut decoder = J2kDecoder::new(case.fixture.as_slice()).expect("classic decoder");
+ let surface = decoder
+ .submit_to_device(session, PixelFormat::Rgb8, BackendRequest::Cuda)
+ .expect("strict CUDA classic warmup submission")
+ .wait()
+ .expect("strict CUDA classic warmup");
+ assert_cuda_resident_decode(&surface);
+ std::hint::black_box(surface);
+ });
+ group.bench_function("cuda_rgb8", |b| {
+ b.iter(|| {
+ let mut decoder = J2kDecoder::new(std::hint::black_box(case.fixture.as_slice()))
+ .expect("classic decoder");
+ let surface = decoder
+ .submit_to_device(&mut session, PixelFormat::Rgb8, BackendRequest::Cuda)
+ .expect("strict CUDA classic decode submission")
+ .wait()
+ .expect("strict CUDA classic decode");
+ assert_cuda_resident_decode(&surface);
+ std::hint::black_box(surface)
+ });
+ });
+ }
+ group.finish();
+}
+
+fn bench_classic_tile_batch(c: &mut Criterion, case: &DecodeBenchCase) {
+ let mut group = c.benchmark_group("j2k_cuda_classic_tile_batch_decode");
+ configure_batch_group(&mut group);
+ for &batch_size in BATCH_SIZES {
+ let fixtures = vec![case.fixture.clone(); batch_size];
+ let inputs = fixtures.iter().map(Vec::as_slice).collect::>();
+ group.bench_with_input(
+ BenchmarkId::new("cpu_rgb8", batch_size),
+ &inputs,
+ |b, inputs| {
+ b.iter(|| {
+ let mut ctx = DecoderContext::::new();
+ let mut pool = j2k_cuda::J2kScratchPool::new();
+ let surfaces = Codec::decode_tiles_to_device(
+ &mut ctx,
+ &mut pool,
+ std::hint::black_box(inputs),
+ PixelFormat::Rgb8,
+ BackendRequest::Cpu,
+ )
+ .expect("CPU classic batch decode");
+ std::hint::black_box(surfaces)
+ });
+ },
+ );
+ if case.cuda_available {
+ let mut session = warm_cuda_session(|session| {
+ let surfaces = J2kDecoder::decode_batch_to_device_with_session(
+ &inputs,
+ PixelFormat::Rgb8,
+ session,
+ )
+ .expect("strict CUDA classic batch warmup");
+ assert_eq!(surfaces.len(), inputs.len());
+ assert_cuda_resident_batch_decode(&surfaces);
+ std::hint::black_box(surfaces);
+ });
+ group.bench_with_input(
+ BenchmarkId::new("cuda_rgb8", batch_size),
+ &inputs,
+ |b, inputs| {
+ b.iter(|| {
+ let surfaces = J2kDecoder::decode_batch_to_device_with_session(
+ std::hint::black_box(inputs),
+ PixelFormat::Rgb8,
+ &mut session,
+ )
+ .expect("strict CUDA classic batch decode");
+ assert_eq!(surfaces.len(), inputs.len());
+ assert_cuda_resident_batch_decode(&surfaces);
+ std::hint::black_box(surfaces)
+ });
+ },
+ );
+ }
+ }
+ group.finish();
+}
+
fn bench_full_tile(c: &mut Criterion, cases: &[DecodeBenchCase]) {
let mut group = c.benchmark_group("j2k_cuda_htj2k_full_tile_decode");
for case in cases {
@@ -93,11 +211,20 @@ fn bench_full_tile(c: &mut Criterion, cases: &[DecodeBenchCase]) {
);
if case.cuda_available {
let cuda_id = cuda_benchmark_id(case);
+ let mut session = warm_cuda_session(|session| {
+ let mut decoder = J2kDecoder::new(case.fixture.as_slice()).expect("decoder");
+ let surface = decoder
+ .submit_to_device(session, case.fmt, BackendRequest::Cuda)
+ .expect("strict CUDA HTJ2K warmup submission")
+ .wait()
+ .expect("strict CUDA HTJ2K warmup");
+ assert_cuda_resident_decode(&surface);
+ std::hint::black_box(surface);
+ });
group.bench_with_input(
BenchmarkId::new(cuda_id, dimensions_label(case.dimensions)),
case,
|b, case| {
- let mut session = CudaSession::default();
b.iter(|| {
let mut decoder =
J2kDecoder::new(std::hint::black_box(case.fixture.as_slice()))
@@ -137,8 +264,17 @@ fn bench_roi(c: &mut Criterion, cases: &[DecodeBenchCase]) {
});
if case.cuda_available {
let cuda_id = cuda_benchmark_id(case);
+ let mut session = warm_cuda_session(|session| {
+ let mut decoder = J2kDecoder::new(case.fixture.as_slice()).expect("decoder");
+ let surface = decoder
+ .submit_region_to_device(session, case.fmt, roi, BackendRequest::Cuda)
+ .expect("strict CUDA HTJ2K ROI warmup submission")
+ .wait()
+ .expect("strict CUDA HTJ2K ROI warmup");
+ assert_cuda_resident_decode(&surface);
+ std::hint::black_box(surface);
+ });
group.bench_with_input(BenchmarkId::new(cuda_id, roi.w), case, |b, case| {
- let mut session = CudaSession::default();
b.iter(|| {
let mut decoder =
J2kDecoder::new(std::hint::black_box(case.fixture.as_slice()))
@@ -177,8 +313,17 @@ fn bench_scaled(c: &mut Criterion, cases: &[DecodeBenchCase], scale: Downscale)
});
if case.cuda_available {
let cuda_id = cuda_benchmark_id(case);
+ let mut session = warm_cuda_session(|session| {
+ let mut decoder = J2kDecoder::new(case.fixture.as_slice()).expect("decoder");
+ let surface = decoder
+ .submit_scaled_to_device(session, case.fmt, scale, BackendRequest::Cuda)
+ .expect("strict CUDA HTJ2K scaled warmup submission")
+ .wait()
+ .expect("strict CUDA HTJ2K scaled warmup");
+ assert_cuda_resident_decode(&surface);
+ std::hint::black_box(surface);
+ });
group.bench_with_input(BenchmarkId::new(cuda_id, scaled.w), case, |b, case| {
- let mut session = CudaSession::default();
b.iter(|| {
let mut decoder =
J2kDecoder::new(std::hint::black_box(case.fixture.as_slice()))
@@ -223,8 +368,23 @@ fn bench_roi_scaled(c: &mut Criterion, cases: &[DecodeBenchCase], scale: Downsca
});
if case.cuda_available {
let cuda_id = cuda_benchmark_id(case);
+ let mut session = warm_cuda_session(|session| {
+ let mut decoder = J2kDecoder::new(case.fixture.as_slice()).expect("decoder");
+ let surface = decoder
+ .submit_region_scaled_to_device(
+ session,
+ case.fmt,
+ roi,
+ scale,
+ BackendRequest::Cuda,
+ )
+ .expect("strict CUDA HTJ2K ROI+scaled warmup submission")
+ .wait()
+ .expect("strict CUDA HTJ2K ROI+scaled warmup");
+ assert_cuda_resident_decode(&surface);
+ std::hint::black_box(surface);
+ });
group.bench_with_input(BenchmarkId::new(cuda_id, scaled.w), case, |b, case| {
- let mut session = CudaSession::default();
b.iter(|| {
let mut decoder =
J2kDecoder::new(std::hint::black_box(case.fixture.as_slice()))
@@ -280,11 +440,18 @@ fn bench_tile_batch(c: &mut Criterion, cases: &[DecodeBenchCase]) {
);
if case.cuda_available && cuda_batch_decode_supported(fmt) {
let cuda_id = cuda_benchmark_id(case);
+ let mut session = warm_cuda_session(|session| {
+ let surfaces =
+ J2kDecoder::decode_batch_to_device_with_session(&inputs, fmt, session)
+ .expect("strict CUDA HTJ2K batch warmup");
+ assert_eq!(surfaces.len(), inputs.len());
+ assert_cuda_resident_batch_decode(&surfaces);
+ std::hint::black_box(surfaces);
+ });
group.bench_with_input(
BenchmarkId::new(cuda_id, batch_size),
&inputs,
|b, inputs| {
- let mut session = CudaSession::default();
b.iter(|| {
let surfaces = J2kDecoder::decode_batch_to_device_with_session(
std::hint::black_box(inputs),
@@ -353,6 +520,14 @@ fn bench_mixed_external_tile_batch(c: &mut Criterion, cases: &[DecodeBenchCase])
if cuda_batch_decode_supported(fmt)
&& selected_cases.iter().all(|case| case.cuda_available)
{
+ let mut session = warm_cuda_session(|session| {
+ let surfaces =
+ J2kDecoder::decode_batch_to_device_with_session(&inputs, fmt, session)
+ .expect("strict CUDA HTJ2K mixed external batch warmup");
+ assert_eq!(surfaces.len(), inputs.len());
+ assert_cuda_resident_batch_decode(&surfaces);
+ std::hint::black_box(surfaces);
+ });
group.bench_with_input(
BenchmarkId::new(
format!("cuda_external_mixed_{}", pixel_format_label(fmt)),
@@ -360,7 +535,6 @@ fn bench_mixed_external_tile_batch(c: &mut Criterion, cases: &[DecodeBenchCase])
),
&inputs,
|b, inputs| {
- let mut session = CudaSession::default();
b.iter(|| {
let surfaces = J2kDecoder::decode_batch_to_device_with_session(
std::hint::black_box(inputs),
@@ -584,7 +758,7 @@ fn external_decode_cases(enabled_cases: &[&str]) -> ExternalDecodeCases {
ExternalDecodeCases { cases, stats }
}
-fn emit_input_metadata(corpus: &DecodeBenchCorpus) {
+fn emit_input_metadata(corpus: &DecodeBenchCorpus, classic: Option<&DecodeBenchCase>) {
let external_count = corpus
.cases
.iter()
@@ -600,20 +774,30 @@ fn emit_input_metadata(corpus: &DecodeBenchCorpus) {
.map(usize::to_string)
.collect::>()
.join(",");
+ let classic_batch_sizes = BATCH_SIZES
+ .iter()
+ .map(usize::to_string)
+ .collect::>()
+ .join(",");
println!(
"j2k_cuda_decode_generated_included\t{}",
include_generated_decode_cases()
);
println!("j2k_cuda_decode_batch_sizes\t{mixed_batch_sizes}");
println!("j2k_cuda_decode_case_batch_sizes\t{case_batch_sizes}");
+ println!("j2k_cuda_decode_classic_batch_sizes\t{classic_batch_sizes}");
println!("j2k_cuda_decode_mixed_batch_sizes\t{mixed_batch_sizes}");
println!("j2k_cuda_decode_sample_size\t{}", decode_sample_size());
+ println!(
+ "j2k_cuda_decode_measurement_seconds\t{}",
+ decode_measurement_time().as_secs()
+ );
println!(
"j2k_cuda_decode_batch_sample_size\t{}",
decode_sample_size()
);
println!(
- "j2k_cuda_decode_batch_policy\tper-fixture-batch-rows-use-case-batch-sizes;mixed-external-rows-use-public-large-batch-sizes"
+ "j2k_cuda_decode_batch_policy\tper-fixture-ht-rows-use-case-batch-sizes;classic-rows-use-classic-batch-sizes;mixed-external-rows-use-public-large-batch-sizes"
);
println!(
"j2k_cuda_decode_mixed_large_batch_policy\tfull-external-corpus-up-to-batch-{MIXED_BATCH_FULL_CORPUS_MAX_BATCH};tile-sized-external-cases-above-that"
@@ -624,6 +808,7 @@ fn emit_input_metadata(corpus: &DecodeBenchCorpus) {
println!(
"j2k_cuda_decode_io_policy\thost-memory-fixture-bytes-preloaded-no-filesystem-io-in-timed-loop;cuda-rows-return-device-resident-surfaces"
);
+ println!("j2k_cuda_decode_session_policy\tsteady_state_route_warmed");
println!(
"j2k_cuda_decode_input_dirs\t{}",
std::env::var("J2K_CUDA_DECODE_INPUT_DIRS").unwrap_or_else(|_| "not set".to_string())
@@ -632,7 +817,10 @@ fn emit_input_metadata(corpus: &DecodeBenchCorpus) {
"j2k_cuda_decode_manifest\t{}",
std::env::var("J2K_CUDA_DECODE_MANIFEST").unwrap_or_else(|_| "not set".to_string())
);
- println!("j2k_cuda_decode_case_count\t{}", corpus.cases.len());
+ println!(
+ "j2k_cuda_decode_case_count\t{}",
+ corpus.cases.len() + usize::from(classic.is_some())
+ );
println!("j2k_cuda_decode_external_case_count\t{external_count}");
println!(
"j2k_cuda_decode_external_fixture_count\t{}",
@@ -946,7 +1134,7 @@ fn configure_batch_group(
) {
group.sample_size(decode_sample_size());
group.warm_up_time(DECODE_WARM_UP);
- group.measurement_time(DECODE_MEASUREMENT);
+ group.measurement_time(decode_measurement_time());
}
fn mixed_external_cases_for_batch<'a>(
@@ -972,7 +1160,7 @@ fn cuda_decode_criterion() -> Criterion {
Criterion::default()
.sample_size(decode_sample_size())
.warm_up_time(DECODE_WARM_UP)
- .measurement_time(DECODE_MEASUREMENT)
+ .measurement_time(decode_measurement_time())
}
fn decode_sample_size() -> usize {
@@ -990,6 +1178,21 @@ fn decode_sample_size() -> usize {
sample_size
}
+fn decode_measurement_time() -> Duration {
+ let Some(value) = std::env::var_os("J2K_CUDA_DECODE_MEASUREMENT_SECONDS") else {
+ return DECODE_MEASUREMENT;
+ };
+ let value = value.to_string_lossy();
+ let seconds = value.parse::().unwrap_or_else(|error| {
+ panic!("invalid J2K_CUDA_DECODE_MEASUREMENT_SECONDS `{value}`: {error}")
+ });
+ assert!(
+ seconds > 0,
+ "J2K_CUDA_DECODE_MEASUREMENT_SECONDS must be positive"
+ );
+ Duration::from_secs(seconds)
+}
+
fn decode_case_batch_sizes() -> Vec {
parse_batch_sizes_env("J2K_CUDA_DECODE_CASE_BATCH_SIZES", CASE_BATCH_SIZES)
}
@@ -1042,6 +1245,12 @@ fn cuda_benchmark_id(case: &DecodeBenchCase) -> String {
}
}
+fn warm_cuda_session(warm: impl FnOnce(&mut CudaSession)) -> CudaSession {
+ let mut session = CudaSession::default();
+ warm(&mut session);
+ session
+}
+
fn assert_cuda_resident_decode(surface: &j2k_cuda::Surface) {
let cuda = assert_cuda_resident_surface(surface);
assert!(cuda.stats().decode_kernel_dispatches() > 0);
@@ -1105,6 +1314,21 @@ fn htj2k_gray8_fixture(width: u32, height: u32) -> Vec {
encode_htj2k(&pixels, width, height, 1, 8, false, &options).expect("encode HTJ2K fixture")
}
+fn classic_rgb8_fixture(width: u32, height: u32) -> Vec {
+ let mut pixels = Vec::with_capacity(width as usize * height as usize * 3);
+ for index in 0..width * height {
+ pixels.push(((index * 17 + index / 3) & 0xff) as u8);
+ pixels.push(((index * 29 + 7) & 0xff) as u8);
+ pixels.push(((index * 43 + 19) & 0xff) as u8);
+ }
+ let options = EncodeOptions {
+ reversible: true,
+ num_decomposition_levels: 1,
+ ..EncodeOptions::default()
+ };
+ encode(&pixels, width, height, 3, 8, false, &options).expect("encode classic fixture")
+}
+
fn htj2k_rgb8_fixture(width: u32, height: u32) -> Vec {
let mut pixels = Vec::with_capacity(width as usize * height as usize * 3);
for idx in 0..width * height {
diff --git a/crates/j2k-cuda/src/decoder.rs b/crates/j2k-cuda/src/decoder.rs
index fed2da61..4253c34f 100644
--- a/crates/j2k-cuda/src/decoder.rs
+++ b/crates/j2k-cuda/src/decoder.rs
@@ -16,8 +16,9 @@ use j2k_core::{
};
#[cfg(feature = "cuda-runtime")]
use j2k_cuda_runtime::{
- CudaBufferPool, CudaBufferPoolTakeTrace, CudaContext, CudaDeviceBuffer, CudaError,
- CudaExecutionStats, CudaHtj2kCleanupTarget, CudaHtj2kCodeBlockJob, CudaHtj2kDecodeResources,
+ CudaBufferPool, CudaBufferPoolTakeTrace, CudaClassicCodeBlockJob, CudaClassicDecodeTarget,
+ CudaClassicSegment, CudaContext, CudaDeviceBuffer, CudaError, CudaExecutionStats,
+ CudaHtj2kCleanupTarget, CudaHtj2kCodeBlockJob, CudaHtj2kDecodeResources,
CudaHtj2kDecodeTableResources, CudaHtj2kDequantizeTarget, CudaJ2kIdwtJob, CudaJ2kIdwtTarget,
CudaJ2kRect, CudaJ2kStoreGray16Job, CudaJ2kStoreGray8Job, CudaPooledDeviceBuffer,
CudaQueuedExecution, CudaQueuedHtj2kCleanup,
@@ -112,9 +113,18 @@ struct CudaPendingDequantBand {
output_words: usize,
}
+#[cfg(feature = "cuda-runtime")]
+struct CudaPendingClassicBand {
+ band_index: usize,
+ jobs: Vec,
+ segments: Vec,
+ output_words: usize,
+}
+
#[cfg(feature = "cuda-runtime")]
struct CudaComponentDecodeWork {
bands: Vec,
+ pending_classic_bands: Vec,
pending_dequant_bands: Vec,
store: CudaHtj2kStoreStep,
dispatches: usize,
@@ -390,14 +400,20 @@ mod tests {
let mut report = crate::CudaHtj2kProfileReport::default();
let timings = CudaDecodeStageTimings {
h2d: 17,
+ table_upload: 7,
+ job_upload: 10,
status_d2h: 5,
+ classic_tier1: 11,
..CudaDecodeStageTimings::default()
};
timings.add_to_report(&mut report);
assert_eq!(report.h2d_us, 17);
+ assert_eq!(report.detail.table_upload_us, 7);
+ assert_eq!(report.detail.job_upload_us, 10);
assert_eq!(report.detail.status_d2h_us, 5);
+ assert_eq!(report.classic_tier1_us, 11);
}
fn cuda_runtime_gate() -> bool {
diff --git a/crates/j2k-cuda/src/decoder/api.rs b/crates/j2k-cuda/src/decoder/api.rs
index c3181afc..bbd6156e 100644
--- a/crates/j2k-cuda/src/decoder/api.rs
+++ b/crates/j2k-cuda/src/decoder/api.rs
@@ -175,6 +175,29 @@ impl<'a> J2kDecoder<'a> {
self.decode_to_surface_impl(session, fmt, BackendRequest::Cuda)
}
+ /// Strictly decode a geometry request into a CUDA-backed surface using an
+ /// existing backend session.
+ #[doc(hidden)]
+ pub fn decode_request_to_device_with_session(
+ &mut self,
+ fmt: PixelFormat,
+ request: DeviceDecodeRequest,
+ session: &mut CudaSession,
+ ) -> Result {
+ match request {
+ DeviceDecodeRequest::Full => self.decode_to_device_with_session(fmt, session),
+ DeviceDecodeRequest::Region { roi } => {
+ self.decode_region_to_device_with_session(fmt, roi, session)
+ }
+ DeviceDecodeRequest::Scaled { scale } => {
+ self.decode_scaled_to_device_with_session(fmt, scale, session)
+ }
+ DeviceDecodeRequest::RegionScaled { roi, scale } => {
+ self.decode_region_scaled_to_device_with_session(fmt, roi, scale, session)
+ }
+ }
+ }
+
/// Strictly decode a full HTJ2K image into a CUDA-backed surface and return
/// a structured profile report for CPU planning and CUDA stages.
#[doc(hidden)]
diff --git a/crates/j2k-cuda/src/decoder/color_batch.rs b/crates/j2k-cuda/src/decoder/color_batch.rs
index 0de8fc16..6b842694 100644
--- a/crates/j2k-cuda/src/decoder/color_batch.rs
+++ b/crates/j2k-cuda/src/decoder/color_batch.rs
@@ -3,11 +3,15 @@
#[cfg(all(test, feature = "cuda-runtime"))]
use core::cell::Cell;
+#[cfg(feature = "cuda-runtime")]
+mod finish;
#[cfg(feature = "cuda-runtime")]
mod host_owners;
#[cfg(feature = "cuda-runtime")]
mod store;
+#[cfg(feature = "cuda-runtime")]
+use self::finish::finish_color_cuda_resident_surface_with_component_work;
#[cfg(feature = "cuda-runtime")]
use self::host_owners::{append_color_payload_to_shared, take_component_work};
#[cfg(feature = "cuda-runtime")]
@@ -150,7 +154,15 @@ pub(super) fn decode_color_cuda_resident_surface_with_plans_profile(
let context = session.cuda_context()?;
let pool = session.decode_buffer_pool()?;
let table_upload_start = profile::profile_now(collect_stage_timings);
- let table_resources = session.htj2k_decode_table_resources()?;
+ let table_resources = if color
+ .components
+ .iter()
+ .all(|plan| plan.subbands().is_empty())
+ {
+ None
+ } else {
+ Some(session.htj2k_decode_table_resources()?)
+ };
let table_upload_us = profile::elapsed_us(table_upload_start);
color.report.h2d_us = color.report.h2d_us.saturating_add(table_upload_us);
color.report.detail.table_upload_us = color
@@ -159,9 +171,11 @@ pub(super) fn decode_color_cuda_resident_surface_with_plans_profile(
.table_upload_us
.saturating_add(table_upload_us);
let payload_upload_start = profile::profile_now(collect_stage_timings);
- let decode_resources = context
- .upload_htj2k_decode_resources_with_tables(&color.payload, &table_resources)
- .map_err(cuda_error)?;
+ let decode_resources = match table_resources.as_ref() {
+ Some(tables) => context.upload_htj2k_decode_resources_with_tables(&color.payload, tables),
+ None => context.upload_j2k_decode_payload(&color.payload),
+ }
+ .map_err(cuda_error)?;
let payload_upload_us = profile::elapsed_us(payload_upload_start);
profile::add_payload_resource_upload_us(&mut color.report, payload_upload_us);
let mut host_budget = HostPhaseBudget::new("j2k CUDA color decode execution graph");
@@ -240,16 +254,27 @@ pub(super) fn decode_color_cuda_resident_batch_surfaces_with_profile(
let context = session.cuda_context()?;
let pool = session.decode_batch_buffer_pool()?;
let table_upload_start = profile::profile_now(collect_stage_timings);
- let table_resources = session.htj2k_decode_table_resources()?;
+ let table_resources = if colors.iter().all(|color| {
+ color
+ .components
+ .iter()
+ .all(|plan| plan.subbands().is_empty())
+ }) {
+ None
+ } else {
+ Some(session.htj2k_decode_table_resources()?)
+ };
let table_upload_us = profile::elapsed_us(table_upload_start);
let payload_upload_start = profile::profile_now(collect_stage_timings);
- let decode_resources = context
- .upload_htj2k_decode_resources_with_tables_and_pool(
+ let decode_resources = match table_resources.as_ref() {
+ Some(tables) => context.upload_htj2k_decode_resources_with_tables_and_pool(
&shared_payload,
- &table_resources,
+ tables,
&pool,
- )
- .map_err(cuda_error)?;
+ ),
+ None => context.upload_j2k_decode_payload_with_pool(&shared_payload, &pool),
+ }
+ .map_err(cuda_error)?;
let payload_upload_us = profile::elapsed_us(payload_upload_start);
drop(shared_payload);
@@ -695,95 +720,3 @@ fn finalize_color_surface(
};
(surface, color.report)
}
-
-#[cfg(feature = "cuda-runtime")]
-fn finish_color_cuda_resident_surface_with_component_work(
- request: FinishColorCudaResidentSurfaceRequest<'_>,
-) -> Result<(Surface, CudaHtj2kProfileReport), Error> {
- let FinishColorCudaResidentSurfaceRequest {
- context,
- pool,
- fmt,
- mut color,
- mut component_work,
- wall_started,
- collect_stage_timings,
- run_idwt,
- emit_report,
- } = request;
- let mut host_budget =
- host_owners::color_work_budget(&color, &component_work, "j2k CUDA color completion graph")?;
- let pending_idwt_batch = if run_idwt {
- run_pending_color_idwt(
- context,
- &color,
- &mut component_work,
- pool,
- collect_stage_timings,
- &mut host_budget,
- )?
- } else {
- None
- };
- let completion_result = (|| {
- let prepared = finish_color_components(component_work, &mut color)?;
- let inputs = ColorStoreInputs {
- context,
- buffers: [
- pooled_cuda_buffer(&prepared.components[0].buffer)?,
- pooled_cuda_buffer(&prepared.components[1].buffer)?,
- pooled_cuda_buffer(&prepared.components[2].buffer)?,
- ],
- stores: [
- &prepared.components[0].store,
- &prepared.components[1].store,
- &prepared.components[2].store,
- ],
- bit_depths: color.bit_depths,
- };
- let mct = run_color_mct(
- inputs,
- color.mct_dimensions,
- color.mct,
- color.transform,
- collect_stage_timings,
- )?;
- let dispatches = prepared.dispatches.saturating_add(mct.kernel_dispatches);
- let decode_dispatches = prepared
- .decode_dispatches
- .saturating_add(mct.decode_kernel_dispatches);
- color.report.mct_us = color.report.mct_us.saturating_add(mct.elapsed_us);
- color.report.detail.mct_dispatch_count = color
- .report
- .detail
- .mct_dispatch_count
- .saturating_add(mct.kernel_dispatches);
- let (store_output, store_us) = context
- .time_default_stream_named_us_if(
- collect_stage_timings,
- "j2k.htj2k.decode.store.color",
- || dispatch_color_store(inputs, mct, fmt),
- )
- .map_err(cuda_error)?;
- let (surface_buffer, store_stats) = store_output.into_parts();
- // Both runtime paths synchronize their kernel launch before success.
- let completion_established =
- mct.kernel_dispatches != 0 || store_stats.kernel_dispatches() != 0;
- let output = finalize_color_surface(FinalizeColorSurfaceRequest {
- fmt,
- color,
- surface_buffer,
- dispatches,
- decode_dispatches,
- store_stats,
- store_us,
- wall_started,
- emit_report,
- });
- Ok((output, completion_established))
- })();
- CudaQueuedIdwtBatch::resolve_optional_after_completed_work(
- pending_idwt_batch,
- completion_result,
- )
-}
diff --git a/crates/j2k-cuda/src/decoder/color_batch/finish.rs b/crates/j2k-cuda/src/decoder/color_batch/finish.rs
new file mode 100644
index 00000000..376438d9
--- /dev/null
+++ b/crates/j2k-cuda/src/decoder/color_batch/finish.rs
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use super::{
+ cuda_error, dispatch_color_store, finalize_color_surface, finish_color_components, host_owners,
+ pooled_cuda_buffer, run_color_mct, run_pending_color_idwt, ColorStoreInputs,
+ CudaHtj2kProfileReport, CudaQueuedIdwtBatch, Error, FinalizeColorSurfaceRequest,
+ FinishColorCudaResidentSurfaceRequest, Surface,
+};
+
+pub(super) fn finish_color_cuda_resident_surface_with_component_work(
+ request: FinishColorCudaResidentSurfaceRequest<'_>,
+) -> Result<(Surface, CudaHtj2kProfileReport), Error> {
+ let FinishColorCudaResidentSurfaceRequest {
+ context,
+ pool,
+ fmt,
+ mut color,
+ mut component_work,
+ wall_started,
+ collect_stage_timings,
+ run_idwt,
+ emit_report,
+ } = request;
+ let mut host_budget =
+ host_owners::color_work_budget(&color, &component_work, "j2k CUDA color completion graph")?;
+ let pending_idwt_batch = if run_idwt {
+ run_pending_color_idwt(
+ context,
+ &color,
+ &mut component_work,
+ pool,
+ collect_stage_timings,
+ &mut host_budget,
+ )?
+ } else {
+ None
+ };
+ let completion_result = (|| {
+ let prepared = finish_color_components(component_work, &mut color)?;
+ let inputs = ColorStoreInputs {
+ context,
+ buffers: [
+ pooled_cuda_buffer(&prepared.components[0].buffer)?,
+ pooled_cuda_buffer(&prepared.components[1].buffer)?,
+ pooled_cuda_buffer(&prepared.components[2].buffer)?,
+ ],
+ stores: [
+ &prepared.components[0].store,
+ &prepared.components[1].store,
+ &prepared.components[2].store,
+ ],
+ bit_depths: color.bit_depths,
+ };
+ let mct = run_color_mct(
+ inputs,
+ color.mct_dimensions,
+ color.mct,
+ color.transform,
+ collect_stage_timings,
+ )?;
+ let dispatches = prepared.dispatches.saturating_add(mct.kernel_dispatches);
+ let decode_dispatches = prepared
+ .decode_dispatches
+ .saturating_add(mct.decode_kernel_dispatches);
+ color.report.mct_us = color.report.mct_us.saturating_add(mct.elapsed_us);
+ color.report.detail.mct_dispatch_count = color
+ .report
+ .detail
+ .mct_dispatch_count
+ .saturating_add(mct.kernel_dispatches);
+ let (store_output, store_us) = context
+ .time_default_stream_named_us_if(
+ collect_stage_timings,
+ "j2k.htj2k.decode.store.color",
+ || dispatch_color_store(inputs, mct, fmt),
+ )
+ .map_err(cuda_error)?;
+ let (surface_buffer, store_stats) = store_output.into_parts();
+ // Both runtime paths synchronize their kernel launch before success.
+ let completion_established =
+ mct.kernel_dispatches != 0 || store_stats.kernel_dispatches() != 0;
+ let output = finalize_color_surface(FinalizeColorSurfaceRequest {
+ fmt,
+ color,
+ surface_buffer,
+ dispatches,
+ decode_dispatches,
+ store_stats,
+ store_us,
+ wall_started,
+ emit_report,
+ });
+ Ok((output, completion_established))
+ })();
+ CudaQueuedIdwtBatch::resolve_optional_after_completed_work(
+ pending_idwt_batch,
+ completion_result,
+ )
+}
diff --git a/crates/j2k-cuda/src/decoder/plan.rs b/crates/j2k-cuda/src/decoder/plan.rs
index 3841c707..676ed67f 100644
--- a/crates/j2k-cuda/src/decoder/plan.rs
+++ b/crates/j2k-cuda/src/decoder/plan.rs
@@ -39,7 +39,9 @@ impl J2kDecoder<'_> {
plan_us,
flatten_us,
total_us: profile::elapsed_us(total_start),
- block_count: cuda_plan.code_blocks().len(),
+ block_count: cuda_plan.block_count(),
+ classic_block_count: cuda_plan.classic_code_blocks().len(),
+ ht_block_count: cuda_plan.code_blocks().len(),
payload_bytes: cuda_plan.payload().len(),
dispatch_count: 0,
residency: crate::SurfaceResidency::CudaResidentDecode,
@@ -87,7 +89,9 @@ impl J2kDecoder<'_> {
plan_us,
flatten_us,
total_us: profile::elapsed_us(total_start),
- block_count: cuda_plan.code_blocks().len(),
+ block_count: cuda_plan.block_count(),
+ classic_block_count: cuda_plan.classic_code_blocks().len(),
+ ht_block_count: cuda_plan.code_blocks().len(),
payload_bytes: cuda_plan.payload().len(),
dispatch_count: 0,
residency: crate::SurfaceResidency::CudaResidentDecode,
@@ -134,7 +138,9 @@ impl J2kDecoder<'_> {
plan_us,
flatten_us,
total_us: profile::elapsed_us(total_start),
- block_count: cuda_plan.code_blocks().len(),
+ block_count: cuda_plan.block_count(),
+ classic_block_count: cuda_plan.classic_code_blocks().len(),
+ ht_block_count: cuda_plan.code_blocks().len(),
payload_bytes: cuda_plan.payload().len(),
dispatch_count: 0,
residency: crate::SurfaceResidency::CudaResidentDecode,
@@ -190,7 +196,9 @@ impl J2kDecoder<'_> {
plan_us,
flatten_us,
total_us: profile::elapsed_us(total_start),
- block_count: cuda_plan.code_blocks().len(),
+ block_count: cuda_plan.block_count(),
+ classic_block_count: cuda_plan.classic_code_blocks().len(),
+ ht_block_count: cuda_plan.code_blocks().len(),
payload_bytes: cuda_plan.payload().len(),
dispatch_count: 0,
residency: crate::SurfaceResidency::CudaResidentDecode,
@@ -246,6 +254,14 @@ impl J2kDecoder<'_> {
)?;
let flatten_us = profile::elapsed_us(flatten_start);
let block_count = components
+ .iter()
+ .map(CudaHtj2kDecodePlan::block_count)
+ .sum::();
+ let classic_block_count = components
+ .iter()
+ .map(|plan| plan.classic_code_blocks().len())
+ .sum::();
+ let ht_block_count = components
.iter()
.map(|plan| plan.code_blocks().len())
.sum::();
@@ -256,6 +272,8 @@ impl J2kDecoder<'_> {
flatten_us,
total_us: profile::elapsed_us(total_start),
block_count,
+ classic_block_count,
+ ht_block_count,
payload_bytes,
dispatch_count: 0,
residency: crate::SurfaceResidency::CudaResidentDecode,
@@ -305,6 +323,14 @@ impl J2kDecoder<'_> {
)?;
let flatten_us = profile::elapsed_us(flatten_start);
let block_count = components
+ .iter()
+ .map(CudaHtj2kDecodePlan::block_count)
+ .sum::();
+ let classic_block_count = components
+ .iter()
+ .map(|plan| plan.classic_code_blocks().len())
+ .sum::();
+ let ht_block_count = components
.iter()
.map(|plan| plan.code_blocks().len())
.sum::();
@@ -315,6 +341,8 @@ impl J2kDecoder<'_> {
flatten_us,
total_us: profile::elapsed_us(total_start),
block_count,
+ classic_block_count,
+ ht_block_count,
payload_bytes,
dispatch_count: 0,
residency: crate::SurfaceResidency::CudaResidentDecode,
@@ -371,6 +399,14 @@ impl J2kDecoder<'_> {
)?;
let flatten_us = profile::elapsed_us(flatten_start);
let block_count = components
+ .iter()
+ .map(CudaHtj2kDecodePlan::block_count)
+ .sum::();
+ let classic_block_count = components
+ .iter()
+ .map(|plan| plan.classic_code_blocks().len())
+ .sum::();
+ let ht_block_count = components
.iter()
.map(|plan| plan.code_blocks().len())
.sum::();
@@ -381,6 +417,8 @@ impl J2kDecoder<'_> {
flatten_us,
total_us: profile::elapsed_us(total_start),
block_count,
+ classic_block_count,
+ ht_block_count,
payload_bytes,
dispatch_count: 0,
residency: crate::SurfaceResidency::CudaResidentDecode,
@@ -425,6 +463,14 @@ pub(super) fn build_cuda_htj2k_color_plans_from_bytes_with_profile<'a>(
flatten_cuda_color_components(&native_plan, fmt, None, "j2k CUDA color decode plans")?;
let flatten_us = profile::elapsed_us(flatten_start);
let block_count = components
+ .iter()
+ .map(CudaHtj2kDecodePlan::block_count)
+ .sum::();
+ let classic_block_count = components
+ .iter()
+ .map(|plan| plan.classic_code_blocks().len())
+ .sum::();
+ let ht_block_count = components
.iter()
.map(|plan| plan.code_blocks().len())
.sum::();
@@ -435,6 +481,8 @@ pub(super) fn build_cuda_htj2k_color_plans_from_bytes_with_profile<'a>(
flatten_us,
total_us: profile::elapsed_us(total_start),
block_count,
+ classic_block_count,
+ ht_block_count,
payload_bytes,
dispatch_count: 0,
residency: crate::SurfaceResidency::CudaResidentDecode,
diff --git a/crates/j2k-cuda/src/decoder/profile.rs b/crates/j2k-cuda/src/decoder/profile.rs
index 95279d96..0e11bd38 100644
--- a/crates/j2k-cuda/src/decoder/profile.rs
+++ b/crates/j2k-cuda/src/decoder/profile.rs
@@ -105,12 +105,16 @@ pub(super) fn elapsed_host_us(start: Option) -> u128 {
#[derive(Clone, Copy, Debug, Default)]
pub(super) struct CudaDecodeStageTimings {
pub(super) h2d: u128,
+ pub(super) table_upload: u128,
pub(super) payload_upload: u128,
+ pub(super) job_upload: u128,
pub(super) status_d2h: u128,
pub(super) ht_cleanup: u128,
pub(super) ht_refine: u128,
+ pub(super) classic_tier1: u128,
pub(super) dequant: u128,
pub(super) ht_dispatch_count: usize,
+ pub(super) classic_dispatch_count: usize,
pub(super) idwt: u128,
pub(super) dequant_dispatch_count: usize,
pub(super) idwt_dispatch_count: usize,
@@ -124,15 +128,25 @@ impl CudaDecodeStageTimings {
.detail
.payload_upload_us
.saturating_add(self.payload_upload);
+ report.detail.table_upload_us = report
+ .detail
+ .table_upload_us
+ .saturating_add(self.table_upload);
+ report.detail.job_upload_us = report.detail.job_upload_us.saturating_add(self.job_upload);
report.detail.status_d2h_us = report.detail.status_d2h_us.saturating_add(self.status_d2h);
report.ht_cleanup_us = report.ht_cleanup_us.saturating_add(self.ht_cleanup);
report.ht_refine_us = report.ht_refine_us.saturating_add(self.ht_refine);
+ report.classic_tier1_us = report.classic_tier1_us.saturating_add(self.classic_tier1);
report.dequant_us = report.dequant_us.saturating_add(self.dequant);
report.idwt_us = report.idwt_us.saturating_add(self.idwt);
report.detail.ht_dispatch_count = report
.detail
.ht_dispatch_count
.saturating_add(self.ht_dispatch_count);
+ report.detail.classic_dispatch_count = report
+ .detail
+ .classic_dispatch_count
+ .saturating_add(self.classic_dispatch_count);
report.detail.dequant_dispatch_count = report
.detail
.dequant_dispatch_count
@@ -169,11 +183,20 @@ pub(super) fn add_decode_report(
aggregate.h2d_us = aggregate.h2d_us.saturating_add(report.h2d_us);
aggregate.ht_cleanup_us = aggregate.ht_cleanup_us.saturating_add(report.ht_cleanup_us);
aggregate.ht_refine_us = aggregate.ht_refine_us.saturating_add(report.ht_refine_us);
+ aggregate.classic_tier1_us = aggregate
+ .classic_tier1_us
+ .saturating_add(report.classic_tier1_us);
aggregate.dequant_us = aggregate.dequant_us.saturating_add(report.dequant_us);
aggregate.idwt_us = aggregate.idwt_us.saturating_add(report.idwt_us);
aggregate.mct_us = aggregate.mct_us.saturating_add(report.mct_us);
aggregate.store_us = aggregate.store_us.saturating_add(report.store_us);
aggregate.block_count = aggregate.block_count.saturating_add(report.block_count);
+ aggregate.classic_block_count = aggregate
+ .classic_block_count
+ .saturating_add(report.classic_block_count);
+ aggregate.ht_block_count = aggregate
+ .ht_block_count
+ .saturating_add(report.ht_block_count);
aggregate.payload_bytes = aggregate.payload_bytes.saturating_add(report.payload_bytes);
aggregate.dispatch_count = aggregate
.dispatch_count
@@ -202,6 +225,10 @@ pub(super) fn add_decode_report(
.detail
.ht_dispatch_count
.saturating_add(report.detail.ht_dispatch_count);
+ aggregate.detail.classic_dispatch_count = aggregate
+ .detail
+ .classic_dispatch_count
+ .saturating_add(report.detail.classic_dispatch_count);
aggregate.detail.dequant_dispatch_count = aggregate
.detail
.dequant_dispatch_count
diff --git a/crates/j2k-cuda/src/decoder/resident/cleanup_dequant.rs b/crates/j2k-cuda/src/decoder/resident/cleanup_dequant.rs
index 40ae5e48..4d9973e3 100644
--- a/crates/j2k-cuda/src/decoder/resident/cleanup_dequant.rs
+++ b/crates/j2k-cuda/src/decoder/resident/cleanup_dequant.rs
@@ -8,6 +8,11 @@ use super::super::{
use super::buffer_access::pooled_cuda_buffer;
use crate::allocation::HostPhaseBudget;
+#[cfg(feature = "cuda-runtime")]
+mod classic;
+#[cfg(feature = "cuda-runtime")]
+use classic::run_component_classic_batches;
+
#[cfg(test)]
pub(in crate::decoder) fn split_htj2k_subband_decode_dispatches(
kernel_dispatches: usize,
@@ -61,6 +66,14 @@ pub(in crate::decoder) fn run_component_cleanup_dequant_batches(
collect_stage_timings: bool,
live_host_bytes: usize,
) -> Result<(), Error> {
+ run_component_classic_batches(
+ context,
+ decode_resources,
+ component_work,
+ pool,
+ collect_stage_timings,
+ live_host_bytes,
+ )?;
let pending_count = component_work
.iter()
.map(|work| work.pending_dequant_bands.len())
diff --git a/crates/j2k-cuda/src/decoder/resident/cleanup_dequant/classic.rs b/crates/j2k-cuda/src/decoder/resident/cleanup_dequant/classic.rs
new file mode 100644
index 00000000..a4d3daba
--- /dev/null
+++ b/crates/j2k-cuda/src/decoder/resident/cleanup_dequant/classic.rs
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use super::super::super::{
+ cuda_error, CudaBufferPool, CudaClassicDecodeTarget, CudaComponentDecodeWork,
+ CudaHtj2kDecodeResources, Error, CUDA_HTJ2K_KERNELS_NOT_READY,
+};
+use super::super::buffer_access::pooled_cuda_buffer;
+use crate::allocation::HostPhaseBudget;
+
+pub(super) fn run_component_classic_batches(
+ context: &j2k_cuda_runtime::CudaContext,
+ decode_resources: &CudaHtj2kDecodeResources,
+ component_work: &mut [CudaComponentDecodeWork],
+ pool: &CudaBufferPool,
+ collect_stage_timings: bool,
+ live_host_bytes: usize,
+) -> Result<(), Error> {
+ let pending_count = component_work
+ .iter()
+ .map(|work| work.pending_classic_bands.len())
+ .sum::();
+ if pending_count == 0 {
+ return Ok(());
+ }
+ let accounting_index = component_work
+ .iter()
+ .position(|work| !work.pending_classic_bands.is_empty())
+ .ok_or(Error::UnsupportedCudaRequest {
+ reason: CUDA_HTJ2K_KERNELS_NOT_READY,
+ })?;
+ let mut budget =
+ HostPhaseBudget::with_live_bytes("j2k CUDA classic Tier-1 target phase", live_host_bytes)?;
+ let mut targets = budget.try_vec_with_capacity(pending_count)?;
+ for work in component_work.iter() {
+ for pending in &work.pending_classic_bands {
+ targets.push(CudaClassicDecodeTarget {
+ coefficients: pooled_cuda_buffer(&work.bands[pending.band_index].buffer)?,
+ jobs: &pending.jobs,
+ segments: &pending.segments,
+ output_words: pending.output_words,
+ });
+ }
+ }
+ let (_, runtime_timings) = context
+ .decode_classic_codeblocks_multi_with_resources_and_pool_timed(
+ decode_resources,
+ &targets,
+ pool,
+ budget.live_bytes(),
+ collect_stage_timings,
+ )
+ .map_err(cuda_error)?;
+ let accounting = &mut component_work[accounting_index];
+ accounting.timings.h2d = accounting
+ .timings
+ .h2d
+ .saturating_add(runtime_timings.job_upload_us)
+ .saturating_add(runtime_timings.table_upload_us);
+ accounting.timings.job_upload = accounting
+ .timings
+ .job_upload
+ .saturating_add(runtime_timings.job_upload_us);
+ accounting.timings.table_upload = accounting
+ .timings
+ .table_upload
+ .saturating_add(runtime_timings.table_upload_us);
+ accounting.timings.status_d2h = accounting
+ .timings
+ .status_d2h
+ .saturating_add(runtime_timings.status_d2h_us);
+ accounting.timings.classic_tier1 = accounting
+ .timings
+ .classic_tier1
+ .saturating_add(runtime_timings.kernel_us);
+ accounting.timings.classic_dispatch_count =
+ accounting.timings.classic_dispatch_count.saturating_add(1);
+ accounting.dispatches = accounting.dispatches.saturating_add(1);
+ accounting.decode_dispatches = accounting.decode_dispatches.saturating_add(1);
+ for work in component_work {
+ work.pending_classic_bands.clear();
+ }
+ Ok(())
+}
diff --git a/crates/j2k-cuda/src/decoder/resident/component.rs b/crates/j2k-cuda/src/decoder/resident/component.rs
index 61f9037f..9c23ce72 100644
--- a/crates/j2k-cuda/src/decoder/resident/component.rs
+++ b/crates/j2k-cuda/src/decoder/resident/component.rs
@@ -2,55 +2,37 @@
use super::super::{
cuda_error, profile, CudaBufferPool, CudaCoefficientBand, CudaComponentDecodeWork,
- CudaDecodeStageTimings, CudaDecodedComponent, CudaHtj2kCodeBlockJob, CudaHtj2kDecodePlan,
- CudaHtj2kDecodeResources, CudaHtj2kDecodeTableResources, CudaPendingDequantBand, Error,
- CUDA_HTJ2K_KERNELS_NOT_READY, CUDA_HTJ2K_PLAN_INVARIANT_FAILED, CUDA_HTJ2K_STORE_UNSUPPORTED,
+ CudaDecodeStageTimings, CudaDecodedComponent, CudaHtj2kDecodePlan, CudaHtj2kDecodeResources,
+ CudaHtj2kDecodeTableResources, CudaPendingDequantBand, Error, CUDA_HTJ2K_KERNELS_NOT_READY,
+ CUDA_HTJ2K_PLAN_INVARIANT_FAILED, CUDA_HTJ2K_STORE_UNSUPPORTED,
};
use super::cleanup_dequant::run_component_cleanup_dequant_batches;
use super::idwt::run_cuda_component_idwt_steps;
use crate::allocation::{checked_cuda_element_count, host_allocation_error, HostPhaseBudget};
#[cfg(feature = "cuda-runtime")]
-pub(in crate::decoder) fn cuda_code_block_job_from_plan_block(
- block: &crate::CudaHtj2kCodeBlock,
- subband_width: u32,
-) -> Result {
- let output_offset = block
- .output_y
- .checked_mul(subband_width)
- .and_then(|base| base.checked_add(block.output_x))
- .ok_or(Error::UnsupportedCudaRequest {
- reason: CUDA_HTJ2K_KERNELS_NOT_READY,
- })?;
- Ok(CudaHtj2kCodeBlockJob {
- payload_offset: block.payload_offset,
- width: block.width,
- height: block.height,
- payload_len: block.payload_len,
- cleanup_length: block.cleanup_length,
- refinement_length: block.refinement_length,
- missing_bit_planes: block.missing_bit_planes,
- num_bitplanes: block.num_bitplanes,
- number_of_coding_passes: block.number_of_coding_passes,
- output_stride: block.output_stride,
- output_offset,
- dequantization_step: block.dequantization_step,
- stripe_causal: block.stripe_causal != 0,
- })
-}
+mod classic;
+#[cfg(feature = "cuda-runtime")]
+use classic::append_classic_subbands;
+#[cfg(feature = "cuda-runtime")]
+mod ht;
+#[cfg(feature = "cuda-runtime")]
+pub(in crate::decoder) use ht::cuda_code_block_job_from_plan_block;
#[cfg(feature = "cuda-runtime")]
pub(super) fn decode_cuda_component_plan(
context: &j2k_cuda_runtime::CudaContext,
plan: &CudaHtj2kDecodePlan,
- tables: &CudaHtj2kDecodeTableResources,
+ tables: Option<&CudaHtj2kDecodeTableResources>,
pool: &CudaBufferPool,
collect_stage_timings: bool,
) -> Result {
let resource_upload_start = profile::profile_now(collect_stage_timings);
- let decode_resources = context
- .upload_htj2k_decode_resources_with_tables(plan.payload(), tables)
- .map_err(cuda_error)?;
+ let decode_resources = match tables {
+ Some(tables) => context.upload_htj2k_decode_resources_with_tables(plan.payload(), tables),
+ None => context.upload_j2k_decode_payload(plan.payload()),
+ }
+ .map_err(cuda_error)?;
let resource_upload_us = profile::elapsed_us(resource_upload_start);
let mut component = decode_cuda_component_plan_with_resources(
context,
@@ -113,7 +95,8 @@ pub(in crate::decoder) fn decode_cuda_component_subbands_with_resources(
let band_capacity = plan
.subbands()
.len()
- .checked_add(plan.idwt_steps().len())
+ .checked_add(plan.classic_subbands().len())
+ .and_then(|count| count.checked_add(plan.idwt_steps().len()))
.ok_or_else(|| {
host_allocation_error::(
usize::MAX,
@@ -121,6 +104,8 @@ pub(in crate::decoder) fn decode_cuda_component_subbands_with_resources(
)
})?;
let mut bands = host_budget.try_vec_with_capacity(band_capacity)?;
+ let mut pending_classic_bands =
+ host_budget.try_vec_with_capacity(plan.classic_subbands().len())?;
let mut pending_dequant_bands = host_budget.try_vec_with_capacity(plan.subbands().len())?;
let dispatches = 0usize;
let decode_dispatches = 0usize;
@@ -170,6 +155,17 @@ pub(in crate::decoder) fn decode_cuda_component_subbands_with_resources(
}
}
+ let classic_allocate_us = append_classic_subbands(
+ context,
+ plan,
+ pool,
+ collect_stage_timings,
+ host_budget,
+ &mut bands,
+ &mut pending_classic_bands,
+ )?;
+ timings.h2d = timings.h2d.saturating_add(classic_allocate_us);
+
let [store] = plan.store_steps() else {
return Err(Error::UnsupportedCudaRequest {
reason: CUDA_HTJ2K_STORE_UNSUPPORTED,
@@ -178,6 +174,7 @@ pub(in crate::decoder) fn decode_cuda_component_subbands_with_resources(
Ok(CudaComponentDecodeWork {
bands,
+ pending_classic_bands,
pending_dequant_bands,
store: *store,
dispatches,
diff --git a/crates/j2k-cuda/src/decoder/resident/component/classic.rs b/crates/j2k-cuda/src/decoder/resident/component/classic.rs
new file mode 100644
index 00000000..c77f0a4d
--- /dev/null
+++ b/crates/j2k-cuda/src/decoder/resident/component/classic.rs
@@ -0,0 +1,134 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use super::super::super::{
+ cuda_error, profile, CudaBufferPool, CudaClassicCodeBlockJob, CudaClassicSegment,
+ CudaCoefficientBand, CudaHtj2kDecodePlan, CudaPendingClassicBand, Error,
+ CUDA_HTJ2K_KERNELS_NOT_READY, CUDA_HTJ2K_PLAN_INVARIANT_FAILED,
+};
+use crate::allocation::{checked_cuda_element_count, HostPhaseBudget};
+
+pub(super) fn append_classic_subbands(
+ context: &j2k_cuda_runtime::CudaContext,
+ plan: &CudaHtj2kDecodePlan,
+ pool: &CudaBufferPool,
+ collect_stage_timings: bool,
+ host_budget: &mut HostPhaseBudget,
+ bands: &mut Vec,
+ pending_bands: &mut Vec,
+) -> Result {
+ let mut allocate_us = 0u128;
+ for subband in plan.classic_subbands() {
+ let start = subband.code_block_start as usize;
+ let end = start.checked_add(subband.code_block_count as usize).ok_or(
+ Error::UnsupportedCudaRequest {
+ reason: CUDA_HTJ2K_PLAN_INVARIANT_FAILED,
+ },
+ )?;
+ let code_blocks =
+ plan.classic_code_blocks()
+ .get(start..end)
+ .ok_or(Error::UnsupportedCudaRequest {
+ reason: CUDA_HTJ2K_PLAN_INVARIANT_FAILED,
+ })?;
+ let segment_start = code_blocks
+ .first()
+ .map_or(0, |block| block.segment_start as usize);
+ let segment_base =
+ u32::try_from(segment_start).map_err(|_| Error::UnsupportedCudaRequest {
+ reason: CUDA_HTJ2K_PLAN_INVARIANT_FAILED,
+ })?;
+ let segment_end = code_blocks.last().map_or(segment_start, |block| {
+ block.segment_start as usize + block.segment_count as usize
+ });
+ let plan_segments = plan
+ .classic_segments()
+ .get(segment_start..segment_end)
+ .ok_or(Error::UnsupportedCudaRequest {
+ reason: CUDA_HTJ2K_PLAN_INVARIANT_FAILED,
+ })?;
+ let segments = host_budget.try_collect_results_exact(
+ plan_segments
+ .iter()
+ .map(|segment| Ok::<_, Error>(cuda_classic_segment_from_plan(segment))),
+ )?;
+ let jobs = host_budget.try_collect_results_exact(
+ code_blocks
+ .iter()
+ .map(|block| cuda_classic_job_from_plan(block, subband.width, segment_base)),
+ )?;
+ let output_words = checked_cuda_element_count(subband.width, subband.height).ok_or(
+ Error::UnsupportedCudaRequest {
+ reason: CUDA_HTJ2K_KERNELS_NOT_READY,
+ },
+ )?;
+ let allocate_start = profile::profile_now(collect_stage_timings);
+ let buffer = context
+ .allocate_classic_coefficients_with_pool(output_words, pool)
+ .map_err(cuda_error)?;
+ allocate_us = allocate_us.saturating_add(profile::elapsed_us(allocate_start));
+ let band_index = bands.len();
+ bands.push(CudaCoefficientBand {
+ band_id: subband.band_id,
+ buffer,
+ });
+ if !jobs.is_empty() {
+ pending_bands.push(CudaPendingClassicBand {
+ band_index,
+ jobs,
+ segments,
+ output_words,
+ });
+ }
+ }
+ Ok(allocate_us)
+}
+
+fn cuda_classic_segment_from_plan(
+ segment: &crate::direct_plan::CudaClassicSegment,
+) -> CudaClassicSegment {
+ CudaClassicSegment {
+ data_offset: segment.data_offset,
+ data_length: segment.data_length,
+ start_coding_pass: u32::from(segment.start_coding_pass),
+ end_coding_pass: u32::from(segment.end_coding_pass),
+ use_arithmetic: segment.use_arithmetic,
+ }
+}
+
+fn cuda_classic_job_from_plan(
+ block: &crate::direct_plan::CudaClassicCodeBlock,
+ subband_width: u32,
+ segment_base: u32,
+) -> Result {
+ let output_offset = block
+ .output_y
+ .checked_mul(subband_width)
+ .and_then(|base| base.checked_add(block.output_x))
+ .ok_or(Error::UnsupportedCudaRequest {
+ reason: CUDA_HTJ2K_PLAN_INVARIANT_FAILED,
+ })?;
+ let segment_start =
+ block
+ .segment_start
+ .checked_sub(segment_base)
+ .ok_or(Error::UnsupportedCudaRequest {
+ reason: CUDA_HTJ2K_PLAN_INVARIANT_FAILED,
+ })?;
+ Ok(CudaClassicCodeBlockJob {
+ payload_offset: block.payload_offset,
+ payload_len: block.payload_len,
+ segment_start,
+ segment_count: block.segment_count,
+ width: block.width,
+ height: block.height,
+ output_stride: block.output_stride,
+ output_offset,
+ missing_bitplanes: u32::from(block.missing_bit_planes),
+ total_bitplanes: u32::from(block.total_bitplanes),
+ number_of_coding_passes: u32::from(block.number_of_coding_passes),
+ sub_band_type: u32::from(block.sub_band_type),
+ style_flags: block.style_flags,
+ strict: block.strict,
+ dequantization_step: block.dequantization_step,
+ })
+}
diff --git a/crates/j2k-cuda/src/decoder/resident/component/ht.rs b/crates/j2k-cuda/src/decoder/resident/component/ht.rs
new file mode 100644
index 00000000..df908159
--- /dev/null
+++ b/crates/j2k-cuda/src/decoder/resident/component/ht.rs
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use super::super::super::{CudaHtj2kCodeBlockJob, Error, CUDA_HTJ2K_KERNELS_NOT_READY};
+
+pub(in crate::decoder) fn cuda_code_block_job_from_plan_block(
+ block: &crate::CudaHtj2kCodeBlock,
+ subband_width: u32,
+) -> Result {
+ let output_offset = block
+ .output_y
+ .checked_mul(subband_width)
+ .and_then(|base| base.checked_add(block.output_x))
+ .ok_or(Error::UnsupportedCudaRequest {
+ reason: CUDA_HTJ2K_KERNELS_NOT_READY,
+ })?;
+ Ok(CudaHtj2kCodeBlockJob {
+ payload_offset: block.payload_offset,
+ width: block.width,
+ height: block.height,
+ payload_len: block.payload_len,
+ cleanup_length: block.cleanup_length,
+ refinement_length: block.refinement_length,
+ missing_bit_planes: block.missing_bit_planes,
+ num_bitplanes: block.num_bitplanes,
+ number_of_coding_passes: block.number_of_coding_passes,
+ output_stride: block.output_stride,
+ output_offset,
+ dequantization_step: block.dequantization_step,
+ stripe_causal: block.stripe_causal != 0,
+ })
+}
diff --git a/crates/j2k-cuda/src/decoder/resident/surface.rs b/crates/j2k-cuda/src/decoder/resident/surface.rs
index 67640b6e..143d272b 100644
--- a/crates/j2k-cuda/src/decoder/resident/surface.rs
+++ b/crates/j2k-cuda/src/decoder/resident/surface.rs
@@ -1,9 +1,10 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use super::super::{
- cuda_error, profile, BackendKind, CudaError, CudaHtj2kDecodePlan, CudaHtj2kProfileReport,
- CudaJ2kStoreGray16Job, CudaJ2kStoreGray8Job, CudaSession, CudaSurfaceStats, Error, PixelFormat,
- Storage, Surface, SurfaceResidency, CUDA_HTJ2K_OUTPUT_FORMAT_UNSUPPORTED,
+ cuda_error, profile, BackendKind, CudaError, CudaHtj2kDecodePlan,
+ CudaHtj2kDecodeTableResources, CudaHtj2kProfileReport, CudaJ2kStoreGray16Job,
+ CudaJ2kStoreGray8Job, CudaSession, CudaSurfaceStats, Error, PixelFormat, Storage, Surface,
+ SurfaceResidency, CUDA_HTJ2K_OUTPUT_FORMAT_UNSUPPORTED,
};
use super::buffer_access::pooled_cuda_buffer;
use super::component::decode_cuda_component_plan;
@@ -18,9 +19,8 @@ pub(super) fn decode_grayscale_cuda_resident_surface_with_plan_profile(
collect_stage_timings: bool,
) -> Result<(Surface, CudaHtj2kProfileReport), Error> {
let context = session.cuda_context()?;
- let table_upload_start = profile::profile_now(collect_stage_timings);
- let table_resources = session.htj2k_decode_table_resources()?;
- let table_upload_us = profile::elapsed_us(table_upload_start);
+ let (table_resources, table_upload_us) =
+ decode_table_resources(session, plan, collect_stage_timings)?;
report.h2d_us = report.h2d_us.saturating_add(table_upload_us);
report.detail.table_upload_us = report
.detail
@@ -30,7 +30,7 @@ pub(super) fn decode_grayscale_cuda_resident_surface_with_plan_profile(
let component = decode_cuda_component_plan(
&context,
plan,
- &table_resources,
+ table_resources.as_ref(),
&pool,
collect_stage_timings,
)?;
@@ -117,3 +117,18 @@ pub(super) fn decode_grayscale_cuda_resident_surface_with_plan_profile(
};
Ok((surface, report.clone()))
}
+
+#[cfg(feature = "cuda-runtime")]
+fn decode_table_resources(
+ session: &mut CudaSession,
+ plan: &CudaHtj2kDecodePlan,
+ collect_stage_timings: bool,
+) -> Result<(Option, u128), Error> {
+ let started = profile::profile_now(collect_stage_timings);
+ let tables = if plan.subbands().is_empty() {
+ None
+ } else {
+ Some(session.htj2k_decode_table_resources()?)
+ };
+ Ok((tables, profile::elapsed_us(started)))
+}
diff --git a/crates/j2k-cuda/src/direct_plan.rs b/crates/j2k-cuda/src/direct_plan.rs
index 3f7e9138..8efd5394 100644
--- a/crates/j2k-cuda/src/direct_plan.rs
+++ b/crates/j2k-cuda/src/direct_plan.rs
@@ -1,7 +1,7 @@
use j2k_core::PixelFormat;
use j2k_native::{
- J2kDirectGrayscalePlan, J2kDirectGrayscaleStep, J2kDirectIdwtStep, J2kDirectStoreStep, J2kRect,
- J2kWaveletTransform,
+ J2kCodeBlockSegment, J2kCodeBlockStyle, J2kDirectGrayscalePlan, J2kDirectGrayscaleStep,
+ J2kDirectIdwtStep, J2kDirectStoreStep, J2kRect, J2kSubBandType, J2kWaveletTransform,
};
use crate::{allocation::HostPhaseBudget, Error};
@@ -9,9 +9,7 @@ use crate::{allocation::HostPhaseBudget, Error};
mod required_regions;
use self::required_regions::required_regions_for_direct_plan;
-const CLASSIC_J2K_NOT_CUDA_HTJ2K: &str =
- "strict CUDA codestream decode only accepts HTJ2K direct-plan subbands";
-const EMPTY_HTJ2K_PLAN: &str = "strict CUDA HTJ2K plan contains no HT code blocks";
+const EMPTY_CUDA_PLAN: &str = "strict CUDA plan contains no entropy code blocks";
const MIXED_TRANSFORMS_UNSUPPORTED: &str = "strict CUDA HTJ2K plan contains mixed DWT transforms";
const PLAN_PAYLOAD_TOO_LARGE: &str = "strict CUDA HTJ2K plan payload is too large";
const PLAN_BLOCK_LENGTH_MISMATCH: &str =
@@ -20,6 +18,13 @@ const PLAN_OUTPUT_RECT_MISMATCH: &str =
"strict CUDA HTJ2K plan store does not fit the requested output rectangle";
const ROI_MAXSHIFT_UNSUPPORTED: &str =
"strict CUDA HTJ2K plan does not support ROI maxshift decode";
+const CLASSIC_PLAN_INVALID: &str = "strict CUDA classic Tier-1 plan is invalid";
+
+pub(crate) const J2K_CLASSIC_STYLE_RESET_CONTEXT_PROBABILITIES: u32 = 1 << 0;
+pub(crate) const J2K_CLASSIC_STYLE_TERMINATION_ON_EACH_PASS: u32 = 1 << 1;
+pub(crate) const J2K_CLASSIC_STYLE_VERTICALLY_CAUSAL_CONTEXT: u32 = 1 << 2;
+pub(crate) const J2K_CLASSIC_STYLE_SEGMENTATION_SYMBOLS: u32 = 1 << 3;
+pub(crate) const J2K_CLASSIC_STYLE_SELECTIVE_ARITHMETIC_CODING_BYPASS: u32 = 1 << 4;
/// CUDA-side DWT transform selector for a flat HTJ2K plan.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -103,6 +108,51 @@ pub(crate) struct CudaHtj2kSubband {
pub(crate) code_block_count: u32,
}
+/// Flat classic JPEG 2000 code-block metadata consumed by CUDA kernels.
+#[derive(Debug, Clone, Copy, PartialEq)]
+#[repr(C)]
+pub(crate) struct CudaClassicCodeBlock {
+ pub(crate) subband_index: u32,
+ pub(crate) payload_offset: u64,
+ pub(crate) payload_len: u32,
+ pub(crate) segment_start: u32,
+ pub(crate) segment_count: u32,
+ pub(crate) output_x: u32,
+ pub(crate) output_y: u32,
+ pub(crate) width: u32,
+ pub(crate) height: u32,
+ pub(crate) output_stride: u32,
+ pub(crate) missing_bit_planes: u8,
+ pub(crate) number_of_coding_passes: u8,
+ pub(crate) total_bitplanes: u8,
+ pub(crate) sub_band_type: u8,
+ pub(crate) style_flags: u32,
+ pub(crate) strict: bool,
+ pub(crate) dequantization_step: f32,
+}
+
+/// Flat classic JPEG 2000 segment metadata consumed by CUDA kernels.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[repr(C)]
+pub(crate) struct CudaClassicSegment {
+ pub(crate) data_offset: u32,
+ pub(crate) data_length: u32,
+ pub(crate) start_coding_pass: u8,
+ pub(crate) end_coding_pass: u8,
+ pub(crate) use_arithmetic: bool,
+}
+
+/// Flat classic JPEG 2000 sub-band geometry.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[repr(C)]
+pub(crate) struct CudaClassicSubband {
+ pub(crate) band_id: CudaHtj2kBandId,
+ pub(crate) width: u32,
+ pub(crate) height: u32,
+ pub(crate) code_block_start: u32,
+ pub(crate) code_block_count: u32,
+}
+
/// Flat POD IDWT step consumed by CUDA kernels.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
@@ -221,6 +271,16 @@ pub(crate) struct CudaHtj2kDecodePlan {
transform: CudaHtj2kTransform,
payload: Vec,
code_blocks: Vec,
+ classic_code_blocks: Vec,
+ classic_segments: Vec,
+ #[cfg_attr(
+ not(feature = "cuda-runtime"),
+ expect(
+ dead_code,
+ reason = "classic subband metadata is consumed only by CUDA decode routes"
+ )
+ )]
+ classic_subbands: Vec,
#[cfg_attr(
not(feature = "cuda-runtime"),
expect(
@@ -270,12 +330,17 @@ impl CudaHtj2kDecodePlan {
let mut host_budget = HostPhaseBudget::new("CUDA direct-plan owner graph");
let mut payload = host_budget.try_vec_with_capacity(capacity_hint.payload_bytes)?;
let mut code_blocks = host_budget.try_vec_with_capacity(capacity_hint.code_blocks)?;
+ let mut classic_code_blocks =
+ host_budget.try_vec_with_capacity(capacity_hint.classic_code_blocks)?;
+ let mut classic_segments =
+ host_budget.try_vec_with_capacity(capacity_hint.classic_segments)?;
+ let mut classic_subbands =
+ host_budget.try_vec_with_capacity(capacity_hint.classic_subbands)?;
let mut subbands = host_budget.try_vec_with_capacity(capacity_hint.subbands)?;
let mut idwt_steps = host_budget.try_vec_with_capacity(capacity_hint.idwt_steps)?;
let mut store_steps = host_budget.try_vec_with_capacity(capacity_hint.store_steps)?;
let retained_plan_capacity = host_budget.live_bytes();
let mut transform = None;
- let mut saw_classic = false;
let required_regions = if output_origin == (0, 0) && output_dimensions == plan.dimensions {
None
} else {
@@ -382,7 +447,95 @@ impl CudaHtj2kDecodePlan {
code_block_count,
});
}
- J2kDirectGrayscaleStep::ClassicSubBand(_) => saw_classic = true,
+ J2kDirectGrayscaleStep::ClassicSubBand(subband) => {
+ let subband_index = u32::try_from(classic_subbands.len()).map_err(|_| {
+ Error::UnsupportedCudaRequest {
+ reason: PLAN_PAYLOAD_TOO_LARGE,
+ }
+ })?;
+ let code_block_start =
+ u32::try_from(classic_code_blocks.len()).map_err(|_| {
+ Error::UnsupportedCudaRequest {
+ reason: PLAN_PAYLOAD_TOO_LARGE,
+ }
+ })?;
+ for job in &subband.jobs {
+ if let Some(required_regions) = &required_regions {
+ if !required_regions
+ .get(subband.band_id)
+ .is_some_and(|required| {
+ required.intersects(
+ job.output_x,
+ job.output_y,
+ job.width,
+ job.height,
+ )
+ })
+ {
+ continue;
+ }
+ }
+ validate_classic_job(job)?;
+ let payload_offset = u64::try_from(payload.len()).map_err(|_| {
+ Error::UnsupportedCudaRequest {
+ reason: PLAN_PAYLOAD_TOO_LARGE,
+ }
+ })?;
+ let payload_len = u32::try_from(job.data.len()).map_err(|_| {
+ Error::UnsupportedCudaRequest {
+ reason: PLAN_PAYLOAD_TOO_LARGE,
+ }
+ })?;
+ let segment_start =
+ u32::try_from(classic_segments.len()).map_err(|_| {
+ Error::UnsupportedCudaRequest {
+ reason: PLAN_PAYLOAD_TOO_LARGE,
+ }
+ })?;
+ let output_stride = u32::try_from(job.output_stride).map_err(|_| {
+ Error::UnsupportedCudaRequest {
+ reason: PLAN_PAYLOAD_TOO_LARGE,
+ }
+ })?;
+ payload.extend_from_slice(&job.data);
+ classic_segments.extend(job.segments.iter().map(convert_classic_segment));
+ classic_code_blocks.push(CudaClassicCodeBlock {
+ subband_index,
+ payload_offset,
+ payload_len,
+ segment_start,
+ segment_count: u32::try_from(job.segments.len()).map_err(|_| {
+ Error::UnsupportedCudaRequest {
+ reason: PLAN_PAYLOAD_TOO_LARGE,
+ }
+ })?,
+ output_x: job.output_x,
+ output_y: job.output_y,
+ width: job.width,
+ height: job.height,
+ output_stride,
+ missing_bit_planes: job.missing_bit_planes,
+ number_of_coding_passes: job.number_of_coding_passes,
+ total_bitplanes: job.total_bitplanes,
+ sub_band_type: classic_subband_type(job.sub_band_type),
+ style_flags: classic_style_flags(job.style),
+ strict: job.strict,
+ dequantization_step: job.dequantization_step,
+ });
+ }
+ classic_subbands.push(CudaClassicSubband {
+ band_id: subband.band_id,
+ width: subband.width,
+ height: subband.height,
+ code_block_start,
+ code_block_count: u32::try_from(
+ classic_code_blocks.len() - code_block_start as usize,
+ )
+ .map_err(|_| Error::UnsupportedCudaRequest {
+ reason: PLAN_PAYLOAD_TOO_LARGE,
+ })?,
+ });
+ }
J2kDirectGrayscaleStep::Idwt(step) => {
let step_transform = CudaHtj2kTransform::from_native(step.transform);
match transform {
@@ -402,14 +555,9 @@ impl CudaHtj2kDecodePlan {
}
}
- if saw_classic {
- return Err(Error::UnsupportedCudaRequest {
- reason: CLASSIC_J2K_NOT_CUDA_HTJ2K,
- });
- }
- if code_blocks.is_empty() {
+ if code_blocks.is_empty() && classic_code_blocks.is_empty() {
return Err(Error::UnsupportedCudaRequest {
- reason: EMPTY_HTJ2K_PLAN,
+ reason: EMPTY_CUDA_PLAN,
});
}
@@ -421,6 +569,9 @@ impl CudaHtj2kDecodePlan {
transform: transform.unwrap_or(CudaHtj2kTransform::Reversible53),
payload,
code_blocks,
+ classic_code_blocks,
+ classic_segments,
+ classic_subbands,
subbands,
idwt_steps,
store_steps,
@@ -519,6 +670,15 @@ impl CudaHtj2kDecodePlan {
reason: PLAN_PAYLOAD_TOO_LARGE,
})?;
}
+ for block in &mut self.classic_code_blocks {
+ block.payload_offset =
+ block
+ .payload_offset
+ .checked_add(base)
+ .ok_or(Error::UnsupportedCudaRequest {
+ reason: PLAN_PAYLOAD_TOO_LARGE,
+ })?;
+ }
if shared_payload.is_empty() {
*shared_payload = core::mem::take(&mut self.payload);
} else {
@@ -532,6 +692,9 @@ impl CudaHtj2kDecodePlan {
pub(crate) fn account_host_owners(&self, budget: &mut HostPhaseBudget) -> Result<(), Error> {
budget.account_vec(&self.payload)?;
budget.account_vec(&self.code_blocks)?;
+ budget.account_vec(&self.classic_code_blocks)?;
+ budget.account_vec(&self.classic_segments)?;
+ budget.account_vec(&self.classic_subbands)?;
budget.account_vec(&self.subbands)?;
budget.account_vec(&self.idwt_steps)?;
budget.account_vec(&self.store_steps)?;
@@ -555,6 +718,15 @@ impl CudaHtj2kDecodePlan {
reason: PLAN_PAYLOAD_TOO_LARGE,
})?;
}
+ for block in &mut self.classic_code_blocks {
+ block.payload_offset =
+ block
+ .payload_offset
+ .checked_add(base)
+ .ok_or(Error::UnsupportedCudaRequest {
+ reason: PLAN_PAYLOAD_TOO_LARGE,
+ })?;
+ }
Ok(())
}
@@ -563,6 +735,44 @@ impl CudaHtj2kDecodePlan {
&self.code_blocks
}
+ #[cfg_attr(
+ not(feature = "cuda-runtime"),
+ expect(
+ dead_code,
+ reason = "classic block metadata is consumed only by CUDA decode routes"
+ )
+ )]
+ pub(crate) fn classic_code_blocks(&self) -> &[CudaClassicCodeBlock] {
+ &self.classic_code_blocks
+ }
+
+ #[cfg_attr(
+ not(feature = "cuda-runtime"),
+ expect(
+ dead_code,
+ reason = "classic segment metadata is consumed only by CUDA decode routes"
+ )
+ )]
+ pub(crate) fn classic_segments(&self) -> &[CudaClassicSegment] {
+ &self.classic_segments
+ }
+
+ #[cfg(feature = "cuda-runtime")]
+ pub(crate) fn classic_subbands(&self) -> &[CudaClassicSubband] {
+ &self.classic_subbands
+ }
+
+ #[cfg_attr(
+ not(feature = "cuda-runtime"),
+ expect(
+ dead_code,
+ reason = "combined block counts are consumed only by CUDA decode routes"
+ )
+ )]
+ pub(crate) fn block_count(&self) -> usize {
+ self.code_blocks.len() + self.classic_code_blocks.len()
+ }
+
/// Flat sub-band metadata.
#[cfg(feature = "cuda-runtime")]
pub(crate) fn subbands(&self) -> &[CudaHtj2kSubband] {
@@ -588,7 +798,7 @@ impl CudaHtj2kDecodePlan {
expect(dead_code, reason = "dispatch hint accessor supports CUDA plan tests")
)]
pub(crate) fn dispatch_count_hint(&self) -> usize {
- self.code_blocks.len()
+ self.block_count()
}
}
@@ -597,6 +807,9 @@ struct CudaPlanCapacityHint {
payload_bytes: usize,
code_blocks: usize,
subbands: usize,
+ classic_code_blocks: usize,
+ classic_segments: usize,
+ classic_subbands: usize,
idwt_steps: usize,
store_steps: usize,
}
@@ -620,7 +833,28 @@ fn cuda_plan_capacity_hint(plan: &J2kDirectGrayscalePlan) -> Result {}
+ J2kDirectGrayscaleStep::ClassicSubBand(subband) => {
+ hint.classic_subbands = hint.classic_subbands.saturating_add(1);
+ hint.classic_code_blocks = hint
+ .classic_code_blocks
+ .checked_add(subband.jobs.len())
+ .ok_or(Error::UnsupportedCudaRequest {
+ reason: PLAN_PAYLOAD_TOO_LARGE,
+ })?;
+ for job in &subband.jobs {
+ hint.payload_bytes = hint.payload_bytes.checked_add(job.data.len()).ok_or(
+ Error::UnsupportedCudaRequest {
+ reason: PLAN_PAYLOAD_TOO_LARGE,
+ },
+ )?;
+ hint.classic_segments = hint
+ .classic_segments
+ .checked_add(job.segments.len())
+ .ok_or(Error::UnsupportedCudaRequest {
+ reason: PLAN_PAYLOAD_TOO_LARGE,
+ })?;
+ }
+ }
J2kDirectGrayscaleStep::Idwt(_) => {
hint.idwt_steps = hint.idwt_steps.saturating_add(1);
}
@@ -632,6 +866,80 @@ fn cuda_plan_capacity_hint(plan: &J2kDirectGrayscalePlan) -> Result Result<(), Error> {
+ if job.roi_shift != 0
+ || !(1..=64).contains(&job.width)
+ || !(1..=64).contains(&job.height)
+ || !(1..=31).contains(&job.total_bitplanes)
+ || job.missing_bit_planes >= job.total_bitplanes
+ {
+ return Err(Error::UnsupportedCudaRequest {
+ reason: CLASSIC_PLAN_INVALID,
+ });
+ }
+ let coded_bitplanes = job.total_bitplanes - job.missing_bit_planes;
+ let max_passes = 1 + 3 * (coded_bitplanes - 1);
+ if job.number_of_coding_passes > max_passes {
+ return Err(Error::UnsupportedCudaRequest {
+ reason: CLASSIC_PLAN_INVALID,
+ });
+ }
+ let mut expected_pass = 0u8;
+ let mut expected_offset = 0u32;
+ for segment in &job.segments {
+ if segment.start_coding_pass != expected_pass
+ || segment.end_coding_pass < segment.start_coding_pass
+ || segment.data_offset != expected_offset
+ {
+ return Err(Error::UnsupportedCudaRequest {
+ reason: CLASSIC_PLAN_INVALID,
+ });
+ }
+ expected_pass = segment.end_coding_pass;
+ expected_offset = segment.data_offset.checked_add(segment.data_length).ok_or(
+ Error::UnsupportedCudaRequest {
+ reason: CLASSIC_PLAN_INVALID,
+ },
+ )?;
+ }
+ if expected_pass != job.number_of_coding_passes
+ || usize::try_from(expected_offset).ok() != Some(job.data.len())
+ {
+ return Err(Error::UnsupportedCudaRequest {
+ reason: CLASSIC_PLAN_INVALID,
+ });
+ }
+ Ok(())
+}
+
+fn convert_classic_segment(segment: &J2kCodeBlockSegment) -> CudaClassicSegment {
+ CudaClassicSegment {
+ data_offset: segment.data_offset,
+ data_length: segment.data_length,
+ start_coding_pass: segment.start_coding_pass,
+ end_coding_pass: segment.end_coding_pass,
+ use_arithmetic: segment.use_arithmetic,
+ }
+}
+
+fn classic_subband_type(value: J2kSubBandType) -> u8 {
+ match value {
+ J2kSubBandType::LowLow => 0,
+ J2kSubBandType::HighLow => 1,
+ J2kSubBandType::LowHigh => 2,
+ J2kSubBandType::HighHigh => 3,
+ }
+}
+
+fn classic_style_flags(style: J2kCodeBlockStyle) -> u32 {
+ (u32::from(style.reset_context_probabilities) * J2K_CLASSIC_STYLE_RESET_CONTEXT_PROBABILITIES)
+ | (u32::from(style.termination_on_each_pass) * J2K_CLASSIC_STYLE_TERMINATION_ON_EACH_PASS)
+ | (u32::from(style.vertically_causal_context) * J2K_CLASSIC_STYLE_VERTICALLY_CAUSAL_CONTEXT)
+ | (u32::from(style.segmentation_symbols) * J2K_CLASSIC_STYLE_SEGMENTATION_SYMBOLS)
+ | (u32::from(style.selective_arithmetic_coding_bypass)
+ * J2K_CLASSIC_STYLE_SELECTIVE_ARITHMETIC_CODING_BYPASS)
+}
+
fn convert_idwt_step(step: J2kDirectIdwtStep) -> CudaHtj2kIdwtStep {
CudaHtj2kIdwtStep {
output_band_id: step.output_band_id,
diff --git a/crates/j2k-cuda/src/htj2k_plan_tests.rs b/crates/j2k-cuda/src/htj2k_plan_tests.rs
index f992b247..5d6bd0a3 100644
--- a/crates/j2k-cuda/src/htj2k_plan_tests.rs
+++ b/crates/j2k-cuda/src/htj2k_plan_tests.rs
@@ -1,5 +1,5 @@
use crate::{CudaHtj2kTransform, J2kDecoder, SurfaceResidency};
-use j2k_core::{CodecError, PixelFormat, Rect};
+use j2k_core::{PixelFormat, Rect};
#[cfg(feature = "cuda-runtime")]
use j2k_cuda_runtime::{CudaContext, CudaHtj2kCodeBlockJob, CudaHtj2kDecodeTables};
#[cfg(feature = "cuda-runtime")]
@@ -211,14 +211,17 @@ fn flat_htj2k_scaled_region_plan_stores_compact_scaled_rect() {
}
#[test]
-fn flat_htj2k_plan_rejects_classic_j2k_subband_steps() {
+fn flat_cuda_plan_preserves_classic_j2k_subband_steps() {
let bytes = classic_gray8_fixture();
let mut decoder = J2kDecoder::new(&bytes).expect("decoder");
- let error = decoder
+ let (plan, report) = decoder
.build_cuda_htj2k_grayscale_plan_with_profile(PixelFormat::Gray8)
- .expect_err("classic plans must be rejected");
+ .expect("classic CUDA plan");
- assert!(error.is_unsupported());
+ assert!(!plan.payload().is_empty());
+ assert!(!plan.classic_code_blocks().is_empty());
+ assert!(!plan.classic_segments().is_empty());
+ assert_eq!(report.block_count, plan.classic_code_blocks().len());
}
#[test]
diff --git a/crates/j2k-cuda/src/profile.rs b/crates/j2k-cuda/src/profile.rs
index a191a6b8..6d94a033 100644
--- a/crates/j2k-cuda/src/profile.rs
+++ b/crates/j2k-cuda/src/profile.rs
@@ -7,7 +7,7 @@ use crate::SurfaceResidency;
mod emit;
mod trace;
-/// Detailed route-overhead timings for strict CUDA HTJ2K decode.
+/// Detailed route-overhead timings for strict CUDA JPEG 2000 decode.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
#[doc(hidden)]
@@ -24,14 +24,16 @@ pub struct CudaHtj2kDecodeProfileDetail {
/// payload bytes plus decode metadata. Metadata-only job upload is not
/// split out until the CUDA runtime exposes separate timings.
pub payload_upload_us: u128,
- /// CUDA decode job upload time, reserved as zero until split runtime timings exist.
+ /// CUDA decode job and segment metadata upload time.
pub job_upload_us: u128,
- /// CUDA status download time, reserved as zero until split runtime timings exist.
+ /// CUDA status download time.
pub status_d2h_us: u128,
/// CUDA output download time, reserved as zero until split runtime timings exist.
pub output_d2h_us: u128,
/// HT cleanup/refinement CUDA dispatch count.
pub ht_dispatch_count: usize,
+ /// Classic Tier-1 CUDA dispatch count.
+ pub classic_dispatch_count: usize,
/// Dequantization CUDA dispatch count.
pub dequant_dispatch_count: usize,
/// Inverse DWT CUDA dispatch count.
@@ -50,7 +52,7 @@ impl CudaHtj2kDecodeProfileDetail {
}
}
-/// Structured stage timings for a strict CUDA HTJ2K operation.
+/// Structured stage timings for a strict CUDA JPEG 2000 or HTJ2K operation.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
#[doc(hidden)]
@@ -67,6 +69,8 @@ pub struct CudaHtj2kProfileReport {
pub ht_cleanup_us: u128,
/// HT refinement kernel time.
pub ht_refine_us: u128,
+ /// Classic Tier-1 kernel time.
+ pub classic_tier1_us: u128,
/// Dequantization kernel time.
pub dequant_us: u128,
/// Inverse DWT kernel time.
@@ -79,8 +83,12 @@ pub struct CudaHtj2kProfileReport {
///
/// End-to-end wall time is reported in `detail.wall_total_us`.
pub total_us: u128,
- /// Number of HTJ2K code blocks in the flat plan.
+ /// Total number of entropy code blocks in the flat plan.
pub block_count: usize,
+ /// Number of classic Tier-1 code blocks in the flat plan.
+ pub classic_block_count: usize,
+ /// Number of HTJ2K code blocks in the flat plan.
+ pub ht_block_count: usize,
/// Number of compressed payload bytes uploaded to CUDA.
pub payload_bytes: usize,
/// Number of CUDA kernel dispatches.
diff --git a/crates/j2k-cuda/src/profile/emit.rs b/crates/j2k-cuda/src/profile/emit.rs
index ea669d8e..c057bb8b 100644
--- a/crates/j2k-cuda/src/profile/emit.rs
+++ b/crates/j2k-cuda/src/profile/emit.rs
@@ -56,10 +56,12 @@ pub(crate) fn finalize_decode_total_us(report: &mut CudaHtj2kProfileReport) {
report.h2d_us,
report.ht_cleanup_us,
report.ht_refine_us,
+ report.classic_tier1_us,
report.dequant_us,
report.idwt_us,
report.mct_us,
report.store_us,
+ report.detail.status_d2h_us,
]
.into_iter()
.fold(0u128, u128::saturating_add);
@@ -76,12 +78,15 @@ pub(crate) fn emit_htj2k_profile_row(path: &str, report: &CudaHtj2kProfileReport
ProfileField::metric("h2d_us", report.h2d_us)?,
ProfileField::metric("ht_cleanup_us", report.ht_cleanup_us)?,
ProfileField::metric("ht_refine_us", report.ht_refine_us)?,
+ ProfileField::metric("classic_tier1_us", report.classic_tier1_us)?,
ProfileField::metric("dequant_us", report.dequant_us)?,
ProfileField::metric("idwt_us", report.idwt_us)?,
ProfileField::metric("mct_us", report.mct_us)?,
ProfileField::metric("store_us", report.store_us)?,
ProfileField::metric("total_us", report.total_us)?,
ProfileField::metric("block_count", report.block_count)?,
+ ProfileField::metric("classic_block_count", report.classic_block_count)?,
+ ProfileField::metric("ht_block_count", report.ht_block_count)?,
ProfileField::metric("payload_bytes", report.payload_bytes)?,
ProfileField::metric("dispatch_count", report.dispatch_count)?,
ProfileField::label("residency", DebugValue(report.residency))?,
@@ -93,6 +98,10 @@ pub(crate) fn emit_htj2k_profile_row(path: &str, report: &CudaHtj2kProfileReport
ProfileField::metric("status_d2h_us", report.detail.status_d2h_us)?,
ProfileField::metric("output_d2h_us", report.detail.output_d2h_us)?,
ProfileField::metric("ht_dispatch_count", report.detail.ht_dispatch_count)?,
+ ProfileField::metric(
+ "classic_dispatch_count",
+ report.detail.classic_dispatch_count,
+ )?,
ProfileField::metric(
"dequant_dispatch_count",
report.detail.dequant_dispatch_count,
diff --git a/crates/j2k-cuda/src/profile/tests.rs b/crates/j2k-cuda/src/profile/tests.rs
index 8e42472c..37a8cdfc 100644
--- a/crates/j2k-cuda/src/profile/tests.rs
+++ b/crates/j2k-cuda/src/profile/tests.rs
@@ -18,11 +18,12 @@ use crate::SurfaceResidency;
fn finalize_decode_total_us_includes_cpu_and_cuda_stages() {
let mut report = decode_report();
report.total_us = 3;
+ report.detail.status_d2h_us = 11;
finalize_decode_total_us(&mut report);
- assert_eq!(report.total_us, 55);
- assert_eq!(report.detail.stage_sum_us, 55);
+ assert_eq!(report.total_us, 66);
+ assert_eq!(report.detail.stage_sum_us, 66);
}
#[test]
@@ -65,14 +66,17 @@ fn profile_field_build_failure_is_diagnostic_only() {
#[test]
fn decode_trace_json_contains_ordered_stage_spans() {
- let trace = chrome_trace_json("decode", &decode_report()).expect("bounded decode trace");
+ let mut report = decode_report();
+ report.detail.status_d2h_us = 11;
+ let trace = chrome_trace_json("decode", &report).expect("bounded decode trace");
assert!(trace.starts_with("{\"traceEvents\":["));
assert!(trace.contains("\"name\":\"parse\",\"cat\":\"decode\",\"ph\":\"X\""));
assert!(trace.contains("\"name\":\"ht_cleanup\",\"cat\":\"decode\",\"ph\":\"X\""));
+ assert!(trace.contains("\"name\":\"status_d2h\",\"cat\":\"decode\",\"ph\":\"X\""));
assert!(trace.contains("\"name\":\"store\",\"cat\":\"decode\",\"ph\":\"X\""));
assert!(trace.contains("\"ts\":0,\"dur\":1"));
- assert!(trace.contains("\"ts\":39,\"dur\":10"));
+ assert!(trace.contains("\"ts\":50,\"dur\":10"));
assert!(trace.ends_with("]}"));
}
@@ -161,12 +165,15 @@ fn decode_report() -> CudaHtj2kProfileReport {
h2d_us: 4,
ht_cleanup_us: 5,
ht_refine_us: 6,
+ classic_tier1_us: 0,
dequant_us: 7,
idwt_us: 8,
mct_us: 9,
store_us: 10,
total_us: 55,
block_count: 1,
+ classic_block_count: 0,
+ ht_block_count: 1,
payload_bytes: 2,
dispatch_count: 3,
residency: SurfaceResidency::CudaResidentDecode,
diff --git a/crates/j2k-cuda/src/profile/trace.rs b/crates/j2k-cuda/src/profile/trace.rs
index b3c6bea1..da707061 100644
--- a/crates/j2k-cuda/src/profile/trace.rs
+++ b/crates/j2k-cuda/src/profile/trace.rs
@@ -72,8 +72,10 @@ pub(super) fn chrome_trace_json(
("plan", report.plan_us),
("flatten", report.flatten_us),
("h2d", report.h2d_us),
+ ("classic_tier1", report.classic_tier1_us),
("ht_cleanup", report.ht_cleanup_us),
("ht_refine", report.ht_refine_us),
+ ("status_d2h", report.detail.status_d2h_us),
("dequant", report.dequant_us),
("idwt", report.idwt_us),
("mct", report.mct_us),
diff --git a/crates/j2k-cuda/src/session.rs b/crates/j2k-cuda/src/session.rs
index 52792bbd..55aaa28f 100644
--- a/crates/j2k-cuda/src/session.rs
+++ b/crates/j2k-cuda/src/session.rs
@@ -45,6 +45,16 @@ pub struct CudaSession {
}
impl CudaSession {
+ /// Create a session bound to an existing CUDA context.
+ #[cfg(feature = "cuda-runtime")]
+ #[doc(hidden)]
+ pub fn with_context(context: CudaContext) -> Self {
+ Self {
+ context: Some(context),
+ ..Self::default()
+ }
+ }
+
/// Number of submissions recorded by this session.
pub fn submissions(&self) -> u64 {
self.submissions
diff --git a/crates/j2k-cuda/tests/bench_harness.rs b/crates/j2k-cuda/tests/bench_harness.rs
index f46c9799..b0480661 100644
--- a/crates/j2k-cuda/tests/bench_harness.rs
+++ b/crates/j2k-cuda/tests/bench_harness.rs
@@ -17,6 +17,8 @@ fn cuda_htj2k_decode_bench_exposes_gray_rgb_rgba_rows() {
"j2k_cuda_htj2k_roi_scaled_decode",
"j2k_cuda_htj2k_tile_batch_decode",
"j2k_cuda_htj2k_external_mixed_tile_batch_decode",
+ "j2k_cuda_classic_full_tile_decode",
+ "j2k_cuda_classic_tile_batch_decode",
"cpu_external_mixed_",
"cuda_external_mixed_",
"BATCH_SIZES",
@@ -24,6 +26,7 @@ fn cuda_htj2k_decode_bench_exposes_gray_rgb_rgba_rows() {
"J2K_CUDA_DECODE_BATCH_SIZES",
"J2K_CUDA_DECODE_CASE_BATCH_SIZES",
"J2K_CUDA_DECODE_SAMPLE_SIZE",
+ "J2K_CUDA_DECODE_MEASUREMENT_SECONDS",
"J2K_CUDA_DECODE_FORMATS",
"J2K_CUDA_DECODE_INPUT_DIRS",
"J2K_CUDA_DECODE_MANIFEST",
@@ -31,13 +34,17 @@ fn cuda_htj2k_decode_bench_exposes_gray_rgb_rgba_rows() {
"J2K_REQUIRE_CUDA_BENCH",
"j2k_cuda_decode_batch_sizes",
"j2k_cuda_decode_case_batch_sizes",
+ "j2k_cuda_decode_classic_batch_sizes",
"j2k_cuda_decode_mixed_batch_sizes",
"j2k_cuda_decode_sample_size",
+ "j2k_cuda_decode_measurement_seconds",
"j2k_cuda_decode_batch_policy",
"j2k_cuda_decode_mixed_large_batch_policy",
"j2k_cuda_decode_mixed_large_batch_tile_pixels",
"mixed_external_cases_for_batch",
"j2k_cuda_decode_io_policy",
+ "j2k_cuda_decode_session_policy",
+ "steady_state_route_warmed",
"j2k_cuda_decode_external_case_count",
"j2k_cuda_decode_external_fixture_count",
"j2k_cuda_decode_external_skipped_non_htj2k_count",
@@ -56,18 +63,73 @@ fn cuda_htj2k_decode_bench_exposes_gray_rgb_rgba_rows() {
}
}
+#[test]
+fn cuda_classic_decode_bench_obeys_generated_and_format_filters() {
+ let bench = include_str!("../benches/htj2k_decode.rs");
+ let main = extract_function_body(bench, "fn bench_htj2k_decode");
+ assert!(main.contains("let classic = classic_decode_case_if_enabled();"));
+ assert!(main.contains("if let Some(classic) = classic.as_ref()"));
+ assert!(main.contains("emit_input_metadata(&corpus, classic.as_ref())"));
+
+ let selection = extract_function_body(bench, "fn classic_decode_case_if_enabled");
+ assert!(selection.contains("include_generated_decode_cases()"));
+ assert!(selection.contains("enabled_decode_cases().contains(&\"rgb8\")"));
+
+ let metadata = extract_function_body(bench, "fn emit_input_metadata");
+ assert!(metadata.contains("j2k_cuda_decode_classic_batch_sizes"));
+ assert!(metadata.contains("corpus.cases.len() + usize::from(classic.is_some())"));
+ assert_eq!(
+ bench
+ .matches(".measurement_time(decode_measurement_time())")
+ .count(),
+ 2
+ );
+}
+
#[test]
fn cuda_htj2k_decode_bench_reuses_session_in_timed_cuda_rows() {
let bench = include_str!("../benches/htj2k_decode.rs");
- assert!(
+ assert_eq!(
bench
.matches("let mut session = CudaSession::default();")
- .count()
- >= 6,
- "CUDA decode benchmarks must create reusable sessions outside timed iterations"
+ .count(),
+ 2,
+ "only CUDA availability detection and the route-warmup helper may create sessions"
+ );
+
+ let warm_helper = extract_function_body(bench, "fn warm_cuda_session");
+ assert!(
+ warm_helper.contains("let mut session = CudaSession::default();")
+ && warm_helper.contains("warm(&mut session)")
+ && warm_helper.contains("session"),
+ "CUDA decode benchmark warmup helper must return the session it warmed"
);
+ for function in [
+ "fn bench_classic_full_tile",
+ "fn bench_classic_tile_batch",
+ "fn bench_full_tile",
+ "fn bench_roi",
+ "fn bench_scaled",
+ "fn bench_roi_scaled",
+ "fn bench_tile_batch",
+ "fn bench_mixed_external_tile_batch",
+ ] {
+ let body = extract_function_body(bench, function);
+ assert!(
+ !body.contains("CudaSession::default()"),
+ "{function} must capture a session created before Criterion's sample callback"
+ );
+ let warmup = body
+ .find("warm_cuda_session(")
+ .unwrap_or_else(|| panic!("{function} must warm its CUDA route"));
+ assert!(
+ body[warmup..].contains("b.iter("),
+ "{function} must warm CUDA before its timed iteration"
+ );
+ }
+
for forbidden in [
".decode_to_device(case.fmt, BackendRequest::Cuda)",
".decode_region_to_device(case.fmt, roi, BackendRequest::Cuda)",
@@ -87,10 +149,18 @@ fn cuda_htj2k_decode_bench_reuses_session_in_timed_cuda_rows() {
".submit_region_scaled_to_device(",
] {
assert!(
- bench.contains(expected),
- "CUDA decode benchmark is missing reusable-session path `{expected}`"
+ bench.matches(expected).count() >= 2,
+ "CUDA decode benchmark must use `{expected}` for warmup and measurement"
);
}
+
+ assert!(
+ bench
+ .matches("J2kDecoder::decode_batch_to_device_with_session(")
+ .count()
+ >= 6,
+ "CUDA batch rows must warm and measure through the same batch entrypoint"
+ );
}
#[test]
@@ -211,6 +281,8 @@ fn cuda_runtime_exposes_steady_state_async_decode_helpers() {
for expected in [
"pub fn synchronize(&self) -> Result<(), CudaError>",
"pub fn time_default_stream_named_us_if",
+ "pub struct CudaClassicDecodeStageTimings",
+ "decode_classic_codeblocks_multi_with_resources_and_pool_timed",
concat!(
"pub un",
"safe fn decode_htj2k_codeblocks_cleanup_multi_enqueue_with_resources_and_pool"
@@ -247,6 +319,7 @@ fn read_cuda_runtime_sources() -> String {
"memory/pinned_staging.rs",
"memory/pinned_staging/operations.rs",
"memory/pool.rs",
+ "classic_decode.rs",
"htj2k_decode.rs",
"htj2k_decode/api.rs",
"htj2k_decode/completion.rs",
diff --git a/crates/j2k-cuda/tests/classic_tier1_parity.rs b/crates/j2k-cuda/tests/classic_tier1_parity.rs
new file mode 100644
index 00000000..6d9ef42a
--- /dev/null
+++ b/crates/j2k-cuda/tests/classic_tier1_parity.rs
@@ -0,0 +1,381 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+#![cfg(feature = "cuda-runtime")]
+
+use j2k_cuda_runtime::{
+ CudaBufferPool, CudaClassicCodeBlockJob, CudaClassicDecodeTarget, CudaClassicSegment,
+ CudaContext,
+};
+use j2k_native::{
+ decode_j2k_code_block_scalar, encode_j2k_code_block_scalar_with_style, J2kCodeBlockDecodeJob,
+ J2kCodeBlockSegment, J2kCodeBlockStyle, J2kSubBandType,
+};
+use j2k_test_support::cuda_runtime_and_strict_oxide_gate;
+
+struct Tier1Case {
+ name: &'static str,
+ width: u32,
+ height: u32,
+ total_bitplanes: u8,
+ subband: J2kSubBandType,
+ style: J2kCodeBlockStyle,
+ seed: u32,
+}
+
+fn generated_coefficients(case: &Tier1Case) -> Vec {
+ if case.name == "normal_ll_1x1_31bit" {
+ return vec![i32::MAX];
+ }
+ let mut coefficients = Vec::with_capacity(case.width as usize * case.height as usize);
+ let mut state = case.seed ^ 0x9e37_79b9;
+ for index in 0..case.width * case.height {
+ state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
+ let value = i32::try_from((state >> 16) & 0x01ff).expect("masked coefficient") - 255;
+ coefficients.push(if (index + case.seed).is_multiple_of(11) {
+ 0
+ } else {
+ value
+ });
+ }
+ coefficients
+}
+
+fn style_flags(style: J2kCodeBlockStyle) -> u32 {
+ u32::from(style.reset_context_probabilities)
+ | (u32::from(style.termination_on_each_pass) << 1)
+ | (u32::from(style.vertically_causal_context) << 2)
+ | (u32::from(style.segmentation_symbols) << 3)
+ | (u32::from(style.selective_arithmetic_coding_bypass) << 4)
+}
+
+fn subband_tag(subband: J2kSubBandType) -> u32 {
+ match subband {
+ J2kSubBandType::LowLow => 0,
+ J2kSubBandType::HighLow => 1,
+ J2kSubBandType::LowHigh => 2,
+ J2kSubBandType::HighHigh => 3,
+ }
+}
+
+#[test]
+fn classic_tier1_cuda_matches_native_style_and_dimension_matrix() {
+ if !cuda_runtime_and_strict_oxide_gate(module_path!()) {
+ return;
+ }
+ let default_style = J2kCodeBlockStyle {
+ selective_arithmetic_coding_bypass: false,
+ reset_context_probabilities: false,
+ termination_on_each_pass: false,
+ vertically_causal_context: false,
+ segmentation_symbols: false,
+ };
+ let cases = [
+ Tier1Case {
+ name: "normal_ll_1x1_31bit",
+ width: 1,
+ height: 1,
+ total_bitplanes: 31,
+ subband: J2kSubBandType::LowLow,
+ style: default_style,
+ seed: 0x5100,
+ },
+ Tier1Case {
+ name: "bypass_lh",
+ width: 13,
+ height: 9,
+ total_bitplanes: 10,
+ subband: J2kSubBandType::LowHigh,
+ style: J2kCodeBlockStyle {
+ selective_arithmetic_coding_bypass: true,
+ ..default_style
+ },
+ seed: 0x5200,
+ },
+ Tier1Case {
+ name: "term_reset_hl",
+ width: 13,
+ height: 9,
+ total_bitplanes: 10,
+ subband: J2kSubBandType::HighLow,
+ style: J2kCodeBlockStyle {
+ reset_context_probabilities: true,
+ termination_on_each_pass: true,
+ ..default_style
+ },
+ seed: 0x5300,
+ },
+ Tier1Case {
+ name: "segmentation_hh",
+ width: 13,
+ height: 9,
+ total_bitplanes: 10,
+ subband: J2kSubBandType::HighHigh,
+ style: J2kCodeBlockStyle {
+ segmentation_symbols: true,
+ ..default_style
+ },
+ seed: 0x5400,
+ },
+ Tier1Case {
+ name: "vcausal_ll",
+ width: 13,
+ height: 9,
+ total_bitplanes: 10,
+ subband: J2kSubBandType::LowLow,
+ style: J2kCodeBlockStyle {
+ vertically_causal_context: true,
+ ..default_style
+ },
+ seed: 0x5500,
+ },
+ Tier1Case {
+ name: "combined_64x64",
+ width: 64,
+ height: 64,
+ total_bitplanes: 10,
+ subband: J2kSubBandType::HighHigh,
+ style: J2kCodeBlockStyle {
+ selective_arithmetic_coding_bypass: true,
+ reset_context_probabilities: true,
+ termination_on_each_pass: true,
+ vertically_causal_context: true,
+ segmentation_symbols: true,
+ },
+ seed: 0x5600,
+ },
+ ];
+
+ let context = CudaContext::system_default().expect("CUDA context");
+ let pool = context.buffer_pool();
+ for case in cases {
+ run_case(&context, &pool, &case);
+ }
+}
+
+fn run_case(context: &CudaContext, pool: &CudaBufferPool, case: &Tier1Case) {
+ let coefficients = generated_coefficients(case);
+ let encoded = encode_j2k_code_block_scalar_with_style(
+ &coefficients,
+ case.width,
+ case.height,
+ case.subband,
+ case.total_bitplanes,
+ case.style,
+ )
+ .unwrap_or_else(|error| panic!("{} encode: {error}", case.name));
+ if case.name == "normal_ll_1x1_31bit" {
+ assert_eq!(encoded.missing_bit_planes, 0);
+ assert_eq!(encoded.number_of_coding_passes, 91);
+ }
+ let expected = native_decode(case, &encoded, &encoded.data, &encoded.segments, true)
+ .unwrap_or_else(|error| panic!("{} native decode: {error}", case.name));
+ let job = cuda_job(case, &encoded, encoded.data.len(), true);
+ let segments = cuda_segments(&encoded.segments);
+ let actual = cuda_decode(
+ context,
+ pool,
+ &encoded.data,
+ job,
+ &segments,
+ coefficients.len(),
+ )
+ .unwrap_or_else(|error| panic!("{} CUDA decode: {error}", case.name));
+ assert_eq!(actual, expected, "{} coefficient parity", case.name);
+
+ if case.name == "normal_ll_1x1_31bit" {
+ check_empty_mq(context, pool, case, &encoded, coefficients.len());
+ }
+ if case.name == "bypass_lh" {
+ check_truncated_bypass(context, pool, case, &encoded, coefficients.len());
+ }
+}
+
+fn native_decode(
+ case: &Tier1Case,
+ encoded: &j2k_native::EncodedJ2kCodeBlock,
+ data: &[u8],
+ segments: &[J2kCodeBlockSegment],
+ strict: bool,
+) -> Result, String> {
+ let mut output = vec![0.0; case.width as usize * case.height as usize];
+ decode_j2k_code_block_scalar(
+ J2kCodeBlockDecodeJob {
+ data,
+ segments,
+ width: case.width,
+ height: case.height,
+ output_stride: case.width as usize,
+ missing_bit_planes: encoded.missing_bit_planes,
+ number_of_coding_passes: encoded.number_of_coding_passes,
+ total_bitplanes: case.total_bitplanes,
+ roi_shift: 0,
+ sub_band_type: case.subband,
+ style: case.style,
+ strict,
+ dequantization_step: 1.0,
+ },
+ &mut output,
+ )
+ .map_err(|error| error.to_string())?;
+ Ok(output)
+}
+
+fn cuda_job(
+ case: &Tier1Case,
+ encoded: &j2k_native::EncodedJ2kCodeBlock,
+ payload_len: usize,
+ strict: bool,
+) -> CudaClassicCodeBlockJob {
+ CudaClassicCodeBlockJob {
+ payload_offset: 0,
+ payload_len: u32::try_from(payload_len).expect("payload length"),
+ segment_start: 0,
+ segment_count: u32::try_from(encoded.segments.len()).expect("segment count"),
+ width: case.width,
+ height: case.height,
+ output_stride: case.width,
+ output_offset: 0,
+ missing_bitplanes: u32::from(encoded.missing_bit_planes),
+ total_bitplanes: u32::from(case.total_bitplanes),
+ number_of_coding_passes: u32::from(encoded.number_of_coding_passes),
+ sub_band_type: subband_tag(case.subband),
+ style_flags: style_flags(case.style),
+ strict,
+ dequantization_step: 1.0,
+ }
+}
+
+fn cuda_segments(segments: &[J2kCodeBlockSegment]) -> Vec {
+ segments
+ .iter()
+ .map(|segment| CudaClassicSegment {
+ data_offset: segment.data_offset,
+ data_length: segment.data_length,
+ start_coding_pass: u32::from(segment.start_coding_pass),
+ end_coding_pass: u32::from(segment.end_coding_pass),
+ use_arithmetic: segment.use_arithmetic,
+ })
+ .collect()
+}
+
+fn cuda_decode(
+ context: &CudaContext,
+ pool: &CudaBufferPool,
+ payload: &[u8],
+ job: CudaClassicCodeBlockJob,
+ segments: &[CudaClassicSegment],
+ output_words: usize,
+) -> Result, String> {
+ let resources = context
+ .upload_j2k_decode_payload(payload)
+ .map_err(|error| error.to_string())?;
+ let output = context
+ .allocate_classic_coefficients_with_pool(output_words, pool)
+ .map_err(|error| error.to_string())?;
+ context
+ .decode_classic_codeblocks_multi_with_resources_and_pool(
+ &resources,
+ &[CudaClassicDecodeTarget {
+ coefficients: output
+ .as_device_buffer()
+ .ok_or_else(|| "classic output is not device-resident".to_string())?,
+ jobs: &[job],
+ segments,
+ output_words,
+ }],
+ pool,
+ 0,
+ )
+ .map_err(|error| error.to_string())?;
+ let mut bytes = vec![0; output.byte_len()];
+ output
+ .copy_to_host(&mut bytes)
+ .map_err(|error| error.to_string())?;
+ Ok(bytes
+ .chunks_exact(4)
+ .map(|word| f32::from_ne_bytes(word.try_into().expect("f32 word")))
+ .collect())
+}
+
+fn check_empty_mq(
+ context: &CudaContext,
+ pool: &CudaBufferPool,
+ case: &Tier1Case,
+ encoded: &j2k_native::EncodedJ2kCodeBlock,
+ output_words: usize,
+) {
+ let segments = encoded
+ .segments
+ .iter()
+ .copied()
+ .map(|mut segment| {
+ segment.data_offset = 0;
+ segment.data_length = 0;
+ segment
+ })
+ .collect::>();
+ for strict in [false, true] {
+ let native = native_decode(case, encoded, &[], &segments, strict);
+ let cuda = cuda_decode(
+ context,
+ pool,
+ &[],
+ cuda_job(case, encoded, 0, strict),
+ &cuda_segments(&segments),
+ output_words,
+ );
+ match (native, cuda) {
+ (Ok(expected), Ok(actual)) => assert_eq!(actual, expected, "empty MQ strict={strict}"),
+ (Err(_), Err(_)) => {}
+ (native, cuda) => {
+ panic!("empty MQ strict={strict} result mismatch: native={native:?} cuda={cuda:?}")
+ }
+ }
+ }
+}
+
+fn check_truncated_bypass(
+ context: &CudaContext,
+ pool: &CudaBufferPool,
+ case: &Tier1Case,
+ encoded: &j2k_native::EncodedJ2kCodeBlock,
+ output_words: usize,
+) {
+ let first_raw = encoded
+ .segments
+ .iter()
+ .position(|segment| !segment.use_arithmetic)
+ .expect("bypass fixture raw segment");
+ let truncated_len = encoded.segments[first_raw].data_offset as usize;
+ let data = &encoded.data[..truncated_len];
+ let mut segments = encoded.segments.clone();
+ for segment in &mut segments[first_raw..] {
+ segment.data_offset = u32::try_from(truncated_len).expect("truncated offset");
+ segment.data_length = 0;
+ }
+ let expected = native_decode(case, encoded, data, &segments, false)
+ .expect("native lenient truncated bypass decode");
+ assert!(native_decode(case, encoded, data, &segments, true).is_err());
+ let actual = cuda_decode(
+ context,
+ pool,
+ data,
+ cuda_job(case, encoded, truncated_len, false),
+ &cuda_segments(&segments),
+ output_words,
+ )
+ .expect("CUDA lenient truncated bypass decode");
+ assert_eq!(actual, expected, "lenient truncated parity");
+ assert!(
+ cuda_decode(
+ context,
+ pool,
+ data,
+ cuda_job(case, encoded, truncated_len, true),
+ &cuda_segments(&segments),
+ output_words,
+ )
+ .is_err(),
+ "CUDA strict truncated bypass decode must fail"
+ );
+}
diff --git a/crates/j2k-cuda/tests/host_surface.rs b/crates/j2k-cuda/tests/host_surface.rs
index e0a2cd96..177d2777 100644
--- a/crates/j2k-cuda/tests/host_surface.rs
+++ b/crates/j2k-cuda/tests/host_surface.rs
@@ -9,7 +9,7 @@ use j2k_test_support::{
cuda_device_unavailable_is_skip, cuda_runtime_and_strict_oxide_gate, cuda_runtime_gate,
htj2k_gray8_97_fixture, htj2k_gray8_fixture, htj2k_rgb8_97_fixture,
htj2k_rgb8_fixture_with_pixels, htj2k_rgb8_pattern_fixture, openhtj2k_refinement_odd_fixture,
- rgb16ne_to_opaque_rgba16ne,
+ rgb16ne_to_opaque_rgba16ne, wrap_jp2_codestream,
};
fn fixture() -> Vec {
@@ -22,6 +22,20 @@ fn fixture() -> Vec {
encode(&pixels, 2, 2, 3, 8, false, &options).expect("encode")
}
+fn fixture_classic(width: u32, height: u32, components: u16, reversible: bool) -> Vec {
+ let components_u32 = u32::from(components);
+ let pixels = (0..width * height * components_u32)
+ .map(|index| ((index * 29 + index / components_u32) & 0xff) as u8)
+ .collect::>();
+ let options = EncodeOptions {
+ reversible,
+ num_decomposition_levels: 2,
+ ..EncodeOptions::default()
+ };
+ encode(&pixels, width, height, components, 8, false, &options)
+ .expect("encode classic matrix fixture")
+}
+
fn fixture_ht_gray8() -> Vec {
htj2k_gray8_fixture(4, 4)
}
@@ -171,28 +185,118 @@ fn auto_falls_back_to_cpu_surface() {
}
#[test]
-fn explicit_cuda_classic_j2k_request_rejects_cpu_staged_upload() {
+fn explicit_cuda_classic_j2k_request_matches_native() {
+ if !cuda_runtime_and_strict_oxide_gate(module_path!()) {
+ return;
+ }
let bytes = fixture();
let mut decoder = J2kDecoder::new(&bytes).expect("decoder");
-
- let error = decoder
+ let surface = decoder
.decode_to_device(PixelFormat::Rgb8, BackendRequest::Cuda)
- .expect_err("classic J2K must not be CPU-decoded and uploaded");
+ .expect("strict CUDA classic J2K surface");
+ assert_eq!(surface.backend_kind(), j2k_core::BackendKind::Cuda);
+ assert_eq!(surface.residency(), SurfaceResidency::CudaResidentDecode);
+ assert_eq!(surface.as_host_bytes(), None);
+
+ let mut downloaded = vec![0u8; surface.byte_len()];
+ surface
+ .download_into(&mut downloaded, surface.pitch_bytes())
+ .expect("download CUDA classic surface");
+ let mut expected = vec![0u8; downloaded.len()];
+ let mut host_decoder = J2kDecoder::new(&bytes).expect("host decoder");
+ host_decoder
+ .decode_into(&mut expected, 2 * 3, PixelFormat::Rgb8)
+ .expect("host classic decode");
- assert!(error.is_unsupported());
+ assert_eq!(downloaded, expected);
+}
+
+#[test]
+fn explicit_cuda_classic_image_contract_matches_native() {
+ if !cuda_runtime_and_strict_oxide_gate(module_path!()) {
+ return;
+ }
+ let dimensions = (8, 8);
+ let roi = Rect {
+ x: 1,
+ y: 2,
+ w: 6,
+ h: 5,
+ };
+ let cases = [
+ StrictDecodeCase::Full,
+ StrictDecodeCase::Region(roi),
+ StrictDecodeCase::Scaled(Downscale::Half),
+ StrictDecodeCase::RegionScaled(roi, Downscale::Half),
+ ];
+
+ for reversible in [true, false] {
+ let gray_raw = fixture_classic(dimensions.0, dimensions.1, 1, reversible);
+ let gray_jp2 = wrap_jp2_codestream(&gray_raw, dimensions.0, dimensions.1, 1, 8, 17);
+ let color_raw = fixture_classic(dimensions.0, dimensions.1, 3, reversible);
+ let color_jp2 = wrap_jp2_codestream(&color_raw, dimensions.0, dimensions.1, 3, 8, 16);
+ let tolerance = if reversible { 0 } else { 2 };
+
+ for bytes in [&gray_raw, &gray_jp2] {
+ for format in [PixelFormat::Gray8, PixelFormat::Gray16] {
+ for case in cases {
+ let surface = decode_strict_cuda_case(bytes, format, case)
+ .expect("strict CUDA classic grayscale surface");
+ assert_eq!(surface.residency(), SurfaceResidency::CudaResidentDecode);
+ let mut actual = vec![0; surface.byte_len()];
+ surface
+ .download_into(&mut actual, surface.pitch_bytes())
+ .expect("download classic grayscale surface");
+ let expected = expected_host_decode_case(bytes, format, case, dimensions);
+ assert_bytes_within(
+ &actual,
+ &expected,
+ tolerance,
+ &format!("classic reversible={reversible} grayscale {format:?} {case:?}"),
+ );
+ }
+ }
+ }
+
+ for bytes in [&color_raw, &color_jp2] {
+ for format in [
+ PixelFormat::Rgb8,
+ PixelFormat::Rgba8,
+ PixelFormat::Rgb16,
+ PixelFormat::Rgba16,
+ ] {
+ for case in cases {
+ let surface = decode_strict_cuda_case(bytes, format, case)
+ .expect("strict CUDA classic color surface");
+ assert_eq!(surface.residency(), SurfaceResidency::CudaResidentDecode);
+ let mut actual = vec![0; surface.byte_len()];
+ surface
+ .download_into(&mut actual, surface.pitch_bytes())
+ .expect("download classic color surface");
+ let expected = expected_host_decode_case(bytes, format, case, dimensions);
+ assert_bytes_within(
+ &actual,
+ &expected,
+ tolerance,
+ &format!("classic reversible={reversible} color {format:?} {case:?}"),
+ );
+ }
+ }
+ }
+ }
}
#[cfg(feature = "cuda-runtime")]
#[test]
-fn explicit_cuda_request_validates_decode_before_upload() {
+fn explicit_cuda_classic_rgba16_reaches_runtime_boundary() {
let bytes = fixture();
let mut decoder = J2kDecoder::new(&bytes).expect("decoder");
- let error = decoder
- .decode_to_device(PixelFormat::Rgba16, BackendRequest::Cuda)
- .expect_err("unsupported decode");
- assert!(error.is_unsupported());
- assert!(!matches!(error, Error::CudaUnavailable));
+ match decoder.decode_to_device(PixelFormat::Rgba16, BackendRequest::Cuda) {
+ Ok(surface) => assert_eq!(surface.residency(), SurfaceResidency::CudaResidentDecode),
+ Err(Error::CudaUnavailable | Error::CudaRuntime { .. }) => {}
+ Err(error) => panic!("classic Rgba16 must reach the CUDA runtime boundary: {error}"),
+ }
}
#[test]
@@ -249,6 +353,29 @@ fn explicit_cuda_profile_reports_gpu_stage_timings_when_cuda_runtime_required()
assert_eq!(report.residency, SurfaceResidency::CudaResidentDecode);
}
+#[test]
+fn explicit_cuda_classic_profile_splits_entropy_route_timings_when_cuda_runtime_required() {
+ if !cuda_runtime_and_strict_oxide_gate(module_path!()) {
+ return;
+ }
+
+ let bytes = fixture_classic(64, 64, 1, true);
+ let mut decoder = J2kDecoder::new(&bytes).expect("decoder");
+ let mut session = CudaSession::default();
+ let (surface, report) = decoder
+ .decode_to_device_with_session_and_profile(PixelFormat::Gray8, &mut session)
+ .expect("strict CUDA classic profiled surface");
+
+ assert_eq!(surface.residency(), SurfaceResidency::CudaResidentDecode);
+ assert_resident_cuda_surface(&surface);
+ assert!(report.classic_tier1_us > 0);
+ assert_eq!(report.ht_cleanup_us, 0);
+ assert!(report.detail.job_upload_us > 0);
+ assert!(report.detail.table_upload_us > 0);
+ assert!(report.detail.status_d2h_us > 0);
+ assert!(report.h2d_us >= report.detail.job_upload_us + report.detail.table_upload_us);
+}
+
#[test]
fn explicit_cuda_region_surface_matches_host_when_cuda_runtime_required() {
if !cuda_runtime_and_strict_oxide_gate(module_path!()) {
@@ -1435,6 +1562,86 @@ fn decode_tiles_to_device_explicit_cuda_rgb8_batch_matches_host_bytes() {
assert_eq!(downloaded, expected);
}
+#[test]
+fn decode_tiles_to_device_explicit_cuda_mixed_classic_ht_batch_matches_host_bytes() {
+ if !cuda_runtime_and_strict_oxide_gate(module_path!()) {
+ return;
+ }
+ let classic = fixture_classic(32, 32, 3, true);
+ let ht = fixture_ht_rgb8_pattern(32, 32, 29);
+ let inputs = [classic.as_slice(), ht.as_slice()];
+ let mut ctx = DecoderContext::::new();
+ let mut pool = j2k_cuda::J2kScratchPool::new();
+
+ let surfaces = Codec::decode_tiles_to_device(
+ &mut ctx,
+ &mut pool,
+ &inputs,
+ PixelFormat::Rgb8,
+ BackendRequest::Cuda,
+ )
+ .expect("strict CUDA mixed classic/HT batch");
+ assert_eq!(surfaces.len(), inputs.len());
+ for surface in &surfaces {
+ assert_cuda_batch_surface(surface);
+ }
+
+ let actual = j2k_cuda::Surface::download_batch_tight(&surfaces).expect("download mixed batch");
+ let expected = inputs
+ .iter()
+ .flat_map(|input| {
+ let mut output = vec![0; 32 * 32 * 3];
+ J2kDecoder::new(input)
+ .expect("host decoder")
+ .decode_into(&mut output, 32 * 3, PixelFormat::Rgb8)
+ .expect("host mixed batch decode");
+ output
+ })
+ .collect::>();
+ assert_eq!(actual, expected);
+}
+
+#[test]
+fn decode_tiles_to_device_explicit_cuda_mixed_grayscale_batch_matches_host_bytes() {
+ if !cuda_runtime_and_strict_oxide_gate(module_path!()) {
+ return;
+ }
+ let classic = fixture_classic(8, 8, 1, true);
+ let ht = fixture_ht_gray8();
+ let inputs = [classic.as_slice(), ht.as_slice()];
+ let mut ctx = DecoderContext::::new();
+ let mut pool = j2k_cuda::J2kScratchPool::new();
+
+ let surfaces = Codec::decode_tiles_to_device(
+ &mut ctx,
+ &mut pool,
+ &inputs,
+ PixelFormat::Gray8,
+ BackendRequest::Cuda,
+ )
+ .expect("strict CUDA mixed grayscale batch");
+ assert_eq!(surfaces.len(), 2);
+ for surface in &surfaces {
+ assert_resident_cuda_surface(surface);
+ }
+
+ let actual =
+ j2k_cuda::Surface::download_batch_tight(&surfaces).expect("download mixed grayscale batch");
+ let expected = inputs
+ .iter()
+ .zip([(8usize, 8usize), (4, 4)])
+ .flat_map(|(input, (width, height))| {
+ let mut output = vec![0; width * height];
+ J2kDecoder::new(input)
+ .expect("host decoder")
+ .decode_into(&mut output, width, PixelFormat::Gray8)
+ .expect("host mixed grayscale decode");
+ output
+ })
+ .collect::>();
+ assert_eq!(actual, expected);
+}
+
#[test]
fn decode_tiles_to_device_explicit_cuda_rgba8_batch_matches_host_bytes() {
let first = fixture_ht_rgb8_pattern(32, 32, 31);
diff --git a/crates/j2k-jpeg-cuda/Cargo.toml b/crates/j2k-jpeg-cuda/Cargo.toml
index 049554f7..2a5e4434 100644
--- a/crates/j2k-jpeg-cuda/Cargo.toml
+++ b/crates/j2k-jpeg-cuda/Cargo.toml
@@ -27,10 +27,10 @@ cuda-runtime = [
]
[dependencies]
-j2k-core = { path = "../j2k-core", version = "=0.7.0" }
-j2k-cuda-runtime = { path = "../j2k-cuda-runtime", version = "=0.7.0", optional = true }
-j2k-jpeg = { path = "../j2k-jpeg", version = "=0.7.0" }
-j2k-profile = { path = "../j2k-profile", version = "=0.7.0" }
+j2k-core = { path = "../j2k-core", version = "=0.7.1" }
+j2k-cuda-runtime = { path = "../j2k-cuda-runtime", version = "=0.7.1", optional = true }
+j2k-jpeg = { path = "../j2k-jpeg", version = "=0.7.1" }
+j2k-profile = { path = "../j2k-profile", version = "=0.7.1" }
thiserror = { workspace = true }
[dev-dependencies]
diff --git a/crates/j2k-jpeg-metal/Cargo.toml b/crates/j2k-jpeg-metal/Cargo.toml
index 785b4635..23565d26 100644
--- a/crates/j2k-jpeg-metal/Cargo.toml
+++ b/crates/j2k-jpeg-metal/Cargo.toml
@@ -19,10 +19,10 @@ name = "j2k_jpeg_metal"
path = "src/lib.rs"
[dependencies]
-j2k-core = { path = "../j2k-core", version = "=0.7.0" }
-j2k-jpeg = { path = "../j2k-jpeg", version = "=0.7.0" }
-j2k-metal-support = { path = "../j2k-metal-support", version = "=0.7.0" }
-j2k-profile = { path = "../j2k-profile", version = "=0.7.0" }
+j2k-core = { path = "../j2k-core", version = "=0.7.1" }
+j2k-jpeg = { path = "../j2k-jpeg", version = "=0.7.1" }
+j2k-metal-support = { path = "../j2k-metal-support", version = "=0.7.1" }
+j2k-profile = { path = "../j2k-profile", version = "=0.7.1" }
thiserror = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
diff --git a/crates/j2k-jpeg-metal/src/compute.rs b/crates/j2k-jpeg-metal/src/compute.rs
index 63b25bf5..819d7af3 100644
--- a/crates/j2k-jpeg-metal/src/compute.rs
+++ b/crates/j2k-jpeg-metal/src/compute.rs
@@ -264,7 +264,7 @@ struct FastRgbDecodeBuffer {
#[cfg(target_os = "macos")]
fn private_jpeg_tile_from_fast_rgb_buffer(
decoded: FastRgbDecodeBuffer,
-) -> crate::ResidentPrivateJpegTile {
+) -> Result {
crate::ResidentPrivateJpegTile::new(
decoded.buffer,
0,
diff --git a/crates/j2k-jpeg-metal/src/compute/batch_support.rs b/crates/j2k-jpeg-metal/src/compute/batch_support.rs
index d512114d..e21066e3 100644
--- a/crates/j2k-jpeg-metal/src/compute/batch_support.rs
+++ b/crates/j2k-jpeg-metal/src/compute/batch_support.rs
@@ -422,11 +422,12 @@ pub(super) fn surface_batch_success_results(
let mut results = budget.try_vec(tile_count, "JPEG Metal surface batch result slots")?;
for index in 0..tile_count {
let offset = index * out_tile_len;
- results.push(Ok(if let Some(output) = output {
+ let surface = if let Some(output) = output {
Surface::from_batch_output_buffer_offset(output, dimensions, pixel_format, offset)
} else {
- Surface::from_metal_buffer_offset(out_buffer.clone(), dimensions, pixel_format, offset)
- }));
+ Surface::from_metal_buffer_offset(out_buffer.clone(), dimensions, pixel_format, offset)?
+ };
+ results.push(Ok(surface));
}
Ok(results)
}
diff --git a/crates/j2k-jpeg-metal/src/compute/pack_dispatch/subsampled.rs b/crates/j2k-jpeg-metal/src/compute/pack_dispatch/subsampled.rs
index 864324e9..114e92ad 100644
--- a/crates/j2k-jpeg-metal/src/compute/pack_dispatch/subsampled.rs
+++ b/crates/j2k-jpeg-metal/src/compute/pack_dispatch/subsampled.rs
@@ -132,7 +132,7 @@ pub(in crate::compute) fn encode_fast_subsampled_region_batch_item {
- return Ok(Surface::from_metal_buffer(plane0.clone(), dims, fmt));
+ return Surface::from_metal_buffer(plane0.clone(), dims, fmt);
}
(
PlaneMode::Gray | PlaneMode::YCbCr | PlaneMode::Rgb,
@@ -90,5 +90,5 @@ pub(in crate::compute) fn encode_jpeg_pack_to_surface_in_command_buffer(
dispatch_2d_pipeline(&encoder, &runtime.pack_pipeline, dims);
encoder.end_encoding();
- Ok(Surface::from_metal_buffer(out_buffer, dims, fmt))
+ Surface::from_metal_buffer(out_buffer, dims, fmt)
}
diff --git a/crates/j2k-jpeg-metal/src/compute/single_decode/routing.rs b/crates/j2k-jpeg-metal/src/compute/single_decode/routing.rs
index fad1c704..e4d48932 100644
--- a/crates/j2k-jpeg-metal/src/compute/single_decode/routing.rs
+++ b/crates/j2k-jpeg-metal/src/compute/single_decode/routing.rs
@@ -66,7 +66,7 @@ pub(crate) fn decode_private_rgb8_tile_with_session(
PixelFormat::Rgb8,
MTLResourceOptions::StorageModePrivate,
)? {
- return Ok(private_jpeg_tile_from_fast_rgb_buffer(decoded));
+ return private_jpeg_tile_from_fast_rgb_buffer(decoded);
}
if let Some(decoded) = decode_fast420_to_rgb_buffer(
runtime,
@@ -75,7 +75,7 @@ pub(crate) fn decode_private_rgb8_tile_with_session(
PixelFormat::Rgb8,
MTLResourceOptions::StorageModePrivate,
)? {
- return Ok(private_jpeg_tile_from_fast_rgb_buffer(decoded));
+ return private_jpeg_tile_from_fast_rgb_buffer(decoded);
}
Err(Error::UnsupportedMetalRequest {
reason:
diff --git a/crates/j2k-jpeg-metal/src/compute/single_decode/subsampled.rs b/crates/j2k-jpeg-metal/src/compute/single_decode/subsampled.rs
index e5ee473c..db9fdd72 100644
--- a/crates/j2k-jpeg-metal/src/compute/single_decode/subsampled.rs
+++ b/crates/j2k-jpeg-metal/src/compute/single_decode/subsampled.rs
@@ -60,7 +60,7 @@ fn try_decode_fast_subsampled_to_surface(
decoded.buffer,
decoded.dimensions,
fmt,
- )))
+ )?))
}
#[cfg(target_os = "macos")]
@@ -417,7 +417,7 @@ fn try_decode_fast_subsampled_scaled_region_to_surface(
out_buffer,
(scaled_roi.w, scaled_roi.h),
fmt,
- )))
+ )?))
}
#[cfg(target_os = "macos")]
diff --git a/crates/j2k-jpeg-metal/src/compute/viewport_cache.rs b/crates/j2k-jpeg-metal/src/compute/viewport_cache.rs
index cb9809a9..8236d9bb 100644
--- a/crates/j2k-jpeg-metal/src/compute/viewport_cache.rs
+++ b/crates/j2k-jpeg-metal/src/compute/viewport_cache.rs
@@ -210,9 +210,9 @@ impl PlaneStage {
) -> Result {
let cache_owned = self.cache_lease.is_some();
match (self.mode, fmt) {
- (PlaneMode::Gray | PlaneMode::YCbCr, PixelFormat::Gray8) if !cache_owned => Ok(
- surface_from_plane_buffer(self.plane0, self.dims, fmt, residency),
- ),
+ (PlaneMode::Gray | PlaneMode::YCbCr, PixelFormat::Gray8) if !cache_owned => {
+ surface_from_plane_buffer(self.plane0, self.dims, fmt, residency)
+ }
(
PlaneMode::Gray | PlaneMode::YCbCr | PlaneMode::Rgb,
PixelFormat::Gray8 | PixelFormat::Rgb8 | PixelFormat::Rgba8,
@@ -270,9 +270,7 @@ impl PlaneStage {
encoder.end_encoding();
commit_and_wait_jpeg(&command_buffer)?;
- Ok(surface_from_plane_buffer(
- out_buffer, self.dims, fmt, residency,
- ))
+ surface_from_plane_buffer(out_buffer, self.dims, fmt, residency)
}
#[cfg(test)]
@@ -460,7 +458,7 @@ impl PlaneStage {
commit_and_wait_jpeg(&command_buffer)?;
let command_buffer = command_buffer.clone();
- Ok(crate::ResidentPrivateJpegTile::new(
+ crate::ResidentPrivateJpegTile::new(
out_buffer,
0,
self.dims,
@@ -468,7 +466,7 @@ impl PlaneStage {
pitch_bytes,
status_buffer,
command_buffer,
- ))
+ )
}
}
@@ -478,7 +476,7 @@ fn surface_from_plane_buffer(
dims: (u32, u32),
fmt: PixelFormat,
residency: PlaneStageResidency,
-) -> Surface {
+) -> Result {
match residency {
PlaneStageResidency::CpuStagedMetalUpload => {
Surface::from_cpu_staged_metal_buffer(buffer, dims, fmt)
diff --git a/crates/j2k-jpeg-metal/src/encode.rs b/crates/j2k-jpeg-metal/src/encode.rs
index dd31a9fe..11e0992a 100644
--- a/crates/j2k-jpeg-metal/src/encode.rs
+++ b/crates/j2k-jpeg-metal/src/encode.rs
@@ -12,6 +12,8 @@ use metal::{Buffer, BufferRef};
mod adapter;
#[cfg(any(target_os = "macos", test))]
pub(crate) mod allocation;
+#[cfg(target_os = "macos")]
+mod resident_tile;
#[cfg(target_os = "macos")]
#[derive(Debug, Clone, Copy)]
@@ -98,6 +100,22 @@ impl<'a> JpegBaselineMetalEncodeTile<'a> {
pub(crate) fn buffer_trusted(&self) -> &'a Buffer {
self.buffer
}
+
+ fn validate_device(&self, device: &metal::DeviceRef) -> Result<(), crate::Error> {
+ let image_registry_id = self.buffer.device().registry_id();
+ let requested_registry_id = device.registry_id();
+ if image_registry_id == requested_registry_id {
+ Ok(())
+ } else {
+ Err(crate::error::metal_kernel_support_error(
+ "JPEG input belongs to a different Metal device",
+ j2k_metal_support::MetalSupportError::MetalImageDeviceMismatch {
+ image_registry_id,
+ requested_registry_id,
+ },
+ ))
+ }
+ }
}
#[cfg(not(target_os = "macos"))]
@@ -114,6 +132,7 @@ pub fn encode_jpeg_baseline_from_metal_buffer(
options: JpegEncodeOptions,
session: &crate::MetalBackendSession,
) -> Result {
+ tile.validate_device(session.device())?;
let mut adapter = MetalJpegBaselineEncodeAdapter { session };
encode_jpeg_baseline_gpu_tile(tile, options, &mut adapter)
}
@@ -129,6 +148,9 @@ pub fn encode_jpeg_baseline_batch_from_metal_buffers(
options: JpegEncodeOptions,
session: &crate::MetalBackendSession,
) -> Result, crate::Error> {
+ for tile in tiles {
+ tile.validate_device(session.device())?;
+ }
let mut adapter = MetalJpegBaselineEncodeAdapter { session };
encode_jpeg_baseline_gpu_batch(tiles, options, &mut adapter)
}
diff --git a/crates/j2k-jpeg-metal/src/encode/resident_tile.rs b/crates/j2k-jpeg-metal/src/encode/resident_tile.rs
new file mode 100644
index 00000000..9fb1a7bb
--- /dev/null
+++ b/crates/j2k-jpeg-metal/src/encode/resident_tile.rs
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use j2k_metal_support::ResidentMetalImage;
+
+use super::JpegBaselineMetalEncodeTile;
+
+impl<'a> JpegBaselineMetalEncodeTile<'a> {
+ /// Describe a validated resident image for baseline JPEG encoding.
+ ///
+ /// The image allocation remains owned and logically immutable for the
+ /// lifetime of this tile. The encode operation validates device identity
+ /// before submitting any Metal work.
+ #[must_use]
+ pub fn from_resident(image: &'a ResidentMetalImage, output_dimensions: (u32, u32)) -> Self {
+ let layout = image.layout();
+ Self {
+ // SAFETY: the resident abstraction exposes the handle only for
+ // audited read-only backend binding, and this tile retains the
+ // resident owner for its entire lifetime.
+ buffer: unsafe { image.raw_buffer() },
+ byte_offset: layout.byte_offset(),
+ width: layout.dimensions().0,
+ height: layout.dimensions().1,
+ pitch_bytes: layout.pitch_bytes(),
+ output_width: output_dimensions.0,
+ output_height: output_dimensions.1,
+ format: layout.pixel_format(),
+ }
+ }
+}
diff --git a/crates/j2k-jpeg-metal/src/lib.rs b/crates/j2k-jpeg-metal/src/lib.rs
index a88c1ce5..d951c960 100644
--- a/crates/j2k-jpeg-metal/src/lib.rs
+++ b/crates/j2k-jpeg-metal/src/lib.rs
@@ -870,18 +870,7 @@ pub(crate) fn upload_surface(
{
let device = Device::system_default().ok_or(Error::MetalUnavailable)?;
let buffer = buffers::new_shared_buffer_with_data(&device, &bytes)?;
- Ok(Surface {
- backend: BackendKind::Metal,
- residency: SurfaceResidency::CpuStagedMetalUpload,
- dimensions,
- fmt,
- pitch_bytes,
- storage: Storage::Metal {
- buffer,
- offset: 0,
- access_gate: None,
- },
- })
+ Surface::from_cpu_staged_metal_buffer(buffer, dimensions, fmt)
}
#[cfg(not(target_os = "macos"))]
{
diff --git a/crates/j2k-jpeg-metal/src/surface.rs b/crates/j2k-jpeg-metal/src/surface.rs
index e1290ce5..be0c7029 100644
--- a/crates/j2k-jpeg-metal/src/surface.rs
+++ b/crates/j2k-jpeg-metal/src/surface.rs
@@ -3,40 +3,50 @@
use std::borrow::Cow;
use std::sync::Arc;
#[cfg(target_os = "macos")]
-use std::sync::{Mutex, MutexGuard};
+use std::sync::Mutex;
use j2k_core::{
copy_tight_pixels_to_strided_output, BackendKind, DeviceMemoryRange, DeviceSurface,
PixelFormat, SurfaceMetadata, SurfaceResidency,
};
#[cfg(target_os = "macos")]
-use j2k_core::{BufferError, Downscale, Rect};
+use j2k_metal_support::{MetalImageLayout, ResidentMetalImage};
#[cfg(target_os = "macos")]
-use crate::buffers::{checked_buffer_slice_at, new_shared_buffer};
+use crate::buffers::checked_buffer_slice_at;
#[cfg(target_os = "macos")]
use crate::error::metal_kernel_support_error;
use crate::Error;
-#[cfg(target_os = "macos")]
-use crate::{
- report_required_output_dimensions, scaled_dims, JpegMetalResidentBatchReport,
- MetalBackendSession,
-};
#[cfg(target_os = "macos")]
use metal::foreign_types::ForeignType;
#[cfg(target_os = "macos")]
-use metal::{
- Buffer, BufferRef, CommandBuffer, MTLPixelFormat, MTLStorageMode, MTLTextureType,
- MTLTextureUsage, Texture, TextureRef,
-};
+use metal::Buffer;
+
+#[cfg(target_os = "macos")]
+mod batch_buffer;
+#[cfg(target_os = "macos")]
+mod batch_texture;
+#[cfg(target_os = "macos")]
+mod resident_tile;
+#[cfg(target_os = "macos")]
+mod texture_tile;
+#[cfg(target_os = "macos")]
+pub use batch_buffer::MetalBatchOutputBuffer;
+#[cfg(target_os = "macos")]
+pub use batch_texture::MetalBatchTextureOutput;
+#[cfg(target_os = "macos")]
+pub use resident_tile::ResidentPrivateJpegTile;
+#[cfg(target_os = "macos")]
+pub use texture_tile::MetalTextureTile;
#[derive(Clone)]
pub(crate) enum Storage {
Host(Arc>),
#[cfg(target_os = "macos")]
Metal {
- buffer: Buffer,
+ resident: Option,
+ reusable_buffer: Option,
offset: usize,
access_gate: Option>>,
},
@@ -104,7 +114,8 @@ impl Surface {
Storage::Host(bytes) => Ok(Cow::Borrowed(bytes.as_slice())),
#[cfg(target_os = "macos")]
Storage::Metal {
- buffer,
+ resident,
+ reusable_buffer,
offset,
access_gate,
} => {
@@ -115,6 +126,18 @@ impl Surface {
None => None,
};
let len = self.byte_len();
+ let buffer = match (resident, reusable_buffer) {
+ (Some(image), None) => {
+ // SAFETY: completed resident surfaces are read only.
+ unsafe { image.raw_buffer() }
+ }
+ (None, Some(buffer)) => buffer,
+ _ => {
+ return Err(Error::MetalKernel {
+ message: "JPEG Metal surface storage invariant failed".to_string(),
+ })
+ }
+ };
checked_buffer_slice_at::(buffer, *offset, len, "surface bytes").map(Cow::Owned)
}
}
@@ -145,8 +168,50 @@ impl Surface {
#[cfg(target_os = "macos")]
pub(crate) fn metal_buffer_trusted(&self) -> Option<(&Buffer, usize)> {
match &self.storage {
- Storage::Metal { buffer, offset, .. } => Some((buffer, *offset)),
- Storage::Host(_) => None,
+ Storage::Metal {
+ resident: Some(image),
+ reusable_buffer: None,
+ offset,
+ ..
+ } => {
+ // SAFETY: backend code binds this private handle under the
+ // immutable resident-image contract.
+ Some((unsafe { image.raw_buffer() }, *offset))
+ }
+ Storage::Metal {
+ resident: None,
+ reusable_buffer: Some(buffer),
+ offset,
+ ..
+ } => Some((buffer, *offset)),
+ Storage::Metal { .. } | Storage::Host(_) => None,
+ }
+ }
+
+ #[cfg(target_os = "macos")]
+ /// Borrow the immutable resident image for a completed, non-reusable surface.
+ pub fn resident_metal_image(&self) -> Option<&ResidentMetalImage> {
+ match &self.storage {
+ Storage::Metal {
+ resident: Some(image),
+ ..
+ } => Some(image),
+ Storage::Host(_) | Storage::Metal { .. } => None,
+ }
+ }
+
+ #[cfg(target_os = "macos")]
+ /// Consume this surface and return its immutable resident image.
+ ///
+ /// Reusable batch-output surfaces return `None` because their backing
+ /// allocation remains intentionally mutable behind an access gate.
+ pub fn into_resident_metal_image(self) -> Option {
+ match self.storage {
+ Storage::Metal {
+ resident: Some(image),
+ ..
+ } => Some(image),
+ Storage::Host(_) | Storage::Metal { .. } => None,
}
}
@@ -155,7 +220,7 @@ impl Surface {
buffer: Buffer,
dimensions: (u32, u32),
fmt: PixelFormat,
- ) -> Self {
+ ) -> Result {
Self::from_metal_buffer_offset(buffer, dimensions, fmt, 0)
}
@@ -165,19 +230,49 @@ impl Surface {
dimensions: (u32, u32),
fmt: PixelFormat,
offset: usize,
- ) -> Self {
- Self {
+ ) -> Result {
+ Self::from_owned_metal_buffer_offset(
+ buffer,
+ dimensions,
+ fmt,
+ offset,
+ SurfaceResidency::MetalResidentDecode,
+ )
+ }
+
+ #[cfg(target_os = "macos")]
+ fn from_owned_metal_buffer_offset(
+ buffer: Buffer,
+ dimensions: (u32, u32),
+ fmt: PixelFormat,
+ offset: usize,
+ residency: SurfaceResidency,
+ ) -> Result {
+ let pitch_bytes = usize::try_from(dimensions.0)
+ .ok()
+ .and_then(|width| width.checked_mul(fmt.bytes_per_pixel()))
+ .ok_or_else(|| Error::MetalKernel {
+ message: "JPEG Metal surface pitch overflows usize".to_string(),
+ })?;
+ let layout = MetalImageLayout::new(offset, dimensions, pitch_bytes, fmt)
+ .map_err(|source| metal_kernel_support_error("JPEG Metal surface layout", source))?;
+ // SAFETY: these crate-private constructors are used by producer paths
+ // that retain and complete the sole writer before exposing a surface.
+ let image = unsafe { ResidentMetalImage::from_exclusive_pending_buffer(buffer, layout) }
+ .map_err(|source| metal_kernel_support_error("JPEG Metal resident surface", source))?;
+ Ok(Self {
backend: BackendKind::Metal,
- residency: SurfaceResidency::MetalResidentDecode,
+ residency,
dimensions,
fmt,
- pitch_bytes: dimensions.0 as usize * fmt.bytes_per_pixel(),
+ pitch_bytes,
storage: Storage::Metal {
- buffer,
+ resident: Some(image),
+ reusable_buffer: None,
offset,
access_gate: None,
},
- }
+ })
}
#[cfg(target_os = "macos")]
@@ -185,7 +280,7 @@ impl Surface {
buffer: Buffer,
dimensions: (u32, u32),
fmt: PixelFormat,
- ) -> Self {
+ ) -> Result {
Self::from_cpu_staged_metal_buffer_offset(buffer, dimensions, fmt, 0)
}
@@ -195,19 +290,14 @@ impl Surface {
dimensions: (u32, u32),
fmt: PixelFormat,
offset: usize,
- ) -> Self {
- Self {
- backend: BackendKind::Metal,
- residency: SurfaceResidency::CpuStagedMetalUpload,
+ ) -> Result {
+ Self::from_owned_metal_buffer_offset(
+ buffer,
dimensions,
fmt,
- pitch_bytes: dimensions.0 as usize * fmt.bytes_per_pixel(),
- storage: Storage::Metal {
- buffer,
- offset,
- access_gate: None,
- },
- }
+ offset,
+ SurfaceResidency::CpuStagedMetalUpload,
+ )
}
#[cfg(target_os = "macos")]
@@ -224,7 +314,8 @@ impl Surface {
fmt,
pitch_bytes: dimensions.0 as usize * fmt.bytes_per_pixel(),
storage: Storage::Metal {
- buffer: output.buffer.clone(),
+ resident: None,
+ reusable_buffer: Some(output.buffer.clone()),
offset,
access_gate: Some(Arc::clone(&output.access_gate)),
},
@@ -258,644 +349,15 @@ impl DeviceSurface for Surface {
match &self.storage {
Storage::Host(_) => None,
#[cfg(target_os = "macos")]
- Storage::Metal { buffer, offset, .. } => Some(DeviceMemoryRange::new(
- BackendKind::Metal,
- u64::try_from(buffer.as_ptr() as usize).ok()?,
- *offset,
- self.byte_len(),
- )),
- }
- }
-}
-
-#[cfg(target_os = "macos")]
-#[doc(hidden)]
-pub struct ResidentPrivateJpegTile {
- buffer: Buffer,
- byte_offset: usize,
- dimensions: (u32, u32),
- pixel_format: PixelFormat,
- pitch_bytes: usize,
- // Keep the producer resources alive for the lifetime of every tile clone.
- status_buffer: Buffer,
- command_buffer: CommandBuffer,
-}
-
-#[cfg(target_os = "macos")]
-impl ResidentPrivateJpegTile {
- pub(crate) fn new(
- buffer: Buffer,
- byte_offset: usize,
- dimensions: (u32, u32),
- pixel_format: PixelFormat,
- pitch_bytes: usize,
- status_buffer: Buffer,
- command_buffer: CommandBuffer,
- ) -> Self {
- Self {
- buffer,
- byte_offset,
- dimensions,
- pixel_format,
- pitch_bytes,
- status_buffer,
- command_buffer,
- }
- }
-
- /// Byte offset of the first decoded pixel in the backing buffer.
- pub fn byte_offset(&self) -> usize {
- self.byte_offset
- }
-
- /// Dimensions of the decoded tile.
- pub fn dimensions(&self) -> (u32, u32) {
- self.dimensions
- }
-
- /// Pixel format of the decoded tile.
- pub fn pixel_format(&self) -> PixelFormat {
- self.pixel_format
- }
-
- /// Number of bytes between consecutive decoded rows.
- pub fn pitch_bytes(&self) -> usize {
- self.pitch_bytes
- }
-
- /// Return the raw private Metal output buffer.
- ///
- /// # Safety
- ///
- /// The producer command has completed before this tile is returned, but
- /// the caller must synchronize every later access made through the returned
- /// buffer or a handle cloned from it. That obligation covers raw handles
- /// obtained from every clone of this tile; no two accesses may overlap when
- /// either can write the decoded range.
- pub unsafe fn buffer(&self) -> &BufferRef {
- self.buffer_trusted()
- }
-
- pub(crate) fn buffer_trusted(&self) -> &BufferRef {
- self.buffer.as_ref()
- }
-
- /// Consume this wrapper and transfer ownership of its decoded buffer.
- ///
- /// The producer command has already completed. Other tile clones, and
- /// buffers obtained by consuming them, can still refer to the same Metal
- /// allocation. No surviving tile offers safe host readback, and borrowed
- /// raw access remains unsafe; normal Metal synchronization remains each
- /// buffer recipient's responsibility after a handoff.
- pub fn into_buffer(self) -> Buffer {
- self.buffer
- }
-
- #[cfg(test)]
- pub(crate) fn status_buffer_trusted(&self) -> &BufferRef {
- self.status_buffer.as_ref()
- }
-}
-
-#[cfg(target_os = "macos")]
-impl Clone for ResidentPrivateJpegTile {
- fn clone(&self) -> Self {
- Self {
- buffer: self.buffer.clone(),
- byte_offset: self.byte_offset,
- dimensions: self.dimensions,
- pixel_format: self.pixel_format,
- pitch_bytes: self.pitch_bytes,
- status_buffer: self.status_buffer.clone(),
- command_buffer: self.command_buffer.clone(),
- }
- }
-}
-
-#[cfg(target_os = "macos")]
-#[derive(Clone)]
-/// Reusable caller-owned Metal buffer for full-tile JPEG batch output.
-pub struct MetalBatchOutputBuffer {
- buffer: Buffer,
- access_gate: Arc>,
- dimensions: (u32, u32),
- fmt: PixelFormat,
- pitch_bytes: usize,
- tile_stride_bytes: usize,
- tile_capacity: usize,
-}
-
-#[cfg(target_os = "macos")]
-impl MetalBatchOutputBuffer {
- /// Allocate a reusable RGB8 output buffer for `tile_capacity` full-size tiles.
- pub fn new_rgb8_tiles(
- session: &MetalBackendSession,
- dimensions: (u32, u32),
- tile_capacity: usize,
- ) -> Result {
- Self::new_tiles(session, dimensions, PixelFormat::Rgb8, tile_capacity)
- }
-
- /// Ensure this output buffer can hold `tile_capacity` RGB8 tiles with `dimensions`.
- ///
- /// The existing allocation is retained when it already has the requested
- /// layout and at least the requested capacity. Otherwise the buffer is
- /// replaced with a new allocation.
- pub fn ensure_rgb8_tiles(
- &mut self,
- session: &MetalBackendSession,
- dimensions: (u32, u32),
- tile_capacity: usize,
- ) -> Result<(), Error> {
- if self.dimensions == dimensions
- && self.fmt == PixelFormat::Rgb8
- && self.tile_capacity >= tile_capacity
- {
- return Ok(());
- }
-
- *self = Self::new_rgb8_tiles(session, dimensions, tile_capacity)?;
- Ok(())
- }
-
- /// Ensure this output buffer fits a full-image scaled RGB8 batch.
- pub fn ensure_rgb8_scaled_tiles(
- &mut self,
- session: &MetalBackendSession,
- full_dimensions: (u32, u32),
- scale: Downscale,
- tile_capacity: usize,
- ) -> Result<(), Error> {
- self.ensure_rgb8_tiles(session, scaled_dims(full_dimensions, scale), tile_capacity)
- }
-
- /// Ensure this output buffer fits a region-scaled RGB8 batch.
- pub fn ensure_rgb8_region_scaled_tiles(
- &mut self,
- session: &MetalBackendSession,
- roi: Rect,
- scale: Downscale,
- tile_capacity: usize,
- ) -> Result<(), Error> {
- let scaled = roi.scaled_covering(scale);
- self.ensure_rgb8_tiles(session, (scaled.w, scaled.h), tile_capacity)
- }
-
- /// Ensure this output buffer fits a preflighted RGB8 Metal resident batch.
- ///
- /// Ineligible reports return an error without replacing the existing
- /// allocation. Eligible empty reports are a no-op.
- #[doc(hidden)]
- pub fn ensure_rgb8_batch_report(
- &mut self,
- session: &MetalBackendSession,
- report: &JpegMetalResidentBatchReport,
- ) -> Result<(), Error> {
- let Some(dimensions) = report_required_output_dimensions(report)? else {
- return Ok(());
- };
- self.ensure_rgb8_tiles(session, dimensions, report.required_tile_capacity())
- }
-
- fn new_tiles(
- session: &MetalBackendSession,
- dimensions: (u32, u32),
- fmt: PixelFormat,
- tile_capacity: usize,
- ) -> Result {
- if dimensions.0 == 0 || dimensions.1 == 0 || tile_capacity == 0 {
- return Err(Error::UnsupportedMetalRequest {
- reason: "JPEG Metal batch output requires nonzero dimensions and tile capacity",
- });
- }
- let row_bytes = dimensions
- .0
- .checked_mul(u32::try_from(fmt.bytes_per_pixel()).map_err(|_| {
- BufferError::SizeOverflow {
- what: "JPEG Metal output row bytes",
- }
- })?)
- .ok_or(BufferError::SizeOverflow {
- what: "JPEG Metal output row bytes",
- })? as usize;
- let tile_stride_bytes =
- row_bytes
- .checked_mul(dimensions.1 as usize)
- .ok_or(BufferError::SizeOverflow {
- what: "JPEG Metal output tile bytes",
- })?;
- let byte_len =
- tile_stride_bytes
- .checked_mul(tile_capacity)
- .ok_or(BufferError::SizeOverflow {
- what: "JPEG Metal batch output bytes",
- })?;
- let buffer = new_shared_buffer(session.device(), byte_len)?;
- Ok(Self {
- buffer,
- access_gate: Arc::new(Mutex::new(())),
- dimensions,
- fmt,
- pitch_bytes: row_bytes,
- tile_stride_bytes,
- tile_capacity,
- })
- }
-
- /// Return the raw backing Metal buffer.
- ///
- /// # Safety
- ///
- /// The caller must synchronize every CPU and GPU access made through the
- /// returned buffer or any handle cloned from it. The internal safe-access
- /// gate cannot observe work submitted through raw handles. No such access
- /// may overlap a safe decode into this output or readback from a [`Surface`]
- /// that aliases this allocation.
- pub unsafe fn buffer(&self) -> &BufferRef {
- self.buffer_trusted()
- }
-
- pub(crate) fn buffer_trusted(&self) -> &BufferRef {
- self.buffer.as_ref()
- }
-
- pub(crate) fn lock_for_safe_access(&self) -> Result, Error> {
- self.access_gate.lock().map_err(|_| Error::MetalKernel {
- message: "JPEG Metal batch output access gate was poisoned".to_string(),
- })
- }
-
- #[cfg(test)]
- pub(crate) fn shares_access_gate_with(&self, surface: &Surface) -> bool {
- matches!(
- &surface.storage,
- Storage::Metal {
- access_gate: Some(access_gate),
- ..
- } if Arc::ptr_eq(&self.access_gate, access_gate)
- )
- }
-
- /// Tile dimensions for this output allocation.
- pub fn dimensions(&self) -> (u32, u32) {
- self.dimensions
- }
-
- /// Pixel format for this output allocation.
- pub fn pixel_format(&self) -> PixelFormat {
- self.fmt
- }
-
- /// Number of reusable tile slots in the buffer.
- pub fn tile_capacity(&self) -> usize {
- self.tile_capacity
- }
-
- /// Number of bytes between rows in one tile.
- pub fn pitch_bytes(&self) -> usize {
- self.pitch_bytes
- }
-
- /// Number of bytes reserved for each tile slot.
- pub fn tile_stride_bytes(&self) -> usize {
- self.tile_stride_bytes
- }
-
- /// Total byte length of the backing allocation.
- pub fn byte_len(&self) -> usize {
- self.tile_stride_bytes * self.tile_capacity
- }
-
- pub(crate) fn clone_buffer(&self) -> Buffer {
- self.buffer.clone()
- }
-}
-
-#[cfg(target_os = "macos")]
-#[derive(Clone)]
-/// Reusable caller-owned Metal textures for full-tile JPEG batch output.
-pub struct MetalBatchTextureOutput {
- set: Arc,
-}
-
-#[cfg(target_os = "macos")]
-struct MetalBatchTextureSet {
- textures: Vec,
- access_gate: Arc>,
- dimensions: (u32, u32),
- fmt: PixelFormat,
- metal_fmt: MTLPixelFormat,
-}
-
-#[cfg(target_os = "macos")]
-impl MetalBatchTextureOutput {
- /// Allocate reusable private RGBA8 textures for `tile_capacity` full-size tiles.
- pub fn new_rgba8_tiles(
- session: &MetalBackendSession,
- dimensions: (u32, u32),
- tile_capacity: usize,
- ) -> Result {
- if dimensions.0 == 0 || dimensions.1 == 0 || tile_capacity == 0 {
- return Err(Error::UnsupportedMetalRequest {
- reason:
- "JPEG Metal batch texture output requires nonzero dimensions and tile capacity",
- });
- }
-
- let descriptor = j2k_metal_support::checked_texture_descriptor().map_err(|source| {
- metal_kernel_support_error("JPEG Metal texture descriptor creation", source)
- })?;
- descriptor.set_texture_type(MTLTextureType::D2);
- descriptor.set_pixel_format(MTLPixelFormat::RGBA8Unorm);
- descriptor.set_width(u64::from(dimensions.0));
- descriptor.set_height(u64::from(dimensions.1));
- descriptor.set_depth(1);
- descriptor.set_mipmap_level_count(1);
- descriptor.set_sample_count(1);
- descriptor.set_storage_mode(MTLStorageMode::Private);
- descriptor.set_usage(MTLTextureUsage::ShaderRead | MTLTextureUsage::ShaderWrite);
-
- let pixels = crate::batch_allocation::checked_count_product(
- dimensions.0 as usize,
- dimensions.1 as usize,
- "JPEG Metal batch texture pixels",
- )?;
- let tile_bytes = crate::batch_allocation::checked_count_product(
- pixels,
- PixelFormat::Rgba8.bytes_per_pixel(),
- "JPEG Metal batch texture bytes",
- )?;
- let heap_texture_bytes = usize::try_from(
- session
- .device()
- .heap_texture_size_and_align(&descriptor)
- .size,
- )
- .map_err(|_| j2k_core::BatchInfrastructureError::AllocationTooLarge {
- what: "JPEG Metal batch texture planned bytes",
- requested: usize::MAX,
- cap: j2k_core::DEFAULT_MAX_HOST_ALLOCATION_BYTES,
- })?;
- let planned_texture_bytes = crate::batch_allocation::checked_count_product(
- tile_bytes.max(heap_texture_bytes),
- tile_capacity,
- "JPEG Metal batch texture planned allocation",
- )?;
- let mut budget = crate::batch_allocation::BatchMetadataBudget::new(
- "JPEG Metal batch texture collection",
- );
- budget.preflight(&[
- crate::batch_allocation::BatchMetadataRequest::of::(tile_capacity),
- crate::batch_allocation::BatchMetadataRequest::of::(planned_texture_bytes),
- ])?;
- let mut textures = budget.try_vec(tile_capacity, "JPEG Metal batch texture handles")?;
- for _ in 0..tile_capacity {
- let texture = j2k_metal_support::checked_texture(session.device(), &descriptor)
- .map_err(|source| {
- metal_kernel_support_error("JPEG Metal batch texture allocation", source)
- })?;
- let texture_bytes = usize::try_from(texture.allocated_size()).map_err(|_| {
- j2k_core::BatchInfrastructureError::AllocationTooLarge {
- what: "JPEG Metal batch texture allocated bytes",
- requested: usize::MAX,
- cap: j2k_core::DEFAULT_MAX_HOST_ALLOCATION_BYTES,
- }
- })?;
- budget.account_capacity::(texture_bytes)?;
- textures.push(texture);
- }
-
- Ok(Self {
- set: Arc::new(MetalBatchTextureSet {
- textures,
- access_gate: Arc::new(Mutex::new(())),
- dimensions,
- fmt: PixelFormat::Rgba8,
- metal_fmt: MTLPixelFormat::RGBA8Unorm,
- }),
- })
- }
-
- /// Ensure this output set can hold `tile_capacity` RGBA8 textures with `dimensions`.
- ///
- /// Existing textures are retained when they already have the requested
- /// layout and at least the requested capacity. Otherwise the texture set is
- /// replaced with new private RGBA8 textures.
- pub fn ensure_rgba8_tiles(
- &mut self,
- session: &MetalBackendSession,
- dimensions: (u32, u32),
- tile_capacity: usize,
- ) -> Result<(), Error> {
- if self.set.dimensions == dimensions
- && self.set.fmt == PixelFormat::Rgba8
- && self.set.metal_fmt == MTLPixelFormat::RGBA8Unorm
- && self.tile_capacity() >= tile_capacity
- {
- return Ok(());
- }
-
- let replacement = Self::new_rgba8_tiles(session, dimensions, tile_capacity)?;
- self.set = replacement.set;
- Ok(())
- }
-
- /// Ensure this output set fits a full-image scaled RGBA8 texture batch.
- pub fn ensure_rgba8_scaled_tiles(
- &mut self,
- session: &MetalBackendSession,
- full_dimensions: (u32, u32),
- scale: Downscale,
- tile_capacity: usize,
- ) -> Result<(), Error> {
- self.ensure_rgba8_tiles(session, scaled_dims(full_dimensions, scale), tile_capacity)
- }
-
- /// Ensure this output set fits a region-scaled RGBA8 texture batch.
- pub fn ensure_rgba8_region_scaled_tiles(
- &mut self,
- session: &MetalBackendSession,
- roi: Rect,
- scale: Downscale,
- tile_capacity: usize,
- ) -> Result<(), Error> {
- let scaled = roi.scaled_covering(scale);
- self.ensure_rgba8_tiles(session, (scaled.w, scaled.h), tile_capacity)
- }
-
- /// Ensure this texture set fits a preflighted RGB8 Metal resident batch.
- ///
- /// Ineligible reports return an error without replacing the existing
- /// textures. Eligible empty reports are a no-op.
- #[doc(hidden)]
- pub fn ensure_rgba8_batch_report(
- &mut self,
- session: &MetalBackendSession,
- report: &JpegMetalResidentBatchReport,
- ) -> Result<(), Error> {
- let Some(dimensions) = report_required_output_dimensions(report)? else {
- return Ok(());
- };
- self.ensure_rgba8_tiles(session, dimensions, report.required_tile_capacity())
- }
-
- /// Tile dimensions for this output allocation.
- pub fn dimensions(&self) -> (u32, u32) {
- self.set.dimensions
- }
-
- /// Pixel format for this output allocation.
- pub fn pixel_format(&self) -> PixelFormat {
- self.set.fmt
- }
-
- /// Metal pixel format for each backing texture.
- pub fn metal_pixel_format(&self) -> MTLPixelFormat {
- self.set.metal_fmt
- }
-
- /// Number of reusable tile texture slots.
- pub fn tile_capacity(&self) -> usize {
- self.set.textures.len()
- }
-
- /// Return a raw reusable output texture by tile slot.
- ///
- /// # Safety
- ///
- /// The caller must synchronize every CPU and GPU access made through the
- /// returned texture or any handle cloned from it. The internal safe-access
- /// gate cannot observe work submitted through raw handles. No such access
- /// may overlap a safe decode into this output, any clone or subset that
- /// shares its allocation gate, or access through a derived
- /// [`MetalTextureTile`].
- pub unsafe fn texture(&self, index: usize) -> Option<&TextureRef> {
- self.texture_trusted(index)
- }
-
- pub(crate) fn texture_trusted(&self, index: usize) -> Option<&TextureRef> {
- self.set
- .textures
- .get(index)
- .map(std::convert::AsRef::as_ref)
- }
-
- pub(crate) fn clone_texture_trusted(&self, index: usize) -> Option {
- self.set.textures.get(index).cloned()
- }
-
- pub(crate) fn clone_access_gate(&self) -> Arc> {
- Arc::clone(&self.set.access_gate)
- }
-
- pub(crate) fn lock_for_safe_access(&self) -> Result, Error> {
- self.set.access_gate.lock().map_err(|_| Error::MetalKernel {
- message: "JPEG Metal batch texture output access gate was poisoned".to_string(),
- })
- }
-
- pub(crate) fn clone_slots(&self, indices: &[usize]) -> Result {
- let mut budget = crate::batch_allocation::BatchMetadataBudget::new(
- "JPEG Metal cloned texture slot collection",
- );
- let mut textures = budget.try_vec(indices.len(), "JPEG Metal cloned texture handles")?;
- for &index in indices {
- textures.push(
- self.clone_texture_trusted(index)
- .ok_or_else(|| Error::MetalKernel {
- message: "JPEG Metal batch texture output slot was missing".to_string(),
- })?,
- );
- }
- Ok(Self {
- set: Arc::new(MetalBatchTextureSet {
- textures,
- access_gate: Arc::clone(&self.set.access_gate),
- dimensions: self.set.dimensions,
- fmt: self.set.fmt,
- metal_fmt: self.set.metal_fmt,
- }),
- })
- }
-
- #[cfg(test)]
- pub(crate) fn shares_allocation_set_with(&self, other: &Self) -> bool {
- Arc::ptr_eq(&self.set, &other.set)
- }
-
- #[cfg(test)]
- pub(crate) fn shares_access_gate_with(&self, other: &Self) -> bool {
- Arc::ptr_eq(&self.set.access_gate, &other.set.access_gate)
- }
-
- #[cfg(test)]
- pub(crate) fn shares_access_gate_with_tile(&self, tile: &MetalTextureTile) -> bool {
- Arc::ptr_eq(&self.set.access_gate, &tile.access_gate)
- }
-}
-
-#[cfg(target_os = "macos")]
-/// One decoded JPEG tile resident in a caller-owned Metal texture.
-pub struct MetalTextureTile {
- texture: Texture,
- access_gate: Arc>,
- dimensions: (u32, u32),
- fmt: PixelFormat,
-}
-
-#[cfg(target_os = "macos")]
-impl MetalTextureTile {
- pub(crate) fn new(
- texture: Texture,
- access_gate: Arc>,
- dimensions: (u32, u32),
- fmt: PixelFormat,
- ) -> Self {
- Self {
- texture,
- access_gate,
- dimensions,
- fmt,
- }
- }
-
- /// Return the raw Metal texture containing the decoded tile.
- ///
- /// # Safety
- ///
- /// The caller must synchronize every CPU and GPU access made through the
- /// returned texture or any handle cloned from it. The safe decode gate
- /// shared with the originating [`MetalBatchTextureOutput`] cannot observe
- /// work submitted through raw handles. No raw access may overlap a safe
- /// decode through that output, one of its clones or subsets, or another
- /// tile derived from the same allocation.
- pub unsafe fn texture(&self) -> &TextureRef {
- self.texture_trusted()
- }
-
- pub(crate) fn texture_trusted(&self) -> &TextureRef {
- self.texture.as_ref()
- }
-
- /// Decoded tile dimensions.
- pub fn dimensions(&self) -> (u32, u32) {
- self.dimensions
- }
-
- /// Decoded tile pixel format.
- pub fn pixel_format(&self) -> PixelFormat {
- self.fmt
- }
-}
-
-#[cfg(target_os = "macos")]
-impl Clone for MetalTextureTile {
- fn clone(&self) -> Self {
- Self {
- texture: self.texture.clone(),
- access_gate: Arc::clone(&self.access_gate),
- dimensions: self.dimensions,
- fmt: self.fmt,
+ Storage::Metal { .. } => {
+ let (buffer, offset) = self.metal_buffer_trusted()?;
+ Some(DeviceMemoryRange::new(
+ BackendKind::Metal,
+ u64::try_from(buffer.as_ptr() as usize).ok()?,
+ offset,
+ self.byte_len(),
+ ))
+ }
}
}
}
diff --git a/crates/j2k-jpeg-metal/src/surface/batch_buffer.rs b/crates/j2k-jpeg-metal/src/surface/batch_buffer.rs
new file mode 100644
index 00000000..d9c55cd8
--- /dev/null
+++ b/crates/j2k-jpeg-metal/src/surface/batch_buffer.rs
@@ -0,0 +1,211 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use std::sync::{Arc, Mutex, MutexGuard};
+
+use j2k_core::{BufferError, Downscale, PixelFormat, Rect};
+use metal::{Buffer, BufferRef};
+
+#[cfg(test)]
+use super::{Storage, Surface};
+use crate::buffers::new_shared_buffer;
+use crate::{
+ report_required_output_dimensions, scaled_dims, Error, JpegMetalResidentBatchReport,
+ MetalBackendSession,
+};
+
+#[derive(Clone)]
+/// Reusable caller-owned Metal buffer for full-tile JPEG batch output.
+pub struct MetalBatchOutputBuffer {
+ pub(super) buffer: Buffer,
+ pub(super) access_gate: Arc>,
+ dimensions: (u32, u32),
+ fmt: PixelFormat,
+ pitch_bytes: usize,
+ tile_stride_bytes: usize,
+ tile_capacity: usize,
+}
+
+impl MetalBatchOutputBuffer {
+ /// Allocate a reusable RGB8 output buffer for `tile_capacity` full-size tiles.
+ pub fn new_rgb8_tiles(
+ session: &MetalBackendSession,
+ dimensions: (u32, u32),
+ tile_capacity: usize,
+ ) -> Result {
+ Self::new_tiles(session, dimensions, PixelFormat::Rgb8, tile_capacity)
+ }
+
+ /// Ensure this output buffer can hold `tile_capacity` RGB8 tiles with `dimensions`.
+ ///
+ /// The existing allocation is retained when it already has the requested
+ /// layout and at least the requested capacity. Otherwise the buffer is
+ /// replaced with a new allocation.
+ pub fn ensure_rgb8_tiles(
+ &mut self,
+ session: &MetalBackendSession,
+ dimensions: (u32, u32),
+ tile_capacity: usize,
+ ) -> Result<(), Error> {
+ if self.dimensions == dimensions
+ && self.fmt == PixelFormat::Rgb8
+ && self.tile_capacity >= tile_capacity
+ {
+ return Ok(());
+ }
+
+ *self = Self::new_rgb8_tiles(session, dimensions, tile_capacity)?;
+ Ok(())
+ }
+
+ /// Ensure this output buffer fits a full-image scaled RGB8 batch.
+ pub fn ensure_rgb8_scaled_tiles(
+ &mut self,
+ session: &MetalBackendSession,
+ full_dimensions: (u32, u32),
+ scale: Downscale,
+ tile_capacity: usize,
+ ) -> Result<(), Error> {
+ self.ensure_rgb8_tiles(session, scaled_dims(full_dimensions, scale), tile_capacity)
+ }
+
+ /// Ensure this output buffer fits a region-scaled RGB8 batch.
+ pub fn ensure_rgb8_region_scaled_tiles(
+ &mut self,
+ session: &MetalBackendSession,
+ roi: Rect,
+ scale: Downscale,
+ tile_capacity: usize,
+ ) -> Result<(), Error> {
+ let scaled = roi.scaled_covering(scale);
+ self.ensure_rgb8_tiles(session, (scaled.w, scaled.h), tile_capacity)
+ }
+
+ /// Ensure this output buffer fits a preflighted RGB8 Metal resident batch.
+ ///
+ /// Ineligible reports return an error without replacing the existing
+ /// allocation. Eligible empty reports are a no-op.
+ #[doc(hidden)]
+ pub fn ensure_rgb8_batch_report(
+ &mut self,
+ session: &MetalBackendSession,
+ report: &JpegMetalResidentBatchReport,
+ ) -> Result<(), Error> {
+ let Some(dimensions) = report_required_output_dimensions(report)? else {
+ return Ok(());
+ };
+ self.ensure_rgb8_tiles(session, dimensions, report.required_tile_capacity())
+ }
+
+ fn new_tiles(
+ session: &MetalBackendSession,
+ dimensions: (u32, u32),
+ fmt: PixelFormat,
+ tile_capacity: usize,
+ ) -> Result {
+ if dimensions.0 == 0 || dimensions.1 == 0 || tile_capacity == 0 {
+ return Err(Error::UnsupportedMetalRequest {
+ reason: "JPEG Metal batch output requires nonzero dimensions and tile capacity",
+ });
+ }
+ let row_bytes = dimensions
+ .0
+ .checked_mul(u32::try_from(fmt.bytes_per_pixel()).map_err(|_| {
+ BufferError::SizeOverflow {
+ what: "JPEG Metal output row bytes",
+ }
+ })?)
+ .ok_or(BufferError::SizeOverflow {
+ what: "JPEG Metal output row bytes",
+ })? as usize;
+ let tile_stride_bytes =
+ row_bytes
+ .checked_mul(dimensions.1 as usize)
+ .ok_or(BufferError::SizeOverflow {
+ what: "JPEG Metal output tile bytes",
+ })?;
+ let byte_len =
+ tile_stride_bytes
+ .checked_mul(tile_capacity)
+ .ok_or(BufferError::SizeOverflow {
+ what: "JPEG Metal batch output bytes",
+ })?;
+ let buffer = new_shared_buffer(session.device(), byte_len)?;
+ Ok(Self {
+ buffer,
+ access_gate: Arc::new(Mutex::new(())),
+ dimensions,
+ fmt,
+ pitch_bytes: row_bytes,
+ tile_stride_bytes,
+ tile_capacity,
+ })
+ }
+
+ /// Return the raw backing Metal buffer.
+ ///
+ /// # Safety
+ ///
+ /// The caller must synchronize every CPU and GPU access made through the
+ /// returned buffer or any handle cloned from it. The internal safe-access
+ /// gate cannot observe work submitted through raw handles. No such access
+ /// may overlap a safe decode into this output or readback from a [`crate::Surface`]
+ /// that aliases this allocation.
+ pub unsafe fn buffer(&self) -> &BufferRef {
+ self.buffer_trusted()
+ }
+
+ pub(crate) fn buffer_trusted(&self) -> &BufferRef {
+ self.buffer.as_ref()
+ }
+
+ pub(crate) fn lock_for_safe_access(&self) -> Result, Error> {
+ self.access_gate.lock().map_err(|_| Error::MetalKernel {
+ message: "JPEG Metal batch output access gate was poisoned".to_string(),
+ })
+ }
+
+ #[cfg(test)]
+ pub(crate) fn shares_access_gate_with(&self, surface: &Surface) -> bool {
+ matches!(
+ &surface.storage,
+ Storage::Metal {
+ access_gate: Some(access_gate),
+ ..
+ } if Arc::ptr_eq(&self.access_gate, access_gate)
+ )
+ }
+
+ /// Tile dimensions for this output allocation.
+ pub fn dimensions(&self) -> (u32, u32) {
+ self.dimensions
+ }
+
+ /// Pixel format for this output allocation.
+ pub fn pixel_format(&self) -> PixelFormat {
+ self.fmt
+ }
+
+ /// Number of reusable tile slots in the buffer.
+ pub fn tile_capacity(&self) -> usize {
+ self.tile_capacity
+ }
+
+ /// Number of bytes between rows in one tile.
+ pub fn pitch_bytes(&self) -> usize {
+ self.pitch_bytes
+ }
+
+ /// Number of bytes reserved for each tile slot.
+ pub fn tile_stride_bytes(&self) -> usize {
+ self.tile_stride_bytes
+ }
+
+ /// Total byte length of the backing allocation.
+ pub fn byte_len(&self) -> usize {
+ self.tile_stride_bytes * self.tile_capacity
+ }
+
+ pub(crate) fn clone_buffer(&self) -> Buffer {
+ self.buffer.clone()
+ }
+}
diff --git a/crates/j2k-jpeg-metal/src/surface/batch_texture.rs b/crates/j2k-jpeg-metal/src/surface/batch_texture.rs
new file mode 100644
index 00000000..463c27f2
--- /dev/null
+++ b/crates/j2k-jpeg-metal/src/surface/batch_texture.rs
@@ -0,0 +1,274 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use std::sync::{Arc, Mutex, MutexGuard};
+
+use j2k_core::{Downscale, PixelFormat, Rect};
+use metal::{MTLPixelFormat, MTLStorageMode, MTLTextureType, MTLTextureUsage, Texture, TextureRef};
+
+#[cfg(test)]
+use super::MetalTextureTile;
+use crate::error::metal_kernel_support_error;
+use crate::{
+ report_required_output_dimensions, scaled_dims, Error, JpegMetalResidentBatchReport,
+ MetalBackendSession,
+};
+
+#[derive(Clone)]
+/// Reusable caller-owned Metal textures for full-tile JPEG batch output.
+pub struct MetalBatchTextureOutput {
+ set: Arc,
+}
+
+struct MetalBatchTextureSet {
+ textures: Vec,
+ access_gate: Arc>,
+ dimensions: (u32, u32),
+ fmt: PixelFormat,
+ metal_fmt: MTLPixelFormat,
+}
+
+impl MetalBatchTextureOutput {
+ /// Allocate reusable private RGBA8 textures for `tile_capacity` full-size tiles.
+ pub fn new_rgba8_tiles(
+ session: &MetalBackendSession,
+ dimensions: (u32, u32),
+ tile_capacity: usize,
+ ) -> Result {
+ if dimensions.0 == 0 || dimensions.1 == 0 || tile_capacity == 0 {
+ return Err(Error::UnsupportedMetalRequest {
+ reason:
+ "JPEG Metal batch texture output requires nonzero dimensions and tile capacity",
+ });
+ }
+
+ let descriptor = j2k_metal_support::checked_texture_descriptor().map_err(|source| {
+ metal_kernel_support_error("JPEG Metal texture descriptor creation", source)
+ })?;
+ descriptor.set_texture_type(MTLTextureType::D2);
+ descriptor.set_pixel_format(MTLPixelFormat::RGBA8Unorm);
+ descriptor.set_width(u64::from(dimensions.0));
+ descriptor.set_height(u64::from(dimensions.1));
+ descriptor.set_depth(1);
+ descriptor.set_mipmap_level_count(1);
+ descriptor.set_sample_count(1);
+ descriptor.set_storage_mode(MTLStorageMode::Private);
+ descriptor.set_usage(MTLTextureUsage::ShaderRead | MTLTextureUsage::ShaderWrite);
+
+ let pixels = crate::batch_allocation::checked_count_product(
+ dimensions.0 as usize,
+ dimensions.1 as usize,
+ "JPEG Metal batch texture pixels",
+ )?;
+ let tile_bytes = crate::batch_allocation::checked_count_product(
+ pixels,
+ PixelFormat::Rgba8.bytes_per_pixel(),
+ "JPEG Metal batch texture bytes",
+ )?;
+ let heap_texture_bytes = usize::try_from(
+ session
+ .device()
+ .heap_texture_size_and_align(&descriptor)
+ .size,
+ )
+ .map_err(|_| j2k_core::BatchInfrastructureError::AllocationTooLarge {
+ what: "JPEG Metal batch texture planned bytes",
+ requested: usize::MAX,
+ cap: j2k_core::DEFAULT_MAX_HOST_ALLOCATION_BYTES,
+ })?;
+ let planned_texture_bytes = crate::batch_allocation::checked_count_product(
+ tile_bytes.max(heap_texture_bytes),
+ tile_capacity,
+ "JPEG Metal batch texture planned allocation",
+ )?;
+ let mut budget = crate::batch_allocation::BatchMetadataBudget::new(
+ "JPEG Metal batch texture collection",
+ );
+ budget.preflight(&[
+ crate::batch_allocation::BatchMetadataRequest::of::(tile_capacity),
+ crate::batch_allocation::BatchMetadataRequest::of::(planned_texture_bytes),
+ ])?;
+ let mut textures = budget.try_vec(tile_capacity, "JPEG Metal batch texture handles")?;
+ for _ in 0..tile_capacity {
+ let texture = j2k_metal_support::checked_texture(session.device(), &descriptor)
+ .map_err(|source| {
+ metal_kernel_support_error("JPEG Metal batch texture allocation", source)
+ })?;
+ let texture_bytes = usize::try_from(texture.allocated_size()).map_err(|_| {
+ j2k_core::BatchInfrastructureError::AllocationTooLarge {
+ what: "JPEG Metal batch texture allocated bytes",
+ requested: usize::MAX,
+ cap: j2k_core::DEFAULT_MAX_HOST_ALLOCATION_BYTES,
+ }
+ })?;
+ budget.account_capacity::(texture_bytes)?;
+ textures.push(texture);
+ }
+
+ Ok(Self {
+ set: Arc::new(MetalBatchTextureSet {
+ textures,
+ access_gate: Arc::new(Mutex::new(())),
+ dimensions,
+ fmt: PixelFormat::Rgba8,
+ metal_fmt: MTLPixelFormat::RGBA8Unorm,
+ }),
+ })
+ }
+
+ /// Ensure this output set can hold `tile_capacity` RGBA8 textures with `dimensions`.
+ ///
+ /// Existing textures are retained when they already have the requested
+ /// layout and at least the requested capacity. Otherwise the texture set is
+ /// replaced with new private RGBA8 textures.
+ pub fn ensure_rgba8_tiles(
+ &mut self,
+ session: &MetalBackendSession,
+ dimensions: (u32, u32),
+ tile_capacity: usize,
+ ) -> Result<(), Error> {
+ if self.set.dimensions == dimensions
+ && self.set.fmt == PixelFormat::Rgba8
+ && self.set.metal_fmt == MTLPixelFormat::RGBA8Unorm
+ && self.tile_capacity() >= tile_capacity
+ {
+ return Ok(());
+ }
+
+ let replacement = Self::new_rgba8_tiles(session, dimensions, tile_capacity)?;
+ self.set = replacement.set;
+ Ok(())
+ }
+
+ /// Ensure this output set fits a full-image scaled RGBA8 texture batch.
+ pub fn ensure_rgba8_scaled_tiles(
+ &mut self,
+ session: &MetalBackendSession,
+ full_dimensions: (u32, u32),
+ scale: Downscale,
+ tile_capacity: usize,
+ ) -> Result<(), Error> {
+ self.ensure_rgba8_tiles(session, scaled_dims(full_dimensions, scale), tile_capacity)
+ }
+
+ /// Ensure this output set fits a region-scaled RGBA8 texture batch.
+ pub fn ensure_rgba8_region_scaled_tiles(
+ &mut self,
+ session: &MetalBackendSession,
+ roi: Rect,
+ scale: Downscale,
+ tile_capacity: usize,
+ ) -> Result<(), Error> {
+ let scaled = roi.scaled_covering(scale);
+ self.ensure_rgba8_tiles(session, (scaled.w, scaled.h), tile_capacity)
+ }
+
+ /// Ensure this texture set fits a preflighted RGB8 Metal resident batch.
+ ///
+ /// Ineligible reports return an error without replacing the existing
+ /// textures. Eligible empty reports are a no-op.
+ #[doc(hidden)]
+ pub fn ensure_rgba8_batch_report(
+ &mut self,
+ session: &MetalBackendSession,
+ report: &JpegMetalResidentBatchReport,
+ ) -> Result<(), Error> {
+ let Some(dimensions) = report_required_output_dimensions(report)? else {
+ return Ok(());
+ };
+ self.ensure_rgba8_tiles(session, dimensions, report.required_tile_capacity())
+ }
+
+ /// Tile dimensions for this output allocation.
+ pub fn dimensions(&self) -> (u32, u32) {
+ self.set.dimensions
+ }
+
+ /// Pixel format for this output allocation.
+ pub fn pixel_format(&self) -> PixelFormat {
+ self.set.fmt
+ }
+
+ /// Metal pixel format for each backing texture.
+ pub fn metal_pixel_format(&self) -> MTLPixelFormat {
+ self.set.metal_fmt
+ }
+
+ /// Number of reusable tile texture slots.
+ pub fn tile_capacity(&self) -> usize {
+ self.set.textures.len()
+ }
+
+ /// Return a raw reusable output texture by tile slot.
+ ///
+ /// # Safety
+ ///
+ /// The caller must synchronize every CPU and GPU access made through the
+ /// returned texture or any handle cloned from it. The internal safe-access
+ /// gate cannot observe work submitted through raw handles. No such access
+ /// may overlap a safe decode into this output, any clone or subset that
+ /// shares its allocation gate, or access through a derived
+ /// [`crate::MetalTextureTile`].
+ pub unsafe fn texture(&self, index: usize) -> Option<&TextureRef> {
+ self.texture_trusted(index)
+ }
+
+ pub(crate) fn texture_trusted(&self, index: usize) -> Option<&TextureRef> {
+ self.set
+ .textures
+ .get(index)
+ .map(std::convert::AsRef::as_ref)
+ }
+
+ pub(crate) fn clone_texture_trusted(&self, index: usize) -> Option {
+ self.set.textures.get(index).cloned()
+ }
+
+ pub(crate) fn clone_access_gate(&self) -> Arc> {
+ Arc::clone(&self.set.access_gate)
+ }
+
+ pub(crate) fn lock_for_safe_access(&self) -> Result, Error> {
+ self.set.access_gate.lock().map_err(|_| Error::MetalKernel {
+ message: "JPEG Metal batch texture output access gate was poisoned".to_string(),
+ })
+ }
+
+ pub(crate) fn clone_slots(&self, indices: &[usize]) -> Result {
+ let mut budget = crate::batch_allocation::BatchMetadataBudget::new(
+ "JPEG Metal cloned texture slot collection",
+ );
+ let mut textures = budget.try_vec(indices.len(), "JPEG Metal cloned texture handles")?;
+ for &index in indices {
+ textures.push(
+ self.clone_texture_trusted(index)
+ .ok_or_else(|| Error::MetalKernel {
+ message: "JPEG Metal batch texture output slot was missing".to_string(),
+ })?,
+ );
+ }
+ Ok(Self {
+ set: Arc::new(MetalBatchTextureSet {
+ textures,
+ access_gate: Arc::clone(&self.set.access_gate),
+ dimensions: self.set.dimensions,
+ fmt: self.set.fmt,
+ metal_fmt: self.set.metal_fmt,
+ }),
+ })
+ }
+
+ #[cfg(test)]
+ pub(crate) fn shares_allocation_set_with(&self, other: &Self) -> bool {
+ Arc::ptr_eq(&self.set, &other.set)
+ }
+
+ #[cfg(test)]
+ pub(crate) fn shares_access_gate_with(&self, other: &Self) -> bool {
+ Arc::ptr_eq(&self.set.access_gate, &other.set.access_gate)
+ }
+
+ #[cfg(test)]
+ pub(crate) fn shares_access_gate_with_tile(&self, tile: &MetalTextureTile) -> bool {
+ Arc::ptr_eq(&self.set.access_gate, &tile.access_gate)
+ }
+}
diff --git a/crates/j2k-jpeg-metal/src/surface/resident_tile.rs b/crates/j2k-jpeg-metal/src/surface/resident_tile.rs
new file mode 100644
index 00000000..31787a3e
--- /dev/null
+++ b/crates/j2k-jpeg-metal/src/surface/resident_tile.rs
@@ -0,0 +1,122 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use j2k_core::PixelFormat;
+use j2k_metal_support::{MetalImageLayout, ResidentMetalImage};
+use metal::{Buffer, BufferRef, CommandBuffer};
+
+use crate::error::metal_kernel_support_error;
+use crate::Error;
+
+#[doc(hidden)]
+pub struct ResidentPrivateJpegTile {
+ image: ResidentMetalImage,
+ // Keep the producer resources alive for the lifetime of every tile clone.
+ status_buffer: Buffer,
+ command_buffer: CommandBuffer,
+}
+
+impl ResidentPrivateJpegTile {
+ pub(crate) fn new(
+ buffer: Buffer,
+ byte_offset: usize,
+ dimensions: (u32, u32),
+ pixel_format: PixelFormat,
+ pitch_bytes: usize,
+ status_buffer: Buffer,
+ command_buffer: CommandBuffer,
+ ) -> Result {
+ let layout = MetalImageLayout::new(byte_offset, dimensions, pitch_bytes, pixel_format)
+ .map_err(|source| {
+ metal_kernel_support_error("JPEG private resident tile layout", source)
+ })?;
+ // SAFETY: both private-tile producers wait for the command buffer and
+ // validate its status before constructing this wrapper.
+ let image = unsafe { ResidentMetalImage::from_completed_buffer(buffer, layout) }.map_err(
+ |source| metal_kernel_support_error("JPEG private resident tile adoption", source),
+ )?;
+ Ok(Self {
+ image,
+ status_buffer,
+ command_buffer,
+ })
+ }
+
+ /// Byte offset of the first decoded pixel in the backing buffer.
+ pub fn byte_offset(&self) -> usize {
+ self.image.byte_offset()
+ }
+
+ /// Dimensions of the decoded tile.
+ pub fn dimensions(&self) -> (u32, u32) {
+ self.image.dimensions()
+ }
+
+ /// Pixel format of the decoded tile.
+ pub fn pixel_format(&self) -> PixelFormat {
+ self.image.pixel_format()
+ }
+
+ /// Number of bytes between consecutive decoded rows.
+ pub fn pitch_bytes(&self) -> usize {
+ self.image.pitch_bytes()
+ }
+
+ /// Borrow the common immutable resident image.
+ pub fn resident_image(&self) -> &ResidentMetalImage {
+ &self.image
+ }
+
+ /// Consume the private tile and return the common resident image.
+ pub fn into_resident_image(self) -> ResidentMetalImage {
+ self.image
+ }
+
+ /// Return the raw private Metal output buffer.
+ ///
+ /// # Safety
+ ///
+ /// The producer command has completed before this tile is returned, but
+ /// the caller must synchronize every later access made through the returned
+ /// buffer or a handle cloned from it. That obligation covers raw handles
+ /// obtained from every clone of this tile; no two accesses may overlap when
+ /// either can write the decoded range.
+ pub unsafe fn buffer(&self) -> &BufferRef {
+ self.buffer_trusted()
+ }
+
+ pub(crate) fn buffer_trusted(&self) -> &BufferRef {
+ // SAFETY: this crate-private accessor is used only for read-only Metal
+ // binding after the producer has completed.
+ unsafe { self.image.raw_buffer() }.as_ref()
+ }
+
+ /// Consume this wrapper and transfer ownership of its decoded buffer.
+ ///
+ /// # Safety
+ ///
+ /// The producer command has already completed. Other tile clones, and
+ /// buffers obtained by consuming them, can still refer to the same Metal
+ /// allocation. No surviving tile offers safe host readback, and borrowed
+ /// raw access remains unsafe; normal Metal synchronization remains each
+ /// buffer recipient's responsibility after a handoff.
+ #[deprecated(note = "use into_resident_image; raw Metal handles require unsafe interop")]
+ pub unsafe fn into_buffer(self) -> Buffer {
+ // SAFETY: the caller accepts the raw-handle synchronization contract.
+ unsafe { self.image.raw_buffer() }.to_owned()
+ }
+
+ #[cfg(test)]
+ pub(crate) fn status_buffer_trusted(&self) -> &BufferRef {
+ self.status_buffer.as_ref()
+ }
+}
+
+impl Clone for ResidentPrivateJpegTile {
+ fn clone(&self) -> Self {
+ Self {
+ image: self.image.clone(),
+ status_buffer: self.status_buffer.clone(),
+ command_buffer: self.command_buffer.clone(),
+ }
+ }
+}
diff --git a/crates/j2k-jpeg-metal/src/surface/texture_tile.rs b/crates/j2k-jpeg-metal/src/surface/texture_tile.rs
new file mode 100644
index 00000000..283ca574
--- /dev/null
+++ b/crates/j2k-jpeg-metal/src/surface/texture_tile.rs
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use std::sync::{Arc, Mutex};
+
+use j2k_core::PixelFormat;
+use metal::{Texture, TextureRef};
+
+/// One decoded JPEG tile resident in a caller-owned Metal texture.
+pub struct MetalTextureTile {
+ pub(super) texture: Texture,
+ pub(super) access_gate: Arc>,
+ dimensions: (u32, u32),
+ fmt: PixelFormat,
+}
+
+impl MetalTextureTile {
+ pub(crate) fn new(
+ texture: Texture,
+ access_gate: Arc>,
+ dimensions: (u32, u32),
+ fmt: PixelFormat,
+ ) -> Self {
+ Self {
+ texture,
+ access_gate,
+ dimensions,
+ fmt,
+ }
+ }
+
+ /// Return the raw Metal texture containing the decoded tile.
+ ///
+ /// # Safety
+ ///
+ /// The caller must synchronize every CPU and GPU access made through the
+ /// returned texture or any handle cloned from it. The safe decode gate
+ /// shared with the originating [`crate::MetalBatchTextureOutput`] cannot observe
+ /// work submitted through raw handles. No raw access may overlap a safe
+ /// decode through that output, one of its clones or subsets, or another
+ /// tile derived from the same allocation.
+ pub unsafe fn texture(&self) -> &TextureRef {
+ self.texture_trusted()
+ }
+
+ pub(crate) fn texture_trusted(&self) -> &TextureRef {
+ self.texture.as_ref()
+ }
+
+ /// Decoded tile dimensions.
+ pub fn dimensions(&self) -> (u32, u32) {
+ self.dimensions
+ }
+
+ /// Decoded tile pixel format.
+ pub fn pixel_format(&self) -> PixelFormat {
+ self.fmt
+ }
+}
+
+impl Clone for MetalTextureTile {
+ fn clone(&self) -> Self {
+ Self {
+ texture: self.texture.clone(),
+ access_gate: Arc::clone(&self.access_gate),
+ dimensions: self.dimensions,
+ fmt: self.fmt,
+ }
+ }
+}
diff --git a/crates/j2k-jpeg-metal/src/tests/textures/residency.rs b/crates/j2k-jpeg-metal/src/tests/textures/residency.rs
index 7e368415..90d51bff 100644
--- a/crates/j2k-jpeg-metal/src/tests/textures/residency.rs
+++ b/crates/j2k-jpeg-metal/src/tests/textures/residency.rs
@@ -15,6 +15,10 @@ fn jpeg_device_decode_uses_private_internal_planes() {
.decode_to_device_with_session(PixelFormat::Rgb8, &session)
.expect("resident JPEG Metal decode");
assert_eq!(surface.residency(), SurfaceResidency::MetalResidentDecode);
+ let resident = surface
+ .resident_metal_image()
+ .expect("completed JPEG decode is resident");
+ assert_eq!(resident.dimensions(), surface.dimensions());
assert!(
compute::jpeg_private_buffer_allocations_for_test() > 0,
"resident JPEG Metal decode should use Private internal planes"
@@ -44,8 +48,10 @@ fn jpeg_private_rgb8_tile_uses_private_output_buffer() {
assert_eq!(raw_buffer.storage_mode(), metal::MTLStorageMode::Private);
assert!(tile.status_buffer_trusted().length() > 0);
- let handed_off = tile.clone().into_buffer();
- assert_eq!(handed_off.storage_mode(), metal::MTLStorageMode::Private);
+ let resident = tile.resident_image();
+ assert_eq!(resident.dimensions(), (16, 16));
+ let handed_off = tile.clone().into_resident_image();
+ assert_eq!(handed_off.pixel_format(), PixelFormat::Rgb8);
assert_eq!(tile.dimensions(), (16, 16));
}
diff --git a/crates/j2k-jpeg-metal/tests/encode.rs b/crates/j2k-jpeg-metal/tests/encode.rs
index a0a3fb40..c0b04e37 100644
--- a/crates/j2k-jpeg-metal/tests/encode.rs
+++ b/crates/j2k-jpeg-metal/tests/encode.rs
@@ -135,19 +135,19 @@ fn metal_baseline_encoder_round_trips_rgb_422() {
let session = MetalBackendSession::system_default().expect("Metal backend session");
let buffer = j2k_metal_support::checked_shared_buffer_with_slice(session.device(), &rgb)
.expect("upload test RGB pixels");
-
- // SAFETY: the buffer was initialized before tile construction and no CPU
- // or GPU writer accesses it while the tile is alive.
- let tile = unsafe {
- JpegBaselineMetalEncodeTile::new(
- &buffer,
- 0,
- (width, height),
- width as usize * 3,
- (width, height),
- PixelFormat::Rgb8,
- )
- };
+ let layout = j2k_metal_support::MetalImageLayout::new(
+ 0,
+ (width, height),
+ width as usize * 3,
+ PixelFormat::Rgb8,
+ )
+ .expect("valid resident JPEG layout");
+ // SAFETY: the upload completed synchronously and the raw buffer is moved
+ // into the immutable resident owner without a surviving writable alias.
+ let resident =
+ unsafe { j2k_metal_support::ResidentMetalImage::from_completed_buffer(buffer, layout) }
+ .expect("resident JPEG input");
+ let tile = JpegBaselineMetalEncodeTile::from_resident(&resident, (width, height));
let encoded = encode_jpeg_baseline_from_metal_buffer(
tile,
JpegEncodeOptions {
diff --git a/crates/j2k-jpeg/Cargo.toml b/crates/j2k-jpeg/Cargo.toml
index 485a8183..e1c0ce2d 100644
--- a/crates/j2k-jpeg/Cargo.toml
+++ b/crates/j2k-jpeg/Cargo.toml
@@ -27,9 +27,9 @@ bench-libjpeg-turbo = []
bench-internals = []
[dependencies]
-j2k-codec-math = { path = "../j2k-codec-math", version = "=0.7.0" }
-j2k-core = { path = "../j2k-core", version = "=0.7.0" }
-j2k-profile = { path = "../j2k-profile", version = "=0.7.0" }
+j2k-codec-math = { path = "../j2k-codec-math", version = "=0.7.1" }
+j2k-core = { path = "../j2k-core", version = "=0.7.1" }
+j2k-profile = { path = "../j2k-profile", version = "=0.7.1" }
thiserror = { workspace = true }
memchr = { workspace = true }
rayon = { workspace = true }
diff --git a/crates/j2k-jpeg/fuzz/Cargo.lock b/crates/j2k-jpeg/fuzz/Cargo.lock
index 3459a6bd..463c59a2 100644
--- a/crates/j2k-jpeg/fuzz/Cargo.lock
+++ b/crates/j2k-jpeg/fuzz/Cargo.lock
@@ -77,18 +77,18 @@ dependencies = [
[[package]]
name = "j2k-codec-math"
-version = "0.7.0"
+version = "0.7.1"
[[package]]
name = "j2k-core"
-version = "0.7.0"
+version = "0.7.1"
dependencies = [
"thiserror",
]
[[package]]
name = "j2k-jpeg"
-version = "0.7.0"
+version = "0.7.1"
dependencies = [
"j2k-codec-math",
"j2k-core",
@@ -108,7 +108,7 @@ dependencies = [
[[package]]
name = "j2k-profile"
-version = "0.7.0"
+version = "0.7.1"
[[package]]
name = "jobserver"
diff --git a/crates/j2k-metal-support/Cargo.toml b/crates/j2k-metal-support/Cargo.toml
index 22327777..b31dac06 100644
--- a/crates/j2k-metal-support/Cargo.toml
+++ b/crates/j2k-metal-support/Cargo.toml
@@ -19,7 +19,7 @@ name = "j2k_metal_support"
path = "src/lib.rs"
[dependencies]
-j2k-core = { path = "../j2k-core", version = "=0.7.0" }
+j2k-core = { path = "../j2k-core", version = "=0.7.1" }
log = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
diff --git a/crates/j2k-metal-support/src/error.rs b/crates/j2k-metal-support/src/error.rs
index c08fd2cb..cca54607 100644
--- a/crates/j2k-metal-support/src/error.rs
+++ b/crates/j2k-metal-support/src/error.rs
@@ -88,6 +88,18 @@ pub enum MetalSupportError {
/// Metal buffer length in bytes.
buffer_len: usize,
},
+ /// Metal image layout metadata is internally inconsistent or overflows.
+ MetalImageLayout {
+ /// Stable description of the invalid layout property.
+ reason: &'static str,
+ },
+ /// A resident image was used with a different Metal device.
+ MetalImageDeviceMismatch {
+ /// Registry identifier recorded by the resident image.
+ image_registry_id: u64,
+ /// Registry identifier of the requested Metal device.
+ requested_registry_id: u64,
+ },
/// A requested typed buffer view is not aligned for the element type.
BufferAlignment {
/// Byte offset requested by the caller.
@@ -170,15 +182,11 @@ impl fmt::Display for MetalSupportError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MetalUnavailable => f.write_str("Metal is unavailable on this host"),
- Self::CommandQueueUnavailable => {
- f.write_str("Metal command queue is unavailable on this host")
- }
+ Self::CommandQueueUnavailable => f.write_str("Metal command queue is unavailable"),
Self::CommandQueue { message } => {
write!(f, "Metal command queue creation failed: {message}")
}
- Self::CommandBufferUnavailable => {
- f.write_str("Metal command buffer is unavailable for the selected queue")
- }
+ Self::CommandBufferUnavailable => f.write_str("Metal command buffer is unavailable"),
Self::CommandBufferCreation { message } => {
write!(f, "Metal command buffer creation failed: {message}")
}
@@ -216,6 +224,14 @@ impl fmt::Display for MetalSupportError {
f,
"Metal buffer range offset {offset_bytes} length {byte_len} exceeds buffer length {buffer_len}"
),
+ Self::MetalImageLayout { reason } => write!(f, "invalid Metal image layout: {reason}"),
+ Self::MetalImageDeviceMismatch {
+ image_registry_id,
+ requested_registry_id,
+ } => write!(
+ f,
+ "Metal image device {image_registry_id} does not match requested device {requested_registry_id}"
+ ),
Self::BufferAlignment {
offset_bytes,
align,
@@ -260,12 +276,8 @@ impl fmt::Display for MetalSupportError {
Self::TextureAllocation { message } => {
write!(f, "Metal texture allocation dispatch failed: {message}")
}
- Self::TextureDescriptorUnavailable => {
- f.write_str("Metal texture descriptor allocation returned nil")
- }
- Self::BufferContentsUnavailable => {
- f.write_str("Metal buffer contents are not CPU-visible")
- }
+ Self::TextureDescriptorUnavailable => f.write_str("Metal texture is unavailable"),
+ Self::BufferContentsUnavailable => f.write_str("Metal buffer is not CPU-visible"),
}
}
}
diff --git a/crates/j2k-metal-support/src/lib.rs b/crates/j2k-metal-support/src/lib.rs
index a40ec95f..ed305cea 100644
--- a/crates/j2k-metal-support/src/lib.rs
+++ b/crates/j2k-metal-support/src/lib.rs
@@ -26,6 +26,8 @@ mod dispatch;
#[cfg(target_os = "macos")]
mod pipeline;
#[cfg(target_os = "macos")]
+mod resident;
+#[cfg(target_os = "macos")]
mod runtime;
#[cfg(target_os = "macos")]
@@ -46,6 +48,8 @@ pub use dispatch::{
#[cfg(target_os = "macos")]
pub use pipeline::{named_pipeline, shader_library, MetalPipelineLoader};
#[cfg(target_os = "macos")]
+pub use resident::{MetalImageLayout, ResidentMetalImage, SubmittedMetalImages};
+#[cfg(target_os = "macos")]
pub use runtime::{
checked_blit_command_encoder, checked_command_buffer, checked_command_queue,
checked_compute_command_encoder, commit_and_wait, ensure_completed, system_default_device,
diff --git a/crates/j2k-metal-support/src/resident.rs b/crates/j2k-metal-support/src/resident.rs
new file mode 100644
index 00000000..e1b91aa6
--- /dev/null
+++ b/crates/j2k-metal-support/src/resident.rs
@@ -0,0 +1,412 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use std::{fmt, mem};
+
+use j2k_core::{DeviceSubmission, PixelFormat};
+use metal::{Buffer, BufferRef, CommandBuffer, DeviceRef, MTLCommandBufferStatus};
+
+use crate::{wait_for_completion, MetalSupportError};
+
+/// Validated byte layout for one image stored in a Metal buffer.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct MetalImageLayout {
+ byte_offset: usize,
+ dimensions: (u32, u32),
+ pitch_bytes: usize,
+ pixel_format: PixelFormat,
+ byte_len: usize,
+}
+
+impl MetalImageLayout {
+ /// Validate image geometry and construct a Metal buffer layout.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`MetalSupportError::MetalImageLayout`] when row geometry or
+ /// the represented byte range overflows, or when the pitch is shorter than
+ /// one pixel row.
+ pub fn new(
+ byte_offset: usize,
+ dimensions: (u32, u32),
+ pitch_bytes: usize,
+ pixel_format: PixelFormat,
+ ) -> Result {
+ if dimensions.0 == 0 || dimensions.1 == 0 {
+ return Err(MetalSupportError::MetalImageLayout {
+ reason: "image dimensions must be nonzero",
+ });
+ }
+ let row_bytes = usize::try_from(dimensions.0)
+ .ok()
+ .and_then(|width| width.checked_mul(pixel_format.bytes_per_pixel()))
+ .ok_or(MetalSupportError::MetalImageLayout {
+ reason: "pixel row byte count overflows usize",
+ })?;
+ if pitch_bytes < row_bytes {
+ return Err(MetalSupportError::MetalImageLayout {
+ reason: "pitch is shorter than one pixel row",
+ });
+ }
+ let byte_len = pitch_bytes.checked_mul(dimensions.1 as usize).ok_or(
+ MetalSupportError::MetalImageLayout {
+ reason: "pitched image byte length overflows usize",
+ },
+ )?;
+ byte_offset
+ .checked_add(byte_len)
+ .ok_or(MetalSupportError::MetalImageLayout {
+ reason: "image byte range overflows usize",
+ })?;
+ Ok(Self {
+ byte_offset,
+ dimensions,
+ pitch_bytes,
+ pixel_format,
+ byte_len,
+ })
+ }
+
+ /// Byte offset of the first pixel row.
+ #[must_use]
+ pub const fn byte_offset(self) -> usize {
+ self.byte_offset
+ }
+
+ /// Image dimensions in pixels.
+ #[must_use]
+ pub const fn dimensions(self) -> (u32, u32) {
+ self.dimensions
+ }
+
+ /// Number of bytes between consecutive image rows.
+ #[must_use]
+ pub const fn pitch_bytes(self) -> usize {
+ self.pitch_bytes
+ }
+
+ /// Pixel format stored in the image range.
+ #[must_use]
+ pub const fn pixel_format(self) -> PixelFormat {
+ self.pixel_format
+ }
+
+ /// Number of bytes represented by the pitched image.
+ #[must_use]
+ pub const fn byte_len(self) -> usize {
+ self.byte_len
+ }
+}
+
+/// Owned, logically immutable Metal-resident image.
+///
+/// Clones retain the private Metal allocation for read-only GPU use. Safe APIs
+/// never expose the raw Metal handle, so mutation requires crossing the
+/// documented unsafe interop boundary.
+#[derive(Clone)]
+pub struct ResidentMetalImage {
+ buffer: Buffer,
+ device_registry_id: u64,
+ buffer_len: usize,
+ layout: MetalImageLayout,
+}
+
+impl fmt::Debug for ResidentMetalImage {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("ResidentMetalImage")
+ .field("device_registry_id", &self.device_registry_id())
+ .field("buffer_len", &self.buffer_len)
+ .field("layout", &self.layout)
+ .finish_non_exhaustive()
+ }
+}
+
+impl ResidentMetalImage {
+ /// Adopt a completed, uniquely controlled Metal buffer as an immutable image.
+ ///
+ /// # Safety
+ ///
+ /// All CPU and GPU writes to the described range must have completed. The
+ /// caller must ensure no surviving raw handle can mutate the allocation for
+ /// the lifetime of this image or any clone derived from it.
+ ///
+ /// # Errors
+ ///
+ /// Returns a typed layout or bounds error when the image range does not fit
+ /// in the allocation.
+ pub unsafe fn from_completed_buffer(
+ buffer: Buffer,
+ layout: MetalImageLayout,
+ ) -> Result {
+ let buffer_len = buffer_len(&buffer)?;
+ validate_bounds(layout, buffer_len)?;
+ Ok(Self::from_validated(buffer, buffer_len, layout))
+ }
+
+ /// Adopt an exclusively controlled Metal buffer before its producer completes.
+ ///
+ /// This is a low-level building block for backends that already own a
+ /// command submission. Prefer [`SubmittedMetalImages`] for new submission
+ /// APIs because it makes the completion lifetime explicit.
+ ///
+ /// # Safety
+ ///
+ /// The only incomplete access may be writes encoded by the producer
+ /// submission retained by the caller. The image and every raw allocation
+ /// alias must remain inaccessible to readers and other writers until that
+ /// submission completes successfully. On producer failure, the image must
+ /// not be exposed. After completion, no writer may access the allocation
+ /// for the lifetime of this image or its clones.
+ ///
+ /// # Errors
+ ///
+ /// Returns a typed layout or bounds error when the image range does not fit
+ /// in the allocation.
+ #[doc(hidden)]
+ pub unsafe fn from_exclusive_pending_buffer(
+ buffer: Buffer,
+ layout: MetalImageLayout,
+ ) -> Result {
+ let buffer_len = buffer_len(&buffer)?;
+ validate_bounds(layout, buffer_len)?;
+ Ok(Self::from_validated(buffer, buffer_len, layout))
+ }
+
+ fn from_validated(buffer: Buffer, buffer_len: usize, layout: MetalImageLayout) -> Self {
+ let device_registry_id = buffer.device().registry_id();
+ Self {
+ buffer,
+ device_registry_id,
+ buffer_len,
+ layout,
+ }
+ }
+
+ /// Construct another immutable view into the same allocation.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`MetalSupportError::BufferBounds`] when the requested view does
+ /// not fit in the allocation.
+ pub fn view(&self, layout: MetalImageLayout) -> Result {
+ validate_bounds(layout, self.buffer_len)?;
+ let parent_end = self
+ .layout
+ .byte_offset()
+ .checked_add(self.layout.byte_len())
+ .ok_or(MetalSupportError::MetalImageLayout {
+ reason: "resident image byte range overflows usize",
+ })?;
+ let child_end = layout.byte_offset().checked_add(layout.byte_len()).ok_or(
+ MetalSupportError::MetalImageLayout {
+ reason: "image view byte range overflows usize",
+ },
+ )?;
+ if layout.byte_offset() < self.layout.byte_offset() || child_end > parent_end {
+ return Err(MetalSupportError::MetalImageLayout {
+ reason: "image view falls outside the resident image range",
+ });
+ }
+ Ok(Self {
+ buffer: self.buffer.clone(),
+ device_registry_id: self.device_registry_id,
+ buffer_len: self.buffer_len,
+ layout,
+ })
+ }
+
+ /// Validated layout of this image view.
+ #[must_use]
+ pub const fn layout(&self) -> MetalImageLayout {
+ self.layout
+ }
+
+ /// Byte offset of the first image row.
+ #[must_use]
+ pub const fn byte_offset(&self) -> usize {
+ self.layout.byte_offset()
+ }
+
+ /// Image dimensions in pixels.
+ #[must_use]
+ pub const fn dimensions(&self) -> (u32, u32) {
+ self.layout.dimensions()
+ }
+
+ /// Number of bytes between consecutive image rows.
+ #[must_use]
+ pub const fn pitch_bytes(&self) -> usize {
+ self.layout.pitch_bytes()
+ }
+
+ /// Pixel format stored in this image.
+ #[must_use]
+ pub const fn pixel_format(&self) -> PixelFormat {
+ self.layout.pixel_format()
+ }
+
+ /// Number of bytes represented by this image view.
+ #[must_use]
+ pub const fn byte_len(&self) -> usize {
+ self.layout.byte_len()
+ }
+
+ /// Metal device registry identifier that owns the allocation.
+ #[must_use]
+ pub fn device_registry_id(&self) -> u64 {
+ self.device_registry_id
+ }
+
+ /// Validate that a Metal device can use this image.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`MetalSupportError::MetalImageDeviceMismatch`] for a different
+ /// Metal device.
+ pub fn validate_device(&self, device: &DeviceRef) -> Result<(), MetalSupportError> {
+ validate_registry_id(self.device_registry_id(), device.registry_id())
+ }
+
+ /// Borrow the raw Metal allocation for an audited backend operation.
+ ///
+ /// # Safety
+ ///
+ /// The caller may bind the returned handle only for GPU reads covered by a
+ /// submission that retains either this image or a private allocation handle
+ /// until completion. Any derived raw handles must remain inside the audited
+ /// backend boundary. No CPU or GPU writer may access the allocation.
+ #[must_use]
+ pub unsafe fn raw_buffer(&self) -> &Buffer {
+ &self.buffer
+ }
+}
+
+/// Submitted Metal work that resolves to immutable resident images.
+pub struct SubmittedMetalImages {
+ command_buffer: Option,
+ outputs: Vec,
+ inputs: Vec,
+}
+
+impl fmt::Debug for SubmittedMetalImages {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("SubmittedMetalImages")
+ .field("pending", &self.command_buffer.is_some())
+ .field("output_count", &self.outputs.len())
+ .field("input_count", &self.inputs.len())
+ .finish()
+ }
+}
+
+impl SubmittedMetalImages {
+ /// Wrap an uncommitted command buffer and its exclusively owned outputs.
+ ///
+ /// # Safety
+ ///
+ /// `command_buffer` must belong to `device`. Each output buffer must be a
+ /// fresh allocation whose only pending writer is encoded in that command
+ /// buffer, and no raw alias capable of later mutation may survive this
+ /// call. Every resident input whose raw buffer was bound by the command must
+ /// be included in `inputs`.
+ ///
+ /// # Errors
+ ///
+ /// Returns a typed layout, bounds, or device-identity error before the
+ /// command buffer is committed.
+ pub unsafe fn from_uncommitted(
+ device: &DeviceRef,
+ command_buffer: CommandBuffer,
+ outputs: Vec<(Buffer, MetalImageLayout)>,
+ inputs: Vec,
+ ) -> Result {
+ if outputs.is_empty() {
+ return Err(MetalSupportError::MetalImageLayout {
+ reason: "a Metal image submission must contain an output",
+ });
+ }
+ let registry_id = device.registry_id();
+ for input in &inputs {
+ validate_registry_id(input.device_registry_id(), registry_id)?;
+ }
+ let outputs = outputs
+ .into_iter()
+ .map(|(buffer, layout)| {
+ validate_registry_id(buffer.device().registry_id(), registry_id)?;
+ let len = buffer_len(&buffer)?;
+ validate_bounds(layout, len)?;
+ Ok(ResidentMetalImage::from_validated(buffer, len, layout))
+ })
+ .collect::, MetalSupportError>>()?;
+ Ok(Self {
+ command_buffer: Some(command_buffer),
+ outputs,
+ inputs,
+ })
+ }
+
+ fn complete(&mut self) -> Result<(), MetalSupportError> {
+ let Some(command_buffer) = self.command_buffer.take() else {
+ return Ok(());
+ };
+ if matches!(
+ command_buffer.status(),
+ MTLCommandBufferStatus::NotEnqueued | MTLCommandBufferStatus::Enqueued
+ ) {
+ command_buffer.commit();
+ }
+ wait_for_completion(&command_buffer)
+ }
+}
+
+impl DeviceSubmission for SubmittedMetalImages {
+ type Output = Vec;
+ type Error = MetalSupportError;
+
+ fn wait(mut self) -> Result {
+ self.complete()?;
+ Ok(mem::take(&mut self.outputs))
+ }
+}
+
+impl Drop for SubmittedMetalImages {
+ fn drop(&mut self) {
+ if let Err(error) = self.complete() {
+ log::error!("Metal image submission failed while being dropped: {error}");
+ }
+ }
+}
+
+fn buffer_len(buffer: &BufferRef) -> Result {
+ usize::try_from(buffer.length()).map_err(|_| MetalSupportError::MetalImageLayout {
+ reason: "Metal buffer length exceeds usize",
+ })
+}
+
+fn validate_bounds(layout: MetalImageLayout, buffer_len: usize) -> Result<(), MetalSupportError> {
+ let end = layout.byte_offset().checked_add(layout.byte_len()).ok_or(
+ MetalSupportError::MetalImageLayout {
+ reason: "image byte range overflows usize",
+ },
+ )?;
+ if end > buffer_len {
+ return Err(MetalSupportError::BufferBounds {
+ offset_bytes: layout.byte_offset(),
+ byte_len: layout.byte_len(),
+ buffer_len,
+ });
+ }
+ Ok(())
+}
+
+pub(crate) fn validate_registry_id(
+ image_registry_id: u64,
+ requested_registry_id: u64,
+) -> Result<(), MetalSupportError> {
+ if image_registry_id == requested_registry_id {
+ Ok(())
+ } else {
+ Err(MetalSupportError::MetalImageDeviceMismatch {
+ image_registry_id,
+ requested_registry_id,
+ })
+ }
+}
diff --git a/crates/j2k-metal-support/src/runtime.rs b/crates/j2k-metal-support/src/runtime.rs
index f4eddbf2..a6f5e7d0 100644
--- a/crates/j2k-metal-support/src/runtime.rs
+++ b/crates/j2k-metal-support/src/runtime.rs
@@ -252,6 +252,26 @@ pub fn wait_for_completion(command_buffer: &CommandBufferRef) -> Result<(), Meta
ensure_completed(command_buffer)
}
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) enum CommandBufferCompletion {
+ Completed,
+ Incomplete,
+ Failed,
+}
+
+pub(crate) const fn classify_command_buffer_status(
+ status: MTLCommandBufferStatus,
+) -> CommandBufferCompletion {
+ match status {
+ MTLCommandBufferStatus::Completed => CommandBufferCompletion::Completed,
+ MTLCommandBufferStatus::Error => CommandBufferCompletion::Failed,
+ MTLCommandBufferStatus::NotEnqueued
+ | MTLCommandBufferStatus::Enqueued
+ | MTLCommandBufferStatus::Committed
+ | MTLCommandBufferStatus::Scheduled => CommandBufferCompletion::Incomplete,
+ }
+}
+
/// Surface a failed command buffer after the caller has already synchronized it.
///
/// # Errors
@@ -260,12 +280,13 @@ pub fn wait_for_completion(command_buffer: &CommandBufferRef) -> Result<(), Meta
/// [`MTLCommandBufferStatus::Completed`].
pub fn ensure_completed(command_buffer: &CommandBufferRef) -> Result<(), MetalSupportError> {
let status = command_buffer.status();
- if status == MTLCommandBufferStatus::Completed {
- Ok(())
- } else {
- Err(MetalSupportError::CommandBuffer {
- label: "unlabeled".to_string(),
- status: format!("{status:?}"),
- })
+ match classify_command_buffer_status(status) {
+ CommandBufferCompletion::Completed => Ok(()),
+ CommandBufferCompletion::Incomplete | CommandBufferCompletion::Failed => {
+ Err(MetalSupportError::CommandBuffer {
+ label: "unlabeled".to_string(),
+ status: format!("{status:?}"),
+ })
+ }
}
}
diff --git a/crates/j2k-metal-support/src/tests.rs b/crates/j2k-metal-support/src/tests.rs
index 580ff395..381e89b9 100644
--- a/crates/j2k-metal-support/src/tests.rs
+++ b/crates/j2k-metal-support/src/tests.rs
@@ -18,9 +18,37 @@ use crate::{
runtime::{
checked_blit_encoder_from_autoreleased_ptr, checked_command_buffer_from_autoreleased_ptr,
checked_command_queue_from_retained_ptr, checked_compute_encoder_from_autoreleased_ptr,
+ classify_command_buffer_status, CommandBufferCompletion,
},
system_default_device, two_d_threads_per_group, MetalCommandEncoderKind, MetalSupportError,
};
+mod resident;
+
+#[test]
+fn command_buffer_status_classification_covers_every_metal_state() {
+ use metal::MTLCommandBufferStatus as Status;
+
+ assert_eq!(
+ classify_command_buffer_status(Status::Completed),
+ CommandBufferCompletion::Completed
+ );
+ assert_eq!(
+ classify_command_buffer_status(Status::Error),
+ CommandBufferCompletion::Failed
+ );
+ for status in [
+ Status::NotEnqueued,
+ Status::Enqueued,
+ Status::Committed,
+ Status::Scheduled,
+ ] {
+ assert_eq!(
+ classify_command_buffer_status(status),
+ CommandBufferCompletion::Incomplete,
+ "{status:?}"
+ );
+ }
+}
#[derive(Clone, Copy)]
struct ZeroSizedAbi;
diff --git a/crates/j2k-metal-support/src/tests/resident.rs b/crates/j2k-metal-support/src/tests/resident.rs
new file mode 100644
index 00000000..d2fac6e7
--- /dev/null
+++ b/crates/j2k-metal-support/src/tests/resident.rs
@@ -0,0 +1,214 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use crate::{
+ checked_blit_command_encoder, checked_buffer_read_vec, checked_command_buffer,
+ checked_command_queue, checked_shared_buffer_for_len, checked_shared_buffer_with_slice,
+ system_default_device, MetalImageLayout, MetalSupportError, ResidentMetalImage,
+ SubmittedMetalImages,
+};
+use j2k_core::{DeviceSubmission, PixelFormat};
+
+#[test]
+fn metal_image_layout_rejects_short_pitch_and_overflow() {
+ assert!(matches!(
+ MetalImageLayout::new(0, (0, 2), 0, PixelFormat::Gray8),
+ Err(MetalSupportError::MetalImageLayout { .. })
+ ));
+ assert!(matches!(
+ MetalImageLayout::new(0, (2, 0), 2, PixelFormat::Gray8),
+ Err(MetalSupportError::MetalImageLayout { .. })
+ ));
+ assert!(matches!(
+ MetalImageLayout::new(0, (4, 2), 11, PixelFormat::Rgb8),
+ Err(MetalSupportError::MetalImageLayout { .. })
+ ));
+ assert!(matches!(
+ MetalImageLayout::new(0, (1, 2), usize::MAX, PixelFormat::Gray8),
+ Err(MetalSupportError::MetalImageLayout { .. })
+ ));
+}
+
+#[test]
+fn resident_metal_image_validates_bounds_and_device_identity() {
+ if !j2k_test_support::metal_runtime_gate(module_path!()) {
+ return;
+ }
+ let Ok(device) = system_default_device() else {
+ j2k_test_support::metal_device_unavailable_is_skip(module_path!());
+ return;
+ };
+ let buffer = checked_shared_buffer_for_len::(&device, 9).expect("bounded buffer");
+ let layout =
+ MetalImageLayout::new(1, (4, 2), 4, PixelFormat::Gray8).expect("valid image layout");
+
+ // SAFETY: The buffer has never been submitted or aliased for mutation.
+ let image = unsafe { ResidentMetalImage::from_completed_buffer(buffer, layout) }
+ .expect("bounded resident image");
+ assert_eq!(image.device_registry_id(), device.registry_id());
+ assert_eq!(image.byte_len(), 8);
+
+ let out_of_bounds =
+ MetalImageLayout::new(2, (4, 2), 4, PixelFormat::Gray8).expect("valid standalone layout");
+ assert!(matches!(
+ image.view(out_of_bounds),
+ Err(MetalSupportError::BufferBounds { .. })
+ ));
+}
+
+#[test]
+fn resident_metal_image_views_stay_within_the_parent_image_range() {
+ if !j2k_test_support::metal_runtime_gate(module_path!()) {
+ return;
+ }
+ let Ok(device) = system_default_device() else {
+ j2k_test_support::metal_device_unavailable_is_skip(module_path!());
+ return;
+ };
+ let buffer = checked_shared_buffer_for_len::(&device, 16).expect("bounded buffer");
+ let parent =
+ MetalImageLayout::new(4, (4, 2), 4, PixelFormat::Gray8).expect("parent image layout");
+ // SAFETY: The buffer has never been submitted or aliased for mutation.
+ let image = unsafe { ResidentMetalImage::from_completed_buffer(buffer, parent) }
+ .expect("resident parent image");
+
+ let child =
+ MetalImageLayout::new(8, (4, 1), 4, PixelFormat::Gray8).expect("child image layout");
+ assert!(image.view(child).is_ok());
+
+ let sibling = MetalImageLayout::new(12, (4, 1), 4, PixelFormat::Gray8)
+ .expect("allocation-bounded sibling layout");
+ assert!(matches!(
+ image.view(sibling),
+ Err(MetalSupportError::MetalImageLayout { .. })
+ ));
+}
+
+#[test]
+fn submitted_metal_images_waits_and_returns_immutable_outputs() {
+ if !j2k_test_support::metal_runtime_gate(module_path!()) {
+ return;
+ }
+ let Ok(device) = system_default_device() else {
+ j2k_test_support::metal_device_unavailable_is_skip(module_path!());
+ return;
+ };
+ let queue = checked_command_queue(&device).expect("Metal command queue");
+ let command_buffer = checked_command_buffer(&queue).expect("Metal command buffer");
+ let output = checked_shared_buffer_for_len::(&device, 4).expect("output buffer");
+ let blit = checked_blit_command_encoder(&command_buffer).expect("Metal blit encoder");
+ blit.fill_buffer(&output, metal::NSRange::new(0, 4), 7);
+ blit.end_encoding();
+ let layout = MetalImageLayout::new(0, (4, 1), 4, PixelFormat::Gray8).expect("output layout");
+
+ // SAFETY: `output` is fresh, the command buffer is its only writer, and no
+ // raw handle to the allocation survives this move into the submission.
+ let submitted = unsafe {
+ SubmittedMetalImages::from_uncommitted(
+ &device,
+ command_buffer,
+ vec![(output, layout)],
+ vec![],
+ )
+ }
+ .expect("valid Metal image submission");
+ let images = submitted.wait().expect("completed Metal image submission");
+
+ assert_eq!(images.len(), 1);
+ assert_eq!(images[0].dimensions(), (4, 1));
+ assert_eq!(images[0].pixel_format(), PixelFormat::Gray8);
+}
+
+#[test]
+fn submitted_metal_images_retain_inputs_until_completion() {
+ if !j2k_test_support::metal_runtime_gate(module_path!()) {
+ return;
+ }
+ let Ok(device) = system_default_device() else {
+ j2k_test_support::metal_device_unavailable_is_skip(module_path!());
+ return;
+ };
+ let queue = checked_command_queue(&device).expect("Metal command queue");
+ let command_buffer = checked_command_buffer(&queue).expect("Metal command buffer");
+ let input_buffer =
+ checked_shared_buffer_with_slice(&device, &[1u8, 2, 3, 4]).expect("resident input buffer");
+ let layout =
+ MetalImageLayout::new(0, (4, 1), 4, PixelFormat::Gray8).expect("resident input layout");
+ // SAFETY: The synchronous upload is complete and no writable alias is
+ // retained after the buffer is moved into the resident image.
+ let input = unsafe { ResidentMetalImage::from_completed_buffer(input_buffer, layout) }
+ .expect("resident input image");
+ let output = checked_shared_buffer_for_len::(&device, 4).expect("output buffer");
+ let blit = checked_blit_command_encoder(&command_buffer).expect("Metal blit encoder");
+ // SAFETY: The raw input is bound only for a read retained by `inputs` until
+ // the submission completes.
+ blit.copy_from_buffer(unsafe { input.raw_buffer() }, 0, &output, 0, 4);
+ blit.end_encoding();
+
+ // SAFETY: `output` is fresh and written only by this command; the resident
+ // input bound above is included in the submission keepalives.
+ let submitted = unsafe {
+ SubmittedMetalImages::from_uncommitted(
+ &device,
+ command_buffer,
+ vec![(output, layout)],
+ vec![input.clone()],
+ )
+ }
+ .expect("valid retained-input submission");
+ drop(input);
+ let images = submitted.wait().expect("retained-input completion");
+ // SAFETY: The submission has completed and the shared output is immutable.
+ let bytes =
+ unsafe { checked_buffer_read_vec::(images[0].raw_buffer(), 0, images[0].byte_len()) }
+ .expect("resident output readback");
+
+ assert_eq!(bytes, [1, 2, 3, 4]);
+}
+
+#[test]
+fn dropping_submitted_metal_images_completes_the_command_buffer() {
+ if !j2k_test_support::metal_runtime_gate(module_path!()) {
+ return;
+ }
+ let Ok(device) = system_default_device() else {
+ j2k_test_support::metal_device_unavailable_is_skip(module_path!());
+ return;
+ };
+ let queue = checked_command_queue(&device).expect("Metal command queue");
+ let command_buffer = checked_command_buffer(&queue).expect("Metal command buffer");
+ let observed_command_buffer = command_buffer.clone();
+ let output = checked_shared_buffer_for_len::(&device, 4).expect("output buffer");
+ let blit = checked_blit_command_encoder(&command_buffer).expect("Metal blit encoder");
+ blit.fill_buffer(&output, metal::NSRange::new(0, 4), 9);
+ blit.end_encoding();
+ let layout = MetalImageLayout::new(0, (4, 1), 4, PixelFormat::Gray8).expect("output layout");
+
+ // SAFETY: `output` is fresh, the command buffer is its only writer, and no
+ // raw allocation handle survives the move into the submission.
+ let submitted = unsafe {
+ SubmittedMetalImages::from_uncommitted(
+ &device,
+ command_buffer,
+ vec![(output, layout)],
+ vec![],
+ )
+ }
+ .expect("valid Metal image submission");
+ drop(submitted);
+
+ assert_eq!(
+ observed_command_buffer.status(),
+ metal::MTLCommandBufferStatus::Completed
+ );
+}
+
+#[test]
+fn metal_image_device_identity_reports_mismatched_registry_ids() {
+ assert!(matches!(
+ crate::resident::validate_registry_id(11, 29),
+ Err(MetalSupportError::MetalImageDeviceMismatch {
+ image_registry_id: 11,
+ requested_registry_id: 29,
+ })
+ ));
+}
diff --git a/crates/j2k-metal/Cargo.toml b/crates/j2k-metal/Cargo.toml
index 85f92877..12475b6d 100644
--- a/crates/j2k-metal/Cargo.toml
+++ b/crates/j2k-metal/Cargo.toml
@@ -24,12 +24,12 @@ name = "j2k_metal"
path = "src/lib.rs"
[dependencies]
-j2k-codec-math = { path = "../j2k-codec-math", version = "=0.7.0" }
-j2k-core = { path = "../j2k-core", version = "=0.7.0" }
-j2k = { path = "../j2k", version = "=0.7.0" }
-j2k-native = { path = "../j2k-native", version = "=0.7.0" }
-j2k-metal-support = { path = "../j2k-metal-support", version = "=0.7.0" }
-j2k-profile = { path = "../j2k-profile", version = "=0.7.0" }
+j2k-codec-math = { path = "../j2k-codec-math", version = "=0.7.1" }
+j2k-core = { path = "../j2k-core", version = "=0.7.1" }
+j2k = { path = "../j2k", version = "=0.7.1" }
+j2k-native = { path = "../j2k-native", version = "=0.7.1" }
+j2k-metal-support = { path = "../j2k-metal-support", version = "=0.7.1" }
+j2k-profile = { path = "../j2k-profile", version = "=0.7.1" }
thiserror = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
diff --git a/crates/j2k-metal/src/compute.rs b/crates/j2k-metal/src/compute.rs
index 66ad3d2e..c218ce7d 100644
--- a/crates/j2k-metal/src/compute.rs
+++ b/crates/j2k-metal/src/compute.rs
@@ -165,8 +165,8 @@ mod direct_flattened;
use self::direct_flattened::hybrid_cpu_decode_worker_count;
#[cfg(target_os = "macos")]
use self::direct_flattened::{
- build_flattened_cpu_tier1_cache, packed_cpu_decode_coefficients, packed_cpu_decode_output_len,
- FlattenedCpuTier1Cache,
+ build_flattened_cpu_tier1_cache, packed_cpu_decode_coefficients,
+ packed_cpu_decode_coefficients_in, packed_cpu_decode_output_len, FlattenedCpuTier1Cache,
};
mod direct_profile;
#[cfg(target_os = "macos")]
diff --git a/crates/j2k-metal/src/compute/abi.rs b/crates/j2k-metal/src/compute/abi.rs
index 4d8ba54b..50798757 100644
--- a/crates/j2k-metal/src/compute/abi.rs
+++ b/crates/j2k-metal/src/compute/abi.rs
@@ -263,6 +263,16 @@ pub(crate) struct J2kIdwtStatus {
pub(crate) reserved1: u32,
}
+#[cfg(target_os = "macos")]
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub(crate) struct J2kIdwt97StepParams {
+ pub(crate) coefficient: f32,
+ pub(crate) parity: u32,
+ pub(crate) _reserved0: u32,
+ pub(crate) _reserved1: u32,
+}
+
#[cfg(target_os = "macos")]
pub(crate) const J2K_MCT_STATUS_OK: u32 = 0;
#[cfg(target_os = "macos")]
@@ -988,6 +998,7 @@ impl_gpu_readback_abi!(
J2kIdwtSingleDecompositionParams,
J2kRepeatedIdwtSingleDecompositionParams,
J2kIdwtStatus,
+ J2kIdwt97StepParams,
J2kInverseMctParams,
J2kForwardRctParams,
J2kForwardIctParams,
diff --git a/crates/j2k-metal/src/compute/decode_dispatch.rs b/crates/j2k-metal/src/compute/decode_dispatch.rs
index 250433a1..abd6aaf9 100644
--- a/crates/j2k-metal/src/compute/decode_dispatch.rs
+++ b/crates/j2k-metal/src/compute/decode_dispatch.rs
@@ -5,14 +5,14 @@ use super::{
copied_slice_buffer, decode_classic_status_error, decode_idwt_status_error,
decode_mct_status_error, dispatch_1d_pipeline, dispatch_2d_pipeline, dispatch_3d_pipeline,
dispatch_ht_cleanup_batched_in_command_buffer, dispatch_ht_cleanup_batched_in_encoder,
- dispatch_ht_cleanup_repeated_batched_in_command_buffer, dispatch_single_thread,
- hybrid_stage_signpost, j2k_u32_param, label_compute_encoder, new_command_buffer,
- new_compute_command_encoder, new_shared_buffer, prepared_ht_buffer, size_of,
- take_classic_coefficients_scratch_buffer, take_classic_states_scratch_buffer, with_runtime,
- zeroed_shared_buffer, Buffer, CommandBufferRef, ComputeCommandEncoderRef,
- DirectIdwtCommandBuffers, DirectScratchBuffer, DirectStatusCheck, Error, HtCodeBlockDecodeJob,
- HtRepeatedCleanupDispatch, J2kClassicCleanupBatchJob, J2kClassicRepeatedBatchParams,
- J2kClassicSegment, J2kClassicStatus, J2kGrayStoreParams, J2kHtCleanupBatchJob,
+ dispatch_ht_cleanup_repeated_batched_in_command_buffer, hybrid_stage_signpost, j2k_u32_param,
+ label_compute_encoder, new_command_buffer, new_compute_command_encoder, new_shared_buffer,
+ prepared_ht_buffer, size_of, take_classic_coefficients_scratch_buffer,
+ take_classic_states_scratch_buffer, with_runtime, zeroed_shared_buffer, Buffer,
+ CommandBufferRef, ComputeCommandEncoderRef, DirectIdwtCommandBuffers, DirectScratchBuffer,
+ DirectStatusCheck, Error, HtCodeBlockDecodeJob, HtRepeatedCleanupDispatch,
+ J2kClassicCleanupBatchJob, J2kClassicRepeatedBatchParams, J2kClassicSegment, J2kClassicStatus,
+ J2kGrayStoreParams, J2kHtCleanupBatchJob, J2kIdwt97StepParams,
J2kIdwtSingleDecompositionParams, J2kIdwtStatus, J2kInverseMctJob, J2kInverseMctParams,
J2kMctStatus, J2kRepeatedGrayStoreParams, J2kRepeatedIdwtSingleDecompositionParams,
J2kRepeatedStoreParams, J2kSingleDecompositionIdwtJob, J2kStoreComponentJob, J2kStoreParams,
@@ -60,6 +60,8 @@ pub(in crate::compute) use self::ht_subband::{
encode_repeated_ht_sub_band_to_buffer_in_command_buffer, ht_batch_output_word_count,
ht_output_word_count, required_ht_output_len,
};
+#[cfg(test)]
+pub(crate) use self::idwt::decode_irreversible97_staged_single_decomposition_idwt;
pub(crate) use self::idwt::{
decode_irreversible97_single_decomposition_idwt, decode_reversible53_single_decomposition_idwt,
};
diff --git a/crates/j2k-metal/src/compute/decode_dispatch/idwt.rs b/crates/j2k-metal/src/compute/decode_dispatch/idwt.rs
index 6a26655e..f9056a66 100644
--- a/crates/j2k-metal/src/compute/decode_dispatch/idwt.rs
+++ b/crates/j2k-metal/src/compute/decode_dispatch/idwt.rs
@@ -1,15 +1,24 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use super::{
- checked_buffer_read, checked_buffer_slice, commit_and_wait_metal, copied_slice_buffer,
- decode_idwt_status_error, dispatch_2d_pipeline, dispatch_3d_pipeline, dispatch_single_thread,
- hybrid_stage_signpost, label_compute_encoder, new_command_buffer, new_compute_command_encoder,
- size_of, with_runtime, zeroed_shared_buffer, Buffer, CommandBufferRef,
- ComputeCommandEncoderRef, DirectIdwtCommandBuffers, DirectStatusCheck, Error,
- J2kIdwtSingleDecompositionParams, J2kIdwtStatus, J2kRepeatedIdwtSingleDecompositionParams,
- J2kSingleDecompositionIdwtJob, MTLSize, MetalRuntime, J2K_IDWT_STATUS_OK,
+ checked_buffer_slice, commit_and_wait_metal, copied_slice_buffer, dispatch_2d_pipeline,
+ dispatch_3d_pipeline, hybrid_stage_signpost, label_compute_encoder, new_command_buffer,
+ new_compute_command_encoder, size_of, with_runtime, Buffer, CommandBufferRef,
+ ComputeCommandEncoderRef, DirectIdwtCommandBuffers, Error, J2kIdwtSingleDecompositionParams,
+ J2kRepeatedIdwtSingleDecompositionParams, J2kSingleDecompositionIdwtJob, MTLSize, MetalRuntime,
SIGNPOST_DECODE_HYBRID_IDWT_COMMAND_ENCODE,
};
+#[cfg(target_os = "macos")]
+mod irreversible;
+#[cfg(target_os = "macos")]
+pub(crate) use irreversible::decode_irreversible97_single_decomposition_idwt;
+#[cfg(all(target_os = "macos", test))]
+pub(crate) use irreversible::decode_irreversible97_staged_single_decomposition_idwt;
+#[cfg(target_os = "macos")]
+pub(in crate::compute) use irreversible::{
+ dispatch_irreversible97_single_decomposition_buffers_in_command_buffer_with_offsets,
+ dispatch_irreversible97_single_decomposition_buffers_in_encoder_with_offsets,
+};
#[cfg(target_os = "macos")]
#[expect(
@@ -368,149 +377,3 @@ pub(in crate::compute) fn dispatch_reversible53_repeated_buffers_in_command_buff
encoder.end_encoding();
Ok(())
}
-
-#[cfg(target_os = "macos")]
-pub(crate) fn decode_irreversible97_single_decomposition_idwt(
- job: J2kSingleDecompositionIdwtJob<'_>,
- output: &mut [f32],
-) -> Result<(), Error> {
- with_runtime(|runtime| {
- let required_len = job.rect.width() as usize * job.rect.height() as usize;
- if output.len() < required_len {
- return Err(Error::MetalKernel {
- message: "J2K Metal IDWT output slice is too small".to_string(),
- });
- }
-
- let params = J2kIdwtSingleDecompositionParams {
- x0: job.rect.x0,
- y0: job.rect.y0,
- output_x: 0,
- output_y: 0,
- width: job.rect.width(),
- height: job.rect.height(),
- ll_x: 0,
- ll_y: 0,
- ll_width: job.ll.rect.width(),
- ll_height: job.ll.rect.height(),
- hl_x: 0,
- hl_y: 0,
- hl_width: job.hl.rect.width(),
- hl_height: job.hl.rect.height(),
- lh_x: 0,
- lh_y: 0,
- lh_width: job.lh.rect.width(),
- lh_height: job.lh.rect.height(),
- hh_x: 0,
- hh_y: 0,
- hh_width: job.hh.rect.width(),
- hh_height: job.hh.rect.height(),
- };
-
- let ll = copied_slice_buffer(&runtime.device, job.ll.coefficients)?;
- let hl = copied_slice_buffer(&runtime.device, job.hl.coefficients)?;
- let lh = copied_slice_buffer(&runtime.device, job.lh.coefficients)?;
- let hh = copied_slice_buffer(&runtime.device, job.hh.coefficients)?;
- let decoded = copied_slice_buffer(&runtime.device, output)?;
- let status_buffer = zeroed_shared_buffer(&runtime.device, size_of::())?;
-
- let command_buffer = new_command_buffer(&runtime.queue)?;
- let encoder = new_compute_command_encoder(&command_buffer)?;
- encoder.set_compute_pipeline_state(&runtime.idwt_irreversible97_single_decomposition);
- encoder.set_buffer(0, Some(&ll), 0);
- encoder.set_buffer(1, Some(&hl), 0);
- encoder.set_buffer(2, Some(&lh), 0);
- encoder.set_buffer(3, Some(&hh), 0);
- encoder.set_buffer(4, Some(&decoded), 0);
- encoder.set_bytes(
- 5,
- size_of::() as u64,
- (&raw const params).cast(),
- );
- encoder.set_buffer(6, Some(&status_buffer), 0);
- dispatch_single_thread(&encoder);
- encoder.end_encoding();
- commit_and_wait_metal(&command_buffer)?;
-
- let status = checked_buffer_read::(&status_buffer, "IDWT status")?;
- if status.code != J2K_IDWT_STATUS_OK {
- return Err(decode_idwt_status_error(status));
- }
- let decoded_host = checked_buffer_slice::(&decoded, output.len(), "IDWT output")?;
- output.copy_from_slice(&decoded_host);
- Ok(())
- })
-}
-
-#[cfg(target_os = "macos")]
-pub(in crate::compute) fn dispatch_irreversible97_single_decomposition_buffers_in_command_buffer_with_offsets(
- command_buffer: &CommandBufferRef,
- dispatch: SingleIdwtDispatch<'_>,
-) -> Result {
- let _signpost = hybrid_stage_signpost(SIGNPOST_DECODE_HYBRID_IDWT_COMMAND_ENCODE);
- let status_buffer = zeroed_shared_buffer(&dispatch.runtime.device, size_of::())?;
-
- let encoder = new_compute_command_encoder(command_buffer)?;
- label_compute_encoder(&encoder, "J2K decode hybrid irreversible97 IDWT");
- dispatch_irreversible97_single_decomposition_buffers_in_encoder_with_status(
- &encoder,
- dispatch,
- &status_buffer,
- );
- encoder.end_encoding();
-
- Ok(DirectStatusCheck::Idwt(status_buffer))
-}
-
-#[cfg(target_os = "macos")]
-pub(in crate::compute) fn dispatch_irreversible97_single_decomposition_buffers_in_encoder_with_offsets(
- encoder: &ComputeCommandEncoderRef,
- dispatch: SingleIdwtDispatch<'_>,
-) -> Result {
- let status_buffer = zeroed_shared_buffer(&dispatch.runtime.device, size_of::())?;
- dispatch_irreversible97_single_decomposition_buffers_in_encoder_with_status(
- encoder,
- dispatch,
- &status_buffer,
- );
-
- Ok(DirectStatusCheck::Idwt(status_buffer))
-}
-
-#[cfg(target_os = "macos")]
-pub(in crate::compute) fn dispatch_irreversible97_single_decomposition_buffers_in_encoder_with_status(
- encoder: &ComputeCommandEncoderRef,
- dispatch: SingleIdwtDispatch<'_>,
- status_buffer: &Buffer,
-) {
- let SingleIdwtDispatch {
- runtime,
- sub_bands,
- params,
- decoded,
- decoded_offset,
- } = dispatch;
- let IdwtSubBandBuffers {
- ll,
- ll_offset,
- hl,
- hl_offset,
- lh,
- lh_offset,
- hh,
- hh_offset,
- } = sub_bands;
- encoder.set_compute_pipeline_state(&runtime.idwt_irreversible97_single_decomposition);
- encoder.set_buffer(0, Some(ll), ll_offset as u64);
- encoder.set_buffer(1, Some(hl), hl_offset as u64);
- encoder.set_buffer(2, Some(lh), lh_offset as u64);
- encoder.set_buffer(3, Some(hh), hh_offset as u64);
- encoder.set_buffer(4, Some(decoded), decoded_offset as u64);
- encoder.set_bytes(
- 5,
- size_of::() as u64,
- (&raw const params).cast(),
- );
- encoder.set_buffer(6, Some(status_buffer), 0);
- dispatch_single_thread(encoder);
-}
diff --git a/crates/j2k-metal/src/compute/decode_dispatch/idwt/irreversible.rs b/crates/j2k-metal/src/compute/decode_dispatch/idwt/irreversible.rs
new file mode 100644
index 00000000..7607a63e
--- /dev/null
+++ b/crates/j2k-metal/src/compute/decode_dispatch/idwt/irreversible.rs
@@ -0,0 +1,279 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use super::super::{
+ checked_buffer_read, checked_buffer_slice, commit_and_wait_metal, copied_slice_buffer,
+ decode_idwt_status_error, dispatch_2d_pipeline, hybrid_stage_signpost, label_compute_encoder,
+ new_command_buffer, new_compute_command_encoder, size_of, with_runtime, zeroed_shared_buffer,
+ Buffer, CommandBufferRef, ComputeCommandEncoderRef, DirectStatusCheck, Error,
+ J2kIdwt97StepParams, J2kIdwtSingleDecompositionParams, J2kIdwtStatus,
+ J2kSingleDecompositionIdwtJob, MetalRuntime, J2K_IDWT_STATUS_OK,
+ SIGNPOST_DECODE_HYBRID_IDWT_COMMAND_ENCODE,
+};
+use super::{IdwtSubBandBuffers, SingleIdwtDispatch};
+use j2k_codec_math::dwt;
+
+pub(crate) fn decode_irreversible97_single_decomposition_idwt(
+ job: J2kSingleDecompositionIdwtJob<'_>,
+ output: &mut [f32],
+) -> Result<(), Error> {
+ decode_irreversible97_staged_single_decomposition_idwt(job, output)
+}
+
+pub(crate) fn decode_irreversible97_staged_single_decomposition_idwt(
+ job: J2kSingleDecompositionIdwtJob<'_>,
+ output: &mut [f32],
+) -> Result<(), Error> {
+ with_runtime(|runtime| {
+ let required_len = job.rect.width() as usize * job.rect.height() as usize;
+ if output.len() < required_len {
+ return Err(Error::MetalKernel {
+ message: "J2K Metal IDWT output slice is too small".to_string(),
+ });
+ }
+
+ let params = J2kIdwtSingleDecompositionParams {
+ x0: job.rect.x0,
+ y0: job.rect.y0,
+ output_x: 0,
+ output_y: 0,
+ width: job.rect.width(),
+ height: job.rect.height(),
+ ll_x: 0,
+ ll_y: 0,
+ ll_width: job.ll.rect.width(),
+ ll_height: job.ll.rect.height(),
+ hl_x: 0,
+ hl_y: 0,
+ hl_width: job.hl.rect.width(),
+ hl_height: job.hl.rect.height(),
+ lh_x: 0,
+ lh_y: 0,
+ lh_width: job.lh.rect.width(),
+ lh_height: job.lh.rect.height(),
+ hh_x: 0,
+ hh_y: 0,
+ hh_width: job.hh.rect.width(),
+ hh_height: job.hh.rect.height(),
+ };
+
+ let ll = copied_slice_buffer(&runtime.device, job.ll.coefficients)?;
+ let hl = copied_slice_buffer(&runtime.device, job.hl.coefficients)?;
+ let lh = copied_slice_buffer(&runtime.device, job.lh.coefficients)?;
+ let hh = copied_slice_buffer(&runtime.device, job.hh.coefficients)?;
+ let decoded = copied_slice_buffer(&runtime.device, output)?;
+ let status_buffer = zeroed_shared_buffer(&runtime.device, size_of::())?;
+
+ let command_buffer = new_command_buffer(&runtime.queue)?;
+ let encoder = new_compute_command_encoder(&command_buffer)?;
+ dispatch_irreversible97_single_decomposition_buffers_in_encoder_with_status(
+ &encoder,
+ SingleIdwtDispatch {
+ runtime,
+ sub_bands: IdwtSubBandBuffers {
+ ll: &ll,
+ ll_offset: 0,
+ hl: &hl,
+ hl_offset: 0,
+ lh: &lh,
+ lh_offset: 0,
+ hh: &hh,
+ hh_offset: 0,
+ },
+ params,
+ decoded: &decoded,
+ decoded_offset: 0,
+ },
+ &status_buffer,
+ );
+ encoder.end_encoding();
+ commit_and_wait_metal(&command_buffer)?;
+
+ let status = checked_buffer_read::(&status_buffer, "IDWT status")?;
+ if status.code != J2K_IDWT_STATUS_OK {
+ return Err(decode_idwt_status_error(status));
+ }
+ let decoded_host = checked_buffer_slice::(&decoded, output.len(), "IDWT output")?;
+ output.copy_from_slice(&decoded_host);
+ Ok(())
+ })
+}
+
+pub(in crate::compute) fn dispatch_irreversible97_single_decomposition_buffers_in_command_buffer_with_offsets(
+ command_buffer: &CommandBufferRef,
+ dispatch: SingleIdwtDispatch<'_>,
+) -> Result {
+ let _signpost = hybrid_stage_signpost(SIGNPOST_DECODE_HYBRID_IDWT_COMMAND_ENCODE);
+ let status_buffer = zeroed_shared_buffer(&dispatch.runtime.device, size_of::())?;
+
+ let encoder = new_compute_command_encoder(command_buffer)?;
+ label_compute_encoder(&encoder, "J2K decode hybrid irreversible97 IDWT");
+ dispatch_irreversible97_single_decomposition_buffers_in_encoder_with_status(
+ &encoder,
+ dispatch,
+ &status_buffer,
+ );
+ encoder.end_encoding();
+
+ Ok(DirectStatusCheck::Idwt(status_buffer))
+}
+
+pub(in crate::compute) fn dispatch_irreversible97_single_decomposition_buffers_in_encoder_with_offsets(
+ encoder: &ComputeCommandEncoderRef,
+ dispatch: SingleIdwtDispatch<'_>,
+) -> Result {
+ let status_buffer = zeroed_shared_buffer(&dispatch.runtime.device, size_of::())?;
+ dispatch_irreversible97_single_decomposition_buffers_in_encoder_with_status(
+ encoder,
+ dispatch,
+ &status_buffer,
+ );
+
+ Ok(DirectStatusCheck::Idwt(status_buffer))
+}
+
+fn dispatch_irreversible97_single_decomposition_buffers_in_encoder_with_status(
+ encoder: &ComputeCommandEncoderRef,
+ dispatch: SingleIdwtDispatch<'_>,
+ _status_buffer: &Buffer,
+) {
+ let SingleIdwtDispatch {
+ runtime,
+ sub_bands,
+ params,
+ decoded,
+ decoded_offset,
+ } = dispatch;
+ let IdwtSubBandBuffers {
+ ll,
+ ll_offset,
+ hl,
+ hl_offset,
+ lh,
+ lh_offset,
+ hh,
+ hh_offset,
+ } = sub_bands;
+ encoder.set_compute_pipeline_state(&runtime.idwt_interleave);
+ encoder.set_buffer(0, Some(ll), ll_offset as u64);
+ encoder.set_buffer(1, Some(hl), hl_offset as u64);
+ encoder.set_buffer(2, Some(lh), lh_offset as u64);
+ encoder.set_buffer(3, Some(hh), hh_offset as u64);
+ encoder.set_buffer(4, Some(decoded), decoded_offset as u64);
+ encoder.set_bytes(
+ 5,
+ size_of::() as u64,
+ (&raw const params).cast(),
+ );
+ dispatch_2d_pipeline(
+ encoder,
+ &runtime.idwt_interleave,
+ (params.width, params.height),
+ );
+ encoder.memory_barrier_with_resources(&[decoded]);
+
+ dispatch_irreversible97_stages(encoder, runtime, decoded, decoded_offset, params);
+}
+
+fn dispatch_irreversible97_stages(
+ encoder: &ComputeCommandEncoderRef,
+ runtime: &MetalRuntime,
+ decoded: &Buffer,
+ decoded_offset: usize,
+ params: J2kIdwtSingleDecompositionParams,
+) {
+ encoder.set_compute_pipeline_state(&runtime.idwt_irreversible97_horizontal_scale);
+ encoder.set_buffer(0, Some(decoded), decoded_offset as u64);
+ encoder.set_bytes(
+ 1,
+ size_of::() as u64,
+ (&raw const params).cast(),
+ );
+ dispatch_2d_pipeline(
+ encoder,
+ &runtime.idwt_irreversible97_horizontal_scale,
+ (params.width, params.height),
+ );
+ encoder.memory_barrier_with_resources(&[decoded]);
+
+ let first_even_x = (params.x0 + params.output_x) & 1;
+ let first_odd_x = 1 - first_even_x;
+ encoder.set_compute_pipeline_state(&runtime.idwt_irreversible97_horizontal_step);
+ encoder.set_buffer(0, Some(decoded), decoded_offset as u64);
+ encoder.set_bytes(
+ 1,
+ size_of::() as u64,
+ (&raw const params).cast(),
+ );
+ for (coefficient, parity) in [
+ (dwt::IDWT97_NEG_DELTA_F32, first_even_x),
+ (dwt::IDWT97_NEG_GAMMA_F32, first_odd_x),
+ (dwt::IDWT97_NEG_BETA_F32, first_even_x),
+ (dwt::IDWT97_NEG_ALPHA_F32, first_odd_x),
+ ] {
+ let step = J2kIdwt97StepParams {
+ coefficient,
+ parity,
+ _reserved0: 0,
+ _reserved1: 0,
+ };
+ encoder.set_bytes(
+ 2,
+ size_of::() as u64,
+ (&raw const step).cast(),
+ );
+ dispatch_2d_pipeline(
+ encoder,
+ &runtime.idwt_irreversible97_horizontal_step,
+ (params.width, params.height),
+ );
+ encoder.memory_barrier_with_resources(&[decoded]);
+ }
+
+ encoder.set_compute_pipeline_state(&runtime.idwt_irreversible97_vertical_scale);
+ encoder.set_buffer(0, Some(decoded), decoded_offset as u64);
+ encoder.set_bytes(
+ 1,
+ size_of::() as u64,
+ (&raw const params).cast(),
+ );
+ dispatch_2d_pipeline(
+ encoder,
+ &runtime.idwt_irreversible97_vertical_scale,
+ (params.width, params.height),
+ );
+ encoder.memory_barrier_with_resources(&[decoded]);
+
+ let first_even_y = (params.y0 + params.output_y) & 1;
+ let first_odd_y = 1 - first_even_y;
+ encoder.set_compute_pipeline_state(&runtime.idwt_irreversible97_vertical_step);
+ encoder.set_buffer(0, Some(decoded), decoded_offset as u64);
+ encoder.set_bytes(
+ 1,
+ size_of::() as u64,
+ (&raw const params).cast(),
+ );
+ for (coefficient, parity) in [
+ (dwt::IDWT97_NEG_DELTA_F32, first_even_y),
+ (dwt::IDWT97_NEG_GAMMA_F32, first_odd_y),
+ (dwt::IDWT97_NEG_BETA_F32, first_even_y),
+ (dwt::IDWT97_NEG_ALPHA_F32, first_odd_y),
+ ] {
+ let step = J2kIdwt97StepParams {
+ coefficient,
+ parity,
+ _reserved0: 0,
+ _reserved1: 0,
+ };
+ encoder.set_bytes(
+ 2,
+ size_of::() as u64,
+ (&raw const step).cast(),
+ );
+ dispatch_2d_pipeline(
+ encoder,
+ &runtime.idwt_irreversible97_vertical_step,
+ (params.width, params.height),
+ );
+ encoder.memory_barrier_with_resources(&[decoded]);
+ }
+}
diff --git a/crates/j2k-metal/src/compute/decode_dispatch/mct.rs b/crates/j2k-metal/src/compute/decode_dispatch/mct.rs
index f72fe09d..ee5c2467 100644
--- a/crates/j2k-metal/src/compute/decode_dispatch/mct.rs
+++ b/crates/j2k-metal/src/compute/decode_dispatch/mct.rs
@@ -89,15 +89,9 @@ pub(crate) fn decode_inverse_mct(job: J2kInverseMctJob<'_>) -> Result(&plane0_buffer, len, "inverse MCT plane 0")?;
let plane1_host = checked_buffer_slice::(&plane1_buffer, len, "inverse MCT plane 1")?;
let plane2_host = checked_buffer_slice::(&plane2_buffer, len, "inverse MCT plane 2")?;
- for (dst, sample) in plane0.iter_mut().zip(plane0_host.iter().copied()) {
- *dst = sample - addend0;
- }
- for (dst, sample) in plane1.iter_mut().zip(plane1_host.iter().copied()) {
- *dst = sample - addend1;
- }
- for (dst, sample) in plane2.iter_mut().zip(plane2_host.iter().copied()) {
- *dst = sample - addend2;
- }
+ plane0.copy_from_slice(&plane0_host);
+ plane1.copy_from_slice(&plane1_host);
+ plane2.copy_from_slice(&plane2_host);
Ok(vec![plane0_buffer, plane1_buffer, plane2_buffer])
})
}
diff --git a/crates/j2k-metal/src/compute/decode_dispatch/store.rs b/crates/j2k-metal/src/compute/decode_dispatch/store.rs
index 8b6e8981..3e93f607 100644
--- a/crates/j2k-metal/src/compute/decode_dispatch/store.rs
+++ b/crates/j2k-metal/src/compute/decode_dispatch/store.rs
@@ -285,7 +285,7 @@ pub(in crate::compute) fn encode_repeated_gray_store_to_surfaces_in_command_buff
dims,
fmt,
instance_idx * surface_bytes,
- ));
+ )?);
}
Ok(surfaces)
}
@@ -326,5 +326,5 @@ pub(in crate::compute) fn encode_gray_store_to_surface_in_encoder(
);
dispatch_2d_pipeline(encoder, pipeline, (params.copy_width, params.copy_height));
- Ok(Surface::from_metal_buffer(out_buffer, dims, fmt))
+ Surface::from_metal_buffer(out_buffer, dims, fmt)
}
diff --git a/crates/j2k-metal/src/compute/direct_cache.rs b/crates/j2k-metal/src/compute/direct_cache.rs
index eda3de6d..43a479df 100644
--- a/crates/j2k-metal/src/compute/direct_cache.rs
+++ b/crates/j2k-metal/src/compute/direct_cache.rs
@@ -42,6 +42,7 @@ impl Default for CpuTier1CoefficientCache {
impl PreparedDirectGrayscalePlan {
pub(super) fn cached_cpu_tier1_coefficients(
&self,
+ budget: &mut crate::batch_allocation::BatchMetadataBudget,
step_idx: usize,
output_len: usize,
) -> Result