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>, Error> { @@ -49,21 +50,7 @@ impl PreparedDirectGrayscalePlan { step_idx, output_len, }; - let state = self - .cpu_tier1_cache - .state - .lock() - .map_err(|_| Error::MetalStatePoisoned { - state: "hybrid CPU Tier-1 coefficient cache", - })?; - if !state.retention_enabled { - return Ok(None); - } - Ok(state - .entries - .iter() - .find(|entry| entry.key == key) - .map(|entry| entry.coefficients.to_vec())) + self.cpu_tier1_cache.cached_coefficients(key, budget) } pub(super) fn store_cpu_tier1_coefficients( @@ -143,6 +130,34 @@ impl PreparedDirectGrayscalePlan { } impl CpuTier1CoefficientCache { + fn cached_coefficients( + &self, + key: CpuTier1CoefficientCacheKey, + budget: &mut crate::batch_allocation::BatchMetadataBudget, + ) -> Result>, Error> { + let state = self.state.lock().map_err(|_| Error::MetalStatePoisoned { + state: "hybrid CPU Tier-1 coefficient cache", + })?; + if !state.retention_enabled { + return Ok(None); + } + let coefficients = state + .entries + .iter() + .find(|entry| entry.key == key) + .map(|entry| entry.coefficients.clone()); + drop(state); + let Some(coefficients) = coefficients else { + return Ok(None); + }; + let mut copied = budget.try_vec( + coefficients.len(), + "J2K MetalDirect hybrid CPU Tier-1 cache-hit coefficients", + )?; + copied.extend_from_slice(&coefficients); + Ok(Some(copied)) + } + pub(super) fn retained_cache_bytes(&self) -> Result { let state = self .state @@ -166,3 +181,53 @@ impl CpuTier1CoefficientCache { }) } } + +#[cfg(test)] +mod tests { + use core::mem::size_of; + use std::sync::Arc; + + use j2k_core::BatchInfrastructureError; + + use super::{ + CpuTier1CoefficientCache, CpuTier1CoefficientCacheEntry, CpuTier1CoefficientCacheKey, + }; + use crate::{batch_allocation::BatchMetadataBudget, Error}; + + #[test] + fn cache_hit_copy_rejects_insufficient_caller_budget() { + let cache = CpuTier1CoefficientCache::default(); + let key = CpuTier1CoefficientCacheKey { + step_idx: 3, + output_len: 2, + }; + cache + .state + .lock() + .expect("cache lock") + .entries + .push(CpuTier1CoefficientCacheEntry { + key, + coefficients: Arc::from([1.0_f32, 2.0_f32]), + }); + let required = 2 * size_of::(); + let mut budget = BatchMetadataBudget::with_cap( + "J2K MetalDirect hybrid CPU Tier-1 coefficients", + required - 1, + ); + + let error = cache + .cached_coefficients(key, &mut budget) + .expect_err("cache-hit copy must honor caller budget"); + + assert!(matches!( + error, + Error::BatchInfrastructure(BatchInfrastructureError::AllocationTooLarge { + requested, + cap, + .. + }) if requested == required && cap == required - 1 + )); + assert_eq!(budget.live_bytes(), 0); + } +} diff --git a/crates/j2k-metal/src/compute/direct_cpu.rs b/crates/j2k-metal/src/compute/direct_cpu.rs index aa70bc5e..4ca3a4ac 100644 --- a/crates/j2k-metal/src/compute/direct_cpu.rs +++ b/crates/j2k-metal/src/compute/direct_cpu.rs @@ -17,13 +17,14 @@ use crate::{error::native_decode_error, Error}; use super::{ checked_coefficient_len, hybrid_stage_signpost, packed_cpu_decode_coefficients, - packed_cpu_decode_output_len, record_hybrid_cpu_decode_inputs, - record_hybrid_cpu_decode_worker_init, required_classic_output_len, required_ht_output_len, - CpuTier1DecodeSubstageCounters, J2kClassicCleanupBatchJob, J2kClassicSegment, - J2kHtCleanupBatchJob, PreparedClassicSubBand, PreparedClassicSubBandGroup, - PreparedDirectGrayscalePlan, PreparedHtSubBand, PreparedHtSubBandGroup, - HYBRID_CPU_DECODE_MIN_INPUTS_PER_TASK, J2K_CLASSIC_STYLE_RESET_CONTEXT_PROBABILITIES, - J2K_CLASSIC_STYLE_SEGMENTATION_SYMBOLS, J2K_CLASSIC_STYLE_SELECTIVE_ARITHMETIC_CODING_BYPASS, + packed_cpu_decode_coefficients_in, packed_cpu_decode_output_len, + record_hybrid_cpu_decode_inputs, record_hybrid_cpu_decode_worker_init, + required_classic_output_len, required_ht_output_len, CpuTier1DecodeSubstageCounters, + J2kClassicCleanupBatchJob, J2kClassicSegment, J2kHtCleanupBatchJob, PreparedClassicSubBand, + PreparedClassicSubBandGroup, PreparedDirectGrayscalePlan, PreparedHtSubBand, + PreparedHtSubBandGroup, HYBRID_CPU_DECODE_MIN_INPUTS_PER_TASK, + J2K_CLASSIC_STYLE_RESET_CONTEXT_PROBABILITIES, J2K_CLASSIC_STYLE_SEGMENTATION_SYMBOLS, + J2K_CLASSIC_STYLE_SELECTIVE_ARITHMETIC_CODING_BYPASS, J2K_CLASSIC_STYLE_TERMINATION_ON_EACH_PASS, J2K_CLASSIC_STYLE_VERTICALLY_CAUSAL_CONTEXT, SIGNPOST_DECODE_HYBRID_CPU_TIER1, }; @@ -45,7 +46,7 @@ pub(super) fn decode_prepared_classic_sub_band_on_cpu_profile( sub_band.height, "classic J2K MetalDirect hybrid sub-band size overflow", )?; - let mut output = vec![0.0_f32; len]; + let mut output = packed_cpu_decode_coefficients(1, len)?; if let Some(counters) = profile_counters { let mut scratch = ClassicCpuDecodeScratch::default(); decode_prepared_classic_jobs_on_cpu_with_scratch_profiled( @@ -72,7 +73,7 @@ pub(super) fn decode_prepared_classic_sub_band_group_on_cpu_profile( profile_counters: Option<&CpuTier1DecodeSubstageCounters>, ) -> Result, Error> { let _signpost = hybrid_stage_signpost(SIGNPOST_DECODE_HYBRID_CPU_TIER1); - let mut output = vec![0.0_f32; group.total_coefficients]; + let mut output = packed_cpu_decode_coefficients(1, group.total_coefficients)?; if let Some(counters) = profile_counters { let mut scratch = ClassicCpuDecodeScratch::default(); decode_prepared_classic_jobs_on_cpu_with_scratch_profiled( @@ -288,7 +289,7 @@ pub(super) fn decode_prepared_ht_sub_band_on_cpu_profile( sub_band.height, "HTJ2K MetalDirect hybrid sub-band size overflow", )?; - let mut output = vec![0.0_f32; len]; + let mut output = packed_cpu_decode_coefficients(1, len)?; if let Some(counters) = profile_counters { let mut workspace = HtCodeBlockDecodeWorkspace::default(); decode_prepared_ht_jobs_on_cpu_with_workspace_profiled( @@ -309,7 +310,7 @@ pub(super) fn decode_prepared_ht_sub_band_group_on_cpu_profile( profile_counters: Option<&CpuTier1DecodeSubstageCounters>, ) -> Result, Error> { let _signpost = hybrid_stage_signpost(SIGNPOST_DECODE_HYBRID_CPU_TIER1); - let mut output = vec![0.0_f32; group.total_coefficients]; + let mut output = packed_cpu_decode_coefficients(1, group.total_coefficients)?; if let Some(counters) = profile_counters { let mut workspace = HtCodeBlockDecodeWorkspace::default(); decode_prepared_ht_jobs_on_cpu_with_workspace_profiled( @@ -457,6 +458,7 @@ pub(super) struct HtCpuDecodeInput<'a> { fn decode_classic_inputs_on_cpu_parallel( inputs: &[ClassicCpuDecodeInput<'_>], profile_counters: Option<&CpuTier1DecodeSubstageCounters>, + budget: &mut crate::batch_allocation::BatchMetadataBudget, ) -> Result, Error> { let _signpost = hybrid_stage_signpost(SIGNPOST_DECODE_HYBRID_CPU_TIER1); record_hybrid_cpu_decode_inputs(inputs.len()); @@ -467,7 +469,7 @@ fn decode_classic_inputs_on_cpu_parallel( else { return Ok(Vec::new()); }; - let mut coefficients = packed_cpu_decode_coefficients(inputs.len(), output_len)?; + let mut coefficients = packed_cpu_decode_coefficients_in(budget, inputs.len(), output_len)?; coefficients .par_chunks_mut(output_len) .zip(inputs.par_iter()) @@ -504,6 +506,7 @@ fn decode_classic_inputs_on_cpu_parallel( fn decode_ht_inputs_on_cpu_parallel( inputs: &[HtCpuDecodeInput<'_>], profile_counters: Option<&CpuTier1DecodeSubstageCounters>, + budget: &mut crate::batch_allocation::BatchMetadataBudget, ) -> Result, Error> { let _signpost = hybrid_stage_signpost(SIGNPOST_DECODE_HYBRID_CPU_TIER1); record_hybrid_cpu_decode_inputs(inputs.len()); @@ -514,7 +517,7 @@ fn decode_ht_inputs_on_cpu_parallel( else { return Ok(Vec::new()); }; - let mut coefficients = packed_cpu_decode_coefficients(inputs.len(), output_len)?; + let mut coefficients = packed_cpu_decode_coefficients_in(budget, inputs.len(), output_len)?; coefficients .par_chunks_mut(output_len) .zip(inputs.par_iter()) @@ -552,16 +555,22 @@ pub(super) fn decode_classic_inputs_on_cpu_with_plan_cache( inputs: &[ClassicCpuDecodeInput<'_>], profile_counters: Option<&CpuTier1DecodeSubstageCounters>, ) -> Result, Error> { + let mut budget = crate::batch_allocation::BatchMetadataBudget::new( + "classic J2K MetalDirect hybrid CPU Tier-1 coefficients", + ); if inputs.len() != 1 { - return decode_classic_inputs_on_cpu_parallel(inputs, profile_counters); + return decode_classic_inputs_on_cpu_parallel(inputs, profile_counters, &mut budget); } let output_len = inputs[0].output_len; - if let Some(coefficients) = plan.cached_cpu_tier1_coefficients(step_idx, output_len)? { + if let Some(coefficients) = + plan.cached_cpu_tier1_coefficients(&mut budget, step_idx, output_len)? + { return Ok(coefficients); } - let coefficients = decode_classic_inputs_on_cpu_parallel(inputs, profile_counters)?; + let coefficients = + decode_classic_inputs_on_cpu_parallel(inputs, profile_counters, &mut budget)?; plan.store_cpu_tier1_coefficients(step_idx, output_len, coefficients) } @@ -571,15 +580,20 @@ pub(super) fn decode_ht_inputs_on_cpu_with_plan_cache( inputs: &[HtCpuDecodeInput<'_>], profile_counters: Option<&CpuTier1DecodeSubstageCounters>, ) -> Result, Error> { + let mut budget = crate::batch_allocation::BatchMetadataBudget::new( + "HTJ2K MetalDirect hybrid CPU Tier-1 coefficients", + ); if inputs.len() != 1 { - return decode_ht_inputs_on_cpu_parallel(inputs, profile_counters); + return decode_ht_inputs_on_cpu_parallel(inputs, profile_counters, &mut budget); } let output_len = inputs[0].output_len; - if let Some(coefficients) = plan.cached_cpu_tier1_coefficients(step_idx, output_len)? { + if let Some(coefficients) = + plan.cached_cpu_tier1_coefficients(&mut budget, step_idx, output_len)? + { return Ok(coefficients); } - let coefficients = decode_ht_inputs_on_cpu_parallel(inputs, profile_counters)?; + let coefficients = decode_ht_inputs_on_cpu_parallel(inputs, profile_counters, &mut budget)?; plan.store_cpu_tier1_coefficients(step_idx, output_len, coefficients) } diff --git a/crates/j2k-metal/src/compute/direct_flattened.rs b/crates/j2k-metal/src/compute/direct_flattened.rs index 2e507d68..6fdd75c0 100644 --- a/crates/j2k-metal/src/compute/direct_flattened.rs +++ b/crates/j2k-metal/src/compute/direct_flattened.rs @@ -398,9 +398,11 @@ fn decode_flattened_cpu_tier1_buckets( budget.try_vec(specs.len(), "J2K Metal flattened Tier-1 cache targets")?; for spec in specs { if let Some(cache_plan) = spec.cache_plan { - if let Some(coefficients) = - cache_plan.cached_cpu_tier1_coefficients(spec.key.step_idx, spec.output_len)? - { + if let Some(coefficients) = cache_plan.cached_cpu_tier1_coefficients( + &mut budget, + spec.key.step_idx, + spec.output_len, + )? { buckets.push(coefficients); cache_targets.push(None); continue; @@ -409,7 +411,8 @@ fn decode_flattened_cpu_tier1_buckets( } else { cache_targets.push(None); } - buckets.push(packed_cpu_decode_coefficients( + buckets.push(packed_cpu_decode_coefficients_in( + &mut budget, spec.inputs.len(), spec.output_len, )?); @@ -604,19 +607,29 @@ pub(super) fn packed_cpu_decode_coefficients( count: usize, output_len: usize, ) -> Result, Error> { - let total_len = count.checked_mul(output_len).ok_or( - j2k_core::BatchInfrastructureError::AllocationTooLarge { - what: "J2K MetalDirect hybrid packed coefficients", - requested: usize::MAX, - cap: j2k_core::DEFAULT_MAX_HOST_ALLOCATION_BYTES, - }, - )?; let mut budget = crate::batch_allocation::BatchMetadataBudget::new( "J2K MetalDirect hybrid packed coefficients", ); + packed_cpu_decode_coefficients_in(&mut budget, count, output_len) +} + +#[cfg(target_os = "macos")] +pub(super) fn packed_cpu_decode_coefficients_in( + budget: &mut crate::batch_allocation::BatchMetadataBudget, + count: usize, + output_len: usize, +) -> Result, Error> { + let total_len = crate::batch_allocation::checked_count_product( + count, + output_len, + "J2K MetalDirect hybrid packed coefficients", + )?; Ok(budget.try_filled( total_len, 0.0_f32, "J2K MetalDirect hybrid packed coefficient values", )?) } + +#[cfg(test)] +mod tests; diff --git a/crates/j2k-metal/src/compute/direct_flattened/tests.rs b/crates/j2k-metal/src/compute/direct_flattened/tests.rs new file mode 100644 index 00000000..b2fc1cbe --- /dev/null +++ b/crates/j2k-metal/src/compute/direct_flattened/tests.rs @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use core::mem::size_of; + +use j2k_core::BatchInfrastructureError; + +use super::packed_cpu_decode_coefficients_in; +use crate::{batch_allocation::BatchMetadataBudget, Error}; + +const PHASE: &str = "J2K MetalDirect hybrid packed coefficients"; + +#[test] +fn packed_coefficients_honor_exact_cap_and_charge_actual_capacity() { + let count = 4; + let exact_cap = count * size_of::(); + let mut budget = BatchMetadataBudget::with_cap(PHASE, exact_cap); + + let coefficients = + packed_cpu_decode_coefficients_in(&mut budget, 1, count).expect("exact coefficient cap"); + + assert_eq!(coefficients, vec![0.0; count]); + assert_eq!( + budget.live_bytes(), + coefficients.capacity() * size_of::() + ); +} + +#[test] +fn packed_coefficients_reject_one_byte_under_exact_cap() { + let count = 4; + let exact_cap = count * size_of::(); + let mut budget = BatchMetadataBudget::with_cap(PHASE, exact_cap - 1); + + let error = packed_cpu_decode_coefficients_in(&mut budget, 1, count) + .expect_err("one byte below coefficient cap"); + + assert!(matches!( + error, + Error::BatchInfrastructure(BatchInfrastructureError::AllocationTooLarge { + what, + requested, + cap, + }) if what == PHASE && requested == exact_cap && cap == exact_cap - 1 + )); + assert_eq!(budget.live_bytes(), 0); +} + +#[test] +fn failed_second_coefficient_allocation_preserves_first_owner_and_budget() { + let first_len = 4; + let first_bytes = first_len * size_of::(); + let cap = first_bytes * 2; + let mut budget = BatchMetadataBudget::with_cap(PHASE, cap); + let first = packed_cpu_decode_coefficients_in(&mut budget, 1, first_len) + .expect("first coefficient allocation"); + let charged_after_first = budget.live_bytes(); + + let error = packed_cpu_decode_coefficients_in(&mut budget, 1, first_len + 1) + .expect_err("aggregate coefficient cap"); + + assert!(matches!( + error, + Error::BatchInfrastructure(BatchInfrastructureError::AllocationTooLarge { + what: PHASE, + requested, + cap: error_cap, + }) if requested == first_bytes + (first_len + 1) * size_of::() && error_cap == cap + )); + assert_eq!(first, vec![0.0; first_len]); + assert_eq!(budget.live_bytes(), charged_after_first); +} + +#[test] +fn packed_coefficient_count_overflow_is_typed_and_does_not_charge_budget() { + let mut budget = BatchMetadataBudget::with_cap(PHASE, usize::MAX); + + let error = packed_cpu_decode_coefficients_in(&mut budget, usize::MAX, 2) + .expect_err("coefficient count overflow"); + + assert!(matches!( + error, + Error::BatchInfrastructure(BatchInfrastructureError::AllocationTooLarge { + what: PHASE, + requested: usize::MAX, + .. + }) + )); + assert_eq!(budget.live_bytes(), 0); +} + +#[test] +fn two_individually_valid_buckets_share_one_aggregate_budget() { + let bucket_len = 4; + let bucket_bytes = bucket_len * size_of::(); + let cap = bucket_bytes * 2 - 1; + let mut budget = BatchMetadataBudget::with_cap(PHASE, cap); + let first = packed_cpu_decode_coefficients_in(&mut budget, 1, bucket_len) + .expect("first bucket is individually below cap"); + + let error = packed_cpu_decode_coefficients_in(&mut budget, 1, bucket_len) + .expect_err("two live buckets exceed aggregate cap"); + + assert!(matches!( + error, + Error::BatchInfrastructure(BatchInfrastructureError::AllocationTooLarge { + what: PHASE, + requested, + cap: error_cap, + }) if requested == bucket_bytes * 2 && error_cap == cap + )); + assert_eq!(first.len(), bucket_len); + assert_eq!(budget.live_bytes(), first.capacity() * size_of::()); +} diff --git a/crates/j2k-metal/src/compute/direct_plane_pack.rs b/crates/j2k-metal/src/compute/direct_plane_pack.rs index e79b1c63..63add0e3 100644 --- a/crates/j2k-metal/src/compute/direct_plane_pack.rs +++ b/crates/j2k-metal/src/compute/direct_plane_pack.rs @@ -201,7 +201,7 @@ pub(super) fn encode_plane_stage_to_surface_in_command_buffer( dispatch_2d_pipeline(&encoder, pipeline, stage.dims); encoder.end_encoding(); - Ok(Surface::from_metal_buffer(out_buffer, stage.dims, fmt)) + Surface::from_metal_buffer(out_buffer, stage.dims, fmt) } #[cfg(target_os = "macos")] @@ -256,11 +256,7 @@ pub(super) fn encode_mct_rgb8_to_surface_in_command_buffer( encoder.end_encoding(); drop(signpost); - Ok(Surface::from_metal_buffer( - out_buffer, - dims, - PixelFormat::Rgb8, - )) + Surface::from_metal_buffer(out_buffer, dims, PixelFormat::Rgb8) } #[cfg(target_os = "macos")] @@ -350,7 +346,7 @@ pub(super) fn encode_batched_mct_rgb8_to_surfaces_in_command_buffer( dims, PixelFormat::Rgb8, index * surface_bytes, - )); + )?); } Ok(surfaces) } @@ -478,7 +474,7 @@ pub(super) fn encode_repeated_mct_rgb8_to_surfaces_in_command_buffer( dims, PixelFormat::Rgb8, index * surface_bytes, - )); + )?); } Ok(surfaces) } diff --git a/crates/j2k-metal/src/compute/direct_surface_pack.rs b/crates/j2k-metal/src/compute/direct_surface_pack.rs index 97ce7cc4..64919d4e 100644 --- a/crates/j2k-metal/src/compute/direct_surface_pack.rs +++ b/crates/j2k-metal/src/compute/direct_surface_pack.rs @@ -144,7 +144,7 @@ pub(super) fn encode_gray_plane_to_surface_in_encoder_with_offset( ); dispatch_2d_pipeline(encoder, pipeline, dims); - Ok(Surface::from_metal_buffer(out_buffer, dims, fmt)) + Surface::from_metal_buffer(out_buffer, dims, fmt) } #[cfg(target_os = "macos")] @@ -213,7 +213,7 @@ pub(super) fn encode_repeated_gray_plane_to_surfaces_in_command_buffer( dims, fmt, instance_idx * surface_bytes, - )); + )?); } Ok(surfaces) } diff --git a/crates/j2k-metal/src/compute/resident_tier1.rs b/crates/j2k-metal/src/compute/resident_tier1.rs index 7f0b43a0..46dcfad5 100644 --- a/crates/j2k-metal/src/compute/resident_tier1.rs +++ b/crates/j2k-metal/src/compute/resident_tier1.rs @@ -64,7 +64,6 @@ pub(in crate::compute) use self::readback::{ }; pub(crate) use self::readback::{ wait_resident_lossless_codestream, wait_resident_lossless_codestream_batch, - wait_resident_lossless_codestream_batches, }; pub(in crate::compute) use self::result_harvest::{ finish_completed_resident_lossless_codestream_batch, wait_resident_codestream_command_buffer, diff --git a/crates/j2k-metal/src/compute/resident_tier1/readback.rs b/crates/j2k-metal/src/compute/resident_tier1/readback.rs index bd68db08..de24fbe7 100644 --- a/crates/j2k-metal/src/compute/resident_tier1/readback.rs +++ b/crates/j2k-metal/src/compute/resident_tier1/readback.rs @@ -1,7 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -use j2k_metal_support::FallibleSubmissionQueue; - use super::{ checked_buffer_read, completed_command_buffers_gpu_duration, encode_status_error, finish_completed_resident_lossless_codestream_batch, new_blit_command_encoder, @@ -58,22 +56,6 @@ pub(crate) fn wait_resident_lossless_codestream_batch( finish_completed_resident_lossless_codestream_batch(pending) } -#[cfg(target_os = "macos")] -pub(crate) fn wait_resident_lossless_codestream_batches( - pending_batches: Vec, -) -> Result, Error> { - if let Some(last) = pending_batches.last() { - // These command buffers are submitted on the same Metal queue before - // harvest, so completing the final one implies earlier chunks are done. - wait_resident_codestream_command_buffer(&last.command_buffer)?; - } - FallibleSubmissionQueue::from_retained(pending_batches).try_finish( - "J2K Metal resident batch submission and result metadata", - "J2K Metal resident batch results", - finish_completed_resident_lossless_codestream_batch, - ) -} - #[cfg(target_os = "macos")] #[derive(Clone, Copy)] pub(in crate::compute) struct ResidentTier1StatusReadbackRequest<'a> { diff --git a/crates/j2k-metal/src/compute/runtime.rs b/crates/j2k-metal/src/compute/runtime.rs index 432f8bda..de0289d5 100644 --- a/crates/j2k-metal/src/compute/runtime.rs +++ b/crates/j2k-metal/src/compute/runtime.rs @@ -62,7 +62,10 @@ pub(crate) struct MetalRuntime { pub(super) idwt_interleave_batched: ComputePipelineState, pub(super) idwt_reversible53_horizontal_batched: ComputePipelineState, pub(super) idwt_reversible53_vertical_batched: ComputePipelineState, - pub(super) idwt_irreversible97_single_decomposition: ComputePipelineState, + pub(super) idwt_irreversible97_horizontal_scale: ComputePipelineState, + pub(super) idwt_irreversible97_vertical_scale: ComputePipelineState, + pub(super) idwt_irreversible97_horizontal_step: ComputePipelineState, + pub(super) idwt_irreversible97_vertical_step: ComputePipelineState, pub(super) fdwt53_horizontal: ComputePipelineState, pub(super) fdwt53_vertical: ComputePipelineState, pub(super) fdwt53_horizontal_batched: ComputePipelineState, @@ -184,9 +187,14 @@ impl MetalRuntime { idwt_reversible53_vertical_batched: pipeline( "j2k_idwt_reversible53_vertical_pass_batched", )?, - idwt_irreversible97_single_decomposition: pipeline( - "j2k_idwt_irreversible97_single_decomposition", + idwt_irreversible97_horizontal_scale: pipeline( + "j2k_idwt_irreversible97_horizontal_scale", )?, + idwt_irreversible97_vertical_scale: pipeline("j2k_idwt_irreversible97_vertical_scale")?, + idwt_irreversible97_horizontal_step: pipeline( + "j2k_idwt_irreversible97_horizontal_step", + )?, + idwt_irreversible97_vertical_step: pipeline("j2k_idwt_irreversible97_vertical_step")?, fdwt53_horizontal: pipeline("j2k_forward_dwt53_horizontal")?, fdwt53_vertical: pipeline("j2k_forward_dwt53_vertical")?, fdwt53_horizontal_batched: pipeline("j2k_forward_dwt53_horizontal_batched")?, diff --git a/crates/j2k-metal/src/compute/symbol_inventory.rs b/crates/j2k-metal/src/compute/symbol_inventory.rs index 4672345a..e862f37e 100644 --- a/crates/j2k-metal/src/compute/symbol_inventory.rs +++ b/crates/j2k-metal/src/compute/symbol_inventory.rs @@ -19,7 +19,8 @@ macro_rules! wire_compute_symbols { J2kForwardDwt53BatchedParams, J2kForwardDwt53Params, J2kForwardIctParams, J2kForwardRctParams, J2kGrayStoreParams, J2kHtCleanupBatchJob, J2kHtCleanupParams, J2kHtEncodeBatchJob, J2kHtEncodeParams, J2kHtEncodeStatus, J2kHtRepeatedBatchParams, - J2kHtStatus, J2kIdwtSingleDecompositionParams, J2kIdwtStatus, J2kInverseMctParams, + J2kHtStatus, J2kIdwt97StepParams, J2kIdwtSingleDecompositionParams, J2kIdwtStatus, + J2kInverseMctParams, J2kLosslessCodestreamAssemblyParams, J2kLosslessCoefficientJob, J2kLosslessDeinterleaveParams, J2kMctRgb8PackParams, J2kMctStatus, J2kPackParams, J2kPacketBlock, J2kPacketDescriptor, J2kPacketEncodeParams, J2kPacketEncodeStatus, @@ -116,8 +117,8 @@ macro_rules! wire_compute_symbols { #[cfg(target_os = "macos")] pub(crate) use crate::compute::resident_tier1::{ wait_resident_lossless_codestream, wait_resident_lossless_codestream_batch, - wait_resident_lossless_codestream_batches, J2kLosslessCodestreamAssemblyJob, - J2kLosslessCodestreamBlockCodingMode, J2kLosslessDeviceBatchPrepareItem, + J2kLosslessCodestreamAssemblyJob, J2kLosslessCodestreamBlockCodingMode, + J2kLosslessDeviceBatchPrepareItem, J2kLosslessDeviceCodeBlock, J2kLosslessDevicePrepareJob, J2kPendingResidentLosslessCodestreamBatch, J2kPreparedLosslessDeviceCodeBlocks, J2kResidentLosslessHtCodeBlocks, J2kResidentLosslessTier1CodeBlocks, @@ -138,6 +139,8 @@ macro_rules! wire_compute_symbols { classic_batch_uses_plain_fast_path, classic_repeated_uses_plain_fast_path, repeated_gray_store_is_contiguous_full_surface, }; + #[cfg(all(target_os = "macos", test))] + pub(crate) use crate::compute::decode_dispatch::decode_irreversible97_staged_single_decomposition_idwt; #[cfg(target_os = "macos")] pub(crate) use crate::compute::decode_dispatch::{ decode_inverse_mct, decode_irreversible97_single_decomposition_idwt, diff --git a/crates/j2k-metal/src/compute/tests.rs b/crates/j2k-metal/src/compute/tests.rs index 775d76f8..3f439392 100644 --- a/crates/j2k-metal/src/compute/tests.rs +++ b/crates/j2k-metal/src/compute/tests.rs @@ -739,22 +739,14 @@ fn cached_direct_component_tier1_input_count(plan: &super::PreparedDirectGraysca let mut step_idx = 0; while step_idx < plan.steps.len() { if let Some(group) = plan.classic_group_starting_at(step_idx) { - if plan - .cached_cpu_tier1_coefficients(step_idx, group.total_coefficients) - .expect("classic group cache lookup") - .is_some() - { + if has_cached_cpu_tier1_coefficients(plan, step_idx, group.total_coefficients) { count += 1; } step_idx = group.end_step; continue; } if let Some(group) = plan.ht_group_starting_at(step_idx) { - if plan - .cached_cpu_tier1_coefficients(step_idx, group.total_coefficients) - .expect("HT group cache lookup") - .is_some() - { + if has_cached_cpu_tier1_coefficients(plan, step_idx, group.total_coefficients) { count += 1; } step_idx = group.end_step; @@ -763,21 +755,13 @@ fn cached_direct_component_tier1_input_count(plan: &super::PreparedDirectGraysca match &plan.steps[step_idx] { PreparedDirectGrayscaleStep::ClassicSubBand(sub_band) => { let output_len = sub_band.width as usize * sub_band.height as usize; - if plan - .cached_cpu_tier1_coefficients(step_idx, output_len) - .expect("classic sub-band cache lookup") - .is_some() - { + if has_cached_cpu_tier1_coefficients(plan, step_idx, output_len) { count += 1; } } PreparedDirectGrayscaleStep::HtSubBand(sub_band) => { let output_len = sub_band.width as usize * sub_band.height as usize; - if plan - .cached_cpu_tier1_coefficients(step_idx, output_len) - .expect("HT sub-band cache lookup") - .is_some() - { + if has_cached_cpu_tier1_coefficients(plan, step_idx, output_len) { count += 1; } } @@ -788,6 +772,18 @@ fn cached_direct_component_tier1_input_count(plan: &super::PreparedDirectGraysca count } +fn has_cached_cpu_tier1_coefficients( + plan: &super::PreparedDirectGrayscalePlan, + step_idx: usize, + output_len: usize, +) -> bool { + let mut budget = + crate::batch_allocation::BatchMetadataBudget::new("J2K Metal test CPU Tier-1 cache lookup"); + plan.cached_cpu_tier1_coefficients(&mut budget, step_idx, output_len) + .expect("CPU Tier-1 cache lookup") + .is_some() +} + fn decode_native_classic_sub_band(plan: &J2kOwnedSubBandPlan) -> Vec { let mut output = vec![0.0_f32; plan.width as usize * plan.height as usize]; let jobs = plan diff --git a/crates/j2k-metal/src/decoder/surface.rs b/crates/j2k-metal/src/decoder/surface.rs index 2f58ce10..4da1927b 100644 --- a/crates/j2k-metal/src/decoder/surface.rs +++ b/crates/j2k-metal/src/decoder/surface.rs @@ -65,18 +65,11 @@ pub(super) fn upload_surface_to_metal_with_device( fmt: PixelFormat, device: &metal::DeviceRef, ) -> Result { - let pitch_bytes = dimensions.0 as usize * fmt.bytes_per_pixel(); let buffer = j2k_metal_support::checked_shared_buffer_with_bytes(device, bytes).map_err(|source| { crate::error::metal_kernel_support_error("J2K Metal surface upload", source) })?; - Ok(Surface { - backend: BackendKind::Metal, - residency: SurfaceResidency::CpuStagedMetalUpload, - dimensions, - fmt, - pitch_bytes, - byte_offset: 0, - storage: Storage::Metal(buffer), - }) + let mut surface = Surface::from_metal_buffer(buffer, dimensions, fmt)?; + surface.residency = SurfaceResidency::CpuStagedMetalUpload; + Ok(surface) } diff --git a/crates/j2k-metal/src/encode.rs b/crates/j2k-metal/src/encode.rs index 956f7205..cd33edee 100644 --- a/crates/j2k-metal/src/encode.rs +++ b/crates/j2k-metal/src/encode.rs @@ -22,6 +22,8 @@ mod resident_plan; #[cfg(target_os = "macos")] mod resident_prepare; #[cfg(target_os = "macos")] +mod resident_schedule; +#[cfg(target_os = "macos")] mod resident_submit; #[cfg(target_os = "macos")] mod resident_types; @@ -95,12 +97,15 @@ use self::resident_estimate::{ resident_codestream_assembly_job_for_metadata, resident_ht_batch_encode_should_retry_conservative, }; +#[cfg(all(test, target_os = "macos"))] +use self::resident_schedule::{ + reset_resident_schedule_counters_for_test, resident_schedule_counters_for_test, +}; #[cfg(target_os = "macos")] use self::resident_types::{ FinishedResidentLosslessBufferEncode, PlannedResidentLosslessBufferEncode, PreparedResidentLosslessBufferEncode, ResidentLosslessBufferEncodeMetadata, SubmittedResidentLosslessMetalBufferEncodeBatch, - SubmittedResidentLosslessMetalBufferEncodeBatchKind, SubmittedResidentLosslessMetalBufferEncodeChunk, }; pub use self::roundtrip_validation::{ @@ -171,7 +176,9 @@ use self::resident_hybrid::{ #[cfg(target_os = "macos")] use self::resident_plan::plan_resident_lossless_buffer_encode; #[cfg(target_os = "macos")] -use self::resident_submit::{duration_share, submit_planned_resident_lossless_tiles}; +use self::resident_schedule::submit_planned_resident_lossless_tiles; +#[cfg(target_os = "macos")] +use self::resident_submit::duration_share; #[cfg(target_os = "macos")] use self::resident_validation::{ validate_lossless_roundtrip_on_metal_region_with_session, diff --git a/crates/j2k-metal/src/encode/batch.rs b/crates/j2k-metal/src/encode/batch.rs index 91befca9..61d0f3e2 100644 --- a/crates/j2k-metal/src/encode/batch.rs +++ b/crates/j2k-metal/src/encode/batch.rs @@ -16,7 +16,6 @@ use super::{ SubmittedJ2kLosslessMetalBufferEncodeBatch, SubmittedJ2kLosslessMetalBufferEncodeBatchState, SubmittedJ2kLosslessMetalEncodeBatch, SubmittedJ2kLosslessMetalEncodeBatchState, SubmittedResidentLosslessMetalBufferEncodeBatch, - SubmittedResidentLosslessMetalBufferEncodeBatchKind, }; #[cfg(target_os = "macos")] @@ -26,6 +25,7 @@ pub fn submit_lossless_batch( options: &J2kLosslessEncodeOptions, session: &crate::MetalBackendSession, ) -> Result { + validate_resident_tile_devices(request.tiles, session)?; submit_lossless_tiles( request.tiles, *options, @@ -42,6 +42,7 @@ pub fn submit_lossless_batch_to_metal( options: &J2kLosslessEncodeOptions, session: &crate::MetalBackendSession, ) -> Result { + validate_resident_tile_devices(request.tiles, session)?; submit_lossless_tiles_to_metal_buffer_batch( request.tiles, *options, @@ -59,6 +60,7 @@ pub fn encode_lossless_batch_with_report( options: &J2kLosslessEncodeOptions, session: &crate::MetalBackendSession, ) -> Result, crate::Error> { + validate_resident_tile_devices(request.tiles, session)?; encode_lossless_tiles_with_report( request.tiles, *options, @@ -68,6 +70,17 @@ pub fn encode_lossless_batch_with_report( ) } +#[cfg(target_os = "macos")] +fn validate_resident_tile_devices( + tiles: &[MetalLosslessEncodeTile<'_>], + session: &crate::MetalBackendSession, +) -> Result<(), crate::Error> { + for &tile in tiles { + tile.validate_device(session.device())?; + } + Ok(()) +} + #[cfg(target_os = "macos")] pub(super) fn host_outcome_from_buffer_outcome( outcome: MetalLosslessBufferEncodeOutcome, @@ -258,7 +271,7 @@ fn try_submit_resident_lossless_tiles_to_metal_buffer_batch( encode_started: Instant::now(), tiles: Vec::new(), staging, - kind: SubmittedResidentLosslessMetalBufferEncodeBatchKind::Empty, + pipeline: None, })); } @@ -298,7 +311,7 @@ fn try_submit_resident_lossless_tiles_to_metal_buffer_batch( stats.stage_stats.plan_duration = started.elapsed(); } let encode_started = Instant::now(); - let kind = submit_planned_resident_lossless_tiles( + let pipeline = submit_planned_resident_lossless_tiles( planned, session, stats.effective_inflight_tiles, @@ -318,7 +331,7 @@ fn try_submit_resident_lossless_tiles_to_metal_buffer_batch( encode_started, tiles: owned_tiles, staging, - kind, + pipeline, })) } diff --git a/crates/j2k-metal/src/encode/resident_schedule.rs b/crates/j2k-metal/src/encode/resident_schedule.rs new file mode 100644 index 00000000..47bcefce --- /dev/null +++ b/crates/j2k-metal/src/encode/resident_schedule.rs @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use std::ops::Range; + +use super::{ + resident_lossless_chunk_ranges_from_code_blocks, resident_lossless_code_block_chunk_cap, + resident_submit::{submit_resident_lossless_chunk, ResidentSubmissionFamily}, + J2kBlockCodingMode, MetalLosslessEncodeBatchStats, PlannedResidentLosslessBufferEncode, + SubmittedResidentLosslessMetalBufferEncodeChunk, +}; + +pub(super) fn submit_planned_resident_lossless_tiles( + planned: Vec, + session: &crate::MetalBackendSession, + inflight_tiles: usize, + stats: &mut MetalLosslessEncodeBatchStats, +) -> Result, crate::Error> { + if planned.is_empty() { + return Ok(None); + } + if planned.iter().all(|planned| { + planned.metadata.plan.block_coding_mode == J2kBlockCodingMode::HighThroughput + }) { + return submit_planned_resident_ht_lossless_tiles_batch( + planned, + session, + inflight_tiles, + stats, + ); + } + if planned + .iter() + .all(|planned| planned.metadata.plan.block_coding_mode == J2kBlockCodingMode::Classic) + { + return submit_planned_resident_classic_lossless_tiles_batch( + planned, + session, + inflight_tiles, + stats, + ); + } + Ok(None) +} + +fn submit_planned_resident_ht_lossless_tiles_batch( + planned: Vec, + session: &crate::MetalBackendSession, + inflight_tiles: usize, + stats: &mut MetalLosslessEncodeBatchStats, +) -> Result, crate::Error> { + let mut budget = + crate::batch_allocation::BatchMetadataBudget::new("J2K Metal resident HT chunk plan"); + let mut code_block_counts = + budget.try_vec(planned.len(), "J2K Metal resident HT code-block counts")?; + code_block_counts.extend( + planned + .iter() + .map(|planned| planned.metadata.plan.code_blocks.len()), + ); + let chunk_ranges = resident_lossless_chunk_ranges_from_code_blocks( + &code_block_counts, + inflight_tiles, + resident_lossless_code_block_chunk_cap(&code_block_counts), + ); + let family = ResidentSubmissionFamily::HighThroughput( + crate::compute::ht_packet_output_capacity_mode_from_env(), + ); + SubmittedResidentLosslessChunkPipeline::new(planned, chunk_ranges, family, session, stats) + .map(Some) +} + +fn submit_planned_resident_classic_lossless_tiles_batch( + planned: Vec, + session: &crate::MetalBackendSession, + inflight_tiles: usize, + stats: &mut MetalLosslessEncodeBatchStats, +) -> Result, crate::Error> { + let batch_limit = inflight_tiles.max(1); + let chunk_count = planned.len().div_ceil(batch_limit); + let mut budget = + crate::batch_allocation::BatchMetadataBudget::new("J2K Metal resident classic chunk plan"); + let mut chunk_ranges = + budget.try_vec(chunk_count, "J2K Metal resident classic chunk ranges")?; + chunk_ranges.extend( + (0..planned.len()) + .step_by(batch_limit) + .map(|start| start..(start + batch_limit).min(planned.len())), + ); + SubmittedResidentLosslessChunkPipeline::new( + planned, + chunk_ranges, + ResidentSubmissionFamily::Classic, + session, + stats, + ) + .map(Some) +} + +pub(super) struct SubmittedResidentLosslessChunkPipeline { + remaining: std::vec::IntoIter, + ranges: std::vec::IntoIter>, + active: Option, + total_tiles: usize, + family: ResidentSubmissionFamily, +} + +impl SubmittedResidentLosslessChunkPipeline { + pub(super) fn new( + planned: Vec, + ranges: Vec>, + family: ResidentSubmissionFamily, + session: &crate::MetalBackendSession, + stats: &mut MetalLosslessEncodeBatchStats, + ) -> Result { + validate_chunk_ranges(planned.len(), &ranges)?; + let total_tiles = planned.len(); + let mut pipeline = Self { + remaining: planned.into_iter(), + ranges: ranges.into_iter(), + active: None, + total_tiles, + family, + }; + pipeline.submit_next(session, stats)?; + Ok(pipeline) + } + + pub(super) const fn total_tiles(&self) -> usize { + self.total_tiles + } + + pub(super) fn take_active( + &mut self, + ) -> Option { + self.active.take() + } + + pub(super) fn submit_next( + &mut self, + session: &crate::MetalBackendSession, + stats: &mut MetalLosslessEncodeBatchStats, + ) -> Result<(), crate::Error> { + if self.active.is_some() { + return Err(crate::Error::MetalKernel { + message: "J2K Metal resident scheduler cannot submit over an active chunk" + .to_string(), + }); + } + let Some(range) = self.ranges.next() else { + if self.remaining.len() != 0 { + return Err(crate::Error::MetalKernel { + message: "J2K Metal resident scheduler exhausted ranges before planned tiles" + .to_string(), + }); + } + return Ok(()); + }; + let mut budget = + crate::batch_allocation::BatchMetadataBudget::new("J2K Metal resident scheduled chunk"); + let mut chunk = budget.try_vec(range.len(), "J2K Metal resident scheduled tiles")?; + for _ in range { + chunk.push( + self.remaining + .next() + .ok_or_else(|| crate::Error::MetalKernel { + message: "J2K Metal resident chunk range exceeds planned tiles".to_string(), + })?, + ); + } + let pending = submit_resident_lossless_chunk(chunk, session, stats, self.family)?; + record_submitted_chunk(pending.metadatas.len()); + self.active = Some(pending); + Ok(()) + } + + pub(super) fn record_active_completed(tile_count: usize) { + record_completed_chunk(tile_count); + } +} + +impl Drop for SubmittedResidentLosslessChunkPipeline { + fn drop(&mut self) { + if let Some(active) = &self.active { + record_completed_chunk(active.metadatas.len()); + } + } +} + +fn validate_chunk_ranges(planned_len: usize, ranges: &[Range]) -> Result<(), crate::Error> { + let mut expected_start = 0usize; + for range in ranges { + if range.start != expected_start || range.end <= range.start || range.end > planned_len { + return Err(crate::Error::MetalKernel { + message: + "J2K Metal resident chunk ranges must be non-empty, contiguous, and in bounds" + .to_string(), + }); + } + expected_start = range.end; + } + if expected_start != planned_len { + return Err(crate::Error::MetalKernel { + message: "J2K Metal resident chunk ranges do not cover every planned tile".to_string(), + }); + } + Ok(()) +} + +#[cfg(test)] +std::thread_local! { + static PENDING_TILES: std::cell::Cell = const { std::cell::Cell::new(0) }; + static PEAK_PENDING_TILES: std::cell::Cell = const { std::cell::Cell::new(0) }; + static SUBMITTED_CHUNKS: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +#[cfg(test)] +fn record_submitted_chunk(tile_count: usize) { + PENDING_TILES.with(|pending| pending.set(tile_count)); + PEAK_PENDING_TILES.with(|peak| peak.set(peak.get().max(tile_count))); + SUBMITTED_CHUNKS.with(|chunks| chunks.set(chunks.get().saturating_add(1))); +} + +#[cfg(not(test))] +fn record_submitted_chunk(_tile_count: usize) {} + +#[cfg(test)] +fn record_completed_chunk(tile_count: usize) { + let previous = PENDING_TILES.with(|pending| pending.replace(0)); + debug_assert_eq!(previous, tile_count); +} + +#[cfg(not(test))] +fn record_completed_chunk(_tile_count: usize) {} + +#[cfg(test)] +pub(super) fn reset_resident_schedule_counters_for_test() { + PENDING_TILES.with(|pending| pending.set(0)); + PEAK_PENDING_TILES.with(|peak| peak.set(0)); + SUBMITTED_CHUNKS.with(|chunks| chunks.set(0)); +} + +#[cfg(test)] +pub(super) fn resident_schedule_counters_for_test() -> (usize, usize, usize) { + let pending = PENDING_TILES.with(std::cell::Cell::get); + let peak = PEAK_PENDING_TILES.with(std::cell::Cell::get); + let submitted = SUBMITTED_CHUNKS.with(std::cell::Cell::get); + (pending, peak, submitted) +} + +#[cfg(test)] +mod tests { + use super::validate_chunk_ranges; + + #[test] + fn chunk_ranges_are_validated_before_submission() { + assert!(validate_chunk_ranges(4, &[0..2, 2..4]).is_ok()); + assert!(validate_chunk_ranges(4, &[]).is_err()); + assert!(validate_chunk_ranges(4, &[0..0, 0..4]).is_err()); + assert!(validate_chunk_ranges(4, &[1..2, 2..4]).is_err()); + assert!(validate_chunk_ranges(4, &[0..2, 3..4]).is_err()); + assert!(validate_chunk_ranges(4, &[0..5, 5..6]).is_err()); + } +} diff --git a/crates/j2k-metal/src/encode/resident_submit.rs b/crates/j2k-metal/src/encode/resident_submit.rs index d761e8cd..05e0e7fa 100644 --- a/crates/j2k-metal/src/encode/resident_submit.rs +++ b/crates/j2k-metal/src/encode/resident_submit.rs @@ -68,220 +68,91 @@ fn build_resident_chunk_items( } use super::{ add_resident_prep_wall_duration, compute, resident_codestream_assembly_job_for_metadata, - resident_lossless_chunk_ranges_from_code_blocks, resident_lossless_code_block_chunk_cap, - Duration, Instant, J2kBlockCodingMode, MetalLosslessEncodeBatchStats, - PlannedResidentLosslessBufferEncode, SubmittedResidentLosslessMetalBufferEncodeBatchKind, + Duration, Instant, MetalLosslessEncodeBatchStats, PlannedResidentLosslessBufferEncode, SubmittedResidentLosslessMetalBufferEncodeChunk, }; -#[cfg(target_os = "macos")] -pub(super) fn submit_planned_resident_lossless_tiles( - planned: Vec, - session: &crate::MetalBackendSession, - inflight_tiles: usize, - stats: &mut MetalLosslessEncodeBatchStats, -) -> Result { - if planned.is_empty() { - return Ok(SubmittedResidentLosslessMetalBufferEncodeBatchKind::Empty); - } - if planned.iter().all(|planned| { - planned.metadata.plan.block_coding_mode == J2kBlockCodingMode::HighThroughput - }) { - return submit_planned_resident_ht_lossless_tiles_batch( - planned, - session, - inflight_tiles, - stats, - ); - } - if planned - .iter() - .all(|planned| planned.metadata.plan.block_coding_mode == J2kBlockCodingMode::Classic) - { - return submit_planned_resident_classic_lossless_tiles_batch( - planned, - session, - inflight_tiles, - stats, - ); +#[derive(Clone, Copy)] +pub(super) enum ResidentSubmissionFamily { + HighThroughput(compute::J2kHtPacketOutputCapacityMode), + Classic, +} + +impl ResidentSubmissionFamily { + const fn time_prepare_in_submit(self) -> bool { + matches!(self, Self::HighThroughput(_)) } - Ok(SubmittedResidentLosslessMetalBufferEncodeBatchKind::Empty) } #[cfg(target_os = "macos")] -fn submit_planned_resident_ht_lossless_tiles_batch( +pub(super) fn submit_resident_lossless_chunk( planned: Vec, session: &crate::MetalBackendSession, - inflight_tiles: usize, stats: &mut MetalLosslessEncodeBatchStats, -) -> Result { + family: ResidentSubmissionFamily, +) -> Result { + let tile_count = planned.len(); + if tile_count == 0 { + return Err(crate::Error::MetalKernel { + message: "J2K Metal resident scheduler produced an empty chunk".to_string(), + }); + } + let profile_stages = compute::metal_profile_stages_enabled(); + if profile_stages { + stats.stage_stats.chunk_count = stats.stage_stats.chunk_count.saturating_add(1); + stats.stage_stats.tile_count = stats.stage_stats.tile_count.saturating_add(tile_count); + } + stats.max_observed_inflight_tiles = stats.max_observed_inflight_tiles.max(tile_count); + let mut budget = - crate::batch_allocation::BatchMetadataBudget::new("J2K Metal resident HT chunk plan"); - let mut code_block_counts = - budget.try_vec(planned.len(), "J2K Metal resident HT code-block counts")?; - code_block_counts.extend( - planned - .iter() - .map(|planned| planned.metadata.plan.code_blocks.len()), - ); - let chunk_ranges = resident_lossless_chunk_ranges_from_code_blocks( - &code_block_counts, - inflight_tiles, - resident_lossless_code_block_chunk_cap(&code_block_counts), - ); - submit_planned_resident_lossless_tiles_chunked( - planned, - session, - stats, - "HT", - chunk_ranges, - true, - |session, batch_items| { + crate::batch_allocation::BatchMetadataBudget::new("J2K Metal resident chunk preparation"); + let early_prepare_submit_started = + (profile_stages && family.time_prepare_in_submit()).then(Instant::now); + let prep_wall_started = profile_stages.then(Instant::now); + let prepared = prepare_planned_resident_lossless_tiles_batch(planned, session)?; + if let Some(started) = prep_wall_started { + add_resident_prep_wall_duration(stats, started.elapsed(), profile_stages); + } + + let ResidentChunkItems { + metadatas, + prepare_durations, + batch_items, + } = build_resident_chunk_items(prepared, &mut budget)?; + let batch_started = Instant::now(); + let prepare_submit_started = if family.time_prepare_in_submit() { + early_prepare_submit_started + } else { + profile_stages.then(Instant::now) + }; + let pending = match family { + ResidentSubmissionFamily::HighThroughput(mode) => { compute::submit_lossless_codestream_buffers_from_prepared_ht_batch( session, batch_items, - compute::ht_packet_output_capacity_mode_from_env(), - ) - }, - ) -} - -#[cfg(target_os = "macos")] -fn submit_planned_resident_classic_lossless_tiles_batch( - planned: Vec, - session: &crate::MetalBackendSession, - inflight_tiles: usize, - stats: &mut MetalLosslessEncodeBatchStats, -) -> Result { - let batch_limit = inflight_tiles.max(1); - let chunk_count = planned.len().div_ceil(batch_limit); - let mut budget = - crate::batch_allocation::BatchMetadataBudget::new("J2K Metal resident classic chunk plan"); - let mut chunk_ranges = - budget.try_vec(chunk_count, "J2K Metal resident classic chunk ranges")?; - chunk_ranges.extend( - (0..planned.len()) - .step_by(batch_limit) - .map(|start| start..(start + batch_limit).min(planned.len())), - ); - submit_planned_resident_lossless_tiles_chunked( - planned, - session, - stats, - "classic", - chunk_ranges, - false, - |session, batch_items| { + mode, + )? + } + ResidentSubmissionFamily::Classic => { compute::submit_lossless_codestream_buffers_from_prepared_classic_batch( session, batch_items, compute::J2kClassicEncodeOutputCapacityMode::Tight, - ) - }, - ) -} - -/// Shared chunked submit driver for the per-family resident lossless batch -/// paths. `time_prepare_in_submit` preserves each family's historical -/// `prepare_submit_duration` semantics: HT (`true`) measures prepare + item -/// build + submit, classic (`false`) measures only the submit call. -#[cfg(target_os = "macos")] -fn submit_planned_resident_lossless_tiles_chunked( - mut planned: Vec, - session: &crate::MetalBackendSession, - stats: &mut MetalLosslessEncodeBatchStats, - family_name: &str, - chunk_ranges: Vec>, - time_prepare_in_submit: bool, - submit_chunk: impl Fn( - &crate::MetalBackendSession, - Vec, - ) - -> Result, -) -> Result { - let planned_len = planned.len(); - let profile_stages = compute::metal_profile_stages_enabled(); - if profile_stages { - stats.stage_stats.chunk_count = stats - .stage_stats - .chunk_count - .saturating_add(chunk_ranges.len()); - stats.stage_stats.tile_count = stats.stage_stats.tile_count.saturating_add(planned_len); - } - stats.max_observed_inflight_tiles = stats.max_observed_inflight_tiles.max( - chunk_ranges - .iter() - .map(std::ops::Range::len) - .max() - .unwrap_or(0), - ); - - let mut chunk_budget = - crate::batch_allocation::BatchMetadataBudget::new("J2K Metal resident submitted chunks"); - let mut chunks = chunk_budget.try_vec( - chunk_ranges.len(), - "J2K Metal resident submitted chunk records", - )?; - for range in chunk_ranges { - let take = range.len(); - let mut iteration_budget = crate::batch_allocation::BatchMetadataBudget::new( - "J2K Metal resident chunk preparation", - ); - let mut chunk_planned = - iteration_budget.try_vec(take, "J2K Metal resident chunk planned tiles")?; - chunk_planned.extend(planned.drain(..take)); - let early_prepare_submit_started = - (profile_stages && time_prepare_in_submit).then(Instant::now); - let prep_wall_started = profile_stages.then(Instant::now); - let prepared = prepare_planned_resident_lossless_tiles_batch(chunk_planned, session)?; - if let Some(started) = prep_wall_started { - add_resident_prep_wall_duration(stats, started.elapsed(), profile_stages); - } - - let ResidentChunkItems { - metadatas, - prepare_durations, - batch_items, - } = build_resident_chunk_items(prepared, &mut iteration_budget)?; - - let batch_started = Instant::now(); - let prepare_submit_started = if time_prepare_in_submit { - early_prepare_submit_started - } else { - profile_stages.then(Instant::now) - }; - let pending = submit_chunk(session, batch_items)?; - if let Some(started) = prepare_submit_started { - stats.stage_stats.prepare_submit_duration = stats - .stage_stats - .prepare_submit_duration - .saturating_add(started.elapsed()); + )? } - chunks.push(SubmittedResidentLosslessMetalBufferEncodeChunk { - metadatas, - prepare_durations, - pending, - batch_started, - }); - } - - if !planned.is_empty() { - return Err(crate::Error::MetalKernel { - message: format!( - "J2K Metal resident {family_name} batch chunking left unsubmitted tiles" - ), - }); - } - - if chunks.is_empty() && planned_len > 0 { - return Err(crate::Error::MetalKernel { - message: format!("J2K Metal resident {family_name} batch chunking produced no chunks"), - }); + }; + if let Some(started) = prepare_submit_started { + stats.stage_stats.prepare_submit_duration = stats + .stage_stats + .prepare_submit_duration + .saturating_add(started.elapsed()); } - - Ok(SubmittedResidentLosslessMetalBufferEncodeBatchKind::Chunks( - chunks, - )) + Ok(SubmittedResidentLosslessMetalBufferEncodeChunk { + metadatas, + prepare_durations, + pending, + batch_started, + }) } #[cfg(target_os = "macos")] diff --git a/crates/j2k-metal/src/encode/resident_types.rs b/crates/j2k-metal/src/encode/resident_types.rs index 1d90f10b..2b737408 100644 --- a/crates/j2k-metal/src/encode/resident_types.rs +++ b/crates/j2k-metal/src/encode/resident_types.rs @@ -8,6 +8,7 @@ use j2k_native::J2kPacketizationPacketDescriptor; use crate::compute; use super::{ + resident_schedule::SubmittedResidentLosslessChunkPipeline, submitted::OwnedMetalLosslessEncodeTile, LosslessDeviceEncodePlan, MetalEncodeInputStaging, MetalEncodedJ2k, MetalLosslessEncodeBatchStats, }; @@ -64,12 +65,7 @@ pub(super) struct SubmittedResidentLosslessMetalBufferEncodeBatch { pub(super) encode_started: Instant, pub(super) tiles: Vec, pub(super) staging: MetalEncodeInputStaging, - pub(super) kind: SubmittedResidentLosslessMetalBufferEncodeBatchKind, -} - -pub(super) enum SubmittedResidentLosslessMetalBufferEncodeBatchKind { - Empty, - Chunks(Vec), + pub(super) pipeline: Option, } pub(super) struct SubmittedResidentLosslessMetalBufferEncodeChunk { diff --git a/crates/j2k-metal/src/encode/resident_wait.rs b/crates/j2k-metal/src/encode/resident_wait.rs index 5f5a348a..fbcff705 100644 --- a/crates/j2k-metal/src/encode/resident_wait.rs +++ b/crates/j2k-metal/src/encode/resident_wait.rs @@ -10,7 +10,6 @@ use super::{ J2kLosslessEncodeOptions, MetalEncodedJ2k, MetalLosslessBufferEncodeBatchOutcome, MetalLosslessBufferEncodeOutcome, MetalLosslessEncodeResidency, MetalLosslessEncodeStageStats, ResidentLosslessBufferEncodeMetadata, SubmittedResidentLosslessMetalBufferEncodeBatch, - SubmittedResidentLosslessMetalBufferEncodeBatchKind, }; #[cfg(target_os = "macos")] @@ -59,146 +58,33 @@ pub(super) fn wait_submitted_resident_lossless_buffer_encode_batch( } #[cfg(target_os = "macos")] -#[expect( - clippy::too_many_lines, - reason = "wait and harvest preserve chunk outcome and timing order" -)] fn wait_submitted_resident_lossless_buffer_encode_batch_once( submitted: &mut SubmittedResidentLosslessMetalBufferEncodeBatch, ) -> Result { - let outcome_count = match &submitted.kind { - SubmittedResidentLosslessMetalBufferEncodeBatchKind::Empty => 0, - SubmittedResidentLosslessMetalBufferEncodeBatchKind::Chunks(chunks) => chunks - .iter() - .try_fold(0usize, |total, chunk| { - total.checked_add(chunk.metadatas.len()) - }) - .ok_or(j2k_core::BatchInfrastructureError::AllocationTooLarge { - what: "J2K Metal resident encode outcome collection", - requested: usize::MAX, - cap: j2k_core::DEFAULT_MAX_HOST_ALLOCATION_BYTES, - })?, - }; + let outcome_count = submitted.pipeline.as_ref().map_or( + 0, + super::resident_schedule::SubmittedResidentLosslessChunkPipeline::total_tiles, + ); let mut outcome_budget = crate::batch_allocation::BatchMetadataBudget::new( "J2K Metal resident encode outcome collection", ); let mut outcomes = outcome_budget.try_vec(outcome_count, "J2K Metal resident encode outcome slots")?; - match std::mem::replace( - &mut submitted.kind, - SubmittedResidentLosslessMetalBufferEncodeBatchKind::Empty, - ) { - SubmittedResidentLosslessMetalBufferEncodeBatchKind::Empty => {} - SubmittedResidentLosslessMetalBufferEncodeBatchKind::Chunks(chunks) => { - if submitted.options.validation == J2kEncodeValidation::External - && submitted.options.block_coding_mode == J2kBlockCodingMode::HighThroughput - && chunks.len() > 1 - { - let wait_started = compute::metal_profile_stages_enabled().then(Instant::now); - let mut chunk_budget = crate::batch_allocation::BatchMetadataBudget::new( - "J2K Metal resident encode chunk wait plan", - ); - let mut chunk_metadatas = chunk_budget.try_vec( - chunks.len(), - "J2K Metal resident encode chunk metadata groups", - )?; - let mut pending_batches = chunk_budget - .try_vec(chunks.len(), "J2K Metal resident encode pending batches")?; - for chunk in chunks { - chunk_metadatas.push(( - chunk.metadatas, - chunk.prepare_durations, - chunk.batch_started, - )); - pending_batches.push(chunk.pending); - } - let batches = compute::wait_resident_lossless_codestream_batches(pending_batches)?; - if let Some(started) = wait_started { - let elapsed = started.elapsed(); - submitted.stats.stage_stats.codestream_wait_duration = submitted - .stats - .stage_stats - .codestream_wait_duration - .saturating_add(elapsed); - submitted.stats.stage_stats.sync_wait_duration = submitted - .stats - .stage_stats - .sync_wait_duration - .saturating_add(elapsed); - } - for ((metadatas, prepare_durations, batch_started), batch) in - chunk_metadatas.into_iter().zip(batches) - { - if compute::metal_profile_stages_enabled() { - submitted - .stats - .stage_stats - .add_assign(MetalLosslessEncodeStageStats::from(batch.stage_stats)); - } - let codestreams = batch.codestreams; - let batch_duration = duration_share(batch_started.elapsed(), codestreams.len()); - for ((metadata, prepare_duration), codestream) in metadatas - .into_iter() - .zip(prepare_durations) - .zip(codestreams) - { - let finished = finished_resident_lossless_buffer_encode( - metadata, - codestream, - prepare_duration.saturating_add(batch_duration), - )?; - outcomes.push(validate_finished_resident_lossless_buffer_encode( - finished, - submitted.options, - &submitted.session, - )?); - } - } - } else { - for chunk in chunks { - let wait_started = compute::metal_profile_stages_enabled().then(Instant::now); - let batch = compute::wait_resident_lossless_codestream_batch(chunk.pending)?; - if let Some(started) = wait_started { - let elapsed = started.elapsed(); - submitted.stats.stage_stats.codestream_wait_duration = submitted - .stats - .stage_stats - .codestream_wait_duration - .saturating_add(elapsed); - submitted.stats.stage_stats.sync_wait_duration = submitted - .stats - .stage_stats - .sync_wait_duration - .saturating_add(elapsed); - submitted - .stats - .stage_stats - .add_assign(MetalLosslessEncodeStageStats::from(batch.stage_stats)); - } - let codestreams = batch.codestreams; - let batch_duration = - duration_share(chunk.batch_started.elapsed(), codestreams.len()); - for ((metadata, prepare_duration), codestream) in chunk - .metadatas - .into_iter() - .zip(chunk.prepare_durations) - .zip(codestreams) - { - let finished = finished_resident_lossless_buffer_encode( - metadata, - codestream, - prepare_duration.saturating_add(batch_duration), - )?; - outcomes.push(validate_finished_resident_lossless_buffer_encode( - finished, - submitted.options, - &submitted.session, - )?); - } - } - } + if let Some(mut pipeline) = submitted.pipeline.take() { + while let Some(chunk) = pipeline.take_active() { + let tile_count = chunk.metadatas.len(); + super::resident_schedule::SubmittedResidentLosslessChunkPipeline::record_active_completed( + tile_count, + ); + wait_and_harvest_resident_chunk(submitted, chunk, &mut outcomes)?; + pipeline.submit_next(&submitted.session, &mut submitted.stats)?; } } + if outcomes.len() != outcome_count { + return Err(crate::Error::MetalKernel { + message: "J2K Metal resident encode produced an unexpected outcome count".to_string(), + }); + } submitted.stats.encode_wall_duration = submitted.encode_started.elapsed(); Ok(MetalLosslessBufferEncodeBatchOutcome { outcomes, @@ -206,6 +92,70 @@ fn wait_submitted_resident_lossless_buffer_encode_batch_once( }) } +#[cfg(target_os = "macos")] +fn wait_and_harvest_resident_chunk( + submitted: &mut SubmittedResidentLosslessMetalBufferEncodeBatch, + chunk: super::SubmittedResidentLosslessMetalBufferEncodeChunk, + outcomes: &mut Vec, +) -> Result<(), crate::Error> { + let wait_started = compute::metal_profile_stages_enabled().then(Instant::now); + let batch = compute::wait_resident_lossless_codestream_batch(chunk.pending)?; + if let Some(started) = wait_started { + let elapsed = started.elapsed(); + submitted.stats.stage_stats.codestream_wait_duration = submitted + .stats + .stage_stats + .codestream_wait_duration + .saturating_add(elapsed); + submitted.stats.stage_stats.sync_wait_duration = submitted + .stats + .stage_stats + .sync_wait_duration + .saturating_add(elapsed); + submitted + .stats + .stage_stats + .add_assign(MetalLosslessEncodeStageStats::from(batch.stage_stats)); + } + validate_resident_chunk_result_counts( + chunk.metadatas.len(), + chunk.prepare_durations.len(), + batch.codestreams.len(), + )?; + let batch_duration = duration_share(chunk.batch_started.elapsed(), batch.codestreams.len()); + for ((metadata, prepare_duration), codestream) in chunk + .metadatas + .into_iter() + .zip(chunk.prepare_durations) + .zip(batch.codestreams) + { + let finished = finished_resident_lossless_buffer_encode( + metadata, + codestream, + prepare_duration.saturating_add(batch_duration), + )?; + outcomes.push(validate_finished_resident_lossless_buffer_encode( + finished, + submitted.options, + &submitted.session, + )?); + } + Ok(()) +} + +fn validate_resident_chunk_result_counts( + metadata_count: usize, + prepare_duration_count: usize, + codestream_count: usize, +) -> Result<(), crate::Error> { + if metadata_count == prepare_duration_count && metadata_count == codestream_count { + return Ok(()); + } + Err(crate::Error::MetalKernel { + message: "J2K Metal resident chunk result count does not match submitted tiles".to_string(), + }) +} + #[cfg(target_os = "macos")] fn finished_resident_lossless_buffer_encode( metadata: ResidentLosslessBufferEncodeMetadata, @@ -290,3 +240,19 @@ fn validate_finished_resident_lossless_buffer_encode( validation_duration, }) } + +#[cfg(test)] +mod tests { + use super::validate_resident_chunk_result_counts; + + #[test] + fn resident_chunk_result_counts_must_match_before_zip() { + assert!(validate_resident_chunk_result_counts(2, 2, 2).is_ok()); + for counts in [(2, 1, 2), (2, 2, 1), (1, 2, 2)] { + assert!( + validate_resident_chunk_result_counts(counts.0, counts.1, counts.2).is_err(), + "accepted mismatched resident counts {counts:?}" + ); + } + } +} diff --git a/crates/j2k-metal/src/encode/structure_tests.rs b/crates/j2k-metal/src/encode/structure_tests.rs index 6b290c7c..423b8c33 100644 --- a/crates/j2k-metal/src/encode/structure_tests.rs +++ b/crates/j2k-metal/src/encode/structure_tests.rs @@ -10,6 +10,7 @@ const MODULE_LIMITS: &[(&str, usize)] = &[ ("resident_hybrid.rs", 200), ("resident_plan.rs", 110), ("resident_prepare.rs", 130), + ("resident_schedule.rs", 300), ("resident_submit.rs", 350), ("resident_validation.rs", 130), ("resident_wait.rs", 310), @@ -54,13 +55,20 @@ const FUNCTION_OWNERS: &[(&str, &[&str])] = &[ &["prepare_planned_resident_lossless_tiles_batch"], ), ( - "resident_submit.rs", + "resident_schedule.rs", &[ "submit_planned_resident_lossless_tiles", - "submit_planned_resident_lossless_tiles_chunked", - "duration_share", + "submit_planned_resident_ht_lossless_tiles_batch", + "submit_planned_resident_classic_lossless_tiles_batch", + "validate_chunk_ranges", + "submit_next", + "take_active", ], ), + ( + "resident_submit.rs", + &["submit_resident_lossless_chunk", "duration_share"], + ), ( "resident_validation.rs", &[ @@ -73,6 +81,8 @@ const FUNCTION_OWNERS: &[(&str, &[&str])] = &[ &[ "wait_submitted_resident_lossless_buffer_encode_batch", "wait_submitted_resident_lossless_buffer_encode_batch_once", + "wait_and_harvest_resident_chunk", + "validate_resident_chunk_result_counts", "finished_resident_lossless_buffer_encode", "validate_finished_resident_lossless_buffer_encode", ], @@ -201,7 +211,11 @@ fn public_cfg_docs_and_metal_command_order_are_pinned() { ] { assert!(batch.contains(documented_api)); } - assert_eq!(batch.matches("#[cfg(target_os = \"macos\")]").count(), 11); + assert_eq!(batch.matches("#[cfg(target_os = \"macos\")]").count(), 12); + + let types = read_source(&root.join("types.rs")); + assert!(types.contains("pub fn from_resident(")); + assert!(types.contains("Device identity is checked against the encode session")); let unavailable = read_source(&root.join("unavailable.rs")); assert_eq!( @@ -226,15 +240,24 @@ fn public_cfg_docs_and_metal_command_order_are_pinned() { assert_in_order( &submit, &[ - "prepare_planned_resident_lossless_tiles_batch(chunk_planned, session)", - "let pending = submit_chunk(session, batch_items)?;", + "prepare_planned_resident_lossless_tiles_batch(planned, session)?;", + "let pending = match family {", ], ); let wait = read_source(&root.join("resident_wait.rs")); assert_in_order( &wait, &[ - "compute::wait_resident_lossless_codestream_batches", + "while let Some(chunk) = pipeline.take_active()", + "SubmittedResidentLosslessChunkPipeline::record_active_completed(", + "wait_and_harvest_resident_chunk(submitted, chunk, &mut outcomes)?;", + "pipeline.submit_next(&submitted.session, &mut submitted.stats)?;", + ], + ); + assert_in_order( + &wait, + &[ + "compute::wait_resident_lossless_codestream_batch(chunk.pending)?;", "finished_resident_lossless_buffer_encode(", "validate_finished_resident_lossless_buffer_encode(", ], diff --git a/crates/j2k-metal/src/encode/tests.rs b/crates/j2k-metal/src/encode/tests.rs index 647b9059..6879a6c1 100644 --- a/crates/j2k-metal/src/encode/tests.rs +++ b/crates/j2k-metal/src/encode/tests.rs @@ -52,6 +52,8 @@ mod resident_batches; #[cfg(target_os = "macos")] mod resident_buffers; #[cfg(target_os = "macos")] +mod resident_schedule; +#[cfg(target_os = "macos")] mod routing; mod stage_validation; mod stats_inflight; diff --git a/crates/j2k-metal/src/encode/tests/batch.rs b/crates/j2k-metal/src/encode/tests/batch.rs index 15b9a852..87516b78 100644 --- a/crates/j2k-metal/src/encode/tests/batch.rs +++ b/crates/j2k-metal/src/encode/tests/batch.rs @@ -219,6 +219,7 @@ fn metal_ht_batch_encode_preserves_order_and_matches_inflight_one() { }; compute::reset_resident_codestream_command_buffer_waits_for_test(); + super::super::reset_resident_schedule_counters_for_test(); let serial = super::super::encode_lossless_from_padded_metal_buffers_to_metal_batch( &tiles, &options, @@ -231,8 +232,13 @@ fn metal_ht_batch_encode_preserves_order_and_matches_inflight_one() { .expect("serial Metal HTJ2K batch"); assert_eq!( compute::resident_codestream_command_buffer_waits_for_test(), - 1, - "multi-chunk HT batch should wait once before harvesting completed chunks" + inputs.len(), + "an in-flight cap of one must wait and retire each tile before submitting the next" + ); + assert_eq!( + super::super::resident_schedule_counters_for_test(), + (0, 1, inputs.len()), + "the scheduler must never own more than one pending tile" ); let cpu_validated_options = lossless_options! { @@ -241,6 +247,7 @@ fn metal_ht_batch_encode_preserves_order_and_matches_inflight_one() { validation: J2kEncodeValidation::CpuRoundTrip, }; compute::reset_resident_codestream_command_buffer_waits_for_test(); + super::super::reset_resident_schedule_counters_for_test(); let cpu_validated = super::super::encode_lossless_from_padded_metal_buffers_to_metal_batch( &tiles, &cpu_validated_options, @@ -257,7 +264,12 @@ fn metal_ht_batch_encode_preserves_order_and_matches_inflight_one() { inputs.len(), "CPU roundtrip validation should keep per-chunk waits to preserve overlap" ); + assert_eq!( + super::super::resident_schedule_counters_for_test(), + (0, 1, inputs.len()) + ); + super::super::reset_resident_schedule_counters_for_test(); let parallel = super::super::encode_lossless_from_padded_metal_buffers_to_metal_batch( &tiles, &options, @@ -268,6 +280,10 @@ fn metal_ht_batch_encode_preserves_order_and_matches_inflight_one() { }, ) .expect("parallel Metal HTJ2K batch"); + assert_eq!( + super::super::resident_schedule_counters_for_test(), + (0, 2, inputs.len().div_ceil(2)) + ); let repeated_parallel = super::super::encode_lossless_from_padded_metal_buffers_to_metal_batch( &tiles, &options, @@ -359,13 +375,14 @@ fn metal_parallel_batch_returns_indexed_injected_failure() { validation: J2kEncodeValidation::External, }; + super::super::reset_resident_schedule_counters_for_test(); super::super::set_test_resident_encode_failure_index(Some(1)); let Err(err) = super::super::encode_lossless_from_padded_metal_buffers_to_metal_batch( &tiles, &options, &session, super::super::MetalLosslessEncodeConfig { - gpu_encode_inflight_tiles: Some(2), + gpu_encode_inflight_tiles: Some(1), gpu_encode_memory_budget_bytes: Some(1024 * 1024 * 1024), }, ) else { @@ -378,4 +395,9 @@ fn metal_parallel_batch_returns_indexed_injected_failure() { crate::Error::MetalKernel { message } if message == "injected J2K Metal resident encode failure at tile 1" )); + assert_eq!( + super::super::resident_schedule_counters_for_test(), + (0, 1, 1), + "a later preparation failure must not submit that or any later chunk" + ); } diff --git a/crates/j2k-metal/src/encode/tests/resident_buffers.rs b/crates/j2k-metal/src/encode/tests/resident_buffers.rs index 67b0779d..cbf61607 100644 --- a/crates/j2k-metal/src/encode/tests/resident_buffers.rs +++ b/crates/j2k-metal/src/encode/tests/resident_buffers.rs @@ -18,18 +18,17 @@ fn metal_buffer_lossless_encode_pads_edge_tile_on_device() { let session = crate::MetalBackendSession::new(device); let buffer = j2k_metal_support::checked_shared_buffer_with_slice(session.device(), &pixels) .expect("upload test pixels"); + let layout = j2k_metal_support::MetalImageLayout::new(0, (7, 5), 7 * 3, PixelFormat::Rgb8) + .expect("valid resident J2K 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 J2K input"); + assert_eq!(resident.dimensions(), (7, 5)); let encoded = super::super::encode_lossless_from_metal_buffer( - super::super::MetalLosslessEncodeTile { - buffer: &buffer, - byte_offset: 0, - width: 7, - height: 5, - pitch_bytes: 7 * 3, - output_width: 8, - output_height: 8, - format: PixelFormat::Rgb8, - }, + super::super::MetalLosslessEncodeTile::from_resident(&resident, (8, 8)), &lossless_options! { backend: EncodeBackendPreference::RequireDevice, }, diff --git a/crates/j2k-metal/src/encode/tests/resident_schedule.rs b/crates/j2k-metal/src/encode/tests/resident_schedule.rs new file mode 100644 index 00000000..c8169540 --- /dev/null +++ b/crates/j2k-metal/src/encode/tests/resident_schedule.rs @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use super::*; + +#[cfg(target_os = "macos")] +#[test] +#[expect( + clippy::cast_sign_loss, + reason = "bounded synthetic pixels are nonnegative" +)] +fn classic_inflight_one_waits_before_submitting_the_next_chunk() { + if !should_run_metal_runtime() { + return; + } + + let first = (0..8 * 8) + .map(|index| ((index * 7 + 3) & 0xFF) as u8) + .collect::>(); + let second = (0..8 * 8) + .map(|index| ((index * 13 + 5) & 0xFF) as u8) + .collect::>(); + let session = crate::MetalBackendSession::system_default().expect("Metal session"); + let first_buffer = + crate::benchmark_private_buffer_with_bytes(&session, &first).expect("first private input"); + let second_buffer = crate::benchmark_private_buffer_with_bytes(&session, &second) + .expect("second private input"); + let tiles = + [&first_buffer, &second_buffer].map(|buffer| super::super::MetalLosslessEncodeTile { + buffer, + byte_offset: 0, + width: 8, + height: 8, + pitch_bytes: 8, + output_width: 8, + output_height: 8, + format: PixelFormat::Gray8, + }); + let options = lossless_options! { + backend: EncodeBackendPreference::RequireDevice, + block_coding_mode: J2kBlockCodingMode::Classic, + validation: J2kEncodeValidation::External, + }; + + compute::reset_resident_codestream_command_buffer_waits_for_test(); + super::super::reset_resident_schedule_counters_for_test(); + let outcome = super::super::encode_lossless_from_padded_metal_buffers_to_metal_batch( + &tiles, + &options, + &session, + super::super::MetalLosslessEncodeConfig { + gpu_encode_inflight_tiles: Some(1), + gpu_encode_memory_budget_bytes: Some(1024 * 1024 * 1024), + }, + ) + .expect("classic resident batch"); + + assert_eq!(outcome.outcomes.len(), 2); + assert_eq!( + compute::resident_codestream_command_buffer_waits_for_test(), + 2 + ); + assert_eq!( + super::super::resident_schedule_counters_for_test(), + (0, 1, 2) + ); +} + +#[cfg(target_os = "macos")] +#[test] +#[expect( + clippy::cast_sign_loss, + reason = "bounded synthetic pixels are nonnegative" +)] +fn dropping_submission_does_not_schedule_remaining_resident_chunks() { + if !should_run_metal_runtime() { + return; + } + + let inputs = (0..4) + .map(|seed| { + (0..8 * 8) + .map(|index| ((index * 11 + seed) & 0xFF) as u8) + .collect::>() + }) + .collect::>(); + let session = crate::MetalBackendSession::system_default().expect("Metal session"); + let buffers = inputs + .iter() + .map(|bytes| { + crate::benchmark_private_buffer_with_bytes(&session, bytes) + .expect("private benchmark input buffer") + }) + .collect::>(); + let tiles = buffers + .iter() + .map(|buffer| super::super::MetalLosslessEncodeTile { + buffer, + byte_offset: 0, + width: 8, + height: 8, + pitch_bytes: 8, + output_width: 8, + output_height: 8, + format: PixelFormat::Gray8, + }) + .collect::>(); + let options = lossless_options! { + backend: EncodeBackendPreference::RequireDevice, + block_coding_mode: J2kBlockCodingMode::HighThroughput, + validation: J2kEncodeValidation::External, + }; + + super::super::reset_resident_schedule_counters_for_test(); + let submitted = crate::submit_lossless_batch_to_metal( + super::super::MetalLosslessEncodeBatchRequest { + tiles: &tiles, + staging: super::super::MetalEncodeInputStaging::AlreadyPaddedContiguous, + config: super::super::MetalLosslessEncodeConfig { + gpu_encode_inflight_tiles: Some(1), + gpu_encode_memory_budget_bytes: Some(1024 * 1024 * 1024), + }, + }, + &options, + &session, + ) + .expect("resident submission"); + assert_eq!( + super::super::resident_schedule_counters_for_test(), + (1, 1, 1), + "submit must schedule exactly one active chunk" + ); + + drop(submitted); + + assert_eq!( + super::super::resident_schedule_counters_for_test(), + (0, 1, 1), + "drop must release scheduler ownership without scheduling a suspended chunk" + ); +} diff --git a/crates/j2k-metal/src/encode/types.rs b/crates/j2k-metal/src/encode/types.rs index 07d6d954..fa1f9890 100644 --- a/crates/j2k-metal/src/encode/types.rs +++ b/crates/j2k-metal/src/encode/types.rs @@ -4,6 +4,8 @@ use j2k::EncodedJ2k; #[cfg(target_os = "macos")] use j2k_core::PixelFormat; #[cfg(target_os = "macos")] +use j2k_metal_support::ResidentMetalImage; +#[cfg(target_os = "macos")] use metal::Buffer; use std::time::Duration; @@ -80,6 +82,43 @@ impl<'a> MetalLosslessEncodeTile<'a> { } } + /// Describe a validated resident image to the lossless encoder. + /// + /// Device identity is checked against the encode session before Metal work + /// is submitted. The resident owner is retained by deferred submissions. + #[must_use] + pub fn from_resident(image: &'a ResidentMetalImage, output_dimensions: (u32, u32)) -> Self { + let layout = image.layout(); + Self { + // SAFETY: the resident image remains borrowed by this tile and the + // handle is used only for read-only backend binding. + 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(), + } + } + + pub(super) 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( + "J2K input belongs to a different Metal device", + j2k_metal_support::MetalSupportError::MetalImageDeviceMismatch { + image_registry_id, + requested_registry_id, + }, + )) + } + } + /// Byte offset of the first source pixel. pub fn byte_offset(self) -> usize { self.byte_offset diff --git a/crates/j2k-metal/src/idwt.metal b/crates/j2k-metal/src/idwt.metal index 030c411a..196e69df 100644 --- a/crates/j2k-metal/src/idwt.metal +++ b/crates/j2k-metal/src/idwt.metal @@ -62,6 +62,13 @@ struct J2kIdwtStatus { uint reserved1; }; +struct J2kIdwt97StepParams { + float coefficient; + uint parity; + uint reserved0; + uint reserved1; +}; + constant uint J2K_IDWT_STATUS_OK = 0; constant uint J2K_IDWT_STATUS_FAIL = 1; @@ -428,186 +435,90 @@ kernel void j2k_idwt_reversible53_vertical_pass_batched( } } -kernel void j2k_idwt_irreversible97_single_decomposition( - device const float *ll [[buffer(0)]], - device const float *hl [[buffer(1)]], - device const float *lh [[buffer(2)]], - device const float *hh [[buffer(3)]], - device float *out [[buffer(4)]], - constant J2kIdwtSingleDecompositionParams ¶ms [[buffer(5)]], - device J2kIdwtStatus *status [[buffer(6)]], - uint gid [[thread_position_in_grid]] +kernel void j2k_idwt_irreversible97_horizontal_scale( + device float *out [[buffer(0)]], + constant J2kIdwtSingleDecompositionParams ¶ms [[buffer(1)]], + uint2 gid [[thread_position_in_grid]] ) { - if (gid != 0u) { - return; - } - - status->code = J2K_IDWT_STATUS_OK; - status->detail = 0u; - status->reserved0 = 0u; - status->reserved1 = 0u; - - if (params.width == 0u || params.height == 0u) { - status->code = J2K_IDWT_STATUS_FAIL; - status->detail = 1u; + if (gid.x >= params.width || gid.y >= params.height) { return; } - const float NEG_ALPHA = CODEC_MATH_IDWT97_NEG_ALPHA; - const float NEG_BETA = CODEC_MATH_IDWT97_NEG_BETA; - const float NEG_GAMMA = CODEC_MATH_IDWT97_NEG_GAMMA; - const float NEG_DELTA = CODEC_MATH_IDWT97_NEG_DELTA; const float KAPPA = CODEC_MATH_DWT97_KAPPA; const float INV_KAPPA = CODEC_MATH_DWT97_INV_KAPPA; - - const uint low_x_parity = params.x0 & 1u; - const uint low_y_parity = params.y0 & 1u; - - for (uint local_y = 0u; local_y < params.height; ++local_y) { - const uint global_y = params.y0 + params.output_y + local_y; - const bool low_y = (global_y & 1u) == low_y_parity; - const uint full_band_y = low_y ? low_index(global_y, params.y0) : high_index(global_y, params.y0); - - for (uint local_x = 0u; local_x < params.width; ++local_x) { - const uint global_x = params.x0 + params.output_x + local_x; - const bool low_x = (global_x & 1u) == low_x_parity; - const uint full_band_x = low_x ? low_index(global_x, params.x0) : high_index(global_x, params.x0); - const uint out_idx = local_y * params.width + local_x; - - if (low_y && low_x) { - const uint band_x = full_band_x - params.ll_x; - const uint band_y = full_band_y - params.ll_y; - out[out_idx] = (band_x < params.ll_width && band_y < params.ll_height) - ? ll[band_y * params.ll_width + band_x] - : 0.0f; - } else if (low_y) { - const uint band_x = full_band_x - params.hl_x; - const uint band_y = full_band_y - params.hl_y; - out[out_idx] = (band_x < params.hl_width && band_y < params.hl_height) - ? hl[band_y * params.hl_width + band_x] - : 0.0f; - } else if (low_x) { - const uint band_x = full_band_x - params.lh_x; - const uint band_y = full_band_y - params.lh_y; - out[out_idx] = (band_x < params.lh_width && band_y < params.lh_height) - ? lh[band_y * params.lh_width + band_x] - : 0.0f; - } else { - const uint band_x = full_band_x - params.hh_x; - const uint band_y = full_band_y - params.hh_y; - out[out_idx] = (band_x < params.hh_width && band_y < params.hh_height) - ? hh[band_y * params.hh_width + band_x] - : 0.0f; - } - } - } + float sample = out[gid.y * params.width + gid.x]; if (params.width == 1u) { if (((params.x0 + params.output_x) & 1u) != 0u) { - for (uint row = 0u; row < params.height; ++row) { - out[row * params.width] *= 0.5f; - } + sample *= 0.5f; } } else { const uint first_even_x = (params.x0 + params.output_x) & 1u; - const uint first_odd_x = 1u - first_even_x; - const float k0 = first_even_x == 0u ? KAPPA : INV_KAPPA; - const float k1 = first_even_x == 0u ? INV_KAPPA : KAPPA; - - for (uint row = 0u; row < params.height; ++row) { - device float *row_ptr = out + row * params.width; - - for (uint x = 0u; x + 1u < params.width; x += 2u) { - row_ptr[x] *= k0; - row_ptr[x + 1u] *= k1; - } - if ((params.width & 1u) != 0u) { - row_ptr[params.width - 1u] *= k0; - } - - irreversible97_horizontal_step(row_ptr, params.width, first_even_x, NEG_DELTA); - irreversible97_horizontal_step(row_ptr, params.width, first_odd_x, NEG_GAMMA); - irreversible97_horizontal_step(row_ptr, params.width, first_even_x, NEG_BETA); - irreversible97_horizontal_step(row_ptr, params.width, first_odd_x, NEG_ALPHA); - } + sample *= (gid.x & 1u) == first_even_x ? KAPPA : INV_KAPPA; } - if (params.height == 1u) { - if (((params.y0 + params.output_y) & 1u) != 0u) { - for (uint col = 0u; col < params.width; ++col) { - out[col] *= 0.5f; - } - } + out[gid.y * params.width + gid.x] = sample; +} + +kernel void j2k_idwt_irreversible97_vertical_scale( + device float *out [[buffer(0)]], + constant J2kIdwtSingleDecompositionParams ¶ms [[buffer(1)]], + uint2 gid [[thread_position_in_grid]] +) { + if (gid.x >= params.width || gid.y >= params.height) { return; } - const uint first_even_y = (params.y0 + params.output_y) & 1u; - const uint first_odd_y = 1u - first_even_y; - const float k0 = first_even_y == 0u ? KAPPA : INV_KAPPA; - const float k1 = first_even_y == 0u ? INV_KAPPA : KAPPA; + const float KAPPA = CODEC_MATH_DWT97_KAPPA; + const float INV_KAPPA = CODEC_MATH_DWT97_INV_KAPPA; + float sample = out[gid.y * params.width + gid.x]; - for (uint row = 0u; row + 1u < params.height; row += 2u) { - for (uint col = 0u; col < params.width; ++col) { - out[row * params.width + col] *= k0; - out[(row + 1u) * params.width + col] *= k1; - } - } - if ((params.height & 1u) != 0u) { - const uint row = params.height - 1u; - for (uint col = 0u; col < params.width; ++col) { - out[row * params.width + col] *= k0; + if (params.height == 1u) { + if (((params.y0 + params.output_y) & 1u) != 0u) { + sample *= 0.5f; } + } else { + const uint first_even_y = (params.y0 + params.output_y) & 1u; + sample *= (gid.y & 1u) == first_even_y ? KAPPA : INV_KAPPA; } - for (uint row = first_even_y; row < params.height; row += 2u) { - const uint row_above = periodic_symmetric_extension_left_u32(row, 1u); - const uint row_below = periodic_symmetric_extension_right_u32(row, 1u, params.height); - for (uint col = 0u; col < params.width; ++col) { - const uint idx = row * params.width + col; - out[idx] = fma( - out[row_above * params.width + col] + out[row_below * params.width + col], - NEG_DELTA, - out[idx] - ); - } - } + out[gid.y * params.width + gid.x] = sample; +} - for (uint row = first_odd_y; row < params.height; row += 2u) { - const uint row_above = periodic_symmetric_extension_left_u32(row, 1u); - const uint row_below = periodic_symmetric_extension_right_u32(row, 1u, params.height); - for (uint col = 0u; col < params.width; ++col) { - const uint idx = row * params.width + col; - out[idx] = fma( - out[row_above * params.width + col] + out[row_below * params.width + col], - NEG_GAMMA, - out[idx] - ); - } +kernel void j2k_idwt_irreversible97_horizontal_step( + device float *out [[buffer(0)]], + constant J2kIdwtSingleDecompositionParams ¶ms [[buffer(1)]], + constant J2kIdwt97StepParams &step [[buffer(2)]], + uint2 gid [[thread_position_in_grid]] +) { + if (gid.x >= params.width || gid.y >= params.height || params.width <= 1u + || (gid.x & 1u) != step.parity) { + return; } - for (uint row = first_even_y; row < params.height; row += 2u) { - const uint row_above = periodic_symmetric_extension_left_u32(row, 1u); - const uint row_below = periodic_symmetric_extension_right_u32(row, 1u, params.height); - for (uint col = 0u; col < params.width; ++col) { - const uint idx = row * params.width + col; - out[idx] = fma( - out[row_above * params.width + col] + out[row_below * params.width + col], - NEG_BETA, - out[idx] - ); - } - } + const uint left = periodic_symmetric_extension_left_u32(gid.x, 1u); + const uint right = periodic_symmetric_extension_right_u32(gid.x, 1u, params.width); + const uint idx = gid.y * params.width + gid.x; + out[idx] = fma(out[gid.y * params.width + left] + out[gid.y * params.width + right], + step.coefficient, + out[idx]); +} - for (uint row = first_odd_y; row < params.height; row += 2u) { - const uint row_above = periodic_symmetric_extension_left_u32(row, 1u); - const uint row_below = periodic_symmetric_extension_right_u32(row, 1u, params.height); - for (uint col = 0u; col < params.width; ++col) { - const uint idx = row * params.width + col; - out[idx] = fma( - out[row_above * params.width + col] + out[row_below * params.width + col], - NEG_ALPHA, - out[idx] - ); - } +kernel void j2k_idwt_irreversible97_vertical_step( + device float *out [[buffer(0)]], + constant J2kIdwtSingleDecompositionParams ¶ms [[buffer(1)]], + constant J2kIdwt97StepParams &step [[buffer(2)]], + uint2 gid [[thread_position_in_grid]] +) { + if (gid.x >= params.width || gid.y >= params.height || params.height <= 1u + || (gid.y & 1u) != step.parity) { + return; } + + const uint above = periodic_symmetric_extension_left_u32(gid.y, 1u); + const uint below = periodic_symmetric_extension_right_u32(gid.y, 1u, params.height); + const uint idx = gid.y * params.width + gid.x; + out[idx] = fma(out[above * params.width + gid.x] + out[below * params.width + gid.x], + step.coefficient, + out[idx]); } diff --git a/crates/j2k-metal/src/idwt.rs b/crates/j2k-metal/src/idwt.rs index da24821d..87413cde 100644 --- a/crates/j2k-metal/src/idwt.rs +++ b/crates/j2k-metal/src/idwt.rs @@ -217,6 +217,347 @@ mod tests { ); } + #[cfg(target_os = "macos")] + #[test] + fn staged_irreversible_idwt_matches_native_odd_geometry() { + const EXPECTED_BITS: [u32; 15] = [ + 3_243_516_307, + 1_088_535_889, + 1_086_781_832, + 3_237_068_116, + 1_072_899_983, + 1_090_811_482, + 3_205_449_560, + 3_240_528_043, + 1_057_965_919, + 1_095_801_595, + 3_240_069_383, + 1_090_703_406, + 1_072_530_023, + 3_238_711_343, + 1_091_758_981, + ]; + if !should_run_metal_runtime() { + return; + } + let rect = j2k_native::J2kRect { + x0: 0, + y0: 0, + x1: 5, + y1: 3, + }; + let ll = [0.5, -1.25, 2.0, 3.5, -4.25, 5.75]; + let hl = [6.5, -7.0, 8.25, -9.5]; + let lh = [10.0, -11.5, 12.75]; + let hh = [-13.0, 14.25]; + let job = j2k_native::J2kSingleDecompositionIdwtJob { + rect, + transform: j2k_native::J2kWaveletTransform::Irreversible97, + ll: j2k_native::J2kIdwtBand { + rect: j2k_native::J2kRect { + x0: 0, + y0: 0, + x1: 3, + y1: 2, + }, + coefficients: &ll, + }, + hl: j2k_native::J2kIdwtBand { + rect: j2k_native::J2kRect { + x0: 0, + y0: 0, + x1: 2, + y1: 2, + }, + coefficients: &hl, + }, + lh: j2k_native::J2kIdwtBand { + rect: j2k_native::J2kRect { + x0: 0, + y0: 0, + x1: 3, + y1: 1, + }, + coefficients: &lh, + }, + hh: j2k_native::J2kIdwtBand { + rect: j2k_native::J2kRect { + x0: 0, + y0: 0, + x1: 2, + y1: 1, + }, + coefficients: &hh, + }, + }; + let mut actual = vec![0.0; EXPECTED_BITS.len()]; + + crate::compute::decode_irreversible97_staged_single_decomposition_idwt(job, &mut actual) + .expect("staged irreversible Metal IDWT"); + + for (index, (actual, expected_bits)) in actual.iter().zip(EXPECTED_BITS).enumerate() { + let expected = f32::from_bits(expected_bits); + assert!( + (actual - expected).abs() <= 2.0e-5, + "sample {index}: expected {expected}, got {actual}" + ); + } + } + + #[cfg(target_os = "macos")] + #[test] + fn staged_irreversible_idwt_preserves_degenerate_origin_scaling() { + if !should_run_metal_runtime() { + return; + } + + let coefficient = [8.0]; + for (x0, y0, expected) in [(0, 0, 8.0_f32), (1, 0, 4.0), (0, 1, 4.0), (1, 1, 2.0)] { + let rect = j2k_native::J2kRect { + x0, + y0, + x1: x0 + 1, + y1: y0 + 1, + }; + let band = j2k_native::J2kIdwtBand { + rect: j2k_native::J2kRect { + x0: 0, + y0: 0, + x1: 1, + y1: 1, + }, + coefficients: &coefficient, + }; + let job = j2k_native::J2kSingleDecompositionIdwtJob { + rect, + transform: j2k_native::J2kWaveletTransform::Irreversible97, + ll: band, + hl: band, + lh: band, + hh: band, + }; + let mut actual = [0.0]; + + crate::compute::decode_irreversible97_staged_single_decomposition_idwt( + job, + &mut actual, + ) + .expect("degenerate staged irreversible Metal IDWT"); + + assert_eq!( + actual[0].to_bits(), + expected.to_bits(), + "origin ({x0}, {y0})" + ); + } + } + + #[cfg(target_os = "macos")] + const IRREVERSIBLE_IDWT_PERF_WIDTH: u32 = 1023; + #[cfg(target_os = "macos")] + const IRREVERSIBLE_IDWT_PERF_HEIGHT: u32 = 767; + + #[cfg(target_os = "macos")] + struct IrreversibleIdwtPerfFixture { + rect: j2k_native::J2kRect, + ll: Vec, + hl: Vec, + lh: Vec, + hh: Vec, + } + + #[cfg(target_os = "macos")] + impl IrreversibleIdwtPerfFixture { + fn new() -> Self { + let low_width = IRREVERSIBLE_IDWT_PERF_WIDTH.div_ceil(2); + let low_height = IRREVERSIBLE_IDWT_PERF_HEIGHT.div_ceil(2); + let high_width = IRREVERSIBLE_IDWT_PERF_WIDTH / 2; + let high_height = IRREVERSIBLE_IDWT_PERF_HEIGHT / 2; + let make_band = |width: u32, height: u32, seed: u32| { + (0..width * height) + .map(|index| { + let value = index.wrapping_mul(37).wrapping_add(seed * 101) % 4093; + (f32::from(u16::try_from(value).expect("pattern value fits u16")) - 2046.0) + * 0.03125 + }) + .collect::>() + }; + Self { + rect: j2k_native::J2kRect { + x0: 0, + y0: 0, + x1: IRREVERSIBLE_IDWT_PERF_WIDTH, + y1: IRREVERSIBLE_IDWT_PERF_HEIGHT, + }, + ll: make_band(low_width, low_height, 1), + hl: make_band(high_width, low_height, 2), + lh: make_band(low_width, high_height, 3), + hh: make_band(high_width, high_height, 4), + } + } + + fn job(&self) -> j2k_native::J2kSingleDecompositionIdwtJob<'_> { + let low_width = IRREVERSIBLE_IDWT_PERF_WIDTH.div_ceil(2); + let low_height = IRREVERSIBLE_IDWT_PERF_HEIGHT.div_ceil(2); + let high_width = IRREVERSIBLE_IDWT_PERF_WIDTH / 2; + let high_height = IRREVERSIBLE_IDWT_PERF_HEIGHT / 2; + let band = |x1, y1, coefficients| j2k_native::J2kIdwtBand { + rect: j2k_native::J2kRect { + x0: 0, + y0: 0, + x1, + y1, + }, + coefficients, + }; + j2k_native::J2kSingleDecompositionIdwtJob { + rect: self.rect, + transform: j2k_native::J2kWaveletTransform::Irreversible97, + ll: band(low_width, low_height, &self.ll), + hl: band(high_width, low_height, &self.hl), + lh: band(low_width, high_height, &self.lh), + hh: band(high_width, high_height, &self.hh), + } + } + + fn output() -> Vec { + vec![ + 0.0; + IRREVERSIBLE_IDWT_PERF_WIDTH as usize * IRREVERSIBLE_IDWT_PERF_HEIGHT as usize + ] + } + } + + #[cfg(target_os = "macos")] + struct MetalCaptureGuard<'a> { + manager: &'a metal::CaptureManagerRef, + } + + #[cfg(target_os = "macos")] + impl Drop for MetalCaptureGuard<'_> { + fn drop(&mut self) { + if self.manager.is_capturing() { + self.manager.stop_capture(); + } + } + } + + #[cfg(target_os = "macos")] + #[test] + #[ignore = "GPU capture harness; run explicitly with --ignored --nocapture"] + fn metal_irreversible_idwt_gpu_capture() { + use metal::{CaptureDescriptor, CaptureManager, MTLCaptureDestination}; + + if !should_run_metal_runtime() { + return; + } + if std::env::var_os("CARGO_LLVM_COV_TARGET_DIR").is_some() { + println!("skipping GPU capture while the Metal coverage lane is instrumented"); + return; + } + assert_eq!( + std::env::var("MTL_CAPTURE_ENABLED").as_deref(), + Ok("1"), + "set MTL_CAPTURE_ENABLED=1 to enable the Metal capture API" + ); + let trace_path = std::path::PathBuf::from( + std::env::var_os("J2K_METAL_CAPTURE_PATH") + .expect("set J2K_METAL_CAPTURE_PATH to an absolute .gputrace output path"), + ); + assert!( + trace_path.is_absolute(), + "J2K_METAL_CAPTURE_PATH must be absolute" + ); + assert_eq!( + trace_path.extension().and_then(std::ffi::OsStr::to_str), + Some("gputrace"), + "J2K_METAL_CAPTURE_PATH must end in .gputrace" + ); + assert!( + !trace_path.exists(), + "refusing to overwrite existing GPU trace {}", + trace_path.display() + ); + + let fixture = IrreversibleIdwtPerfFixture::new(); + let mut output = IrreversibleIdwtPerfFixture::output(); + let device = j2k_metal_support::system_default_device().expect("Metal capture device"); + crate::compute::with_isolated_runtime_for_device_for_test(&device, || { + crate::compute::decode_irreversible97_staged_single_decomposition_idwt( + fixture.job(), + &mut output, + )?; + + let manager = CaptureManager::shared(); + assert!( + manager.supports_destination(MTLCaptureDestination::GpuTraceDocument), + "Metal GPU trace documents are unavailable on this host" + ); + let descriptor = CaptureDescriptor::new(); + descriptor.set_capture_device(&device); + descriptor.set_destination(MTLCaptureDestination::GpuTraceDocument); + descriptor.set_output_url(&trace_path); + manager + .start_capture(&descriptor) + .map_err(|message| crate::Error::MetalRuntime { message })?; + let capture = MetalCaptureGuard { manager }; + let result = crate::compute::decode_irreversible97_staged_single_decomposition_idwt( + fixture.job(), + &mut output, + ); + drop(capture); + result + }) + .expect("captured irreversible Metal IDWT"); + + assert!( + trace_path.is_dir(), + "Metal capture did not create GPU trace package {}", + trace_path.display() + ); + println!("j2k_metal_idwt97_capture path={}", trace_path.display()); + } + + #[cfg(target_os = "macos")] + #[test] + #[ignore = "performance guard harness; run explicitly with --ignored --nocapture"] + fn metal_irreversible_idwt_perf_guard() { + const ITERS: usize = 11; + if !should_run_metal_runtime() { + return; + } + let fixture = IrreversibleIdwtPerfFixture::new(); + let mut output = IrreversibleIdwtPerfFixture::output(); + crate::compute::decode_irreversible97_staged_single_decomposition_idwt( + fixture.job(), + &mut output, + ) + .expect("warm irreversible Metal IDWT"); + let mut samples = Vec::with_capacity(ITERS); + for _ in 0..ITERS { + let started = std::time::Instant::now(); + crate::compute::decode_irreversible97_staged_single_decomposition_idwt( + fixture.job(), + &mut output, + ) + .expect("measured irreversible Metal IDWT"); + samples.push(started.elapsed()); + } + samples.sort_unstable(); + let median = samples[ITERS / 2]; + let p25 = samples[ITERS / 4]; + let p75 = samples[ITERS * 3 / 4]; + println!( + "j2k_metal_idwt97_perf mode=staged size={}x{} iterations={ITERS} median_ms={:.3} p25_ms={:.3} p75_ms={:.3} iqr_ms={:.3}", + IRREVERSIBLE_IDWT_PERF_WIDTH, + IRREVERSIBLE_IDWT_PERF_HEIGHT, + median.as_secs_f64() * 1_000.0, + p25.as_secs_f64() * 1_000.0, + p75.as_secs_f64() * 1_000.0, + p75.saturating_sub(p25).as_secs_f64() * 1_000.0 + ); + } + struct CpuOnlyCodeBlockDecoder; impl HtCodeBlockDecoder for CpuOnlyCodeBlockDecoder {} diff --git a/crates/j2k-metal/src/lib.rs b/crates/j2k-metal/src/lib.rs index 1c3da732..a226115a 100644 --- a/crates/j2k-metal/src/lib.rs +++ b/crates/j2k-metal/src/lib.rs @@ -63,6 +63,7 @@ pub use self::error::{ Error, MetalDirectFallbackReason, MetalKernelRetryClass, NativeBackendError, }; pub use self::session::{MetalBackendSession, MetalSession}; +pub use self::surface::download_surfaces_packed; pub(crate) use self::surface::Storage; pub use self::surface::Surface; pub use self::tile_batch::MetalTileBatch; diff --git a/crates/j2k-metal/src/mct.rs b/crates/j2k-metal/src/mct.rs index 900a820c..530585a3 100644 --- a/crates/j2k-metal/src/mct.rs +++ b/crates/j2k-metal/src/mct.rs @@ -53,6 +53,7 @@ mod tests { use super::MetalMctDecoder; use j2k_native::{ encode, DecodeSettings, DecoderContext, EncodeOptions, HtCodeBlockDecoder, Image, + J2kInverseMctJob, J2kWaveletTransform, }; #[cfg(target_os = "macos")] @@ -80,6 +81,41 @@ mod tests { encode(&pixels, 2, 2, 3, 8, false, &options).expect("encode irreversible rgb8") } + #[test] + fn metal_inverse_mct_job_applies_addends_exactly_once() { + #[cfg(target_os = "macos")] + if !should_run_metal_runtime() { + return; + } + + let mut plane0 = vec![0.0; 9]; + let mut plane1 = vec![0.0; 9]; + let mut plane2 = vec![0.0; 9]; + let mut decoder = MetalMctDecoder::default(); + let handled = decoder + .decode_inverse_mct(J2kInverseMctJob { + transform: J2kWaveletTransform::Reversible53, + plane0: &mut plane0, + plane1: &mut plane1, + plane2: &mut plane2, + addend0: 17.0, + addend1: 31.0, + addend2: 47.0, + }) + .expect("Metal inverse MCT job"); + + #[cfg(target_os = "macos")] + assert!(handled); + #[cfg(not(target_os = "macos"))] + assert!(!handled); + #[cfg(target_os = "macos")] + { + assert_eq!(plane0, vec![17.0; 9]); + assert_eq!(plane1, vec![31.0; 9]); + assert_eq!(plane2, vec![47.0; 9]); + } + } + #[test] fn metal_mct_decoder_matches_native_decode() { #[cfg(target_os = "macos")] diff --git a/crates/j2k-metal/src/resident_limits.rs b/crates/j2k-metal/src/resident_limits.rs index ace27b0e..188536ac 100644 --- a/crates/j2k-metal/src/resident_limits.rs +++ b/crates/j2k-metal/src/resident_limits.rs @@ -3,43 +3,43 @@ /// Default resident chunk used to size warm private-buffer retention. pub(crate) const DEFAULT_RESIDENT_CHUNK_TILES: usize = 512; -#[cfg(test)] const MAX_RESIDENT_COMPONENTS: usize = 3; -#[cfg(test)] const PRIVATE_COMPONENT_PLANES_PER_TILE: usize = MAX_RESIDENT_COMPONENTS; -#[cfg(test)] const PRIVATE_DWT_SCRATCH_PER_COMPONENT: usize = 1; -#[cfg(test)] const MAX_PRIVATE_BUFFERS_PER_RESIDENT_TILE: usize = PRIVATE_COMPONENT_PLANES_PER_TILE + MAX_RESIDENT_COMPONENTS * PRIVATE_DWT_SCRATCH_PER_COMPONENT; -#[cfg(test)] const BASE_PRIVATE_BUFFERS_PER_RESIDENT_BATCH: usize = 7; -#[cfg(test)] const CLASSIC_SPLIT_TOKEN_PRIVATE_BUFFERS_PER_BATCH: usize = 4; -#[cfg(test)] const MAX_PRIVATE_BUFFERS_PER_RESIDENT_BATCH: usize = BASE_PRIVATE_BUFFERS_PER_RESIDENT_BATCH + CLASSIC_SPLIT_TOKEN_PRIVATE_BUFFERS_PER_BATCH; -#[cfg(test)] pub(crate) const DEFAULT_RESIDENT_PRIVATE_WORKING_SET_BUFFERS: usize = DEFAULT_RESIDENT_CHUNK_TILES * MAX_PRIVATE_BUFFERS_PER_RESIDENT_TILE + MAX_PRIVATE_BUFFERS_PER_RESIDENT_BATCH; -/// Structural record ceiling above the largest admitted resident working set. -pub(crate) const RESIDENT_PRIVATE_POOL_BUFFER_LIMIT: usize = 4_096; +const fn private_pool_record_limit(working_set: usize) -> usize { + working_set.next_power_of_two() +} + +/// Power-of-two structural ceiling above the default resident working set. +pub(crate) const RESIDENT_PRIVATE_POOL_BUFFER_LIMIT: usize = + private_pool_record_limit(DEFAULT_RESIDENT_PRIVATE_WORKING_SET_BUFFERS); #[cfg(test)] mod tests { - use super::{DEFAULT_RESIDENT_PRIVATE_WORKING_SET_BUFFERS, RESIDENT_PRIVATE_POOL_BUFFER_LIMIT}; + use super::{ + private_pool_record_limit, DEFAULT_RESIDENT_PRIVATE_WORKING_SET_BUFFERS, + RESIDENT_PRIVATE_POOL_BUFFER_LIMIT, + }; #[test] fn private_pool_record_limit_covers_default_resident_chunk_working_set() { assert_eq!(DEFAULT_RESIDENT_PRIVATE_WORKING_SET_BUFFERS, 3_083); assert_eq!(RESIDENT_PRIVATE_POOL_BUFFER_LIMIT, 4_096); - assert!( - std::hint::black_box(RESIDENT_PRIVATE_POOL_BUFFER_LIMIT) - >= DEFAULT_RESIDENT_PRIVATE_WORKING_SET_BUFFERS + assert_eq!( + private_pool_record_limit(std::hint::black_box(3_083)), + 4_096 ); } } diff --git a/crates/j2k-metal/src/surface.rs b/crates/j2k-metal/src/surface.rs index cb65d6df..86a72d13 100644 --- a/crates/j2k-metal/src/surface.rs +++ b/crates/j2k-metal/src/surface.rs @@ -7,6 +7,8 @@ use j2k_core::{ PixelFormat, SurfaceMetadata, SurfaceResidency, }; #[cfg(target_os = "macos")] +use j2k_metal_support::{MetalImageLayout, ResidentMetalImage}; +#[cfg(target_os = "macos")] use metal::foreign_types::ForeignType; #[cfg(target_os = "macos")] use metal::Buffer; @@ -15,11 +17,15 @@ use metal::Buffer; use crate::error::metal_kernel_support_error; use crate::Error; +mod readback; + +pub use self::readback::download_surfaces_packed; + #[derive(Clone)] pub(crate) enum Storage { Host(Arc>), #[cfg(target_os = "macos")] - Metal(Buffer), + Metal(ResidentMetalImage), } impl Storage { @@ -88,14 +94,14 @@ impl Surface { Ok(Cow::Borrowed(&bytes[range])) } #[cfg(target_os = "macos")] - Storage::Metal(buffer) => { + Storage::Metal(image) => { // SAFETY: A returned `Surface` represents a completed decode. // External access to the handle is unsafe and requires callers // to exclude overlapping mutation during this owned readback. match unsafe { j2k_metal_support::checked_buffer_read_vec::( - buffer, - self.byte_offset, + image.raw_buffer(), + image.byte_offset(), self.byte_len(), ) } { @@ -153,7 +159,29 @@ impl Surface { #[cfg(target_os = "macos")] pub(crate) fn metal_buffer_trusted(&self) -> Option<(&Buffer, usize)> { match &self.storage { - Storage::Metal(buffer) => Some((buffer, self.byte_offset)), + Storage::Metal(image) => { + // SAFETY: backend code binds this private handle under the + // immutable resident-image contract. + Some((unsafe { image.raw_buffer() }, image.byte_offset())) + } + Storage::Host(_) => None, + } + } + + #[cfg(target_os = "macos")] + /// Borrow the immutable resident image when this surface is Metal-backed. + pub fn resident_metal_image(&self) -> Option<&ResidentMetalImage> { + match &self.storage { + Storage::Metal(image) => Some(image), + Storage::Host(_) => None, + } + } + + #[cfg(target_os = "macos")] + /// Consume this surface and return its immutable resident image. + pub fn into_resident_metal_image(self) -> Option { + match self.storage { + Storage::Metal(image) => Some(image), Storage::Host(_) => None, } } @@ -163,16 +191,8 @@ impl Surface { buffer: Buffer, dimensions: (u32, u32), fmt: PixelFormat, - ) -> Self { - Self { - backend: BackendKind::Metal, - residency: SurfaceResidency::MetalResidentDecode, - dimensions, - fmt, - pitch_bytes: dimensions.0 as usize * fmt.bytes_per_pixel(), - byte_offset: 0, - storage: Storage::Metal(buffer), - } + ) -> Result { + Self::from_metal_buffer_with_offset(buffer, dimensions, fmt, 0) } #[cfg(target_os = "macos")] @@ -181,16 +201,30 @@ impl Surface { dimensions: (u32, u32), fmt: PixelFormat, byte_offset: usize, - ) -> Self { - Self { + ) -> 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: "J2K Metal surface pitch overflows usize".to_string(), + })?; + let layout = MetalImageLayout::new(byte_offset, dimensions, pitch_bytes, fmt) + .map_err(|source| metal_kernel_support_error("J2K Metal surface layout", source))?; + // SAFETY: surface constructors are crate-private and producer paths do + // not expose the returned surface until their command buffer completes. + // The producer command owns the only pending write, and later raw + // access remains inside the audited backend. + let image = unsafe { ResidentMetalImage::from_exclusive_pending_buffer(buffer, layout) } + .map_err(|source| metal_kernel_support_error("J2K Metal resident surface", source))?; + Ok(Self { backend: BackendKind::Metal, residency: SurfaceResidency::MetalResidentDecode, dimensions, fmt, - pitch_bytes: dimensions.0 as usize * fmt.bytes_per_pixel(), + pitch_bytes, byte_offset, - storage: Storage::Metal(buffer), - } + storage: Storage::Metal(image), + }) } } @@ -220,10 +254,12 @@ impl DeviceSurface for Surface { match &self.storage { Storage::Host(_) => None, #[cfg(target_os = "macos")] - Storage::Metal(buffer) => Some(DeviceMemoryRange::new( + Storage::Metal(image) => Some(DeviceMemoryRange::new( BackendKind::Metal, - u64::try_from(buffer.as_ptr() as usize).ok()?, - self.byte_offset, + // SAFETY: reading the handle identity does not access or mutate + // the allocation, and this surface retains the resident owner. + u64::try_from(unsafe { image.raw_buffer() }.as_ptr() as usize).ok()?, + image.byte_offset(), self.byte_len(), )), } diff --git a/crates/j2k-metal/src/surface/readback.rs b/crates/j2k-metal/src/surface/readback.rs new file mode 100644 index 00000000..56c7316b --- /dev/null +++ b/crates/j2k-metal/src/surface/readback.rs @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use super::Surface; +use crate::Error; +#[cfg(target_os = "macos")] +use metal::foreign_types::ForeignTypeRef; + +/// Read a group of completed Metal-resident surfaces into one tightly packed +/// host allocation using a single Metal staging buffer. +/// +/// Surface order is preserved. Every surface must have been produced on the +/// supplied session's device; host surfaces are rejected rather than copied. +#[cfg(target_os = "macos")] +#[doc(hidden)] +pub fn download_surfaces_packed( + session: &crate::MetalBackendSession, + surfaces: &[&Surface], +) -> Result, Error> { + use j2k_core::DeviceSurface; + use j2k_metal_support::{ + checked_blit_command_encoder, checked_buffer_read_vec, checked_command_buffer, + checked_command_queue, checked_shared_buffer, commit_and_wait, + }; + + let total = surfaces.iter().try_fold(0usize, |total, surface| { + total + .checked_add(surface.byte_len()) + .ok_or_else(|| Error::MetalKernel { + message: "J2K Metal packed surface readback size overflow".to_string(), + }) + })?; + let map_support = |error: j2k_metal_support::MetalSupportError| Error::MetalKernel { + message: format!("J2K Metal packed surface readback failed: {error}"), + }; + let staging = checked_shared_buffer(session.device(), total).map_err(map_support)?; + let queue = checked_command_queue(session.device()).map_err(map_support)?; + let command = checked_command_buffer(&queue).map_err(map_support)?; + let blit = checked_blit_command_encoder(&command).map_err(map_support)?; + let mut destination_offset = 0usize; + for surface in surfaces { + let (buffer, source_offset) = + surface + .metal_buffer_trusted() + .ok_or_else(|| Error::MetalKernel { + message: "J2K Metal packed surface readback received a host surface" + .to_string(), + })?; + if buffer.device().as_ptr() != session.device().as_ptr() { + return Err(Error::MetalKernel { + message: "J2K Metal packed surface belongs to a different device".to_string(), + }); + } + let len = surface.byte_len(); + blit.copy_from_buffer( + buffer, + u64::try_from(source_offset).map_err(|_| Error::MetalKernel { + message: "J2K Metal packed source offset exceeds u64".to_string(), + })?, + &staging, + u64::try_from(destination_offset).map_err(|_| Error::MetalKernel { + message: "J2K Metal packed destination offset exceeds u64".to_string(), + })?, + u64::try_from(len).map_err(|_| Error::MetalKernel { + message: "J2K Metal packed surface length exceeds u64".to_string(), + })?, + ); + destination_offset = + destination_offset + .checked_add(len) + .ok_or_else(|| Error::MetalKernel { + message: "J2K Metal packed destination offset overflow".to_string(), + })?; + } + blit.end_encoding(); + commit_and_wait(&command).map_err(map_support)?; + // SAFETY: the blit completed above and the local staging buffer has no + // overlapping writer for the duration of this owned copy. + unsafe { checked_buffer_read_vec::(&staging, 0, total) }.map_err(map_support) +} + +/// Return `MetalUnavailable` on platforms without Metal support. +#[cfg(not(target_os = "macos"))] +#[doc(hidden)] +pub fn download_surfaces_packed( + _session: &crate::MetalBackendSession, + _surfaces: &[&Surface], +) -> Result, Error> { + Err(Error::MetalUnavailable) +} diff --git a/crates/j2k-metal/tests/metal_decode_benchmark.rs b/crates/j2k-metal/tests/metal_decode_benchmark.rs index 92b7f0fe..56402cc7 100644 --- a/crates/j2k-metal/tests/metal_decode_benchmark.rs +++ b/crates/j2k-metal/tests/metal_decode_benchmark.rs @@ -178,6 +178,14 @@ fn generated_decode_cases() -> Vec { container: "raw-codestream".to_string(), source: "generated".to_string(), }); + cases.push(DecodeBenchCase { + id: format!("generated_classic_irreversible97_gray8_{dim}"), + bytes: encode_classic_irreversible(&gray, dim, dim, 1), + fmt: PixelFormat::Gray8, + codec: "j2k-irreversible97".to_string(), + container: "raw-codestream".to_string(), + source: "generated".to_string(), + }); cases.push(DecodeBenchCase { id: format!("generated_htj2k_gray8_{dim}"), bytes: encode_ht(&gray, dim, dim, 1), @@ -204,6 +212,15 @@ fn encode_classic(pixels: &[u8], width: u32, height: u32, components: u16) -> Ve encode(pixels, width, height, components, 8, false, &options).expect("encode classic fixture") } +fn encode_classic_irreversible(pixels: &[u8], width: u32, height: u32, components: u16) -> Vec { + let options = EncodeOptions { + reversible: false, + ..encode_options() + }; + encode(pixels, width, height, components, 8, false, &options) + .expect("encode irreversible classic fixture") +} + fn encode_ht(pixels: &[u8], width: u32, height: u32, components: u16) -> Vec { let options = encode_options(); encode_htj2k(pixels, width, height, components, 8, false, &options) diff --git a/crates/j2k-ml/Cargo.toml b/crates/j2k-ml/Cargo.toml new file mode 100644 index 00000000..f75f3eb3 --- /dev/null +++ b/crates/j2k-ml/Cargo.toml @@ -0,0 +1,74 @@ +[package] +name = "j2k-ml" +description = "Independent Burn tensor integration for JPEG 2000 and HTJ2K" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +publish = false +readme = "README.md" + +[features] +default = [] +cpu = [] +cuda = [ + "cpu", + "dep:burn-cuda", + "dep:burn-cubecl", + "dep:burn-fusion", + "dep:burn-ir", + "dep:j2k-cuda", + "dep:j2k-cuda-runtime", +] +metal = ["cpu", "dep:burn-wgpu", "dep:j2k-metal"] + +[dependencies] +burn-core = { workspace = true } +burn-cuda = { workspace = true, optional = true } +burn-cubecl = { workspace = true, optional = true } +burn-fusion = { workspace = true, optional = true } +burn-ir = { workspace = true, optional = true } +burn-wgpu = { workspace = true, optional = true } +j2k = { path = "../j2k", version = "=0.7.1" } +j2k-cuda = { path = "../j2k-cuda", version = "=0.7.1", features = ["cuda-runtime"], optional = true } +j2k-cuda-runtime = { path = "../j2k-cuda-runtime", version = "=0.7.1", features = ["cuda-oxide-j2k-ml"], optional = true } +j2k-metal = { path = "../j2k-metal", version = "=0.7.1", optional = true } +thiserror = { workspace = true } + +[dev-dependencies] +burn-autodiff = { workspace = true } +criterion = { workspace = true } +j2k-test-support = { path = "../j2k-test-support", features = ["j2k-native-fixtures"] } + +[target.'cfg(all(target_arch = "aarch64", target_os = "linux"))'.dev-dependencies] +burn-ndarray = { workspace = true } + +[target.'cfg(not(all(target_arch = "aarch64", target_os = "linux")))'.dev-dependencies] +burn-flex = { workspace = true } + +[lints.rust] +unsafe_code = "allow" +unsafe_op_in_unsafe_fn = "deny" +unreachable_pub = "warn" + +[lints.clippy] +pedantic = { level = "warn", priority = -1 } +module_name_repetitions = "allow" +must_use_candidate = "allow" +missing_errors_doc = "allow" +undocumented_unsafe_blocks = "warn" + +[[test]] +name = "metal" +required-features = ["metal"] + +[[test]] +name = "cuda" +required-features = ["cuda"] + +[[bench]] +name = "tensor_decode" +harness = false +test = false +required-features = ["cpu"] diff --git a/crates/j2k-ml/README.md b/crates/j2k-ml/README.md new file mode 100644 index 00000000..0d20b126 --- /dev/null +++ b/crates/j2k-ml/README.md @@ -0,0 +1,24 @@ +# j2k-ml + +Experimental JPEG 2000 and HTJ2K tensor decoding for Burn 0.21. + +`j2k-ml` is an independent integration maintained by the `j2k` project. It is +not an official Tracel or Burn crate. It remains unpublished during the 0.7 +release cycle. + +It decodes JP2, JPH, raw J2K, and raw HTJ2K inputs into rank-3 or rank-4 +Burn tensors. Defaults are channels-first layout, automatic Gray/RGB channel +selection, and unit-scaled `f32` output. + +Enable one or more explicit routes: + +- `cpu`: portable host decode into any Burn backend. +- `metal`: strict resident J2K Metal decode, one packed batch readback, and one + compact upload to Burn Metal. +- `cuda`: strict direct decode and conversion into Burn's default fused CUDA + allocation. Device conversion kernels are Rust `cuda-oxide`; CUDA C, NVCC, + and C wrappers are not used. + +Every real decode API is fallible. Accelerator routes fail instead of silently +falling back, and batches reject corrupt items and shape mismatches with their +input index. See the [workspace integration guide](../../docs/j2k-ml.md). diff --git a/crates/j2k-ml/benches/tensor_decode.rs b/crates/j2k-ml/benches/tensor_decode.rs new file mode 100644 index 00000000..2bc819bb --- /dev/null +++ b/crates/j2k-ml/benches/tensor_decode.rs @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use burn_core::tensor::backend::Backend; +#[cfg(not(all(target_arch = "aarch64", target_os = "linux")))] +use burn_flex::{Flex as CpuBackend, FlexDevice as CpuDevice}; +#[cfg(all(target_arch = "aarch64", target_os = "linux"))] +use burn_ndarray::NdArrayDevice::Cpu as CpuDevice; +use criterion::{BenchmarkId, Criterion, Throughput}; +use j2k::{DeviceDecodeRequest, Downscale, Rect}; +use j2k_ml::{cpu, TensorDecodeOptions, TensorInput}; +#[cfg(all(feature = "cuda", not(target_os = "macos")))] +use j2k_test_support::htj2k_gray8_large_fixture; +use j2k_test_support::{classic_j2k_gray8_fixture, openhtj2k_refinement_fixture}; + +#[cfg(all(target_arch = "aarch64", target_os = "linux"))] +type CpuBackend = burn_ndarray::NdArray; + +const BATCH_SIZES: &[usize] = &[1, 8, 32]; +#[cfg(not(all(target_arch = "aarch64", target_os = "linux")))] +const CPU_STAGED_BENCHMARK_GROUP: &str = "j2k_ml_decode_to_ready_tensor_cpu_staged_flex"; +#[cfg(all(target_arch = "aarch64", target_os = "linux"))] +const CPU_STAGED_BENCHMARK_GROUP: &str = + "j2k_ml_decode_to_ready_tensor_cpu_staged_ndarray_arm_linux"; + +fn main() { + let mut criterion = Criterion::default().configure_from_args(); + bench_cpu_staged(&mut criterion); + #[cfg(all(feature = "cuda", not(target_os = "macos")))] + bench_cuda_routes(&mut criterion); + #[cfg(all(feature = "metal", target_os = "macos"))] + bench_metal_staged(&mut criterion); + criterion.final_summary(); +} + +fn bench_cpu_staged(criterion: &mut Criterion) { + let gray8 = classic_j2k_gray8_fixture(128, 128); + let gray16 = openhtj2k_refinement_fixture(); + let options = TensorDecodeOptions::default(); + let mut group = criterion.benchmark_group(CPU_STAGED_BENCHMARK_GROUP); + + for (label, encoded) in [("gray8", gray8.as_slice()), ("gray16", gray16)] { + let item_bytes = compact_item_bytes(encoded); + for &batch_size in BATCH_SIZES { + let inputs = repeated_inputs(encoded, batch_size, DeviceDecodeRequest::Full); + let transferred = batch_bytes(item_bytes, batch_size); + group.throughput(Throughput::Bytes(transferred)); + group.bench_with_input( + BenchmarkId::new(format!("{label}/compact_upload_bytes"), transferred), + &inputs, + |bencher, inputs| { + bencher.iter(|| { + let decoded = cpu::decode_float_batch::( + std::hint::black_box(inputs), + &options, + &CpuDevice, + ) + .expect("CPU-staged tensor decode"); + CpuBackend::sync(&CpuDevice).expect("sync CPU backend"); + std::hint::black_box(decoded.tensor) + }); + }, + ); + } + } + + let roi = Rect { + x: 32, + y: 32, + w: 64, + h: 64, + }; + let request = DeviceDecodeRequest::RegionScaled { + roi, + scale: Downscale::Half, + }; + let inputs = repeated_inputs(&gray8, 8, request); + let compact_bytes = 8_u64 * 32 * 32; + group.throughput(Throughput::Bytes(compact_bytes)); + group.bench_function("roi_tiles_8/compact_upload_bytes_8192", |bencher| { + bencher.iter(|| { + let decoded = cpu::decode_float_batch::( + std::hint::black_box(&inputs), + &options, + &CpuDevice, + ) + .expect("CPU-staged ROI batch"); + CpuBackend::sync(&CpuDevice).expect("sync CPU backend"); + std::hint::black_box(decoded.tensor) + }); + }); + group.finish(); +} + +#[cfg(all(feature = "cuda", not(target_os = "macos")))] +fn bench_cuda_routes(criterion: &mut Criterion) { + use burn_cuda::{Cuda, CudaDevice}; + use j2k_ml::cuda; + + type CudaBackend = Cuda; + + let gray8 = htj2k_gray8_large_fixture(128, 128); + let gray16 = openhtj2k_refinement_fixture(); + let options = TensorDecodeOptions::default(); + let device = CudaDevice::default(); + let mut group = criterion.benchmark_group("j2k_ml_decode_to_ready_tensor_cuda"); + + for (label, encoded) in [("gray8", gray8.as_slice()), ("gray16", gray16)] { + let item_bytes = compact_item_bytes(encoded); + for &batch_size in BATCH_SIZES { + let inputs = repeated_inputs(encoded, batch_size, DeviceDecodeRequest::Full); + let transferred = batch_bytes(item_bytes, batch_size); + group.throughput(Throughput::Bytes(transferred)); + group.bench_with_input( + BenchmarkId::new( + format!("{label}/staged_baseline_compact_upload_bytes"), + transferred, + ), + &inputs, + |bencher, inputs| { + bencher.iter(|| { + let decoded = cpu::decode_float_batch::( + std::hint::black_box(inputs), + &options, + &device, + ) + .expect("CUDA-staged baseline"); + CudaBackend::sync(&device).expect("sync CUDA"); + std::hint::black_box(decoded.tensor) + }); + }, + ); + group.bench_with_input( + BenchmarkId::new(format!("{label}/direct_transferred_bytes_0"), batch_size), + &inputs, + |bencher, inputs| { + bencher.iter(|| { + let decoded = cuda::decode_float_batch( + std::hint::black_box(inputs), + &options, + &device, + ) + .expect("CUDA-direct tensor decode"); + CudaBackend::sync(&device).expect("sync CUDA"); + std::hint::black_box(decoded.tensor) + }); + }, + ); + } + } + + let roi_request = DeviceDecodeRequest::RegionScaled { + roi: Rect { + x: 32, + y: 32, + w: 64, + h: 64, + }, + scale: Downscale::Half, + }; + let roi_inputs = repeated_inputs(&gray8, 8, roi_request); + let roi_bytes = 8_u64 * 32 * 32; + group.throughput(Throughput::Bytes(roi_bytes)); + group.bench_function( + "roi_tiles_8/staged_baseline_compact_upload_bytes_8192", + |bencher| { + bencher.iter(|| { + let decoded = + cpu::decode_float_batch::(&roi_inputs, &options, &device) + .expect("CUDA ROI baseline"); + CudaBackend::sync(&device).expect("sync CUDA"); + std::hint::black_box(decoded.tensor) + }); + }, + ); + group.bench_function("roi_tiles_8/direct_transferred_bytes_0", |bencher| { + bencher.iter(|| { + let decoded = cuda::decode_float_batch(&roi_inputs, &options, &device) + .expect("CUDA-direct ROI decode"); + CudaBackend::sync(&device).expect("sync CUDA"); + std::hint::black_box(decoded.tensor) + }); + }); + group.finish(); +} + +#[cfg(all(feature = "metal", target_os = "macos"))] +fn bench_metal_staged(criterion: &mut Criterion) { + use burn_wgpu::{Wgpu, WgpuDevice}; + use j2k_ml::metal; + + let gray8 = classic_j2k_gray8_fixture(128, 128); + let gray16 = openhtj2k_refinement_fixture(); + let options = TensorDecodeOptions::default(); + let device = WgpuDevice::DefaultDevice; + let mut group = criterion.benchmark_group("j2k_ml_decode_to_ready_tensor_metal_staged"); + for (label, encoded) in [("gray8", gray8.as_slice()), ("gray16", gray16)] { + let item_bytes = compact_item_bytes(encoded); + for &batch_size in BATCH_SIZES { + let inputs = repeated_inputs(encoded, batch_size, DeviceDecodeRequest::Full); + let transferred = batch_bytes(item_bytes, batch_size) + .checked_mul(2) + .expect("benchmark transfer byte count fits u64"); + group.throughput(Throughput::Bytes(transferred)); + group.bench_with_input( + BenchmarkId::new( + format!("{label}/packed_readback_plus_upload_bytes"), + transferred, + ), + &inputs, + |bencher, inputs| { + bencher.iter(|| { + let decoded = metal::decode_float_batch( + std::hint::black_box(inputs), + &options, + &device, + ) + .expect("Metal-staged tensor decode"); + as Backend>::sync(&device).expect("sync Metal"); + std::hint::black_box(decoded.tensor) + }); + }, + ); + } + } + + let roi_inputs = repeated_inputs( + &gray8, + 8, + DeviceDecodeRequest::RegionScaled { + roi: Rect { + x: 32, + y: 32, + w: 64, + h: 64, + }, + scale: Downscale::Half, + }, + ); + let roi_transferred = 8_u64 * 32 * 32 * 2; + group.throughput(Throughput::Bytes(roi_transferred)); + group.bench_function( + "roi_tiles_8/packed_readback_plus_upload_bytes_16384", + |bencher| { + bencher.iter(|| { + let decoded = metal::decode_float_batch(&roi_inputs, &options, &device) + .expect("Metal-staged ROI tensor decode"); + as Backend>::sync(&device).expect("sync Metal"); + std::hint::black_box(decoded.tensor) + }); + }, + ); + group.finish(); +} + +fn compact_item_bytes(encoded: &[u8]) -> u64 { + let info = j2k::J2kDecoder::inspect(encoded).expect("inspect benchmark fixture"); + let sample_bytes = if info.bit_depth <= 8 { 1 } else { 2 }; + u64::from(info.dimensions.0) * u64::from(info.dimensions.1) * sample_bytes +} + +fn batch_bytes(item_bytes: u64, batch_size: usize) -> u64 { + item_bytes + .checked_mul(u64::try_from(batch_size).expect("benchmark batch size fits u64")) + .expect("benchmark transfer byte count fits u64") +} + +fn repeated_inputs( + encoded: &[u8], + batch_size: usize, + request: DeviceDecodeRequest, +) -> Vec> { + (0..batch_size) + .map(|_| TensorInput { encoded, request }) + .collect() +} diff --git a/crates/j2k-ml/src/cpu.rs b/crates/j2k-ml/src/cpu.rs new file mode 100644 index 00000000..cc6dcc4d --- /dev/null +++ b/crates/j2k-ml/src/cpu.rs @@ -0,0 +1,578 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! Portable decode into any Burn backend. + +use burn_core::tensor::{backend::Backend, DType, FloatDType, Int, Tensor, TensorData}; +use j2k::{ + DecodeOutcome, DeviceDecodePlan, DeviceDecodeRequest, J2kDecodeWarning, J2kDecoder, + J2kScratchPool, PixelFormat, +}; + +use crate::{ + ChannelSelection, FloatNormalization, TensorBatchDecode, TensorDecode, TensorDecodeError, + TensorDecodeOptions, TensorInput, TensorLayout, TensorRoute, +}; + +#[derive(Debug)] +struct PackedImage { + bytes: Vec, + shape: [usize; 3], + outcome: DecodeOutcome, +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum SampleWidth { + U8, + U16, +} + +impl SampleWidth { + pub(crate) const fn bytes(self) -> usize { + match self { + Self::U8 => 1, + Self::U16 => 2, + } + } + + pub(crate) const fn dtype(self) -> DType { + match self { + Self::U8 => DType::U8, + Self::U16 => DType::U16, + } + } + + const fn unit_denominator(self) -> f32 { + match self { + Self::U8 => 255.0, + Self::U16 => 65_535.0, + } + } +} + +/// Decode one image into a rank-3 U8 tensor. +pub fn decode_u8( + input: TensorInput<'_>, + options: &TensorDecodeOptions, + device: &B::Device, +) -> Result>, TensorDecodeError> { + ensure_dtype::(device, DType::U8)?; + let packed = decode_packed(input, options.channels, SampleWidth::U8)?; + let tensor = integer_tensor_3::(&packed, options.layout, device, DType::U8); + Ok(single_result(tensor, packed.outcome)) +} + +/// Decode one image into a rank-3 U16 tensor. +pub fn decode_u16( + input: TensorInput<'_>, + options: &TensorDecodeOptions, + device: &B::Device, +) -> Result>, TensorDecodeError> { + ensure_dtype::(device, DType::U16)?; + let packed = decode_packed(input, options.channels, SampleWidth::U16)?; + let tensor = integer_tensor_3::(&packed, options.layout, device, DType::U16); + Ok(single_result(tensor, packed.outcome)) +} + +/// Decode one image into a rank-3 F32 tensor. +pub fn decode_float( + input: TensorInput<'_>, + options: &TensorDecodeOptions, + device: &B::Device, +) -> Result>, TensorDecodeError> { + validate_normalization_values(&options.normalization)?; + ensure_dtype::(device, DType::F32)?; + + let info = J2kDecoder::inspect(input.encoded)?; + let width = if info.bit_depth <= 8 { + SampleWidth::U8 + } else { + SampleWidth::U16 + }; + ensure_dtype::(device, width.dtype())?; + validate_normalization_channels( + &options.normalization, + selected_channels(options.channels, info.components), + )?; + let packed = decode_packed(input, options.channels, width)?; + let tensor = + integer_tensor_3::(&packed, options.layout, device, width.dtype()).cast(FloatDType::F32); + let tensor = normalize_3( + tensor, + &options.normalization, + options.layout, + packed.shape[2], + width, + device, + ); + Ok(single_result(tensor, packed.outcome)) +} + +/// Decode a batch into a rank-4 U8 tensor. +pub fn decode_u8_batch( + inputs: &[TensorInput<'_>], + options: &TensorDecodeOptions, + device: &B::Device, +) -> Result>, TensorDecodeError> { + ensure_dtype::(device, DType::U8)?; + let packed = decode_batch(inputs, options.channels, SampleWidth::U8)?; + let tensor = integer_tensor_4::(&packed, options.layout, device, DType::U8); + Ok(batch_result(tensor, packed)) +} + +/// Decode a batch into a rank-4 U16 tensor. +pub fn decode_u16_batch( + inputs: &[TensorInput<'_>], + options: &TensorDecodeOptions, + device: &B::Device, +) -> Result>, TensorDecodeError> { + ensure_dtype::(device, DType::U16)?; + let packed = decode_batch(inputs, options.channels, SampleWidth::U16)?; + let tensor = integer_tensor_4::(&packed, options.layout, device, DType::U16); + Ok(batch_result(tensor, packed)) +} + +/// Decode a batch into a rank-4 F32 tensor. +pub fn decode_float_batch( + inputs: &[TensorInput<'_>], + options: &TensorDecodeOptions, + device: &B::Device, +) -> Result>, TensorDecodeError> { + validate_normalization_values(&options.normalization)?; + ensure_dtype::(device, DType::F32)?; + let first = inputs.first().ok_or(TensorDecodeError::EmptyBatch)?; + let first_info = + J2kDecoder::inspect(first.encoded).map_err(|source| indexed(0, source.into()))?; + let bit_depth = first_info.bit_depth; + let width = if bit_depth <= 8 { + SampleWidth::U8 + } else { + SampleWidth::U16 + }; + ensure_dtype::(device, width.dtype())?; + validate_normalization_channels( + &options.normalization, + selected_channels(options.channels, first_info.components), + ) + .map_err(|error| indexed(0, error))?; + + for (index, input) in inputs.iter().enumerate().skip(1) { + let item_info = + J2kDecoder::inspect(input.encoded).map_err(|source| indexed(index, source.into()))?; + let item_depth = item_info.bit_depth; + if (item_depth <= 8) != (bit_depth <= 8) { + return Err(TensorDecodeError::BatchItem { + index, + source: Box::new(TensorDecodeError::StrictRoute { + route: TensorRoute::CpuStaged, + message: format!( + "mixed canonical integer widths are unsupported: first item is {bit_depth}-bit, item is {item_depth}-bit" + ), + }), + }); + } + validate_normalization_channels( + &options.normalization, + selected_channels(options.channels, item_info.components), + ) + .map_err(|error| indexed(index, error))?; + } + + let packed = decode_batch(inputs, options.channels, width)?; + let tensor = + integer_tensor_4::(&packed, options.layout, device, width.dtype()).cast(FloatDType::F32); + let tensor = normalize_4( + tensor, + &options.normalization, + options.layout, + packed.shape[2], + width, + device, + ); + Ok(batch_result(tensor, packed)) +} + +pub(crate) fn ensure_dtype( + device: &B::Device, + dtype: DType, +) -> Result<(), TensorDecodeError> { + if B::supports_dtype(device, dtype) { + Ok(()) + } else { + Err(TensorDecodeError::UnsupportedDType { dtype }) + } +} + +fn decode_packed( + input: TensorInput<'_>, + selection: ChannelSelection, + width: SampleWidth, +) -> Result { + let mut decoder = J2kDecoder::new(input.encoded)?; + let plan = DeviceDecodePlan::for_image(decoder.info().dimensions, input.request)?; + let channels = selected_channels(selection, decoder.info().components); + let format = pixel_format(channels, width); + let (output_width, output_height) = plan.output_dims(); + let width_usize = usize::try_from(output_width).map_err(|_| TensorDecodeError::SizeOverflow)?; + let height_usize = + usize::try_from(output_height).map_err(|_| TensorDecodeError::SizeOverflow)?; + let stride = width_usize + .checked_mul(format.bytes_per_pixel()) + .ok_or(TensorDecodeError::SizeOverflow)?; + let byte_len = stride + .checked_mul(height_usize) + .ok_or(TensorDecodeError::SizeOverflow)?; + let mut bytes = Vec::new(); + bytes + .try_reserve_exact(byte_len) + .map_err(|_| TensorDecodeError::SizeOverflow)?; + bytes.resize(byte_len, 0); + let mut pool = J2kScratchPool::new(); + let outcome = match input.request { + DeviceDecodeRequest::Full => decoder.decode_into(&mut bytes, stride, format)?, + DeviceDecodeRequest::Region { roi } => { + decoder.decode_region_into(&mut pool, &mut bytes, stride, format, roi)? + } + DeviceDecodeRequest::Scaled { scale } => { + decoder.decode_scaled_into(&mut pool, &mut bytes, stride, format, scale)? + } + DeviceDecodeRequest::RegionScaled { roi, scale } => { + decoder.decode_region_scaled_into(&mut pool, &mut bytes, stride, format, roi, scale)? + } + }; + Ok(PackedImage { + bytes, + shape: [height_usize, width_usize, channels], + outcome, + }) +} + +pub(crate) fn selected_channels(selection: ChannelSelection, components: u16) -> usize { + match selection { + ChannelSelection::Auto if components == 1 => 1, + ChannelSelection::Auto | ChannelSelection::Rgb => 3, + ChannelSelection::Gray => 1, + ChannelSelection::Rgba => 4, + } +} + +pub(crate) fn planned_shape( + input: TensorInput<'_>, + selection: ChannelSelection, +) -> Result<[usize; 3], TensorDecodeError> { + let info = J2kDecoder::inspect(input.encoded)?; + let plan = DeviceDecodePlan::for_image(info.dimensions, input.request)?; + let (width, height) = plan.output_dims(); + Ok([ + usize::try_from(height).map_err(|_| TensorDecodeError::SizeOverflow)?, + usize::try_from(width).map_err(|_| TensorDecodeError::SizeOverflow)?, + selected_channels(selection, info.components), + ]) +} + +pub(crate) fn pixel_format(channels: usize, width: SampleWidth) -> PixelFormat { + match (channels, width) { + (1, SampleWidth::U8) => PixelFormat::Gray8, + (3, SampleWidth::U8) => PixelFormat::Rgb8, + (4, SampleWidth::U8) => PixelFormat::Rgba8, + (1, SampleWidth::U16) => PixelFormat::Gray16, + (3, SampleWidth::U16) => PixelFormat::Rgb16, + (4, SampleWidth::U16) => PixelFormat::Rgba16, + _ => unreachable!("channel selection is confined to 1, 3, or 4"), + } +} + +fn integer_tensor_3( + packed: &PackedImage, + layout: TensorLayout, + device: &B::Device, + dtype: DType, +) -> Tensor { + integer_tensor_3_from_bytes(packed.bytes.clone(), packed.shape, layout, device, dtype) +} + +pub(crate) fn integer_tensor_3_from_bytes( + bytes: Vec, + shape: [usize; 3], + layout: TensorLayout, + device: &B::Device, + dtype: DType, +) -> Tensor { + let tensor = match dtype { + DType::U8 => Tensor::from_data(TensorData::new(bytes, shape), (device, DType::U8)), + DType::U16 => Tensor::from_data( + TensorData::new(bytes_to_u16(&bytes), shape), + (device, DType::U16), + ), + _ => unreachable!("portable integer decode only uses U8 or U16"), + }; + match layout { + TensorLayout::ChannelsFirst => tensor.permute([2, 0, 1]), + TensorLayout::ChannelsLast => tensor, + } +} + +fn integer_tensor_4( + packed: &PackedBatch, + layout: TensorLayout, + device: &B::Device, + dtype: DType, +) -> Tensor { + integer_tensor_4_from_bytes( + packed.bytes.clone(), + packed.outcomes.len(), + packed.shape, + layout, + device, + dtype, + ) +} + +pub(crate) fn integer_tensor_4_from_bytes( + bytes: Vec, + batch: usize, + item_shape: [usize; 3], + layout: TensorLayout, + device: &B::Device, + dtype: DType, +) -> Tensor { + let shape = [batch, item_shape[0], item_shape[1], item_shape[2]]; + let tensor = match dtype { + DType::U8 => Tensor::from_data(TensorData::new(bytes, shape), (device, DType::U8)), + DType::U16 => Tensor::from_data( + TensorData::new(bytes_to_u16(&bytes), shape), + (device, DType::U16), + ), + _ => unreachable!("portable integer decode only uses U8 or U16"), + }; + match layout { + TensorLayout::ChannelsFirst => tensor.permute([0, 3, 1, 2]), + TensorLayout::ChannelsLast => tensor, + } +} + +fn bytes_to_u16(bytes: &[u8]) -> Vec { + bytes + .chunks_exact(2) + .map(|sample| u16::from_ne_bytes([sample[0], sample[1]])) + .collect() +} + +pub(crate) fn validate_normalization_values( + normalization: &FloatNormalization, +) -> Result<(), TensorDecodeError> { + let FloatNormalization::MeanStd { mean, std } = normalization else { + return Ok(()); + }; + if mean.iter().chain(std).any(|value| !value.is_finite()) { + return Err(invalid_normalization("mean and std values must be finite")); + } + if std.contains(&0.0) { + return Err(invalid_normalization("standard deviations must be nonzero")); + } + Ok(()) +} + +pub(crate) fn validate_normalization_channels( + normalization: &FloatNormalization, + channels: usize, +) -> Result<(), TensorDecodeError> { + let FloatNormalization::MeanStd { mean, std } = normalization else { + return Ok(()); + }; + if mean.len() != channels || std.len() != channels { + return Err(invalid_normalization(format!( + "mean and std must each contain {channels} values; got {} and {}", + mean.len(), + std.len() + ))); + } + Ok(()) +} + +fn invalid_normalization(message: impl Into) -> TensorDecodeError { + TensorDecodeError::InvalidNormalization { + message: message.into(), + } +} + +pub(crate) fn normalize_3( + tensor: Tensor, + normalization: &FloatNormalization, + layout: TensorLayout, + channels: usize, + width: SampleWidth, + device: &B::Device, +) -> Tensor { + match normalization { + FloatNormalization::Raw => tensor, + FloatNormalization::Unit => tensor.div_scalar(width.unit_denominator()), + FloatNormalization::MeanStd { mean, std } => { + let unit = tensor.div_scalar(width.unit_denominator()); + let (mean, std) = match layout { + TensorLayout::ChannelsFirst => ( + Tensor::::from_data( + TensorData::new(mean.clone(), [channels]), + (device, DType::F32), + ) + .reshape([channels, 1, 1]), + Tensor::::from_data( + TensorData::new(std.clone(), [channels]), + (device, DType::F32), + ) + .reshape([channels, 1, 1]), + ), + TensorLayout::ChannelsLast => ( + Tensor::::from_data( + TensorData::new(mean.clone(), [channels]), + (device, DType::F32), + ) + .reshape([1, 1, channels]), + Tensor::::from_data( + TensorData::new(std.clone(), [channels]), + (device, DType::F32), + ) + .reshape([1, 1, channels]), + ), + }; + unit.sub(mean).div(std) + } + } +} + +pub(crate) fn normalize_4( + tensor: Tensor, + normalization: &FloatNormalization, + layout: TensorLayout, + channels: usize, + width: SampleWidth, + device: &B::Device, +) -> Tensor { + match normalization { + FloatNormalization::Raw => tensor, + FloatNormalization::Unit => tensor.div_scalar(width.unit_denominator()), + FloatNormalization::MeanStd { mean, std } => { + let unit = tensor.div_scalar(width.unit_denominator()); + let (mean, std) = match layout { + TensorLayout::ChannelsFirst => ( + Tensor::::from_data( + TensorData::new(mean.clone(), [channels]), + (device, DType::F32), + ) + .reshape([1, channels, 1, 1]), + Tensor::::from_data( + TensorData::new(std.clone(), [channels]), + (device, DType::F32), + ) + .reshape([1, channels, 1, 1]), + ), + TensorLayout::ChannelsLast => ( + Tensor::::from_data( + TensorData::new(mean.clone(), [channels]), + (device, DType::F32), + ) + .reshape([1, 1, 1, channels]), + Tensor::::from_data( + TensorData::new(std.clone(), [channels]), + (device, DType::F32), + ) + .reshape([1, 1, 1, channels]), + ), + }; + unit.sub(mean).div(std) + } + } +} + +#[derive(Debug)] +struct PackedBatch { + bytes: Vec, + shape: [usize; 3], + outcomes: Vec>, +} + +fn decode_batch( + inputs: &[TensorInput<'_>], + channels: ChannelSelection, + width: SampleWidth, +) -> Result { + let first = inputs.first().ok_or(TensorDecodeError::EmptyBatch)?; + let expected_shape = planned_shape(*first, channels).map_err(|error| indexed(0, error))?; + for (index, input) in inputs.iter().enumerate().skip(1) { + let actual = planned_shape(*input, channels).map_err(|error| indexed(index, error))?; + if actual != expected_shape { + return Err(TensorDecodeError::BatchShapeMismatch { + index, + expected: expected_shape, + actual, + }); + } + } + let first = decode_packed(*first, channels, width).map_err(|error| indexed(0, error))?; + let shape = first.shape; + let item_bytes = shape + .iter() + .try_fold(width.bytes(), |size, dim| size.checked_mul(*dim)) + .ok_or(TensorDecodeError::SizeOverflow)?; + let capacity = item_bytes + .checked_mul(inputs.len()) + .ok_or(TensorDecodeError::SizeOverflow)?; + let mut bytes = Vec::new(); + bytes + .try_reserve_exact(capacity) + .map_err(|_| TensorDecodeError::SizeOverflow)?; + bytes.extend_from_slice(&first.bytes); + let mut outcomes = Vec::new(); + outcomes + .try_reserve_exact(inputs.len()) + .map_err(|_| TensorDecodeError::SizeOverflow)?; + outcomes.push(first.outcome); + + for (index, input) in inputs.iter().enumerate().skip(1) { + let image = + decode_packed(*input, channels, width).map_err(|error| indexed(index, error))?; + if image.shape != shape { + return Err(TensorDecodeError::BatchShapeMismatch { + index, + expected: shape, + actual: image.shape, + }); + } + bytes.extend_from_slice(&image.bytes); + outcomes.push(image.outcome); + } + Ok(PackedBatch { + bytes, + shape, + outcomes, + }) +} + +fn indexed(index: usize, source: TensorDecodeError) -> TensorDecodeError { + TensorDecodeError::BatchItem { + index, + source: Box::new(source), + } +} + +fn single_result(tensor: T, outcome: DecodeOutcome) -> TensorDecode { + TensorDecode { + tensor, + decoded: outcome.decoded, + warnings: outcome.warnings, + route: TensorRoute::CpuStaged, + } +} + +fn batch_result(tensor: T, packed: PackedBatch) -> TensorBatchDecode { + let (decoded, warnings) = packed + .outcomes + .into_iter() + .map(|outcome| (outcome.decoded, outcome.warnings)) + .unzip(); + TensorBatchDecode { + tensor, + decoded, + warnings, + route: TensorRoute::CpuStaged, + } +} diff --git a/crates/j2k-ml/src/cuda.rs b/crates/j2k-ml/src/cuda.rs new file mode 100644 index 00000000..f6a0e5e9 --- /dev/null +++ b/crates/j2k-ml/src/cuda.rs @@ -0,0 +1,574 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! Strict decode directly into Burn's default fused CUDA backend. + +use burn_core::tensor::{DType, Int, Shape, Tensor, TensorPrimitive}; +use burn_cubecl::cubecl::{cuda::CudaRuntime, Runtime}; +use burn_cubecl::{ops::numeric::empty_device_contiguous_dtype, CubeBackend}; +use burn_cuda::{Cuda, CudaDevice}; +use burn_fusion::{get_client, stream::OperationStreams, NoOp}; +use burn_ir::{BackendIr, InitOperationIr, OperationIr}; +use j2k::{DeviceDecodePlan, J2kDecodeWarning, Rect}; +use j2k_cuda::{CudaSession, J2kDecoder as CudaDecoder, Surface}; +use j2k_cuda_runtime::{ + CudaContext, CudaExternalDeviceBufferViewMut, CudaJ2kMlKernelConfig, CudaJ2kMlLayout, + CudaJ2kMlNormalization, CudaJ2kMlSample, +}; + +use crate::cpu::{ + ensure_dtype, pixel_format, selected_channels, validate_normalization_channels, + validate_normalization_values, SampleWidth, +}; +use crate::{ + FloatNormalization, TensorBatchDecode, TensorDecode, TensorDecodeError, TensorDecodeOptions, + TensorInput, TensorLayout, TensorRoute, +}; + +type InnerCuda = CubeBackend; +type CudaTensor = Tensor; +type CudaIntTensor = Tensor; + +#[derive(Debug, Clone)] +struct PlannedImage<'a> { + input: TensorInput<'a>, + shape: [usize; 3], + decoded: Rect, +} + +/// Decode one image into a rank-3 U8 tensor without decoded-pixel host transfer. +pub fn decode_u8( + input: TensorInput<'_>, + options: &TensorDecodeOptions, + device: &CudaDevice, +) -> Result>, TensorDecodeError> { + ensure_dtype::(device, DType::U8)?; + let plan = plan_image(input, options, SampleWidth::U8)?; + let shape = tensor_shape_3(plan.shape, options.layout); + let context = + CudaContext::retain_primary(device.index).map_err(|error| cuda_runtime_error(&error))?; + let tensor = fill_int_tensor::<3>(shape, DType::U8, device, &context, |destination| { + decode_plans_into( + std::slice::from_ref(&plan), + options, + SampleWidth::U8, + true, + false, + &context, + destination, + ) + })?; + Ok(single_result(tensor, &plan)) +} + +/// Decode one image into a rank-3 U16 tensor without decoded-pixel host transfer. +pub fn decode_u16( + input: TensorInput<'_>, + options: &TensorDecodeOptions, + device: &CudaDevice, +) -> Result>, TensorDecodeError> { + ensure_dtype::(device, DType::U16)?; + let plan = plan_image(input, options, SampleWidth::U16)?; + let shape = tensor_shape_3(plan.shape, options.layout); + let context = + CudaContext::retain_primary(device.index).map_err(|error| cuda_runtime_error(&error))?; + let tensor = fill_int_tensor::<3>(shape, DType::U16, device, &context, |destination| { + decode_plans_into( + std::slice::from_ref(&plan), + options, + SampleWidth::U16, + true, + false, + &context, + destination, + ) + })?; + Ok(single_result(tensor, &plan)) +} + +/// Decode one image into a rank-3 F32 tensor without decoded-pixel host transfer. +pub fn decode_float( + input: TensorInput<'_>, + options: &TensorDecodeOptions, + device: &CudaDevice, +) -> Result>, TensorDecodeError> { + validate_normalization_values(&options.normalization)?; + ensure_dtype::(device, DType::F32)?; + let width = canonical_width(input.encoded)?; + let plan = plan_image(input, options, width)?; + validate_normalization_channels(&options.normalization, plan.shape[2])?; + let shape = tensor_shape_3(plan.shape, options.layout); + let context = + CudaContext::retain_primary(device.index).map_err(|error| cuda_runtime_error(&error))?; + let tensor = fill_float_tensor::<3>(shape, device, &context, |destination| { + decode_plans_into( + std::slice::from_ref(&plan), + options, + width, + false, + false, + &context, + destination, + ) + })?; + Ok(single_result(tensor, &plan)) +} + +/// Decode a batch into one rank-4 U8 tensor without decoded-pixel host transfer. +pub fn decode_u8_batch( + inputs: &[TensorInput<'_>], + options: &TensorDecodeOptions, + device: &CudaDevice, +) -> Result>, TensorDecodeError> { + ensure_dtype::(device, DType::U8)?; + let plans = plan_batch(inputs, options, SampleWidth::U8)?; + let shape = tensor_shape_4(plans.len(), plans[0].shape, options.layout); + let context = + CudaContext::retain_primary(device.index).map_err(|error| cuda_runtime_error(&error))?; + let tensor = fill_int_tensor::<4>(shape, DType::U8, device, &context, |destination| { + decode_plans_into( + &plans, + options, + SampleWidth::U8, + true, + true, + &context, + destination, + ) + })?; + Ok(batch_result(tensor, &plans)) +} + +/// Decode a batch into one rank-4 U16 tensor without decoded-pixel host transfer. +pub fn decode_u16_batch( + inputs: &[TensorInput<'_>], + options: &TensorDecodeOptions, + device: &CudaDevice, +) -> Result>, TensorDecodeError> { + ensure_dtype::(device, DType::U16)?; + let plans = plan_batch(inputs, options, SampleWidth::U16)?; + let shape = tensor_shape_4(plans.len(), plans[0].shape, options.layout); + let context = + CudaContext::retain_primary(device.index).map_err(|error| cuda_runtime_error(&error))?; + let tensor = fill_int_tensor::<4>(shape, DType::U16, device, &context, |destination| { + decode_plans_into( + &plans, + options, + SampleWidth::U16, + true, + true, + &context, + destination, + ) + })?; + Ok(batch_result(tensor, &plans)) +} + +/// Decode a batch into one rank-4 F32 tensor without decoded-pixel host transfer. +pub fn decode_float_batch( + inputs: &[TensorInput<'_>], + options: &TensorDecodeOptions, + device: &CudaDevice, +) -> Result>, TensorDecodeError> { + validate_normalization_values(&options.normalization)?; + ensure_dtype::(device, DType::F32)?; + let first = inputs.first().ok_or(TensorDecodeError::EmptyBatch)?; + let width = canonical_width(first.encoded).map_err(|error| indexed(0, error))?; + for (index, input) in inputs.iter().enumerate().skip(1) { + let item_width = canonical_width(input.encoded).map_err(|error| indexed(index, error))?; + if item_width.dtype() != width.dtype() { + return Err(indexed( + index, + strict(format!( + "mixed canonical integer widths are unsupported: first item uses {:?}, item uses {:?}", + width.dtype(), + item_width.dtype() + )), + )); + } + } + let plans = plan_batch(inputs, options, width)?; + validate_normalization_channels(&options.normalization, plans[0].shape[2])?; + let shape = tensor_shape_4(plans.len(), plans[0].shape, options.layout); + let context = + CudaContext::retain_primary(device.index).map_err(|error| cuda_runtime_error(&error))?; + let tensor = fill_float_tensor::<4>(shape, device, &context, |destination| { + decode_plans_into(&plans, options, width, false, true, &context, destination) + })?; + Ok(batch_result(tensor, &plans)) +} + +fn canonical_width(encoded: &[u8]) -> Result { + Ok(if j2k::J2kDecoder::inspect(encoded)?.bit_depth <= 8 { + SampleWidth::U8 + } else { + SampleWidth::U16 + }) +} + +fn plan_image<'a>( + input: TensorInput<'a>, + options: &TensorDecodeOptions, + width: SampleWidth, +) -> Result, TensorDecodeError> { + let info = j2k::J2kDecoder::inspect(input.encoded)?; + let plan = DeviceDecodePlan::for_image(info.dimensions, input.request)?; + let channels = selected_channels(options.channels, info.components); + let (width_px, height_px) = plan.output_dims(); + let _ = pixel_format(channels, width); + Ok(PlannedImage { + input, + shape: [ + usize::try_from(height_px).map_err(|_| TensorDecodeError::SizeOverflow)?, + usize::try_from(width_px).map_err(|_| TensorDecodeError::SizeOverflow)?, + channels, + ], + decoded: plan.output_rect(), + }) +} + +fn plan_batch<'a>( + inputs: &[TensorInput<'a>], + options: &TensorDecodeOptions, + width: SampleWidth, +) -> Result>, TensorDecodeError> { + if inputs.is_empty() { + return Err(TensorDecodeError::EmptyBatch); + } + let mut plans: Vec> = Vec::new(); + plans + .try_reserve_exact(inputs.len()) + .map_err(|_| TensorDecodeError::SizeOverflow)?; + for (index, input) in inputs.iter().enumerate() { + let plan = plan_image(*input, options, width).map_err(|error| indexed(index, error))?; + if let Some(first) = plans.first() { + if plan.shape != first.shape { + return Err(TensorDecodeError::BatchShapeMismatch { + index, + expected: first.shape, + actual: plan.shape, + }); + } + } + plans.push(plan); + } + Ok(plans) +} + +fn decode_plans_into( + plans: &[PlannedImage<'_>], + options: &TensorDecodeOptions, + width: SampleWidth, + integer_output: bool, + index_errors: bool, + context: &CudaContext, + destination: &mut CudaExternalDeviceBufferViewMut<'_>, +) -> Result<(), TensorDecodeError> { + let mut session = CudaSession::with_context(context.clone()); + let item_elements = plans[0] + .shape + .iter() + .try_fold(1usize, |size, dim| size.checked_mul(*dim)) + .ok_or(TensorDecodeError::SizeOverflow)?; + for (index, plan) in plans.iter().enumerate() { + let mut decoder = CudaDecoder::new(plan.input.encoded) + .map_err(|error| route_item_error(index_errors, index, strict(error.to_string())))?; + let format = pixel_format(plan.shape[2], width); + let surface = decoder + .decode_request_to_device_with_session(format, plan.input.request, &mut session) + .map_err(|error| route_item_error(index_errors, index, strict(error.to_string())))?; + validate_surface(&surface, plan.shape, width) + .map_err(|error| route_item_error(index_errors, index, error))?; + let source = surface.cuda_surface().ok_or_else(|| { + route_item_error( + index_errors, + index, + strict("strict CUDA decode returned a host surface"), + ) + })?; + let source_len = item_elements + .checked_mul(match width { + SampleWidth::U8 => 1, + SampleWidth::U16 => 2, + }) + .ok_or(TensorDecodeError::SizeOverflow)?; + context + .j2k_ml_convert_into_external( + source.device_ptr(), + source_len, + destination, + kernel_config(plan, options, width, integer_output, index, item_elements)?, + ) + .map_err(|error| route_item_error(index_errors, index, cuda_runtime_error(&error)))?; + } + Ok(()) +} + +fn validate_surface( + surface: &Surface, + shape: [usize; 3], + width: SampleWidth, +) -> Result<(), TensorDecodeError> { + let sample_bytes = match width { + SampleWidth::U8 => 1, + SampleWidth::U16 => 2, + }; + let expected_pitch = shape[1] + .checked_mul(shape[2]) + .and_then(|value| value.checked_mul(sample_bytes)) + .ok_or(TensorDecodeError::SizeOverflow)?; + if surface.pitch_bytes() != expected_pitch { + return Err(strict(format!( + "CUDA surface pitch {} does not match compact pitch {expected_pitch}", + surface.pitch_bytes() + ))); + } + Ok(()) +} + +fn kernel_config( + plan: &PlannedImage<'_>, + options: &TensorDecodeOptions, + width: SampleWidth, + integer_output: bool, + index: usize, + item_elements: usize, +) -> Result { + let sample = match width { + SampleWidth::U8 => CudaJ2kMlSample::U8, + SampleWidth::U16 => CudaJ2kMlSample::U16, + }; + let layout = match options.layout { + TensorLayout::ChannelsFirst => CudaJ2kMlLayout::ChannelsFirst, + TensorLayout::ChannelsLast => CudaJ2kMlLayout::ChannelsLast, + }; + let normalization = if integer_output { + CudaJ2kMlNormalization::Integer + } else { + match &options.normalization { + FloatNormalization::Raw => CudaJ2kMlNormalization::Raw, + FloatNormalization::Unit => CudaJ2kMlNormalization::Unit, + FloatNormalization::MeanStd { mean, std } => { + let mut means = [0.0; 4]; + let mut deviations = [1.0; 4]; + means[..plan.shape[2]].copy_from_slice(mean); + deviations[..plan.shape[2]].copy_from_slice(std); + CudaJ2kMlNormalization::MeanStd { + mean: means, + std: deviations, + } + } + } + }; + Ok(CudaJ2kMlKernelConfig { + width: u32::try_from(plan.shape[1]).map_err(|_| TensorDecodeError::SizeOverflow)?, + height: u32::try_from(plan.shape[0]).map_err(|_| TensorDecodeError::SizeOverflow)?, + channels: u32::try_from(plan.shape[2]).map_err(|_| TensorDecodeError::SizeOverflow)?, + sample, + layout, + destination_offset_elements: index + .checked_mul(item_elements) + .ok_or(TensorDecodeError::SizeOverflow)?, + normalization, + }) +} + +fn fill_int_tensor( + shape: [usize; D], + dtype: DType, + device: &CudaDevice, + context: &CudaContext, + fill: impl FnOnce(&mut CudaExternalDeviceBufferViewMut<'_>) -> Result<(), TensorDecodeError>, +) -> Result, TensorDecodeError> { + let cube = fill_cube_tensor(shape, dtype, device, context, fill)?; + Ok(register_int_tensor( + cube, + Shape::from(shape.to_vec()), + dtype, + device, + )) +} + +fn fill_float_tensor( + shape: [usize; D], + device: &CudaDevice, + context: &CudaContext, + fill: impl FnOnce(&mut CudaExternalDeviceBufferViewMut<'_>) -> Result<(), TensorDecodeError>, +) -> Result, TensorDecodeError> { + let cube = fill_cube_tensor(shape, DType::F32, device, context, fill)?; + Ok(register_float_tensor( + cube, + Shape::from(shape.to_vec()), + device, + )) +} + +fn fill_cube_tensor( + shape: [usize; D], + dtype: DType, + device: &CudaDevice, + context: &CudaContext, + fill: impl FnOnce(&mut CudaExternalDeviceBufferViewMut<'_>) -> Result<(), TensorDecodeError>, +) -> Result, TensorDecodeError> { + let logical_len = tensor_byte_len(&shape, dtype)?; + let shape = Shape::from(shape.to_vec()); + let client = CudaRuntime::client(device); + let cube = empty_device_contiguous_dtype(client, device.clone(), shape, dtype); + let handle_len = + usize::try_from(cube.handle.size_in_used()).map_err(|_| TensorDecodeError::SizeOverflow)?; + if handle_len != logical_len { + return Err(strict(format!( + "CubeCL CUDA tensor handle exposes {handle_len} bytes; expected {logical_len}" + ))); + } + let mut resource = cube + .client + .get_resource(cube.handle.clone()) + .map_err(|error| strict(format!("CubeCL CUDA resource access failed: {error}")))?; + let raw = resource.resource(); + let available = usize::try_from(raw.size).map_err(|_| TensorDecodeError::SizeOverflow)?; + if logical_len > available { + return Err(strict(format!( + "CubeCL CUDA resource exposes {available} bytes for a {logical_len}-byte tensor" + ))); + } + let pointer = raw.ptr; + // SAFETY: `resource` is CubeCL's managed allocation guard for `pointer`. + // Its exclusive borrow is retained by the non-owning view through `fill`, + // and the context/length/alignment are validated by the constructor. + let mut destination = unsafe { + CudaExternalDeviceBufferViewMut::from_raw_parts( + context, + pointer, + logical_len, + dtype.size(), + &mut resource, + ) + } + .map_err(|error| cuda_runtime_error(&error))?; + fill(&mut destination)?; + drop(destination); + drop(resource); + Ok(cube) +} + +fn tensor_byte_len(shape: &[usize], dtype: DType) -> Result { + shape + .iter() + .try_fold(dtype.size(), |size, dim| size.checked_mul(*dim)) + .ok_or(TensorDecodeError::SizeOverflow) +} + +fn register_int_tensor( + cube: burn_cubecl::tensor::CubeTensor, + shape: Shape, + dtype: DType, + device: &CudaDevice, +) -> Tensor { + let fusion = get_client::(device); + let handle = ::int_tensor_handle(cube); + let desc = InitOperationIr::create(shape, dtype, || fusion.register_tensor_handle(handle)); + let primitive = fusion + .register( + OperationStreams::default(), + OperationIr::Init(desc), + NoOp::::new(), + ) + .remove(0); + Tensor::::from_primitive(primitive) +} + +fn register_float_tensor( + cube: burn_cubecl::tensor::CubeTensor, + shape: Shape, + device: &CudaDevice, +) -> Tensor { + let fusion = get_client::(device); + let handle = ::float_tensor_handle(cube); + let desc = InitOperationIr::create(shape, DType::F32, || fusion.register_tensor_handle(handle)); + let primitive = fusion + .register( + OperationStreams::default(), + OperationIr::Init(desc), + NoOp::::new(), + ) + .remove(0); + Tensor::::from_primitive(TensorPrimitive::Float(primitive)) +} + +fn tensor_shape_3(shape: [usize; 3], layout: TensorLayout) -> [usize; 3] { + match layout { + TensorLayout::ChannelsFirst => [shape[2], shape[0], shape[1]], + TensorLayout::ChannelsLast => shape, + } +} + +fn tensor_shape_4(batch: usize, shape: [usize; 3], layout: TensorLayout) -> [usize; 4] { + match layout { + TensorLayout::ChannelsFirst => [batch, shape[2], shape[0], shape[1]], + TensorLayout::ChannelsLast => [batch, shape[0], shape[1], shape[2]], + } +} + +fn strict(message: impl Into) -> TensorDecodeError { + TensorDecodeError::StrictRoute { + route: TensorRoute::CudaDirect, + message: message.into(), + } +} + +fn cuda_runtime_error(error: &j2k_cuda_runtime::CudaError) -> TensorDecodeError { + strict(error.to_string()) +} + +fn indexed(index: usize, source: TensorDecodeError) -> TensorDecodeError { + TensorDecodeError::BatchItem { + index, + source: Box::new(source), + } +} + +fn route_item_error(is_batch: bool, index: usize, source: TensorDecodeError) -> TensorDecodeError { + if is_batch { + indexed(index, source) + } else { + source + } +} + +fn single_result(tensor: T, plan: &PlannedImage<'_>) -> TensorDecode { + TensorDecode { + tensor, + decoded: plan.decoded, + warnings: vec![J2kDecodeWarning::LenientDecodeMode], + route: TensorRoute::CudaDirect, + } +} + +fn batch_result(tensor: T, plans: &[PlannedImage<'_>]) -> TensorBatchDecode { + TensorBatchDecode { + tensor, + decoded: plans.iter().map(|plan| plan.decoded).collect(), + warnings: plans + .iter() + .map(|_| vec![J2kDecodeWarning::LenientDecodeMode]) + .collect(), + route: TensorRoute::CudaDirect, + } +} + +#[cfg(test)] +mod tests { + use burn_core::tensor::DType; + + use super::tensor_byte_len; + use crate::TensorDecodeError; + + #[test] + fn tensor_byte_length_is_exact_and_overflow_checked_before_cubecl_allocation() { + assert_eq!(tensor_byte_len(&[2, 3, 4], DType::U16).unwrap(), 48); + assert!(matches!( + tensor_byte_len(&[usize::MAX, 2], DType::F32), + Err(TensorDecodeError::SizeOverflow) + )); + } +} diff --git a/crates/j2k-ml/src/lib.rs b/crates/j2k-ml/src/lib.rs new file mode 100644 index 00000000..fbfc955c --- /dev/null +++ b/crates/j2k-ml/src/lib.rs @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! Independent Burn tensor integration for JPEG 2000 and HTJ2K. + +#![deny(missing_docs)] + +use burn_core::tensor::Tensor; +use j2k::{DeviceDecodeRequest, J2kDecodeWarning, Rect}; + +#[cfg(feature = "cpu")] +pub mod cpu; +#[cfg(feature = "cuda")] +pub mod cuda; +#[cfg(feature = "metal")] +pub mod metal; + +/// Tensor memory layout. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum TensorLayout { + /// Channels precede spatial dimensions (`CHW` or `NCHW`). + #[default] + ChannelsFirst, + /// Channels follow spatial dimensions (`HWC` or `NHWC`). + ChannelsLast, +} + +/// Output channel selection. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum ChannelSelection { + /// Preserve grayscale as one channel and otherwise produce RGB. + #[default] + Auto, + /// Produce one grayscale channel. + Gray, + /// Produce three RGB channels. + Rgb, + /// Produce four RGBA channels. + Rgba, +} + +/// Floating-point sample normalization. +#[derive(Debug, Clone, Default, PartialEq)] +pub enum FloatNormalization { + /// Scale integer samples into the inclusive range `0..=1`. + #[default] + Unit, + /// Cast integer samples without scaling. + Raw, + /// Unit-scale, then apply per-channel `(x - mean) / std`. + MeanStd { + /// Per-channel means. + mean: Vec, + /// Per-channel standard deviations. + std: Vec, + }, +} + +/// Options shared by all tensor decode routes. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct TensorDecodeOptions { + /// Requested tensor layout. + pub layout: TensorLayout, + /// Requested output channels. + pub channels: ChannelSelection, + /// Floating-point normalization. + pub normalization: FloatNormalization, +} + +/// Route that produced a tensor. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TensorRoute { + /// Host decode followed by a Burn upload. + CpuStaged, + /// Decode and conversion directly into a CUDA tensor allocation. + CudaDirect, + /// Metal decode followed by one packed readback and Burn upload. + MetalStaged, +} + +/// Borrowed compressed input and its decode geometry request. +#[derive(Debug, Clone, Copy)] +pub struct TensorInput<'a> { + /// JP2, JPH, raw J2K, or raw HTJ2K bytes. + pub encoded: &'a [u8], + /// Full-frame, ROI, scaled, or ROI-scaled request. + pub request: DeviceDecodeRequest, +} + +impl<'a> TensorInput<'a> { + /// Construct a full-resolution input. + #[must_use] + pub const fn full(encoded: &'a [u8]) -> Self { + Self { + encoded, + request: DeviceDecodeRequest::Full, + } + } +} + +/// Successful single-image tensor decode. +#[derive(Debug)] +pub struct TensorDecode { + /// Decoded Burn tensor. + pub tensor: T, + /// Rectangle actually decoded. + pub decoded: Rect, + /// Non-fatal codec warnings. + pub warnings: Vec, + /// Route actually used. + pub route: TensorRoute, +} + +/// Successful batch tensor decode. +#[derive(Debug)] +pub struct TensorBatchDecode { + /// Decoded Burn tensor. + pub tensor: T, + /// Rectangle decoded for each item, in input order. + pub decoded: Vec, + /// Codec warnings for each item, in input order. + pub warnings: Vec>, + /// Route actually used. + pub route: TensorRoute, +} + +/// Tensor decode failure. +#[derive(Debug, thiserror::Error)] +pub enum TensorDecodeError { + /// The codec rejected the compressed input or decode request. + #[error("JPEG 2000 decode failed: {0}")] + Codec(#[from] j2k::J2kError), + /// A requested integer dtype is unsupported by the selected Burn backend. + #[error("Burn backend does not support requested dtype {dtype:?}")] + UnsupportedDType { + /// Unsupported dtype. + dtype: burn_core::tensor::DType, + }, + /// Normalization parameters are invalid. + #[error("invalid float normalization: {message}")] + InvalidNormalization { + /// Actionable validation detail. + message: String, + }, + /// A batch contained no inputs. + #[error("cannot decode an empty tensor batch")] + EmptyBatch, + /// A batch item shape differs from the first item. + #[error("batch item {index} has shape {actual:?}; expected {expected:?}")] + BatchShapeMismatch { + /// Index of the mismatching item. + index: usize, + /// Expected HWC shape. + expected: [usize; 3], + /// Actual HWC shape. + actual: [usize; 3], + }, + /// A particular batch item failed to decode or convert. + #[error("batch item {index} failed: {source}")] + BatchItem { + /// Input index. + index: usize, + /// Item-specific failure. + #[source] + source: Box, + }, + /// A requested allocation size overflowed `usize`. + #[error("tensor size overflow")] + SizeOverflow, + /// Accelerator route failed without falling back. + #[error("strict {route:?} route failed: {message}")] + StrictRoute { + /// Requested route. + route: TensorRoute, + /// Backend failure detail. + message: String, + }, +} + +/// Infallible Burn batcher that intentionally panics on float decode errors. +#[derive(Debug, Clone)] +pub struct PanicOnDecodeError { + options: TensorDecodeOptions, + backend: core::marker::PhantomData, +} + +impl PanicOnDecodeError { + /// Construct an adapter with explicit decode options. + #[must_use] + pub fn new(options: TensorDecodeOptions) -> Self { + Self { + options, + backend: core::marker::PhantomData, + } + } +} + +#[cfg(feature = "cpu")] +impl<'a, B> burn_core::data::dataloader::batcher::Batcher, Tensor> + for PanicOnDecodeError +where + B: burn_core::tensor::backend::Backend, +{ + fn batch(&self, items: Vec>, device: &B::Device) -> Tensor { + cpu::decode_float_batch(&items, &self.options, device) + .unwrap_or_else(|error| panic!("j2k-ml batch decode failed: {error}")) + .tensor + } +} diff --git a/crates/j2k-ml/src/metal.rs b/crates/j2k-ml/src/metal.rs new file mode 100644 index 00000000..c47bf465 --- /dev/null +++ b/crates/j2k-ml/src/metal.rs @@ -0,0 +1,409 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! Strict J2K Metal decode followed by a compact staged Burn Metal upload. + +// The non-macOS Metal session is intentionally a zero-sized compatibility +// stub. Keep these private helper signatures identical across targets. +#![cfg_attr( + not(target_os = "macos"), + allow( + clippy::trivially_copy_pass_by_ref, + reason = "the non-macOS Metal compatibility session is intentionally zero-sized" + ) +)] + +use burn_core::tensor::{DType, FloatDType, Int, Tensor}; +use burn_wgpu::{Wgpu, WgpuDevice}; +use j2k::{ + BackendRequest, DeviceDecodePlan, DeviceDecodeRequest, J2kDecodeWarning, J2kDecoder, Rect, +}; +#[cfg(target_os = "macos")] +use j2k_metal::download_surfaces_packed; +use j2k_metal::{ + J2kDecoder as MetalDecoder, MetalBackendSession, MetalDecodeRequest, Surface, SurfaceResidency, +}; + +use crate::cpu::{ + ensure_dtype, integer_tensor_3_from_bytes, integer_tensor_4_from_bytes, normalize_3, + normalize_4, pixel_format, planned_shape, selected_channels, validate_normalization_channels, + validate_normalization_values, SampleWidth, +}; +use crate::{ + TensorBatchDecode, TensorDecode, TensorDecodeError, TensorDecodeOptions, TensorInput, + TensorRoute, +}; + +type MetalTensor = Tensor; +type MetalIntTensor = Tensor; + +struct MetalImage { + surface: Surface, + shape: [usize; 3], + sample_width: SampleWidth, + decoded: Rect, + warnings: Vec, +} + +/// Decode one image into a rank-3 U8 Burn Metal tensor. +pub fn decode_u8( + input: TensorInput<'_>, + options: &TensorDecodeOptions, + device: &WgpuDevice, +) -> Result>, TensorDecodeError> { + ensure_dtype::(device, DType::U8)?; + let session = metal_session()?; + let image = decode_surface(&session, input, options, SampleWidth::U8)?; + let bytes = packed_readback(&session, &[(&image.surface, image_byte_len(&image)?)])?; + let tensor = + integer_tensor_3_from_bytes::(bytes, image.shape, options.layout, device, DType::U8); + Ok(single_result(tensor, image)) +} + +/// Decode one image into a rank-3 U16 Burn Metal tensor. +pub fn decode_u16( + input: TensorInput<'_>, + options: &TensorDecodeOptions, + device: &WgpuDevice, +) -> Result>, TensorDecodeError> { + ensure_dtype::(device, DType::U16)?; + let session = metal_session()?; + let image = decode_surface(&session, input, options, SampleWidth::U16)?; + let bytes = packed_readback(&session, &[(&image.surface, image_byte_len(&image)?)])?; + let tensor = + integer_tensor_3_from_bytes::(bytes, image.shape, options.layout, device, DType::U16); + Ok(single_result(tensor, image)) +} + +/// Decode one image into a rank-3 F32 Burn Metal tensor. +pub fn decode_float( + input: TensorInput<'_>, + options: &TensorDecodeOptions, + device: &WgpuDevice, +) -> Result>, TensorDecodeError> { + validate_normalization_values(&options.normalization)?; + ensure_dtype::(device, DType::F32)?; + let info = J2kDecoder::inspect(input.encoded)?; + let width = sample_width(info.bit_depth); + ensure_dtype::(device, width.dtype())?; + validate_normalization_channels( + &options.normalization, + selected_channels(options.channels, info.components), + )?; + let session = metal_session()?; + let image = decode_surface(&session, input, options, width)?; + let bytes = packed_readback(&session, &[(&image.surface, image_byte_len(&image)?)])?; + let tensor = integer_tensor_3_from_bytes::( + bytes, + image.shape, + options.layout, + device, + width.dtype(), + ) + .cast(FloatDType::F32); + let tensor = normalize_3( + tensor, + &options.normalization, + options.layout, + image.shape[2], + width, + device, + ); + Ok(single_result(tensor, image)) +} + +/// Decode a batch into one rank-4 U8 Burn Metal tensor. +pub fn decode_u8_batch( + inputs: &[TensorInput<'_>], + options: &TensorDecodeOptions, + device: &WgpuDevice, +) -> Result>, TensorDecodeError> { + ensure_dtype::(device, DType::U8)?; + let session = metal_session()?; + let images = decode_surfaces(&session, inputs, options, SampleWidth::U8)?; + let (bytes, shape) = readback_batch(&session, &images)?; + let tensor = integer_tensor_4_from_bytes::( + bytes, + images.len(), + shape, + options.layout, + device, + DType::U8, + ); + Ok(batch_result(tensor, images)) +} + +/// Decode a batch into one rank-4 U16 Burn Metal tensor. +pub fn decode_u16_batch( + inputs: &[TensorInput<'_>], + options: &TensorDecodeOptions, + device: &WgpuDevice, +) -> Result>, TensorDecodeError> { + ensure_dtype::(device, DType::U16)?; + let session = metal_session()?; + let images = decode_surfaces(&session, inputs, options, SampleWidth::U16)?; + let (bytes, shape) = readback_batch(&session, &images)?; + let tensor = integer_tensor_4_from_bytes::( + bytes, + images.len(), + shape, + options.layout, + device, + DType::U16, + ); + Ok(batch_result(tensor, images)) +} + +/// Decode a batch into one rank-4 F32 Burn Metal tensor. +pub fn decode_float_batch( + inputs: &[TensorInput<'_>], + options: &TensorDecodeOptions, + device: &WgpuDevice, +) -> Result>, TensorDecodeError> { + validate_normalization_values(&options.normalization)?; + ensure_dtype::(device, DType::F32)?; + let first = inputs.first().ok_or(TensorDecodeError::EmptyBatch)?; + let first_info = + J2kDecoder::inspect(first.encoded).map_err(|error| indexed(0, error.into()))?; + let width = sample_width(first_info.bit_depth); + ensure_dtype::(device, width.dtype())?; + validate_normalization_channels( + &options.normalization, + selected_channels(options.channels, first_info.components), + ) + .map_err(|error| indexed(0, error))?; + for (index, input) in inputs.iter().enumerate().skip(1) { + let item_info = + J2kDecoder::inspect(input.encoded).map_err(|error| indexed(index, error.into()))?; + let item_width = sample_width(item_info.bit_depth); + if item_width.dtype() != width.dtype() { + return Err(indexed( + index, + strict(format!( + "mixed canonical integer widths are unsupported: first item uses {:?}, item uses {:?}", + width.dtype(), + item_width.dtype() + )), + )); + } + validate_normalization_channels( + &options.normalization, + selected_channels(options.channels, item_info.components), + ) + .map_err(|error| indexed(index, error))?; + } + let session = metal_session()?; + let images = decode_surfaces(&session, inputs, options, width)?; + let (bytes, shape) = readback_batch(&session, &images)?; + let tensor = integer_tensor_4_from_bytes::( + bytes, + images.len(), + shape, + options.layout, + device, + width.dtype(), + ) + .cast(FloatDType::F32); + let tensor = normalize_4( + tensor, + &options.normalization, + options.layout, + shape[2], + width, + device, + ); + Ok(batch_result(tensor, images)) +} + +fn sample_width(bit_depth: u8) -> SampleWidth { + if bit_depth <= 8 { + SampleWidth::U8 + } else { + SampleWidth::U16 + } +} + +fn metal_session() -> Result { + MetalBackendSession::system_default().map_err(|error| strict(error.to_string())) +} + +fn decode_surfaces( + session: &MetalBackendSession, + inputs: &[TensorInput<'_>], + options: &TensorDecodeOptions, + width: SampleWidth, +) -> Result, TensorDecodeError> { + let first = inputs.first().ok_or(TensorDecodeError::EmptyBatch)?; + let expected_shape = + planned_shape(*first, options.channels).map_err(|error| indexed(0, error))?; + for (index, input) in inputs.iter().enumerate().skip(1) { + let actual = + planned_shape(*input, options.channels).map_err(|error| indexed(index, error))?; + if actual != expected_shape { + return Err(TensorDecodeError::BatchShapeMismatch { + index, + expected: expected_shape, + actual, + }); + } + } + let mut images: Vec = Vec::new(); + images + .try_reserve_exact(inputs.len()) + .map_err(|_| TensorDecodeError::SizeOverflow)?; + for (index, input) in inputs.iter().enumerate() { + let image = decode_surface(session, *input, options, width) + .map_err(|error| indexed(index, error))?; + if let Some(first) = images.first() { + if image.shape != first.shape { + return Err(TensorDecodeError::BatchShapeMismatch { + index, + expected: first.shape, + actual: image.shape, + }); + } + } + images.push(image); + } + Ok(images) +} + +fn decode_surface( + session: &MetalBackendSession, + input: TensorInput<'_>, + options: &TensorDecodeOptions, + width: SampleWidth, +) -> Result { + let mut decoder = + MetalDecoder::new(input.encoded).map_err(|error| strict(error.to_string()))?; + let info = decoder.inner().info(); + let plan = DeviceDecodePlan::for_image(info.dimensions, input.request)?; + let channels = selected_channels(options.channels, info.components); + let format = pixel_format(channels, width); + let request = metal_request(input.request, format); + let surface = decoder + .decode_request_to_device_with_session(request, session) + .map_err(|error| strict(error.to_string()))?; + if surface.residency() != SurfaceResidency::MetalResidentDecode { + return Err(strict(format!( + "resident decode returned unexpected residency {:?}", + surface.residency() + ))); + } + let (output_width, output_height) = plan.output_dims(); + Ok(MetalImage { + surface, + shape: [ + usize::try_from(output_height).map_err(|_| TensorDecodeError::SizeOverflow)?, + usize::try_from(output_width).map_err(|_| TensorDecodeError::SizeOverflow)?, + channels, + ], + sample_width: width, + decoded: plan.output_rect(), + warnings: vec![J2kDecodeWarning::LenientDecodeMode], + }) +} + +fn metal_request(request: DeviceDecodeRequest, format: j2k::PixelFormat) -> MetalDecodeRequest { + match request { + DeviceDecodeRequest::Full => MetalDecodeRequest::full(format, BackendRequest::Metal), + DeviceDecodeRequest::Region { roi } => { + MetalDecodeRequest::region(format, roi, BackendRequest::Metal) + } + DeviceDecodeRequest::Scaled { scale } => { + MetalDecodeRequest::scaled(format, scale, BackendRequest::Metal) + } + DeviceDecodeRequest::RegionScaled { roi, scale } => { + MetalDecodeRequest::region_scaled(format, roi, scale, BackendRequest::Metal) + } + } +} + +fn readback_batch( + session: &MetalBackendSession, + images: &[MetalImage], +) -> Result<(Vec, [usize; 3]), TensorDecodeError> { + let first = images.first().ok_or(TensorDecodeError::EmptyBatch)?; + let mut surfaces = Vec::new(); + surfaces + .try_reserve_exact(images.len()) + .map_err(|_| TensorDecodeError::SizeOverflow)?; + for image in images { + surfaces.push((&image.surface, image_byte_len(image)?)); + } + Ok((packed_readback(session, &surfaces)?, first.shape)) +} + +fn image_byte_len(image: &MetalImage) -> Result { + let expected_pitch = image.shape[1] + .checked_mul(image.shape[2]) + .and_then(|samples| samples.checked_mul(image.sample_width.bytes())) + .ok_or(TensorDecodeError::SizeOverflow)?; + if image.surface.pitch_bytes() != expected_pitch { + return Err(strict(format!( + "Metal surface pitch {} does not match compact pitch {expected_pitch}", + image.surface.pitch_bytes() + ))); + } + expected_pitch + .checked_mul(image.shape[0]) + .ok_or(TensorDecodeError::SizeOverflow) +} + +#[cfg(target_os = "macos")] +fn packed_readback( + session: &MetalBackendSession, + surfaces: &[(&Surface, usize)], +) -> Result, TensorDecodeError> { + let mut surface_refs = Vec::new(); + surface_refs + .try_reserve_exact(surfaces.len()) + .map_err(|_| TensorDecodeError::SizeOverflow)?; + surface_refs.extend(surfaces.iter().map(|(surface, _)| *surface)); + download_surfaces_packed(session, &surface_refs).map_err(|error| strict(error.to_string())) +} + +#[cfg(not(target_os = "macos"))] +fn packed_readback( + _session: &MetalBackendSession, + _surfaces: &[(&Surface, usize)], +) -> Result, TensorDecodeError> { + Err(strict("Metal is unavailable on this platform")) +} + +fn strict(message: impl Into) -> TensorDecodeError { + TensorDecodeError::StrictRoute { + route: TensorRoute::MetalStaged, + message: message.into(), + } +} + +fn indexed(index: usize, source: TensorDecodeError) -> TensorDecodeError { + TensorDecodeError::BatchItem { + index, + source: Box::new(source), + } +} + +fn single_result(tensor: T, image: MetalImage) -> TensorDecode { + TensorDecode { + tensor, + decoded: image.decoded, + warnings: image.warnings, + route: TensorRoute::MetalStaged, + } +} + +fn batch_result(tensor: T, images: Vec) -> TensorBatchDecode { + let mut decoded = Vec::with_capacity(images.len()); + let mut warnings = Vec::with_capacity(images.len()); + for image in images { + decoded.push(image.decoded); + warnings.push(image.warnings); + } + TensorBatchDecode { + tensor, + decoded, + warnings, + route: TensorRoute::MetalStaged, + } +} diff --git a/crates/j2k-ml/tests/cpu.rs b/crates/j2k-ml/tests/cpu.rs new file mode 100644 index 00000000..4e1c0a68 --- /dev/null +++ b/crates/j2k-ml/tests/cpu.rs @@ -0,0 +1,519 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +#![cfg(feature = "cpu")] + +use burn_core::{ + data::dataloader::batcher::Batcher, + tensor::{backend::Backend, DType}, +}; +#[cfg(not(all(target_arch = "aarch64", target_os = "linux")))] +use burn_flex::{Flex, FlexDevice}; +#[cfg(all(target_arch = "aarch64", target_os = "linux"))] +use burn_ndarray::NdArrayDevice::Cpu as FlexDevice; +use j2k::{ + encode_j2k_lossless, wrap_j2k_codestream, DeviceDecodeRequest, Downscale, J2kBlockCodingMode, + J2kEncodeValidation, J2kFileWrapOptions, J2kLosslessEncodeOptions, J2kLosslessSamples, Rect, + ReversibleTransform, +}; +use j2k_ml::{ + cpu, ChannelSelection, FloatNormalization, PanicOnDecodeError, TensorDecodeError, + TensorDecodeOptions, TensorInput, TensorLayout, +}; +use j2k_test_support::{ + classic_j2k_gray8_fixture, htj2k_gray8_fixture, htj2k_gray8_large_fixture, + htj2k_rgb8_fixture_with_pixels, +}; + +#[cfg(all(target_arch = "aarch64", target_os = "linux"))] +type Flex = burn_ndarray::NdArray; + +#[test] +fn decodes_gray_u8_to_default_chw_tensor() { + let encoded = classic_j2k_gray8_fixture(4, 3); + let decoded = cpu::decode_u8::( + TensorInput::full(&encoded), + &TensorDecodeOptions::default(), + &FlexDevice, + ) + .expect("decode tensor"); + + assert_eq!(decoded.tensor.dims(), [1, 3, 4]); + let data = decoded.tensor.into_data(); + assert_eq!(data.dtype, DType::U8); + assert_eq!( + data.into_vec::().expect("u8 data"), + (0..12).collect::>() + ); +} + +#[test] +fn decodes_raw_j2k_jp2_raw_htj2k_and_jph_with_exact_values() { + let classic = classic_j2k_gray8_fixture(4, 3); + let ht = htj2k_gray8_fixture(4, 3); + let jp2 = wrap_j2k_codestream(&classic, J2kFileWrapOptions::jp2()).expect("wrap JP2"); + let jph = wrap_j2k_codestream(&ht, J2kFileWrapOptions::jph()).expect("wrap JPH"); + + for (name, encoded) in [ + ("raw J2K", classic.as_slice()), + ("JP2", jp2.as_slice()), + ("raw HTJ2K", ht.as_slice()), + ("JPH", jph.as_slice()), + ] { + let data = cpu::decode_u8::( + TensorInput::full(encoded), + &TensorDecodeOptions::default(), + &FlexDevice, + ) + .unwrap_or_else(|error| panic!("decode {name}: {error}")) + .tensor + .into_data(); + assert_eq!(data.dtype, DType::U8, "{name}"); + assert_eq!( + data.into_vec::().expect("u8 data"), + (0..12).collect::>(), + "{name}" + ); + } +} + +#[test] +fn decodes_rgb_float_with_layout_and_unit_normalization() { + let (encoded, expected) = htj2k_rgb8_fixture_with_pixels(3, 2); + let options = TensorDecodeOptions { + layout: TensorLayout::ChannelsLast, + channels: ChannelSelection::Rgb, + normalization: FloatNormalization::Unit, + }; + let decoded = cpu::decode_float::(TensorInput::full(&encoded), &options, &FlexDevice) + .expect("decode tensor"); + + assert_eq!(decoded.tensor.dims(), [2, 3, 3]); + let actual = decoded + .tensor + .into_data() + .into_vec::() + .expect("f32 data"); + let expected = expected + .into_iter() + .map(|sample| f32::from(sample) / 255.0) + .collect::>(); + assert_eq!(actual, expected); +} + +#[test] +fn rejects_invalid_normalization_before_decoding_corrupt_input() { + let options = TensorDecodeOptions { + normalization: FloatNormalization::MeanStd { + mean: vec![0.0], + std: vec![0.0], + }, + ..TensorDecodeOptions::default() + }; + let error = cpu::decode_float::(TensorInput::full(b"corrupt"), &options, &FlexDevice) + .expect_err("zero std must fail first"); + assert!(matches!( + error, + TensorDecodeError::InvalidNormalization { .. } + )); +} + +#[test] +fn batch_preserves_order_and_rejects_shape_mismatch_with_index() { + let first = classic_j2k_gray8_fixture(2, 2); + let second = classic_j2k_gray8_fixture(3, 2); + let error = cpu::decode_u8_batch::( + &[ + TensorInput { + encoded: &first, + request: DeviceDecodeRequest::Full, + }, + TensorInput::full(&second), + ], + &TensorDecodeOptions::default(), + &FlexDevice, + ) + .expect_err("shape mismatch"); + assert!(matches!( + error, + TensorDecodeError::BatchShapeMismatch { index: 1, .. } + )); +} + +#[test] +fn flex_supports_the_integer_dtypes_used_by_the_contract() { + assert!(Flex::supports_dtype(&FlexDevice, DType::U8)); + assert!(Flex::supports_dtype(&FlexDevice, DType::U16)); +} + +#[test] +fn decodes_u16_values_and_preserves_u16_dtype() { + let samples = [0u16, 1, 0x1234, u16::MAX, 4096, 32_768]; + let encoded = encode_gray16(&samples, 3, 2); + let decoded = cpu::decode_u16::( + TensorInput::full(&encoded), + &TensorDecodeOptions::default(), + &FlexDevice, + ) + .expect("decode u16 tensor"); + + assert_eq!(decoded.tensor.dims(), [1, 2, 3]); + let data = decoded.tensor.into_data(); + assert_eq!(data.dtype, DType::U16); + assert_eq!(data.into_vec::().expect("u16 data"), samples); +} + +#[test] +fn decodes_u16_classic_htj2k_jp2_and_jph_with_exact_values() { + let samples = [0u16, 1, 0x1234, u16::MAX, 4096, 32_768]; + let classic = encode_gray16_mode(&samples, 3, 2, J2kBlockCodingMode::Classic); + let ht = encode_gray16_mode(&samples, 3, 2, J2kBlockCodingMode::HighThroughput); + let jp2 = wrap_j2k_codestream(&classic, J2kFileWrapOptions::jp2()).expect("wrap JP2"); + let jph = wrap_j2k_codestream(&ht, J2kFileWrapOptions::jph()).expect("wrap JPH"); + + for (name, encoded) in [ + ("raw J2K", classic.as_slice()), + ("JP2", jp2.as_slice()), + ("raw HTJ2K", ht.as_slice()), + ("JPH", jph.as_slice()), + ] { + let data = cpu::decode_u16::( + TensorInput::full(encoded), + &TensorDecodeOptions::default(), + &FlexDevice, + ) + .unwrap_or_else(|error| panic!("decode {name}: {error}")) + .tensor + .into_data(); + assert_eq!(data.dtype, DType::U16, "{name}"); + assert_eq!(data.into_vec::().expect("u16 data"), samples, "{name}"); + } +} + +#[test] +fn channel_selection_and_nhwc_batch_shapes_follow_the_public_contract() { + let gray = classic_j2k_gray8_fixture(2, 1); + let (rgb, _) = htj2k_rgb8_fixture_with_pixels(2, 1); + let rgba = encode_rgba8(&[1, 2, 3, 4, 5, 6, 7, 8], 2, 1); + for (encoded, selection, channels) in [ + (gray.as_slice(), ChannelSelection::Auto, 1), + (gray.as_slice(), ChannelSelection::Gray, 1), + (rgb.as_slice(), ChannelSelection::Auto, 3), + (rgb.as_slice(), ChannelSelection::Rgb, 3), + (rgba.as_slice(), ChannelSelection::Auto, 3), + (rgba.as_slice(), ChannelSelection::Rgba, 4), + ] { + let options = TensorDecodeOptions { + layout: TensorLayout::ChannelsLast, + channels: selection, + ..TensorDecodeOptions::default() + }; + let single = cpu::decode_u8::(TensorInput::full(encoded), &options, &FlexDevice) + .expect("channel-selected single decode"); + assert_eq!(single.tensor.dims(), [1, 2, channels]); + + let batch = cpu::decode_u8_batch::( + &[TensorInput::full(encoded), TensorInput::full(encoded)], + &options, + &FlexDevice, + ) + .expect("channel-selected batch decode"); + assert_eq!(batch.tensor.dims(), [2, 1, 2, channels]); + } +} + +#[test] +fn u16_unit_float_uses_the_full_canonical_denominator() { + let samples = [0u16, 32_768, u16::MAX]; + let encoded = encode_gray16(&samples, 3, 1); + let actual = cpu::decode_float::( + TensorInput::full(&encoded), + &TensorDecodeOptions::default(), + &FlexDevice, + ) + .expect("u16 unit float decode") + .tensor + .into_data() + .into_vec::() + .expect("f32 data"); + let expected = samples.map(|value| f32::from(value) / 65_535.0); + for (actual, expected) in actual.iter().zip(expected) { + assert!((actual - expected).abs() <= f32::EPSILON); + } +} + +#[test] +fn roi_scaled_decode_reports_shape_and_rectangle() { + let encoded = classic_j2k_gray8_fixture(8, 8); + let roi = Rect { + x: 2, + y: 2, + w: 4, + h: 4, + }; + let decoded = cpu::decode_u8::( + TensorInput { + encoded: &encoded, + request: DeviceDecodeRequest::RegionScaled { + roi, + scale: Downscale::Half, + }, + }, + &TensorDecodeOptions::default(), + &FlexDevice, + ) + .expect("decode ROI tensor"); + + assert_eq!(decoded.tensor.dims(), [1, 2, 2]); + assert_eq!(decoded.decoded, roi.scaled_covering(Downscale::Half)); +} + +#[test] +fn every_supported_power_of_two_scale_has_the_expected_shape() { + let encoded = htj2k_gray8_large_fixture(64, 64); + for (scale, side) in [ + (Downscale::None, 64), + (Downscale::Half, 32), + (Downscale::Quarter, 16), + (Downscale::Eighth, 8), + ] { + let decoded = cpu::decode_u8::( + TensorInput { + encoded: &encoded, + request: DeviceDecodeRequest::Scaled { scale }, + }, + &TensorDecodeOptions::default(), + &FlexDevice, + ) + .expect("scaled decode"); + assert_eq!(decoded.tensor.dims(), [1, side, side], "{scale:?}"); + } +} + +#[test] +fn raw_float_casts_without_scaling() { + let encoded = classic_j2k_gray8_fixture(3, 2); + let options = TensorDecodeOptions { + normalization: FloatNormalization::Raw, + ..TensorDecodeOptions::default() + }; + let actual = cpu::decode_float::(TensorInput::full(&encoded), &options, &FlexDevice) + .expect("raw float decode") + .tensor + .into_data() + .into_vec::() + .expect("f32 data"); + assert_eq!(actual, vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0]); +} + +#[test] +fn mean_std_is_unit_scaled_and_broadcast_per_channel() { + let (encoded, pixels) = htj2k_rgb8_fixture_with_pixels(2, 1); + let options = TensorDecodeOptions { + layout: TensorLayout::ChannelsLast, + channels: ChannelSelection::Rgb, + normalization: FloatNormalization::MeanStd { + mean: vec![0.1, 0.2, 0.3], + std: vec![0.5, 0.25, 2.0], + }, + }; + let actual = cpu::decode_float::(TensorInput::full(&encoded), &options, &FlexDevice) + .expect("normalized tensor") + .tensor + .into_data() + .into_vec::() + .expect("f32 data"); + + for (index, (actual, pixel)) in actual.iter().zip(pixels).enumerate() { + let channel = index % 3; + let mean = [0.1, 0.2, 0.3][channel]; + let std = [0.5, 0.25, 2.0][channel]; + let expected = (f32::from(pixel) / 255.0 - mean) / std; + assert!((actual - expected).abs() <= 1.0e-6); + } +} + +#[test] +fn batch_uses_one_rank_four_tensor_and_preserves_input_order() { + let first = classic_j2k_gray8_fixture(2, 2); + let second = classic_j2k_gray8_fixture(2, 2); + let batch = cpu::decode_u8_batch::( + &[TensorInput::full(&first), TensorInput::full(&second)], + &TensorDecodeOptions::default(), + &FlexDevice, + ) + .expect("decode batch"); + + assert_eq!(batch.tensor.dims(), [2, 1, 2, 2]); + assert_eq!( + batch.tensor.into_data().into_vec::().expect("u8 data"), + vec![0, 1, 2, 3, 0, 1, 2, 3] + ); + assert_eq!(batch.decoded.len(), 2); + assert_eq!(batch.warnings.len(), 2); +} + +#[test] +fn u16_batch_preserves_values_dtype_and_order() { + let first_samples = [0u16, 1, 4096, u16::MAX]; + let second_samples = [32_768u16, 17, 255, 1024]; + let first = encode_gray16(&first_samples, 2, 2); + let second = encode_gray16(&second_samples, 2, 2); + let batch = cpu::decode_u16_batch::( + &[TensorInput::full(&first), TensorInput::full(&second)], + &TensorDecodeOptions::default(), + &FlexDevice, + ) + .expect("decode u16 batch"); + + assert_eq!(batch.tensor.dims(), [2, 1, 2, 2]); + let data = batch.tensor.into_data(); + assert_eq!(data.dtype, DType::U16); + assert_eq!( + data.into_vec::().expect("u16 data"), + first_samples + .into_iter() + .chain(second_samples) + .collect::>() + ); +} + +#[test] +fn float_batch_mean_std_broadcasts_in_both_layouts() { + let (encoded, pixels) = htj2k_rgb8_fixture_with_pixels(2, 1); + let mean = [0.1f32, 0.2, 0.3]; + let std = [0.5f32, 0.25, 2.0]; + + for layout in [TensorLayout::ChannelsFirst, TensorLayout::ChannelsLast] { + let options = TensorDecodeOptions { + layout, + channels: ChannelSelection::Rgb, + normalization: FloatNormalization::MeanStd { + mean: mean.to_vec(), + std: std.to_vec(), + }, + }; + let batch = cpu::decode_float_batch::( + &[TensorInput::full(&encoded), TensorInput::full(&encoded)], + &options, + &FlexDevice, + ) + .expect("decode normalized float batch"); + + assert_eq!( + batch.tensor.dims(), + match layout { + TensorLayout::ChannelsFirst => [2, 3, 1, 2], + TensorLayout::ChannelsLast => [2, 1, 2, 3], + } + ); + let actual = batch + .tensor + .into_data() + .into_vec::() + .expect("f32 data"); + let expected_image = match layout { + TensorLayout::ChannelsFirst => (0..3) + .flat_map(|channel| { + pixels.iter().skip(channel).step_by(3).map(move |pixel| { + (f32::from(*pixel) / 255.0 - mean[channel]) / std[channel] + }) + }) + .collect::>(), + TensorLayout::ChannelsLast => pixels + .iter() + .enumerate() + .map(|(index, pixel)| { + let channel = index % 3; + (f32::from(*pixel) / 255.0 - mean[channel]) / std[channel] + }) + .collect::>(), + }; + let expected = expected_image + .iter() + .copied() + .chain(expected_image.iter().copied()) + .collect::>(); + assert_eq!(actual.len(), expected.len()); + for (actual, expected) in actual.iter().zip(expected) { + assert!((actual - expected).abs() <= 1.0e-6); + } + } +} + +#[test] +fn empty_and_corrupt_batches_are_explicit_and_indexed() { + let empty = cpu::decode_u8_batch::(&[], &TensorDecodeOptions::default(), &FlexDevice) + .expect_err("empty batch"); + assert!(matches!(empty, TensorDecodeError::EmptyBatch)); + + let valid = classic_j2k_gray8_fixture(2, 2); + let corrupt = cpu::decode_u8_batch::( + &[TensorInput::full(&valid), TensorInput::full(b"corrupt")], + &TensorDecodeOptions::default(), + &FlexDevice, + ) + .expect_err("corrupt batch item"); + assert!(matches!( + corrupt, + TensorDecodeError::BatchItem { index: 1, .. } + )); +} + +#[test] +fn panic_batcher_includes_actionable_item_context() { + let valid = classic_j2k_gray8_fixture(2, 2); + let batcher = PanicOnDecodeError::::new(TensorDecodeOptions::default()); + let panic = std::panic::catch_unwind(|| { + let _ = batcher.batch( + vec![TensorInput::full(&valid), TensorInput::full(b"corrupt")], + &FlexDevice, + ); + }) + .expect_err("adapter must panic"); + let message = panic + .downcast_ref::() + .map(String::as_str) + .or_else(|| panic.downcast_ref::<&str>().copied()) + .expect("string panic"); + assert!(message.contains("batch item 1"), "{message}"); + assert!(message.contains("decode failed"), "{message}"); +} + +fn encode_gray16(samples: &[u16], width: u32, height: u32) -> Vec { + encode_gray16_mode(samples, width, height, J2kBlockCodingMode::Classic) +} + +fn encode_gray16_mode( + samples: &[u16], + width: u32, + height: u32, + mode: J2kBlockCodingMode, +) -> Vec { + let mut bytes = Vec::with_capacity(samples.len() * 2); + for sample in samples { + bytes.extend_from_slice(&sample.to_le_bytes()); + } + let samples = + J2kLosslessSamples::new(&bytes, width, height, 1, 16, false).expect("valid gray16 samples"); + encode_j2k_lossless( + samples, + &J2kLosslessEncodeOptions::default() + .with_block_coding_mode(mode) + .with_validation(J2kEncodeValidation::External), + ) + .expect("encode gray16") + .codestream +} + +fn encode_rgba8(samples: &[u8], width: u32, height: u32) -> Vec { + let samples = + J2kLosslessSamples::new(samples, width, height, 4, 8, false).expect("valid rgba8 samples"); + encode_j2k_lossless( + samples, + &J2kLosslessEncodeOptions::default() + .with_reversible_transform(ReversibleTransform::None53) + .with_validation(J2kEncodeValidation::External), + ) + .expect("encode rgba8") + .codestream +} diff --git a/crates/j2k-ml/tests/cuda.rs b/crates/j2k-ml/tests/cuda.rs new file mode 100644 index 00000000..708b6cc5 --- /dev/null +++ b/crates/j2k-ml/tests/cuda.rs @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +#![cfg(all(feature = "cuda", not(target_os = "macos")))] + +use burn_autodiff::Autodiff; +use burn_core::tensor::Tensor; +use burn_cuda::{Cuda, CudaDevice}; +#[cfg(not(all(target_arch = "aarch64", target_os = "linux")))] +use burn_flex::{Flex, FlexDevice}; +#[cfg(all(target_arch = "aarch64", target_os = "linux"))] +use burn_ndarray::NdArrayDevice::Cpu as FlexDevice; +use j2k::{DeviceDecodeRequest, Downscale, Rect}; +use j2k_ml::{ + cpu, cuda, FloatNormalization, TensorDecodeError, TensorDecodeOptions, TensorInput, + TensorLayout, TensorRoute, +}; +use j2k_test_support::{ + cuda_runtime_and_strict_oxide_gate, htj2k_gray8_fixture, htj2k_gray8_large_fixture, + openhtj2k_refinement_fixture, +}; + +#[cfg(all(target_arch = "aarch64", target_os = "linux"))] +type Flex = burn_ndarray::NdArray; + +#[test] +fn direct_cuda_decode_reports_route_and_exact_pixels() { + if !cuda_runtime_and_strict_oxide_gate("j2k-ml CUDA direct") { + return; + } + let encoded = htj2k_gray8_fixture(4, 3); + let decoded = cuda::decode_u8( + TensorInput::full(&encoded), + &TensorDecodeOptions::default(), + &CudaDevice::default(), + ) + .expect("CUDA direct tensor decode"); + assert_eq!(decoded.route, TensorRoute::CudaDirect); + assert_eq!(decoded.tensor.dims(), [1, 3, 4]); + assert_eq!( + decoded + .tensor + .into_data() + .into_vec::() + .expect("u8 data"), + (0..12).collect::>() + ); +} + +#[test] +fn retained_primary_context_matches_cubecl_device_context() { + if !cuda_runtime_and_strict_oxide_gate("j2k-ml CUDA primary context") { + return; + } + let first = j2k_cuda_runtime::CudaContext::retain_primary(0).expect("retain primary"); + let second = j2k_cuda_runtime::CudaContext::retain_primary(0).expect("retain primary again"); + assert!(first.is_same_context(&second)); + assert_eq!(first.device_ordinal(), 0); +} + +#[test] +fn direct_cuda_u16_matches_portable_and_batches() { + if !cuda_runtime_and_strict_oxide_gate("j2k-ml CUDA u16 tensor parity") { + return; + } + let device = CudaDevice::default(); + + let encoded_u16 = openhtj2k_refinement_fixture(); + let expected_u16 = cpu::decode_u16::( + TensorInput::full(encoded_u16), + &TensorDecodeOptions::default(), + &FlexDevice, + ) + .expect("portable u16 decode") + .tensor + .into_data() + .into_vec::() + .expect("portable u16 data"); + let actual_u16 = cuda::decode_u16( + TensorInput::full(encoded_u16), + &TensorDecodeOptions::default(), + &device, + ) + .expect("direct u16 decode") + .tensor + .into_data() + .into_vec::() + .expect("direct u16 data"); + assert_eq!(actual_u16, expected_u16); + + let u16_batch = cuda::decode_u16_batch( + &[ + TensorInput::full(encoded_u16), + TensorInput::full(encoded_u16), + ], + &TensorDecodeOptions::default(), + &device, + ) + .expect("direct u16 batch"); + assert_eq!(u16_batch.tensor.dims()[0], 2); + let expected_u16_batch = expected_u16 + .iter() + .chain(&expected_u16) + .copied() + .collect::>(); + assert_eq!( + u16_batch + .tensor + .into_data() + .into_vec::() + .expect("direct u16 batch data"), + expected_u16_batch + ); +} + +#[test] +fn direct_cuda_float_matches_portable_full_batch_and_roi() { + if !cuda_runtime_and_strict_oxide_gate("j2k-ml CUDA float tensor parity") { + return; + } + let device = CudaDevice::default(); + let encoded = htj2k_gray8_fixture(4, 3); + for layout in [TensorLayout::ChannelsFirst, TensorLayout::ChannelsLast] { + for normalization in [ + FloatNormalization::Raw, + FloatNormalization::Unit, + FloatNormalization::MeanStd { + mean: vec![0.25], + std: vec![0.5], + }, + ] { + let options = TensorDecodeOptions { + layout, + normalization, + ..TensorDecodeOptions::default() + }; + let expected = + cpu::decode_float::(TensorInput::full(&encoded), &options, &FlexDevice) + .expect("portable float decode") + .tensor + .into_data() + .into_vec::() + .expect("portable float data"); + let actual = cuda::decode_float(TensorInput::full(&encoded), &options, &device) + .expect("direct float decode") + .tensor + .into_data() + .into_vec::() + .expect("direct float data"); + for (actual, expected) in actual.iter().zip(expected) { + assert!((actual - expected).abs() <= 1.0e-6); + } + } + } + + let options = TensorDecodeOptions { + layout: TensorLayout::ChannelsLast, + normalization: FloatNormalization::MeanStd { + mean: vec![0.25], + std: vec![0.5], + }, + ..TensorDecodeOptions::default() + }; + let batch = cuda::decode_float_batch( + &[TensorInput::full(&encoded), TensorInput::full(&encoded)], + &options, + &device, + ) + .expect("direct float batch"); + assert_eq!(batch.tensor.dims(), [2, 3, 4, 1]); + + let large = htj2k_gray8_large_fixture(64, 64); + let roi_input = TensorInput { + encoded: &large, + request: DeviceDecodeRequest::RegionScaled { + roi: Rect { + x: 8, + y: 8, + w: 32, + h: 32, + }, + scale: Downscale::Half, + }, + }; + let expected_roi = cpu::decode_float::(roi_input, &options, &FlexDevice) + .expect("portable ROI decode") + .tensor + .into_data() + .into_vec::() + .expect("portable ROI data"); + let direct_roi = cuda::decode_float(roi_input, &options, &device).expect("direct ROI decode"); + assert_eq!(direct_roi.tensor.dims(), [16, 16, 1]); + for (actual, expected) in direct_roi + .tensor + .into_data() + .into_vec::() + .expect("direct ROI data") + .iter() + .zip(expected_roi) + { + assert!((actual - expected).abs() <= 1.0e-6); + } +} + +#[test] +fn direct_cuda_reports_batch_mismatch_and_lifts_to_autodiff() { + if !cuda_runtime_and_strict_oxide_gate("j2k-ml CUDA tensor contracts") { + return; + } + let device = CudaDevice::default(); + let encoded = htj2k_gray8_fixture(4, 3); + let options = TensorDecodeOptions { + layout: TensorLayout::ChannelsLast, + normalization: FloatNormalization::MeanStd { + mean: vec![0.25], + std: vec![0.5], + }, + ..TensorDecodeOptions::default() + }; + let mismatch = htj2k_gray8_fixture(5, 3); + let error = cuda::decode_u8_batch( + &[TensorInput::full(&encoded), TensorInput::full(&mismatch)], + &TensorDecodeOptions::default(), + &device, + ) + .expect_err("direct batch shape mismatch"); + assert!(matches!( + error, + TensorDecodeError::BatchShapeMismatch { index: 1, .. } + )); + + let inner = cuda::decode_float(TensorInput::full(&encoded), &options, &device) + .expect("direct float for autodiff") + .tensor; + let autodiff = Tensor::, 3>::from_inner(inner).require_grad(); + assert_eq!(autodiff.dims(), [3, 4, 1]); +} diff --git a/crates/j2k-ml/tests/metal.rs b/crates/j2k-ml/tests/metal.rs new file mode 100644 index 00000000..3d790675 --- /dev/null +++ b/crates/j2k-ml/tests/metal.rs @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +#![cfg(all(feature = "metal", target_os = "macos"))] + +use burn_flex::{Flex, FlexDevice}; +use burn_wgpu::WgpuDevice; +use j2k::{DeviceDecodeRequest, Downscale, Rect}; +use j2k_ml::{ + cpu, metal, FloatNormalization, TensorDecodeOptions, TensorInput, TensorLayout, TensorRoute, +}; +use j2k_test_support::{ + classic_j2k_gray8_fixture, metal_runtime_gate, openhtj2k_refinement_fixture, +}; + +#[test] +fn strict_metal_staged_decode_reports_route_and_pixels() { + if !metal_runtime_gate("j2k-ml strict Metal staged decode") { + return; + } + let encoded = classic_j2k_gray8_fixture(4, 3); + let decoded = metal::decode_u8( + TensorInput::full(&encoded), + &TensorDecodeOptions::default(), + &WgpuDevice::DefaultDevice, + ) + .expect("strict Metal tensor decode"); + + assert_eq!(decoded.route, TensorRoute::MetalStaged); + assert_eq!(decoded.tensor.dims(), [1, 3, 4]); + assert_eq!( + decoded + .tensor + .into_data() + .into_vec::() + .expect("u8 data"), + (0..12).collect::>() + ); +} + +#[test] +fn metal_batch_returns_one_rank_four_tensor() { + if !metal_runtime_gate("j2k-ml Metal staged batch") { + return; + } + let first = classic_j2k_gray8_fixture(2, 2); + let second = classic_j2k_gray8_fixture(2, 2); + let decoded = metal::decode_u8_batch( + &[TensorInput::full(&first), TensorInput::full(&second)], + &TensorDecodeOptions::default(), + &WgpuDevice::DefaultDevice, + ) + .expect("strict Metal batch"); + assert_eq!(decoded.route, TensorRoute::MetalStaged); + assert_eq!(decoded.tensor.dims(), [2, 1, 2, 2]); +} + +#[test] +fn metal_u16_and_float_match_portable_decode() { + if !metal_runtime_gate("j2k-ml Metal u16 and float parity") { + return; + } + let encoded = openhtj2k_refinement_fixture(); + let expected_u16 = cpu::decode_u16::( + TensorInput::full(encoded), + &TensorDecodeOptions::default(), + &FlexDevice, + ) + .expect("portable u16") + .tensor + .into_data() + .into_vec::() + .expect("portable u16 data"); + let actual_u16 = metal::decode_u16( + TensorInput::full(encoded), + &TensorDecodeOptions::default(), + &WgpuDevice::DefaultDevice, + ) + .expect("Metal u16") + .tensor + .into_data() + .into_vec::() + .expect("Metal u16 data"); + assert_eq!(actual_u16, expected_u16); + + for layout in [TensorLayout::ChannelsFirst, TensorLayout::ChannelsLast] { + for normalization in [ + FloatNormalization::Raw, + FloatNormalization::Unit, + FloatNormalization::MeanStd { + mean: vec![0.25], + std: vec![0.5], + }, + ] { + let options = TensorDecodeOptions { + layout, + normalization, + ..TensorDecodeOptions::default() + }; + let expected = + cpu::decode_float::(TensorInput::full(encoded), &options, &FlexDevice) + .expect("portable float") + .tensor + .into_data() + .into_vec::() + .expect("portable float data"); + let actual = metal::decode_float( + TensorInput::full(encoded), + &options, + &WgpuDevice::DefaultDevice, + ) + .expect("Metal float") + .tensor + .into_data() + .into_vec::() + .expect("Metal float data"); + for (actual, expected) in actual.iter().zip(expected) { + assert!((actual - expected).abs() <= 1.0e-6); + } + } + } +} + +#[test] +fn metal_roi_scale_and_all_batch_output_modes_report_staged_route() { + if !metal_runtime_gate("j2k-ml Metal ROI and batch modes") { + return; + } + let encoded = classic_j2k_gray8_fixture(8, 8); + let roi = Rect { + x: 2, + y: 2, + w: 4, + h: 4, + }; + let input = TensorInput { + encoded: &encoded, + request: DeviceDecodeRequest::RegionScaled { + roi, + scale: Downscale::Half, + }, + }; + let decoded = metal::decode_float( + input, + &TensorDecodeOptions::default(), + &WgpuDevice::DefaultDevice, + ) + .expect("Metal ROI-scaled float"); + assert_eq!(decoded.route, TensorRoute::MetalStaged); + assert_eq!(decoded.tensor.dims(), [1, 2, 2]); + let expected_roi = + cpu::decode_float::(input, &TensorDecodeOptions::default(), &FlexDevice) + .expect("portable ROI-scaled float") + .tensor + .into_data() + .into_vec::() + .expect("portable ROI data"); + assert_eq!( + decoded + .tensor + .into_data() + .into_vec::() + .expect("Metal ROI data"), + expected_roi + ); + + let encoded_u16 = openhtj2k_refinement_fixture(); + let u16_batch = metal::decode_u16_batch( + &[ + TensorInput::full(encoded_u16), + TensorInput::full(encoded_u16), + ], + &TensorDecodeOptions::default(), + &WgpuDevice::DefaultDevice, + ) + .expect("Metal u16 batch"); + assert_eq!(u16_batch.route, TensorRoute::MetalStaged); + assert_eq!(u16_batch.tensor.dims()[0], 2); + let u16_values = u16_batch + .tensor + .into_data() + .into_vec::() + .expect("Metal u16 batch data"); + let midpoint = u16_values.len() / 2; + assert_eq!(&u16_values[..midpoint], &u16_values[midpoint..]); + + let float_batch = metal::decode_float_batch( + &[TensorInput::full(&encoded), TensorInput::full(&encoded)], + &TensorDecodeOptions::default(), + &WgpuDevice::DefaultDevice, + ) + .expect("Metal float batch"); + assert_eq!(float_batch.route, TensorRoute::MetalStaged); + assert_eq!(float_batch.tensor.dims(), [2, 1, 8, 8]); + let float_values = float_batch + .tensor + .into_data() + .into_vec::() + .expect("Metal float batch data"); + let midpoint = float_values.len() / 2; + assert_eq!(&float_values[..midpoint], &float_values[midpoint..]); +} diff --git a/crates/j2k-native/Cargo.toml b/crates/j2k-native/Cargo.toml index 1d277811..dccca6c0 100644 --- a/crates/j2k-native/Cargo.toml +++ b/crates/j2k-native/Cargo.toml @@ -28,9 +28,9 @@ fearless_simd = { workspace = true, optional = true } libm = { workspace = true } log = { workspace = true, optional = true } rayon = { workspace = true, optional = true } -j2k-profile = { path = "../j2k-profile", version = "=0.7.0", default-features = false } -j2k-codec-math = { path = "../j2k-codec-math", version = "=0.7.0" } -j2k-types = { path = "../j2k-types", version = "=0.7.0" } +j2k-profile = { path = "../j2k-profile", version = "=0.7.1", default-features = false } +j2k-codec-math = { path = "../j2k-codec-math", version = "=0.7.1" } +j2k-types = { path = "../j2k-types", version = "=0.7.1" } [dev-dependencies] criterion = { workspace = true } diff --git a/crates/j2k-native/src/j2c/bitplane/context.rs b/crates/j2k-native/src/j2c/bitplane/context.rs index e98dcbbe..6c970e33 100644 --- a/crates/j2k-native/src/j2c/bitplane/context.rs +++ b/crates/j2k-native/src/j2c/bitplane/context.rs @@ -2,83 +2,9 @@ use super::super::build::SubBandType; use super::state::{BitPlaneDecodeContext, HAS_MAGNITUDE_REFINEMENT_MASK}; - -/// See `context_label_sign_coding`. This table contains all context labels -/// for each combination of the bit-packed field. (0, 0) represent -/// impossible combinations. -#[rustfmt::skip] -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), -]; - -#[rustfmt::skip] -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, -]; - -#[rustfmt::skip] -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, -]; - -#[rustfmt::skip] -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, -]; +use j2k_codec_math::classic::{ + SIGN_CONTEXT_LOOKUP, ZERO_CTX_HH_LOOKUP, ZERO_CTX_HL_LOOKUP, ZERO_CTX_LL_LH_LOOKUP, +}; /// Based on Table D.2. #[expect( diff --git a/crates/j2k-native/src/j2c/decode.rs b/crates/j2k-native/src/j2c/decode.rs index c32e24ec..b4f9c74a 100644 --- a/crates/j2k-native/src/j2c/decode.rs +++ b/crates/j2k-native/src/j2c/decode.rs @@ -46,7 +46,7 @@ mod tier1; pub(crate) use self::allocation::DecodeAllocationBudget; use self::direct_plan::collect_classic_code_block_data; pub(crate) use self::direct_plan::{build_direct_color_plan, build_direct_grayscale_plan}; -use self::store::{apply_sign_shift, component_unsigned_level_shift, store}; +use self::store::{apply_sign_shift_after_mct, component_unsigned_level_shift, store}; use self::subband::{code_block_required_by_index, decode_component_tile_bit_planes}; #[cfg(all(test, feature = "parallel"))] use self::subband::{ @@ -138,7 +138,7 @@ pub(crate) fn decode<'a>( if tiles[0].mct { let stage_start = profile::profile_now(profile_enabled); mct::apply_inverse(tile_ctx, &tiles[0].component_infos, header, ht_decoder)?; - apply_sign_shift(tile_ctx, &header.component_infos); + apply_sign_shift_after_mct(tile_ctx, &header.component_infos); profile_timings.mct_us += profile::elapsed_us(stage_start); } diff --git a/crates/j2k-native/src/j2c/decode/store.rs b/crates/j2k-native/src/j2c/decode/store.rs index 610c3739..db79962f 100644 --- a/crates/j2k-native/src/j2c/decode/store.rs +++ b/crates/j2k-native/src/j2c/decode/store.rs @@ -5,12 +5,15 @@ use super::{ J2kStoreComponentJob, OutputRegion, ResolutionTile, Result, Tile, TileDecodeContext, }; -pub(super) fn apply_sign_shift( +pub(super) fn apply_sign_shift_after_mct( tile_ctx: &mut TileDecodeContext, component_infos: &[ComponentInfo], ) { - for (channel_data, component_info) in - tile_ctx.channel_data.iter_mut().zip(component_infos.iter()) + for (channel_data, component_info) in tile_ctx + .channel_data + .iter_mut() + .zip(component_infos) + .skip(3) { if let Some(samples) = channel_data.integer_container.as_mut() { let addend = component_unsigned_level_shift_i64(component_info); diff --git a/crates/j2k-native/src/j2c/mct.rs b/crates/j2k-native/src/j2c/mct.rs index ea9359da..94b029b1 100644 --- a/crates/j2k-native/src/j2c/mct.rs +++ b/crates/j2k-native/src/j2c/mct.rs @@ -38,11 +38,27 @@ pub(crate) fn apply_inverse( bail!(ColorError::Mct); } + let addends = [ + unsigned_level_shift(&component_infos[0]), + unsigned_level_shift(&component_infos[1]), + unsigned_level_shift(&component_infos[2]), + ]; + if s0.integer_container.is_some() || s1.integer_container.is_some() || s2.integer_container.is_some() { - return apply_inverse_i64(transform, s0, s1, s2); + return apply_inverse_i64( + transform, + s0, + s1, + s2, + [ + unsigned_level_shift_i64(&component_infos[0]), + unsigned_level_shift_i64(&component_infos[1]), + unsigned_level_shift_i64(&component_infos[2]), + ], + ); } let handled = if let Some(backend) = backend.as_deref_mut() { @@ -51,9 +67,9 @@ pub(crate) fn apply_inverse( plane0: &mut s0.container, plane1: &mut s1.container, plane2: &mut s2.container, - addend0: unsigned_level_shift(&component_infos[0]), - addend1: unsigned_level_shift(&component_infos[1]), - addend2: unsigned_level_shift(&component_infos[2]), + addend0: addends[0], + addend1: addends[1], + addend2: addends[2], })? } else { false @@ -65,6 +81,7 @@ pub(crate) fn apply_inverse( &mut s0.container, &mut s1.container, &mut s2.container, + addends, ); } @@ -76,6 +93,7 @@ fn apply_inverse_i64( s0: &mut super::ComponentData, s1: &mut super::ComponentData, s2: &mut super::ComponentData, + addends: [i64; 3], ) -> Result<()> { if transform != WaveletTransform::Reversible53 { bail!(ColorError::Mct); @@ -97,9 +115,9 @@ fn apply_inverse_i64( let src1 = *y1; let src2 = *y2; let green = src0 - floor_div_i64(src2 + src1, 4); - *y0 = src2 + green; - *y1 = green; - *y2 = src1 + green; + *y0 = src2 + green + addends[0]; + *y1 = green + addends[1]; + *y2 = src1 + green + addends[2]; } Ok(()) @@ -117,6 +135,14 @@ fn unsigned_level_shift(component_info: &ComponentInfo) -> f32 { } } +fn unsigned_level_shift_i64(component_info: &ComponentInfo) -> i64 { + if component_info.size_info.signed { + 0 + } else { + 1_i64 << (component_info.size_info.precision - 1) + } +} + #[expect( clippy::inline_always, reason = "this scalar primitive is intentionally inlined into the reversible color-transform hot loop" @@ -133,8 +159,14 @@ fn floor_div_i64(numerator: i64, denominator: i64) -> i64 { } } -fn apply_inner(transform: WaveletTransform, s0: &mut [f32], s1: &mut [f32], s2: &mut [f32]) { - dispatch!(Level::new(), simd => apply_inner_impl(simd, transform, s0, s1, s2)); +fn apply_inner( + transform: WaveletTransform, + s0: &mut [f32], + s1: &mut [f32], + s2: &mut [f32], + addends: [f32; 3], +) { + dispatch!(Level::new(), simd => apply_inner_impl(simd, transform, s0, s1, s2, addends)); } #[expect( @@ -148,10 +180,18 @@ fn apply_inner_impl( s0: &mut [f32], s1: &mut [f32], s2: &mut [f32], + addends: [f32; 3], ) { match transform { // Irreversible MCT, specified in G.3. WaveletTransform::Irreversible97 => { + let red_from_chroma = f32x8::splat(simd, mct::ICT_INV_R_CR); + let green_from_red_chroma = f32x8::splat(simd, mct::ICT_INV_G_CR); + let green_from_blue_chroma = f32x8::splat(simd, mct::ICT_INV_G_CB); + let blue_from_chroma = f32x8::splat(simd, mct::ICT_INV_B_CB); + let red_level = f32x8::splat(simd, addends[0]); + let green_level = f32x8::splat(simd, addends[1]); + let blue_level = f32x8::splat(simd, addends[2]); let mut s0_chunks = s0.chunks_exact_mut(8); let mut s1_chunks = s1.chunks_exact_mut(8); let mut s2_chunks = s2.chunks_exact_mut(8); @@ -164,12 +204,12 @@ fn apply_inner_impl( let y_1 = f32x8::from_slice(simd, y1); let y_2 = f32x8::from_slice(simd, y2); - let i0 = y_2.mul_add(f32x8::splat(simd, mct::ICT_INV_R_CR), y_0); + let i0 = y_2.mul_add(red_from_chroma, y_0) + red_level; let i1 = y_2.mul_add( - f32x8::splat(simd, mct::ICT_INV_G_CR), - y_1.mul_add(f32x8::splat(simd, mct::ICT_INV_G_CB), y_0), - ); - let i2 = y_1.mul_add(f32x8::splat(simd, mct::ICT_INV_B_CB), y_0); + green_from_red_chroma, + y_1.mul_add(green_from_blue_chroma, y_0), + ) + green_level; + let i2 = y_1.mul_add(blue_from_chroma, y_0) + blue_level; i0.store(y0); i1.store(y1); @@ -184,13 +224,17 @@ fn apply_inner_impl( let src0 = *y0; let src1 = *y1; let src2 = *y2; - *y0 = src0 + mct::ICT_INV_R_CR * src2; - *y1 = src0 + mct::ICT_INV_G_CB * src1 + mct::ICT_INV_G_CR * src2; - *y2 = src0 + mct::ICT_INV_B_CB * src1; + *y0 = src0 + mct::ICT_INV_R_CR * src2 + addends[0]; + *y1 = src0 + mct::ICT_INV_G_CB * src1 + mct::ICT_INV_G_CR * src2 + addends[1]; + *y2 = src0 + mct::ICT_INV_B_CB * src1 + addends[2]; } } // Reversible MCT, specified in G.2. WaveletTransform::Reversible53 => { + let quarter = f32x8::splat(simd, mct::RCT_QUARTER); + let red_level = f32x8::splat(simd, addends[0]); + let green_level = f32x8::splat(simd, addends[1]); + let blue_level = f32x8::splat(simd, addends[2]); let mut s0_chunks = s0.chunks_exact_mut(8); let mut s1_chunks = s1.chunks_exact_mut(8); let mut s2_chunks = s2.chunks_exact_mut(8); @@ -203,12 +247,12 @@ fn apply_inner_impl( let y_1 = f32x8::from_slice(simd, y1); let y_2 = f32x8::from_slice(simd, y2); - let i1 = y_0 - ((y_2 + y_1) * mct::RCT_QUARTER).floor(); - let i0 = y_2 + i1; - let i2 = y_1 + i1; + let i1 = y_0 - ((y_2 + y_1) * quarter).floor(); + let i0 = y_2 + i1 + red_level; + let i2 = y_1 + i1 + blue_level; i0.store(y0); - i1.store(y1); + (i1 + green_level).store(y1); i2.store(y2); } for ((y0, y1), y2) in s0_chunks @@ -221,10 +265,140 @@ fn apply_inner_impl( let src1 = *y1; let src2 = *y2; let i1 = src0 - floor_f32((src2 + src1) * mct::RCT_QUARTER); - *y0 = src2 + i1; - *y1 = i1; - *y2 = src1 + i1; + *y0 = src2 + i1 + addends[0]; + *y1 = i1 + addends[1]; + *y2 = src1 + i1 + addends[2]; } } } } + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{Duration, Instant}; + + #[test] + fn inverse_mct_applies_mixed_addends_to_simd_chunks_and_scalar_tail() { + let source0 = [-15.0, -8.0, -1.0, 0.0, 1.0, 7.0, 13.0, 21.0, 34.0]; + let source1 = [9.0, -7.0, 5.0, -3.0, 1.0, 2.0, -4.0, 6.0, -8.0]; + let source2 = [-6.0, 4.0, -2.0, 0.0, 2.0, -4.0, 6.0, -8.0, 10.0]; + let addends = [128.0, 0.0, 2048.0]; + + for transform in [ + WaveletTransform::Reversible53, + WaveletTransform::Irreversible97, + ] { + let mut plane0 = source0; + let mut plane1 = source1; + let mut plane2 = source2; + apply_inner(transform, &mut plane0, &mut plane1, &mut plane2, addends); + + for index in 0..source0.len() { + let (expected0, expected1, expected2) = match transform { + WaveletTransform::Reversible53 => { + let green = source0[index] + - floor_f32((source2[index] + source1[index]) * mct::RCT_QUARTER); + (source2[index] + green, green, source1[index] + green) + } + WaveletTransform::Irreversible97 => ( + source0[index] + mct::ICT_INV_R_CR * source2[index], + source0[index] + + mct::ICT_INV_G_CB * source1[index] + + mct::ICT_INV_G_CR * source2[index], + source0[index] + mct::ICT_INV_B_CB * source1[index], + ), + }; + let expected = [ + expected0 + addends[0], + expected1 + addends[1], + expected2 + addends[2], + ]; + let actual = [plane0[index], plane1[index], plane2[index]]; + if transform == WaveletTransform::Reversible53 { + assert_eq!(actual.map(f32::to_bits), expected.map(f32::to_bits)); + } else { + for (actual, expected) in actual.into_iter().zip(expected) { + assert!((actual - expected).abs() <= 0.000_25); + } + } + } + } + } + + #[test] + #[ignore = "performance guard harness; run explicitly with --ignored --nocapture"] + fn inverse_mct_shift_fusion_perf_guard() { + const LEN: usize = 512 * 512; + const SAMPLES: usize = 21; + let source0 = (0..LEN) + .map(|index| { + f32::from(u16::try_from(index % 251).expect("bounded test sample")) - 125.0 + }) + .collect::>(); + let source1 = (0..LEN) + .map(|index| f32::from(u16::try_from(index % 127).expect("bounded test sample")) - 63.0) + .collect::>(); + let source2 = (0..LEN) + .map(|index| f32::from(u16::try_from(index % 61).expect("bounded test sample")) - 30.0) + .collect::>(); + let mut plane0 = source0.clone(); + let mut plane1 = source1.clone(); + let mut plane2 = source2.clone(); + let addends = [128.0, 128.0, 128.0]; + let mut fused = Vec::with_capacity(SAMPLES); + let mut separate = Vec::with_capacity(SAMPLES); + + for _ in 0..SAMPLES { + plane0.copy_from_slice(&source0); + plane1.copy_from_slice(&source1); + plane2.copy_from_slice(&source2); + let started = Instant::now(); + apply_inner( + WaveletTransform::Irreversible97, + &mut plane0, + &mut plane1, + &mut plane2, + addends, + ); + std::hint::black_box((&plane0, &plane1, &plane2)); + fused.push(started.elapsed()); + + plane0.copy_from_slice(&source0); + plane1.copy_from_slice(&source1); + plane2.copy_from_slice(&source2); + let started = Instant::now(); + apply_inner( + WaveletTransform::Irreversible97, + &mut plane0, + &mut plane1, + &mut plane2, + [0.0; 3], + ); + for plane in [&mut plane0, &mut plane1, &mut plane2] { + for sample in plane { + *sample += 128.0; + } + } + std::hint::black_box((&plane0, &plane1, &plane2)); + separate.push(started.elapsed()); + } + + let fused = median(fused); + let separate = median(separate); + eprintln!( + "j2k_native_inverse_mct_shift_perf len={LEN} fused_us={} separate_us={}", + fused.as_micros(), + separate.as_micros() + ); + assert!( + fused.as_nanos().saturating_mul(100) <= separate.as_nanos().saturating_mul(95), + "fused inverse MCT/sign shift must improve its targeted median by at least 5%" + ); + } + + fn median(mut samples: Vec) -> Duration { + samples.sort_unstable(); + samples[samples.len() / 2] + } +} diff --git a/crates/j2k-native/src/j2c/mq.rs b/crates/j2k-native/src/j2c/mq.rs index 11493cb4..4ee348d0 100644 --- a/crates/j2k-native/src/j2c/mq.rs +++ b/crates/j2k-native/src/j2c/mq.rs @@ -2,73 +2,4 @@ //! Shared MQ arithmetic-coder probability table (ITU-T T.800 Table C.2). -/// QE table entry for the MQ coder. -#[derive(Debug, Clone, Copy)] -pub(crate) struct QeData { - /// Probability estimate. - pub(crate) qe: u32, - /// Next state after an MPS event. - pub(crate) nmps: u8, - /// Next state after an LPS event. - pub(crate) nlps: u8, - /// Whether an LPS event switches the MPS bit. - pub(crate) switch: bool, -} - -macro_rules! qe { - ($($qe:expr, $nmps:expr, $nlps:expr, $switch:expr),+ $(,)?) => { - [$(QeData { qe: $qe, nmps: $nmps, nlps: $nlps, switch: $switch }),+] - }; -} - -/// QE values and state transitions from Table C.2. -#[rustfmt::skip] -pub(crate) static QE_TABLE: [QeData; 47] = qe!( - 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, -); +pub(crate) use j2k_codec_math::classic::MQ_STATES as QE_TABLE; diff --git a/crates/j2k-test-support/Cargo.toml b/crates/j2k-test-support/Cargo.toml index 7d72af16..6d41a4a0 100644 --- a/crates/j2k-test-support/Cargo.toml +++ b/crates/j2k-test-support/Cargo.toml @@ -16,7 +16,7 @@ path = "src/lib.rs" j2k-native-fixtures = ["dep:j2k-native"] [dependencies] -j2k-native = { path = "../j2k-native", version = "=0.7.0", optional = true } +j2k-native = { path = "../j2k-native", version = "=0.7.1", optional = true } [lints.rust] unsafe_code = "forbid" diff --git a/crates/j2k-tilecodec/Cargo.toml b/crates/j2k-tilecodec/Cargo.toml index 018c9370..a38fb6f4 100644 --- a/crates/j2k-tilecodec/Cargo.toml +++ b/crates/j2k-tilecodec/Cargo.toml @@ -19,7 +19,7 @@ name = "j2k_tilecodec" path = "src/lib.rs" [dependencies] -j2k-core = { path = "../j2k-core", version = "=0.7.0" } +j2k-core = { path = "../j2k-core", version = "=0.7.1" } thiserror = { workspace = true } flate2 = { workspace = true } zstd = { workspace = true } diff --git a/crates/j2k-tilecodec/fuzz/Cargo.lock b/crates/j2k-tilecodec/fuzz/Cargo.lock index 013d9bac..74675abc 100644 --- a/crates/j2k-tilecodec/fuzz/Cargo.lock +++ b/crates/j2k-tilecodec/fuzz/Cargo.lock @@ -72,14 +72,14 @@ dependencies = [ [[package]] name = "j2k-core" -version = "0.7.0" +version = "0.7.1" dependencies = [ "thiserror", ] [[package]] name = "j2k-tilecodec" -version = "0.7.0" +version = "0.7.1" dependencies = [ "flate2", "j2k-core", diff --git a/crates/j2k-transcode-cuda/Cargo.toml b/crates/j2k-transcode-cuda/Cargo.toml index 0ef1109f..ffc1145a 100644 --- a/crates/j2k-transcode-cuda/Cargo.toml +++ b/crates/j2k-transcode-cuda/Cargo.toml @@ -29,10 +29,10 @@ cuda-runtime = [ cuda-profiling = ["cuda-runtime", "j2k-cuda-runtime/cuda-profiling"] [dependencies] -j2k-core = { path = "../j2k-core", version = "=0.7.0" } -j2k-transcode = { path = "../j2k-transcode", version = "=0.7.0" } -j2k-cuda-runtime = { path = "../j2k-cuda-runtime", version = "=0.7.0", optional = true } -j2k-native = { path = "../j2k-native", version = "=0.7.0" } +j2k-core = { path = "../j2k-core", version = "=0.7.1" } +j2k-transcode = { path = "../j2k-transcode", version = "=0.7.1" } +j2k-cuda-runtime = { path = "../j2k-cuda-runtime", version = "=0.7.1", optional = true } +j2k-native = { path = "../j2k-native", version = "=0.7.1" } [dev-dependencies] j2k-test-support = { path = "../j2k-test-support" } diff --git a/crates/j2k-transcode-metal/Cargo.toml b/crates/j2k-transcode-metal/Cargo.toml index 53e44c01..374c3496 100644 --- a/crates/j2k-transcode-metal/Cargo.toml +++ b/crates/j2k-transcode-metal/Cargo.toml @@ -26,21 +26,21 @@ default = [] bench-internals = ["j2k-transcode/dev-support"] [dependencies] -j2k-codec-math = { path = "../j2k-codec-math", version = "=0.7.0" } -j2k-core = { path = "../j2k-core", version = "=0.7.0" } -j2k-metal-support = { path = "../j2k-metal-support", version = "=0.7.0" } -j2k-transcode = { path = "../j2k-transcode", 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-metal-support = { path = "../j2k-metal-support", version = "=0.7.1" } +j2k-transcode = { path = "../j2k-transcode", version = "=0.7.1" } [target.'cfg(target_os = "macos")'.dependencies] -j2k-metal = { path = "../j2k-metal", version = "=0.7.0" } +j2k-metal = { path = "../j2k-metal", version = "=0.7.1" } metal = { workspace = true } [dev-dependencies] criterion = { workspace = true } rayon = { workspace = true } -j2k-native = { path = "../j2k-native", version = "=0.7.0" } -j2k-jpeg = { path = "../j2k-jpeg", version = "=0.7.0" } -j2k-profile = { path = "../j2k-profile", version = "=0.7.0" } +j2k-native = { path = "../j2k-native", version = "=0.7.1" } +j2k-jpeg = { path = "../j2k-jpeg", version = "=0.7.1" } +j2k-profile = { path = "../j2k-profile", version = "=0.7.1" } j2k-test-support = { path = "../j2k-test-support" } j2k-transcode-test-support = { path = "../j2k-transcode-test-support" } diff --git a/crates/j2k-transcode-metal/tests/dct53.rs b/crates/j2k-transcode-metal/tests/dct53.rs index 6091ff01..d41d36fe 100644 --- a/crates/j2k-transcode-metal/tests/dct53.rs +++ b/crates/j2k-transcode-metal/tests/dct53.rs @@ -180,7 +180,17 @@ fn explicit_metal_reversible_dct53_matches_rayon_for_structured_cases() { let mut expected_accelerator = RayonReversibleDwt53Accelerator::default(); let mut accelerator = MetalDctToWaveletStageAccelerator::new_explicit(); - for (width, height) in [(8, 8), (13, 11), (16, 16)] { + for (width, height) in [ + (1, 1), + (1, 2), + (2, 1), + (2, 2), + (8, 8), + (13, 11), + (13, 12), + (12, 11), + (16, 16), + ] { let job = DctGridToReversibleDwt53Job { dequantized_blocks: &blocks, block_cols: 2, @@ -205,7 +215,7 @@ fn explicit_metal_reversible_dct53_matches_rayon_for_structured_cases() { assert_reversible_eq(&actual, &expected, width, height); } - assert_eq!(accelerator.reversible_dwt53_dispatches(), 3); + assert_eq!(accelerator.reversible_dwt53_dispatches(), 9); } #[cfg(target_os = "macos")] @@ -223,7 +233,7 @@ fn explicit_metal_reversible_dct53_batch_matches_rayon_for_structured_cases() { ]; let mut accelerator = MetalDctToWaveletStageAccelerator::new_explicit(); - for (width, height) in [(8, 8), (13, 11), (16, 16)] { + for (width, height) in [(8, 8), (13, 11), (13, 12), (12, 11), (16, 16)] { let jobs: Vec<_> = batch_blocks .iter() .map(|blocks| DctGridToReversibleDwt53Job { @@ -260,7 +270,7 @@ fn explicit_metal_reversible_dct53_batch_matches_rayon_for_structured_cases() { } } - assert_eq!(accelerator.reversible_dwt53_batch_dispatches(), 3); + assert_eq!(accelerator.reversible_dwt53_batch_dispatches(), 5); } #[test] diff --git a/crates/j2k-transcode-test-support/Cargo.toml b/crates/j2k-transcode-test-support/Cargo.toml index 5e7d9b78..a9d580ac 100644 --- a/crates/j2k-transcode-test-support/Cargo.toml +++ b/crates/j2k-transcode-test-support/Cargo.toml @@ -13,11 +13,11 @@ name = "j2k_transcode_test_support" path = "src/lib.rs" [dependencies] -j2k-transcode = { path = "../j2k-transcode", version = "=0.7.0", features = ["dev-support"] } -j2k-types = { path = "../j2k-types", version = "=0.7.0" } +j2k-transcode = { path = "../j2k-transcode", version = "=0.7.1", features = ["dev-support"] } +j2k-types = { path = "../j2k-types", version = "=0.7.1" } [dev-dependencies] -j2k-native = { path = "../j2k-native", version = "=0.7.0" } +j2k-native = { path = "../j2k-native", version = "=0.7.1" } [lints.rust] unsafe_code = "forbid" diff --git a/crates/j2k-transcode/Cargo.toml b/crates/j2k-transcode/Cargo.toml index ab68749a..3bbb15c3 100644 --- a/crates/j2k-transcode/Cargo.toml +++ b/crates/j2k-transcode/Cargo.toml @@ -22,12 +22,12 @@ dev-support = [] [dependencies] rayon = { workspace = true } -j2k-core = { path = "../j2k-core", version = "=0.7.0" } -j2k-codec-math = { path = "../j2k-codec-math", version = "=0.7.0" } -j2k-jpeg = { path = "../j2k-jpeg", version = "=0.7.0" } -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", default-features = false } +j2k-core = { path = "../j2k-core", version = "=0.7.1" } +j2k-codec-math = { path = "../j2k-codec-math", version = "=0.7.1" } +j2k-jpeg = { path = "../j2k-jpeg", version = "=0.7.1" } +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", default-features = false } [dev-dependencies] criterion = { workspace = true } diff --git a/crates/j2k-transcode/fuzz/Cargo.lock b/crates/j2k-transcode/fuzz/Cargo.lock index 1387a47c..c917e416 100644 --- a/crates/j2k-transcode/fuzz/Cargo.lock +++ b/crates/j2k-transcode/fuzz/Cargo.lock @@ -83,7 +83,7 @@ dependencies = [ [[package]] name = "j2k" -version = "0.7.0" +version = "0.7.1" dependencies = [ "j2k-codec-math", "j2k-core", @@ -94,18 +94,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", @@ -117,7 +117,7 @@ dependencies = [ [[package]] name = "j2k-native" -version = "0.7.0" +version = "0.7.1" dependencies = [ "fearless_simd", "j2k-codec-math", @@ -129,11 +129,11 @@ dependencies = [ [[package]] name = "j2k-profile" -version = "0.7.0" +version = "0.7.1" [[package]] name = "j2k-transcode" -version = "0.7.0" +version = "0.7.1" dependencies = [ "j2k", "j2k-codec-math", @@ -155,7 +155,7 @@ dependencies = [ [[package]] name = "j2k-types" -version = "0.7.0" +version = "0.7.1" [[package]] name = "jobserver" diff --git a/crates/j2k-transcode/src/dct97_2d.rs b/crates/j2k-transcode/src/dct97_2d.rs index efc25cc4..37f81933 100644 --- a/crates/j2k-transcode/src/dct97_2d.rs +++ b/crates/j2k-transcode/src/dct97_2d.rs @@ -49,7 +49,7 @@ impl Dwt97TwoDimensional { /// Scratch storage for repeated DCT-grid to 9/7 transform calls. #[derive(Debug, Default)] -pub(crate) struct Dct97GridScratch { +pub struct Dct97GridScratch { geometry: Option<(usize, usize)>, spatial_samples: Vec, plane: Dwt97PlaneScratch, @@ -103,7 +103,7 @@ pub fn dct8x8_blocks_then_dwt97_float( /// Reference 9/7 path with caller-owned spatial-sample scratch: /// DCT coefficients -> float IDCT samples -> separable linearized 9/7. -pub(crate) fn dct8x8_blocks_then_dwt97_float_with_scratch( +pub fn dct8x8_blocks_then_dwt97_float_with_scratch( blocks: &[[[f64; 8]; 8]], block_cols: usize, block_rows: usize, diff --git a/crates/j2k-transcode/src/lib.rs b/crates/j2k-transcode/src/lib.rs index 41d92697..f190d3df 100644 --- a/crates/j2k-transcode/src/lib.rs +++ b/crates/j2k-transcode/src/lib.rs @@ -191,6 +191,7 @@ pub struct Dwt97TwoDimensional { pub use dct53_2d::{dct8x8_blocks_then_dwt53_float, dct8x8_blocks_to_dwt53_float_linear}; pub use dct97_2d::dct8x8_blocks_then_dwt97_float; +pub use dct97_2d::{dct8x8_blocks_then_dwt97_float_with_scratch, Dct97GridScratch}; pub use dct_grid::{DctGridError, DctTransformError}; pub use htj2k97_codeblock_error::{Htj2k97CodeBlockAxis, Htj2k97CodeBlockOptionsError}; pub use htj2k97_codeblock_oracle::{ diff --git a/crates/j2k/Cargo.toml b/crates/j2k/Cargo.toml index bcaa590b..1b521b46 100644 --- a/crates/j2k/Cargo.toml +++ b/crates/j2k/Cargo.toml @@ -19,16 +19,16 @@ name = "j2k" 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-native = { path = "../j2k-native", version = "=0.7.0" } -j2k-types = { path = "../j2k-types", 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-native = { path = "../j2k-native", version = "=0.7.1" } +j2k-types = { path = "../j2k-types", version = "=0.7.1" } thiserror = { workspace = true } [dev-dependencies] proptest = { workspace = true } criterion = { workspace = true } -j2k-native = { path = "../j2k-native", version = "=0.7.0" } +j2k-native = { path = "../j2k-native", version = "=0.7.1" } j2k-test-support = { path = "../j2k-test-support" } [[bench]] diff --git a/crates/j2k/fuzz/Cargo.lock b/crates/j2k/fuzz/Cargo.lock index 3aaa46f5..6c8ab91b 100644 --- a/crates/j2k/fuzz/Cargo.lock +++ b/crates/j2k/fuzz/Cargo.lock @@ -83,7 +83,7 @@ dependencies = [ [[package]] name = "j2k" -version = "0.7.0" +version = "0.7.1" dependencies = [ "j2k-codec-math", "j2k-core", @@ -94,11 +94,11 @@ 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", ] @@ -113,7 +113,7 @@ dependencies = [ [[package]] name = "j2k-native" -version = "0.7.0" +version = "0.7.1" dependencies = [ "fearless_simd", "j2k-codec-math", @@ -125,11 +125,11 @@ dependencies = [ [[package]] name = "j2k-profile" -version = "0.7.0" +version = "0.7.1" [[package]] name = "j2k-types" -version = "0.7.0" +version = "0.7.1" [[package]] name = "jobserver" diff --git a/deny.toml b/deny.toml index 8c6878aa..07299990 100644 --- a/deny.toml +++ b/deny.toml @@ -13,6 +13,15 @@ ignore = [ # - https://rustsec.org/advisories/RUSTSEC-2024-0436.html # - https://github.com/gfx-rs/metal-rs/issues/349 "RUSTSEC-2024-0436", + # Transitive via burn-core 0.21. RUSTSEC-2025-0141 records that bincode is + # unmaintained; it does not report a memory-safety or code-execution flaw, + # and the advisory has no safe upgrade. Re-evaluate when Burn changes its + # serialization dependency. + # Review-by: 2027-01-31. + # Sources: + # - https://rustsec.org/advisories/RUSTSEC-2025-0141.html + # - https://github.com/tracel-ai/burn/tree/v0.21.0/crates/burn-core + "RUSTSEC-2025-0141", ] [licenses] @@ -26,6 +35,14 @@ allow = [ "Zlib", ] confidence-threshold = 0.93 +exceptions = [ + # Version-scoped transitive exceptions from Burn 0.21. Keeping these + # narrow prevents the licenses from becoming workspace-wide defaults. + { crate = "colored@3.1.1", allow = ["MPL-2.0"] }, + { crate = "hexf-parse@0.2.1", allow = ["CC0-1.0"] }, + { crate = "option-ext@0.2.0", allow = ["MPL-2.0"] }, + { crate = "xxhash-rust@0.8.16", allow = ["BSL-1.0"] }, +] [bans] multiple-versions = "warn" diff --git a/docs/architecture.md b/docs/architecture.md index 3ab54f88..9a7ca550 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -24,6 +24,7 @@ still-image correctness. Keep row-level status synchronized with | `j2k-cuda-runtime` | CUDA engine | CUDA Driver API integration, J2K-owned kernel modules, launch orchestration, and CUDA memory helpers shared by CUDA adapters. | | `j2k-jpeg-cuda`, `j2k-cuda`, `j2k-transcode-cuda` | CUDA adapter | Codec-facing CUDA APIs, route policy, and CUDA device memory integration for supported paths. | | `j2k-jpeg-metal`, `j2k-metal`, `j2k-transcode-metal` | Metal adapter | macOS Metal runtime integration for supported paths. | +| `j2k-ml` | experimental integration | Independent Burn tensor decode integration; unpublished during the 0.7 cycle. | | `j2k-transcode` | transcode | JPEG-to-HTJ2K coefficient-domain transcode algorithms and shared contracts. | | `j2k-cli` | CLI | Command-line inspection and JPEG-to-HTJ2K smoke transcode entry point. | | `j2k-test-support`, `j2k-transcode-test-support` | dev helper | Shared fixture, benchmark input, and transcode oracle helpers for tests, benches, and examples. | @@ -58,6 +59,7 @@ j2k-compare -> j2k-core, j2k, j2k-native, j2k-test-support j2k-transcode -> j2k-codec-math, j2k-core, j2k, j2k-native, j2k-jpeg, j2k-profile j2k-metal-support -> j2k-core j2k-cuda-runtime -> j2k-codec-math, j2k-core +j2k-ml -> j2k, j2k-cuda, j2k-cuda-runtime, j2k-metal j2k-transcode-metal -> j2k-codec-math, j2k-core, j2k-metal, j2k-metal-support, j2k-transcode j2k-transcode-cuda -> j2k-core, j2k-cuda-runtime, j2k-native, j2k-transcode j2k-cli -> j2k, j2k-jpeg, j2k-transcode diff --git a/docs/benchmark-evidence.md b/docs/benchmark-evidence.md index ec855686..d5a2639e 100644 --- a/docs/benchmark-evidence.md +++ b/docs/benchmark-evidence.md @@ -240,6 +240,96 @@ cargo test -p j2k-cuda-runtime --lib \ The validation host also needed its linker search path to include the local LLVM/libffi runtime used by cuda-oxide. +## Metal Irreversible 9/7 Heatmap Experiment - 2026-07-13 + +This local experiment used Apple's GPU performance heat maps to test whether +the staged irreversible 9/7 IDWT should be replaced by full-axis +threadgroup-memory kernels. The prototype was rejected and removed because it +failed the targeted performance gate. The capture and dispersion harness at +`c35b7d43` remains as permanent diagnostic infrastructure. + +Environment: + +- Machine: MacBook Pro `Mac16,8`, Apple M4 Pro, 48 GB +- OS: macOS 26.5 build `25F71` +- Xcode: 26.6 build `17F113` +- Metal toolchain: `com.apple.dt.toolchain.Metal.32023.883`, component build + `17F109` +- Baseline and retained source: `c35b7d43` +- Profile: Cargo `release-bench`, isolated baseline/candidate target + directories, with no concurrent Cargo, CUDA, Metal, Criterion, or heavy + build workload + +The captured 1023 x 767 staged transform exposed the runtime-generated Metal +source in Xcode. Profile GPU Trace reported 214.21 us total GPU time, one +command buffer, one compute encoder, 11 dispatches, and 12.01 MiB of buffers. +The four horizontal lifting dispatches accounted for 37.02% of GPU time, the +four vertical lifting dispatches 36.43%, vertical scale 9.12%, horizontal +scale 8.87%, and interleave 8.55%. The scale and lifting passes therefore +accounted for 91.44% of captured GPU time. + +The trace reported 73.87% Occupancy Manager target, an 87.12% Compute Shader +Launch limiter, and a 32.09% Last Level Cache limiter with 31.94% utilization. +The staged pipelines used 12-14 allocated registers and spilled zero bytes. +Those counters supported prototyping dispatch fusion; they did not establish +that fusion would be faster. + +The prototype used one threadgroup per row or column, dynamic threadgroup +memory, and separate horizontal and vertical kernels. Forced staged/fused +tests matched within the existing irreversible tolerance across degenerate, +odd/even, origin-parity, and 1023 x 767 cases. A pipeline-relative sweep then +tested 1, 2, 4, 8, 16, and 32 SIMD groups per threadgroup: + +| SIMD groups | Threads on M4 Pro | Exploratory median | IQR | +| ---: | ---: | ---: | ---: | +| 1 | 32 | 1.658 ms | 0.227 ms | +| 2 | 64 | 1.563 ms | 0.360 ms | +| 4 | 128 | 1.437 ms | 0.298 ms | +| 8 | 256 | 1.667 ms | 0.256 ms | +| 16 | 512 | 1.686 ms | 0.207 ms | +| 32 | 1024 | 1.745 ms | 0.701 ms | + +Because the 128-thread exploratory row appeared promising, it was remeasured +in five alternating baseline/candidate processes with eleven warmed samples +per process. Each cell reports process median / IQR: + +| Process | Staged baseline | 128-thread fused prototype | +| ---: | ---: | ---: | +| 1 | 1.422 / 0.381 ms | 1.641 / 0.300 ms | +| 2 | 1.467 / 0.262 ms | 1.649 / 0.557 ms | +| 3 | 1.629 / 0.261 ms | 1.677 / 0.288 ms | +| 4 | 1.383 / 0.354 ms | 1.459 / 0.378 ms | +| 5 | 1.324 / 0.410 ms | 1.646 / 0.373 ms | + +The median of process medians regressed from 1.422 ms to 1.646 ms (+15.75%). +The initial maximum-thread configuration also regressed, from 1.550 ms to +1.936 ms (+24.90%). Both miss the required 5% improvement by a wide margin. +The kernel prototype and its temporary routing/tests were therefore removed. +The end-to-end 512/1024 decode rows were not run because the targeted gate had +already required rejection; no routing or performance claim changed. + +Changed-line coverage could not be reported. The canonical command +`cargo xtask coverage metal --base HEAD^` failed before compiling coverage +tests because the repository's Metal lane supplied `cargo-llvm-cov 0.8.7` +with mutually incompatible `--no-report` and `--no-clean` options. The manual +capture body is also intentionally skipped when coverage instrumentation is +active so a coverage run cannot create a GPU trace. Package tests and strict +Clippy passed, but this experiment therefore has no changed-line percentage. + +The permanent guard can be rerun with: + +```bash +J2K_REQUIRE_METAL_RUNTIME=1 CARGO_TARGET_DIR= \ + cargo test -p j2k-metal metal_irreversible_idwt_perf_guard \ + --profile release-bench --locked -- \ + --ignored --nocapture --test-threads=1 +``` + +The ignored GPU-capture test additionally requires `MTL_CAPTURE_ENABLED=1` +and an absolute, nonexistent `.gputrace` path in +`J2K_METAL_CAPTURE_PATH`. Captured timings are diagnostic only and are not +used for acceptance. + ## Metal Status Metal acceleration is selective. Public claims should say Metal-accelerated diff --git a/docs/cuda-jpeg2000-rust/index.html b/docs/cuda-jpeg2000-rust/index.html index b5984a75..be1c2c38 100644 --- a/docs/cuda-jpeg2000-rust/index.html +++ b/docs/cuda-jpeg2000-rust/index.html @@ -54,7 +54,7 @@

CUDA JPEG2000 Rust

j2k-cuda CUDA API docs -

Release status: j2k 0.7.0 is published on crates.io and docs.rs.

+

Release status: j2k 0.7.1 is published on crates.io and docs.rs.

CUDA JPEG2000 Rust pipeline diff --git a/docs/digital-pathology-workflow-audit.md b/docs/digital-pathology-workflow-audit.md new file mode 100644 index 00000000..0b453d42 --- /dev/null +++ b/docs/digital-pathology-workflow-audit.md @@ -0,0 +1,145 @@ +# Digital Pathology and Large-TIFF Workflow Audit + +Audit date: 2026-07-14 + +Audited workspace: `0.7.1` release candidate + +## Decision + +J2K is suitable as the compressed tile and frame codec layer in a digital +pathology viewer, converter, or ingestion pipeline. It is not an end-to-end +OME-TIFF, vendor WSI, BigTIFF, or DICOM Whole Slide Imaging implementation. + +Adopt it behind a container and slide-index layer that owns file parsing, +range reads, pyramid and tile coordinates, DICOM encapsulation, photometric +metadata, ICC application, caching, and output assembly. Do not hand an entire +slide file to the codec or decode an entire level when only a viewport is +needed. + +## Evidence in this workspace + +| Concern | Current evidence | Audit result | +| --- | --- | --- | +| JPEG 2000 and HTJ2K payloads | `j2k` accepts raw codestreams and JP2/JPH still-image wrappers. `J2kView` borrows input bytes, and `J2kDecoder` supports full, ROI, scaled, region-scaled, row, and native component-plane decode. | Ready at the codec boundary. | +| Repeated tile decode | `TileBatchDecode`, reusable decoder contexts and scratch pools, caller-owned outputs, indexed tile errors, and CPU worker limits are public. | Ready when the caller bounds queue depth and total live memory. | +| GPU decode | CUDA and Metal adapters expose strict device requests, reusable sessions, resident surfaces, and bounded scheduling. CPU remains the correctness fallback. | Metal is locally parity-tested. CUDA still requires Linux plus NVIDIA hardware validation. | +| Allocation safety | Codec parsing, output, rows, batches, and device paths use checked accounting. The default per-operation host allocation ceiling is 512 MiB. | Useful safety boundary, but not a slide-wide cache or service memory budget. | +| TIFF-style tile codecs | `j2k-tilecodec` exposes uncompressed, LZW, deflate, and Zstd tile decompression. `j2k-jpeg` prepares TIFF/WSI JPEG segments, including shared JPEG tables. | Partial payload support only. TIFF predictors, planar assembly, tags, IFDs, and pyramids remain caller responsibilities. | +| TIFF and OME containers | No production BigTIFF/IFD/SubIFD parser, 64-bit tile-offset index, OME-XML model, or vendor WSI metadata layer is exposed. | Missing outer integration. | +| DICOM WSI | Raw JPEG 2000-family codestreams and codec-profile passthrough checks exist. No production DICOM dataset parser, transfer-syntax registry, frame-fragment index, offset-table handler, functional-group parser, or slide-coordinate model is exposed. | Missing outer integration. | +| Color | JPEG 2000 component metadata and JP2/JPH color boxes, including ICC payloads, are parsed. Native component-plane output is available on CPU. | Container photometric interpretation and ICC transforms must be validated and applied outside the codec. | +| Domain validation | WSI and DICOM tile corpora are documented, with external-corpus environment controls. | A release-blocking container-level golden corpus is still needed. | + +Relevant code and policy: + +- [`crates/j2k/src/view.rs`](../crates/j2k/src/view.rs) +- [`crates/j2k-core/src/batch.rs`](../crates/j2k-core/src/batch.rs) +- [`crates/j2k-core/src/buffer.rs`](../crates/j2k-core/src/buffer.rs) +- [`crates/j2k-core/src/passthrough.rs`](../crates/j2k-core/src/passthrough.rs) +- [`crates/j2k-jpeg/src/segment.rs`](../crates/j2k-jpeg/src/segment.rs) +- [`crates/j2k-jpeg/src/batch_session.rs`](../crates/j2k-jpeg/src/batch_session.rs) +- [`crates/j2k-tilecodec/src/lib.rs`](../crates/j2k-tilecodec/src/lib.rs) +- [`docs/benchmark-corpora.md`](benchmark-corpora.md) + +## Required production workflow + +1. Parse the outer container with checked 64-bit arithmetic. Build an index of + levels, tiles or frames, byte ranges, dimensions, physical coordinates, + photometric interpretation, and color profiles. Validate every offset and + byte count against the source length before reading. +2. Select a stored pyramid level for the requested magnification, then compute + the intersecting tiles. Use codec downscaling only for residual scaling + within that selected level; it is not a substitute for pyramid selection. +3. Range-read only the selected compressed payloads. Keep reads, compressed + inputs, decoded outputs, GPU residency, and cache entries under one + request-level memory and cancellation budget. +4. Normalize each payload at the container boundary: + - For TIFF, honor compression, predictor, planar configuration, + photometric interpretation, edge-tile geometry, and shared JPEG tables. + - For DICOM, map the exact transfer syntax UID, resolve the frame index, + assemble all fragments belonging to a classic JPEG 2000 frame, remove + encapsulation padding correctly, and pass a raw codestream without a JP2 + header. HTJ2K frames must follow their single-fragment rule. +5. Inspect the compressed payload before decode. Cross-check dimensions, + components, bit depth, signedness, lossless/lossy profile, and color meaning + against the container metadata. Reject contradictions rather than guessing. +6. Submit a bounded tile batch through a reusable CPU, CUDA, or Metal session. + Keep `BackendRequest::Auto` conservative, preserve CPU parity as the + correctness oracle, and retain device surfaces through composition when + that avoids unnecessary readback. +7. Assemble edge and sparse tiles in slide coordinates. Apply the outer + container's ICC profile and photometric rules, including optical-path color + semantics for DICOM. Missing tiles, overlaps, and padded pixels are + container concerns and must not be inferred by the codec. +8. Return or cache only the requested viewport or analysis region. Apply + backpressure across requests and avoid a decoded full-slide cache. + +## Format-specific correctness conditions + +### OME-TIFF and large TIFF + +OME-TIFF can use standard TIFF or BigTIFF, places OME-XML in the first IFD's +`ImageDescription`, and represents pyramids with SubIFDs. Reduced-resolution +levels are not ordinary entries in the primary IFD chain. Large deployments +therefore need a BigTIFF-capable 64-bit index and explicit SubIFD traversal; +sequentially walking the primary IFD chain is not a correct pyramid reader. + +Each level may use a different compression. Route each tile by its level's +compression and tags. For a JPEG 2000 tile, pass only the compatible compressed +payload to `j2k`; do not assume that every TIFF tile is JPEG 2000 because one +level is. Existing TIFF/WSI JPEG preparation in `j2k-jpeg` is complementary, +not a TIFF parser. + +### DICOM WSI + +DICOM WSI stores a pyramid level as a multi-frame image whose frames are +tiles. `TILED_FULL` positions are implicit; `TILED_SPARSE` positions are +explicit. Edge padding, focal planes, optical paths, overlap, total pixel +matrix dimensions, origin, and orientation belong to DICOM metadata and must +survive decode and assembly. + +Each frame is independently JPEG 2000-family encoded and carries no JP2 +header. A classic JPEG 2000 frame may span multiple fragments, while an HTJ2K +frame is constrained to one fragment. The Basic Offset Table can be empty, and +large instances may require extended offsets. The outer DICOM layer must +resolve frame boundaries before invoking this workspace. + +The codec-level `CompressedTransferSyntax` intentionally describes profiles, +not DICOM UIDs. The DICOM integration must preserve the exact UID and its +lossless/progressive semantics rather than reconstructing it from the codec +enum after the fact. + +## Release blockers for an end-to-end pathology claim + +These are not blockers for publishing a codec-focused `0.7.1`, but they are +blockers for claiming that this repository alone implements a complete digital +pathology workflow: + +1. Add or integrate a production TIFF/BigTIFF parser with checked IFD, + SubIFD, tile offset, tile byte-count, predictor, planar, and OME-XML + handling. +2. Add or integrate a production DICOM WSI index with Basic and Extended + Offset Table support, classic multi-fragment frame assembly, `TILED_FULL` + and `TILED_SPARSE` coordinates, optical paths, and ICC metadata. +3. Define one bounded viewport scheduler that accounts for file reads, + compressed bytes, host outputs, GPU surfaces, cache occupancy, cancellation, + and concurrent requests. +4. Add PHI-safe golden integration fixtures covering classic TIFF, BigTIFF, + OME-TIFF SubIFD pyramids, representative vendor WSI files, and DICOM WSI. + Include malformed offsets, truncated fragments, sparse frames, edge tiles, + mixed level compression, signed and high-bit-depth samples, and ICC cases. +5. Compare decoded pixels and component metadata across CPU, Metal, and CUDA. + Run CUDA validation on supported NVIDIA hardware; macOS cannot establish + CUDA runtime parity. +6. Measure peak resident host and device memory while panning and zooming a + large slide under concurrent load. A throughput benchmark without bounded + memory and cancellation evidence is insufficient. + +## Primary specifications used for this audit + +- [OME-TIFF specification 6.2.2](https://docs.openmicroscopy.org/ome-model/6.2.2/ome-tiff/specification.html) +- [DICOM PS3.5: JPEG 2000 and HTJ2K transfer syntaxes](https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_A.4.4.html) +- [DICOM PS3.5: encapsulated pixel data](https://dicom.nema.org/medical/Dicom/current/output/chtml/part05/sect_A.4.html) +- [DICOM Whole Slide Imaging overview](https://dicom.nema.org/dicom/dicomwsi/index.html) +- [DICOM PS3.3: microscope tile organization](https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_c.8.12.14.html) +- [DICOM PS3.3: multi-frame dimensions](https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.17.html) diff --git a/docs/env-vars.md b/docs/env-vars.md index 59bd72a9..a75380b9 100644 --- a/docs/env-vars.md +++ b/docs/env-vars.md @@ -166,6 +166,7 @@ override it. | `J2K_CUDA_DECODE_BATCH_SIZES` | Comma-separated CUDA J2K decode mixed-external batch sizes. | Harness default | Benchmark | | `J2K_CUDA_DECODE_CASE_BATCH_SIZES` | Comma-separated CUDA J2K decode per-fixture batch sizes. Keep this bounded for external adoption runs; use `J2K_CUDA_DECODE_BATCH_SIZES` for huge mixed batches. | Harness default | Benchmark | | `J2K_CUDA_DECODE_SAMPLE_SIZE` | Criterion sample size for CUDA J2K decode benchmark rows. Must be at least 10. | 10 | Benchmark | +| `J2K_CUDA_DECODE_MEASUREMENT_SECONDS` | Positive measurement duration in seconds for each CUDA J2K decode Criterion row. | 1 second | Benchmark | | `J2K_CUDA_DECODE_INPUT_DIRS` | Optional path-list of external HTJ2K `.j2k`, `.j2c`, `.jp2`, `.jph`, or `.jhc` fixtures included in the CUDA decode Criterion benchmark. | Not set | Benchmark | | `J2K_CUDA_DECODE_MANIFEST` | Optional TSV manifest for CUDA decode external fixtures. Uses the same pinned `path`, `input_fnv1a64`, `codec`, and `container` fields as `J2K_FIXTURE_COMPARE_MANIFEST`. | Not set | Benchmark | | `J2K_CUDA_DECODE_INCLUDE_GENERATED` | Set to `0`, `false`, `no`, or `off` to omit generated CUDA decode fixtures when external fixtures are provided. | Generated CUDA decode fixtures included | Benchmark | @@ -176,6 +177,7 @@ override it. | `J2K_METAL_DECODE_INPUT_DIRS` | Optional path-list of external `.j2k`, `.j2c`, `.jp2`, `.jph`, or `.jhc` fixtures included in the Metal decode benchmark. Wrapper containers are emitted as structured skips until wrapper-specific strict Metal parity is claimed. | Not set | Benchmark | | `J2K_METAL_DECODE_MANIFEST` | Optional TSV manifest for Metal decode external fixtures. Uses pinned `path` and `input_fnv1a64`; optional `codec` and `container` labels are recorded in benchmark rows. | Not set | Benchmark | | `J2K_METAL_DECODE_INCLUDE_GENERATED` | Set to `0`, `false`, `no`, or `off` to omit generated Metal decode smoke fixtures when external fixtures are provided. Publication gates require this to be false for Metal decode speed claims. | Generated Metal decode fixtures included | Benchmark | +| `J2K_METAL_CAPTURE_PATH` | Absolute, nonexistent `.gputrace` output path for the ignored Metal GPU-capture diagnostic. Requires `MTL_CAPTURE_ENABLED=1`; captured timings are diagnostic only. | Not set | Benchmark | | `J2K_METAL_ENCODE_INPUT_DIRS` | Optional path-list of staged external `.pgm`, `.ppm`, or `.pnm` source images included in the Metal auto-routing encode benchmark. Use the same canonical PNM source assets as `J2K_ENCODE_COMPARE_INPUT_DIRS` after staging. | Not set | Benchmark | | `J2K_METAL_ENCODE_MANIFEST` | Optional TSV manifest for Metal encode staged PNM sources. Uses `path` and pinned `input_fnv1a64` from `J2K_ENCODE_COMPARE_MANIFEST`. | Not set | Benchmark | | `J2K_METAL_ENCODE_INCLUDE_GENERATED` | Set to `0`, `false`, `no`, or `off` to omit generated Metal host-input auto-routing rows when external staged PNM sources are provided. Stage microbenchmarks remain generated component rows. | Generated Metal host-input rows included | Benchmark | diff --git a/docs/gpu-jpeg2000-rust/index.html b/docs/gpu-jpeg2000-rust/index.html index d469f357..bb094b33 100644 --- a/docs/gpu-jpeg2000-rust/index.html +++ b/docs/gpu-jpeg2000-rust/index.html @@ -54,7 +54,7 @@

Vendor-Independent GPU JPEG2000 Rust

j2k crate Source -

Release status: j2k 0.7.0 is published on crates.io and docs.rs.

+

Release status: j2k 0.7.1 is published on crates.io and docs.rs.

J2K CPU, CUDA, and Metal backend map diff --git a/docs/htj2k-rust/index.html b/docs/htj2k-rust/index.html index 7f7bf0ef..8e96e2a5 100644 --- a/docs/htj2k-rust/index.html +++ b/docs/htj2k-rust/index.html @@ -54,7 +54,7 @@

HTJ2K Rust

j2k crate j2k-transcode -

Release status: j2k 0.7.0 is published on crates.io and docs.rs.

+

Release status: j2k 0.7.1 is published on crates.io and docs.rs.

HTJ2K Rust codec and transcode pipeline diff --git a/docs/index.html b/docs/index.html index 744fa137..8c834b06 100644 --- a/docs/index.html +++ b/docs/index.html @@ -59,7 +59,7 @@

J2K: Rust JPEG 2000 / HTJ2K Codec

API docs Repository -

Release status: j2k 0.7.0 is published on crates.io and docs.rs.

+

Release status: j2k 0.7.1 is published on crates.io and docs.rs.

diff --git a/docs/j2k-ml.md b/docs/j2k-ml.md new file mode 100644 index 00000000..697b5710 --- /dev/null +++ b/docs/j2k-ml.md @@ -0,0 +1,94 @@ +# Burn tensor decoding with `j2k-ml` + +`j2k-ml` is an experimental, independently maintained integration for Burn +0.21. It is not an official Tracel or Burn crate and remains `publish = false` +through the 0.7 release cycle. + +## Contract + +The crate accepts borrowed JP2, JPH, raw J2K, and raw HTJ2K bytes together +with the repository's full, ROI, scaled, or ROI-scaled decode request. A +single decode returns a rank-3 tensor; a batch returns one rank-4 tensor while +preserving input order. + +Defaults are: + +- `ChannelsFirst`, producing `CHW` or `NCHW`. +- `ChannelSelection::Auto`, preserving grayscale as one channel and producing + RGB for color inputs. Alpha is included only when `Rgba` is explicit. +- `FloatNormalization::Unit`, dividing canonical 8-bit samples by 255 or + canonical 16-bit samples by 65535. + +`decode_u8` and `decode_u16` preserve Burn's native unsigned integer dtype. +`decode_float` produces F32 storage in v1. `Raw` only casts. `MeanStd` applies +unit scaling and then `(x - mean) / std` per channel; channel counts, finite +values, and nonzero standard deviations are validated before pixel decoding. + +The result also reports the actual decoded rectangle, codec warnings, and the +route used. Empty batches, corrupt items, allocation overflow, unsupported +dtypes, and mismatched final shapes are errors. The indexed batch error keeps +the original codec or route failure. + +## Routes + +The `cpu` feature decodes one compact interleaved integer buffer, creates one +Burn `TensorData` for the image or entire batch, and then uses Burn operations +for permutation, F32 conversion, and normalization. It works with any Burn +`Backend` that supports the required dtype. + +The `cuda` feature targets Burn's default fused CUDA backend. It retains the +same CUDA primary context used by CubeCL, allocates through Burn/CubeCL, holds +the managed allocation guard while J2K writes, and registers the completed +allocation through public Burn fusion APIs. A Rust `cuda-oxide` kernel fuses +layout conversion, integer-to-F32 conversion, and normalization. There is no +decoded-pixel device-to-host readback, tensor host upload, CUDA C, NVCC, or C +wrapper. Unsupported formats, devices, and direct paths fail with +`CudaDirect`; they never fall back to CPU. + +The `metal` feature requests strict resident J2K Metal decode. Native Metal +buffer sharing is not claimed: the route performs one packed readback for the +whole batch, uploads that compact integer representation once to Burn Metal, +and uses Burn operations for layout, conversion, and normalization. It uses no +private wgpu internals and reports `MetalStaged`. + +## Burn data loading + +The fallible single and batch functions are the primary API. Dataset storage, +labels, sampling, resizing, padding, augmentation, and worker policy remain +application-owned. When Burn's infallible `Batcher` trait is required, +`PanicOnDecodeError` is an explicitly named float-batcher adapter; its panic +includes the failed batch index and codec error. + +## Deferred scope + +V1 is limited to the existing interleaved Gray/RGB/RGBA 8/16-bit contract. +Arbitrary component tensors, signed native components, precision above 16 +bits, F16/BF16 output, resizing, DLPack, dataset formats, and native Metal +buffer sharing are deferred. + +## Validation and benchmarks + +Portable tests run with `cargo test -p j2k-ml --features cpu`. Metal hardware +validation is part of `cargo xtask release-metal`; CUDA hardware validation is +part of `cargo xtask release-cuda`. Both release lanes set the repository's +fail-closed runtime gates, so missing hardware or skipped accelerator work is a +failure. + +Linux AArch64 test and benchmark builds use Burn's NdArray backend. In this +repository's Burn 0.21 all-feature build, Flex selects `gemm-f16` 0.19 and its +debug AArch64 assembly is compiled without the FP16 target-feature gating that +the selected runner requires ([issue +#31](https://github.com/sarah-quinones/gemm/issues/31)). Upstream has a gating +fix under review ([pull request +#43](https://github.com/sarah-quinones/gemm/pull/43)). This substitution is +test-only; the generic `j2k-ml` library API and its release dependencies do not +select either CPU backend. Criterion group names include `flex` or +`ndarray_arm_linux`; results from those groups are not cross-backend +comparisons. + +Run `cargo bench -p j2k-ml --bench tensor_decode --features cpu` for the +portable staged cases. Add `metal` on macOS or `cuda` on a Linux CUDA runner to +include the accelerator routes. Benchmark IDs and throughput record compact +upload bytes, packed Metal readback-plus-upload bytes, or zero decoded-pixel +transfer bytes for CUDA direct. CUDA should be described as the fast path only +after direct results beat its staged baseline on the target hardware. diff --git a/docs/metal-jpeg2000-rust/index.html b/docs/metal-jpeg2000-rust/index.html index b3219d92..2dd9a31e 100644 --- a/docs/metal-jpeg2000-rust/index.html +++ b/docs/metal-jpeg2000-rust/index.html @@ -54,7 +54,7 @@

Metal JPEG2000 Rust

j2k-metal Metal API docs -

Release status: j2k 0.7.0 is published on crates.io and docs.rs.

+

Release status: j2k 0.7.1 is published on crates.io and docs.rs.

Metal JPEG2000 Rust pipeline diff --git a/docs/release.md b/docs/release.md index cb69e639..0e1f1455 100644 --- a/docs/release.md +++ b/docs/release.md @@ -1,24 +1,28 @@ # Release Policy -The `j2k` 0.7.0 public crate release is published. Runtime backend selection defaults to `Auto`; CPU remains the portable baseline while supported device paths are selected only with validation and benchmark evidence. +The `j2k` 0.7.1 public crate release is published and security-supported. +Runtime backend selection defaults to `Auto`; CPU remains the portable baseline +while supported device paths are selected only with validation and benchmark +evidence. ## Release status | Version | Distribution state | Security support | | --- | --- | --- | -| `0.7.0` | Latest publicly published crates and documentation. | Supported. | +| `0.7.1` | Latest publicly published crates and documentation. | Supported. | +| `0.7.0` | Previous published release line. | Supported. | | `0.6.x` | Previous published release line. | Supported for security fixes during the 0.7 transition. | | `<0.6` | Historical releases. | Unsupported. | -Version `0.7.0` was published from annotated tag `v0.7.0`, which peels to the +Version `0.7.1` was published from annotated tag `v0.7.1`, which peels to the exact locally, hosted-CI, Metal, and CUDA verified release commit. GitHub Pages is served directly from `main/docs`; this post-release state is documentation, while the tag and crates.io records remain the publication evidence. -Version `0.7.0` intentionally contracts parts of the published pre-1.0 `0.6.2` +Version `0.7.1` intentionally contracts parts of the published pre-1.0 `0.6.2` API. It does not claim source compatibility with `0.6.x`. The [`CHANGELOG`](../CHANGELOG.md) provides migration notes, and the -[reviewed API report](../engineering/reviewed-public-api-diff-0.7.0.md) +[reviewed API report](../engineering/reviewed-public-api-diff-0.7.1.md) records the additions, removals, and changed signatures. That report was regenerated, independently reviewed, and verified for the published tag. Any report prepared for a future release remains provisional until it is @@ -43,7 +47,7 @@ candidate SHA, and rerun the local and exact-SHA evidence. During remediation, the changelog keeps a real `## [Unreleased]` heading and a structured staged-version line. As the final release-preparation edit before -candidate freeze, replace that heading with `## [0.7.0] - YYYY-MM-DD` using the +candidate freeze, replace that heading with `## [0.7.1] - YYYY-MM-DD` using the actual intended tag date and update every staged-document reference that still says the notes are under `Unreleased`. Do not guess the date early. Any later date or note change creates a new candidate and requires the exact-SHA gates diff --git a/docs/rust-jpeg2000-codec/index.html b/docs/rust-jpeg2000-codec/index.html index c3b2d4a2..29602dd7 100644 --- a/docs/rust-jpeg2000-codec/index.html +++ b/docs/rust-jpeg2000-codec/index.html @@ -54,7 +54,7 @@

Safe Public Rust JPEG 2000 APIs

cargo add j2k API docs -

Release status: j2k 0.7.0 is published on crates.io and docs.rs.

+

Release status: j2k 0.7.1 is published on crates.io and docs.rs.

Rust JPEG 2000 codec pipeline diff --git a/docs/stable-api-1.0.implementation-public-api.txt b/docs/stable-api-1.0.implementation-public-api.txt index bad0f4f9..826b98fc 100644 --- a/docs/stable-api-1.0.implementation-public-api.txt +++ b/docs/stable-api-1.0.implementation-public-api.txt @@ -912,9 +912,10 @@ pub fn j2k_jpeg_metal::MetalSession::peak_collective_host_bytes(&self) -> core:: pub fn j2k_jpeg_metal::ResidentPrivateJpegTile::byte_offset(&self) -> usize pub fn j2k_jpeg_metal::ResidentPrivateJpegTile::clone(&self) -> Self pub fn j2k_jpeg_metal::ResidentPrivateJpegTile::dimensions(&self) -> (u32, u32) -pub fn j2k_jpeg_metal::ResidentPrivateJpegTile::into_buffer(self) -> metal::buffer::Buffer +pub fn j2k_jpeg_metal::ResidentPrivateJpegTile::into_resident_image(self) -> j2k_metal_support::resident::ResidentMetalImage pub fn j2k_jpeg_metal::ResidentPrivateJpegTile::pitch_bytes(&self) -> usize pub fn j2k_jpeg_metal::ResidentPrivateJpegTile::pixel_format(&self) -> j2k_core::pixel::PixelFormat +pub fn j2k_jpeg_metal::ResidentPrivateJpegTile::resident_image(&self) -> &j2k_metal_support::resident::ResidentMetalImage pub fn j2k_jpeg_metal::Surface::backend_kind(&self) -> j2k_core::backend::BackendKind pub fn j2k_jpeg_metal::Surface::byte_len(&self) -> usize pub fn j2k_jpeg_metal::Surface::dimensions(&self) -> (u32, u32) @@ -944,6 +945,7 @@ pub type j2k_jpeg_metal::Decoder<'a>::Session = j2k_jpeg_metal::MetalSession pub type j2k_jpeg_metal::Decoder<'a>::SubmittedSurface = j2k_jpeg_metal::batch::MetalSubmission pub type j2k_jpeg_metal::Decoder<'a>::View = j2k_jpeg::decoder::view::JpegView<'a> pub unsafe fn j2k_jpeg_metal::ResidentPrivateJpegTile::buffer(&self) -> &metal::buffer::BufferRef +pub unsafe fn j2k_jpeg_metal::ResidentPrivateJpegTile::into_buffer(self) -> metal::buffer::Buffer ``` ## `j2k-metal` @@ -1024,6 +1026,7 @@ pub fn j2k_metal::benchmark_group_region_scaled_requests(&[alloc::sync::Arc<[u8] pub fn j2k_metal::benchmark_overwrite_private_buffer_with_bytes(&j2k_metal::MetalBackendSession, &metal::buffer::Buffer, &[u8]) -> core::result::Result<(), j2k_metal::Error> pub fn j2k_metal::benchmark_private_buffer_with_bytes(&j2k_metal::MetalBackendSession, &[u8]) -> core::result::Result pub fn j2k_metal::benchmark_region_scaled_direct_plan_prepare(&[u8], j2k_core::pixel::PixelFormat, j2k_core::types::Rect, j2k_core::scale::Downscale) -> core::result::Result<(), j2k_metal::Error> +pub fn j2k_metal::download_surfaces_packed(&j2k_metal::MetalBackendSession, &[&j2k_metal::Surface]) -> core::result::Result, j2k_metal::Error> pub fn j2k_metal::encode_lossless_batch_with_report(j2k_metal::MetalLosslessEncodeBatchRequest<'_, '_>, &j2k::encode::contracts::J2kLosslessEncodeOptions, &j2k_metal::MetalBackendSession) -> core::result::Result, j2k_metal::Error> pub j2k_metal::BenchmarkGroupedRequests::batch_count: usize pub j2k_metal::BenchmarkGroupedRequests::max_batch_len: usize @@ -1419,6 +1422,7 @@ pub fn j2k_cuda::CudaHtj2kProfileReport::new() -> Self pub fn j2k_cuda::CudaSession::backend_kind(&self) -> j2k_core::backend::BackendKind pub fn j2k_cuda::CudaSession::execution_stats(&self) -> j2k_core::accelerator::ExecutionStats pub fn j2k_cuda::CudaSession::record_submit(&mut self) +pub fn j2k_cuda::CudaSession::with_context(j2k_cuda_runtime::context::CudaContext) -> Self pub fn j2k_cuda::CudaSurface<'_>::stats(&self) -> j2k_cuda::CudaSurfaceStats pub fn j2k_cuda::CudaSurfaceStats::copy_kernel_dispatches(self) -> usize pub fn j2k_cuda::CudaSurfaceStats::decode_kernel_dispatches(self) -> usize @@ -1431,6 +1435,7 @@ pub fn j2k_cuda::Error::is_unsupported(&self) -> bool pub fn j2k_cuda::Error::source_codec_error(&self) -> core::option::Option<&dyn j2k_core::error::CodecError> pub fn j2k_cuda::J2kDecoder<'a>::cpu_decoder_mut(&mut self) -> &mut Self::Cpu pub fn j2k_cuda::J2kDecoder<'a>::decode_batch_to_device_with_session_and_profile(&[&[u8]], j2k_core::pixel::PixelFormat, &mut j2k_cuda::CudaSession) -> core::result::Result<(alloc::vec::Vec, j2k_cuda::CudaHtj2kProfileReport), j2k_cuda::Error> +pub fn j2k_cuda::J2kDecoder<'a>::decode_request_to_device_with_session(&mut self, j2k_core::pixel::PixelFormat, j2k::adapter::device_plan::DeviceDecodeRequest, &mut j2k_cuda::CudaSession) -> core::result::Result pub fn j2k_cuda::J2kDecoder<'a>::decode_to_device_with_session_and_profile(&mut self, j2k_core::pixel::PixelFormat, &mut j2k_cuda::CudaSession) -> core::result::Result<(j2k_cuda::Surface, j2k_cuda::CudaHtj2kProfileReport), j2k_cuda::Error> pub fn j2k_cuda::J2kDecoder<'a>::from_cpu_view(Self::View) -> core::result::Result pub fn j2k_cuda::J2kDecoder<'a>::inspect_cpu(&'a [u8]) -> core::result::Result @@ -1453,6 +1458,7 @@ pub fn j2k_cuda::encode_lossless_from_cuda_buffer_to_cuda_buffer_with_report(j2k pub fn j2k_cuda::encode_lossless_from_cuda_buffer_with_report(j2k_cuda::CudaLosslessEncodeTile<'_>, &j2k::encode::contracts::J2kLosslessEncodeOptions, &mut j2k_cuda::CudaSession) -> core::result::Result pub fn j2k_cuda::encode_lossless_from_cuda_buffers_to_cuda_buffers_with_report(&[j2k_cuda::CudaLosslessEncodeTile<'_>], &j2k::encode::contracts::J2kLosslessEncodeOptions, &mut j2k_cuda::CudaSession) -> core::result::Result, j2k_cuda::Error> pub fn j2k_cuda::encode_lossless_from_cuda_buffers_with_report(&[j2k_cuda::CudaLosslessEncodeTile<'_>], &j2k::encode::contracts::J2kLosslessEncodeOptions, &mut j2k_cuda::CudaSession) -> core::result::Result, j2k_cuda::Error> +pub j2k_cuda::CudaHtj2kDecodeProfileDetail::classic_dispatch_count: usize pub j2k_cuda::CudaHtj2kDecodeProfileDetail::dequant_dispatch_count: usize pub j2k_cuda::CudaHtj2kDecodeProfileDetail::ht_dispatch_count: usize pub j2k_cuda::CudaHtj2kDecodeProfileDetail::idwt_dispatch_count: usize @@ -1478,11 +1484,14 @@ pub j2k_cuda::CudaHtj2kEncodeProfileReport::packetize_us: u128 pub j2k_cuda::CudaHtj2kEncodeProfileReport::quantize_us: u128 pub j2k_cuda::CudaHtj2kEncodeProfileReport::total_us: u128 pub j2k_cuda::CudaHtj2kProfileReport::block_count: usize +pub j2k_cuda::CudaHtj2kProfileReport::classic_block_count: usize +pub j2k_cuda::CudaHtj2kProfileReport::classic_tier1_us: u128 pub j2k_cuda::CudaHtj2kProfileReport::dequant_us: u128 pub j2k_cuda::CudaHtj2kProfileReport::detail: j2k_cuda::CudaHtj2kDecodeProfileDetail pub j2k_cuda::CudaHtj2kProfileReport::dispatch_count: usize pub j2k_cuda::CudaHtj2kProfileReport::flatten_us: u128 pub j2k_cuda::CudaHtj2kProfileReport::h2d_us: u128 +pub j2k_cuda::CudaHtj2kProfileReport::ht_block_count: usize pub j2k_cuda::CudaHtj2kProfileReport::ht_cleanup_us: u128 pub j2k_cuda::CudaHtj2kProfileReport::ht_refine_us: u128 pub j2k_cuda::CudaHtj2kProfileReport::idwt_us: u128 @@ -2031,6 +2040,7 @@ pub fn j2k_metal_support::FallibleSubmissionQueue::try_finish(self, &'s pub fn j2k_metal_support::FallibleSubmissionQueue::try_push_with(&mut self, &'static str, impl core::ops::function::FnOnce(usize, usize) -> core::result::Result) -> core::result::Result where E: core::convert::From pub fn j2k_metal_support::FallibleSubmissionQueue::with_capacity_hint(usize) -> Self pub struct j2k_metal_support::FallibleSubmissionQueue +pub unsafe fn j2k_metal_support::ResidentMetalImage::from_exclusive_pending_buffer(metal::buffer::Buffer, j2k_metal_support::MetalImageLayout) -> core::result::Result ``` ## `j2k-transcode-metal` @@ -2695,6 +2705,7 @@ pub struct j2k_types::J2kResidentHtj2kTileEncodeJob<'a> ## `j2k-cuda-runtime` ```text +#[repr(C)] pub struct j2k_cuda_runtime::CudaClassicStatus #[repr(C)] pub struct j2k_cuda_runtime::CudaHtj2kEncodeStatus #[repr(C)] pub struct j2k_cuda_runtime::CudaHtj2kPacketizationBlock #[repr(C)] pub struct j2k_cuda_runtime::CudaHtj2kPacketizationStatus @@ -2726,6 +2737,7 @@ impl core::ops::drop::Drop for j2k_cuda_runtime::CudaExternalHostReservation<'_> impl core::ops::drop::Drop for j2k_cuda_runtime::CudaPinnedUploadStagingCheckout<'_, '_> impl core::ops::drop::Drop for j2k_cuda_runtime::CudaQueuedExecution impl core::ops::drop::Drop for j2k_cuda_runtime::CudaQueuedHtj2kCleanup +impl j2k_core::accelerator::GpuAbi for j2k_cuda_runtime::CudaClassicStatus impl j2k_core::accelerator::GpuAbi for j2k_cuda_runtime::CudaHtj2kEncodeStatus impl j2k_core::accelerator::GpuAbi for j2k_cuda_runtime::CudaHtj2kPacketizationBlock impl j2k_core::accelerator::GpuAbi for j2k_cuda_runtime::CudaHtj2kPacketizationStatus @@ -2780,10 +2792,12 @@ impl j2k_cuda_runtime::CudaQueuedExecution impl j2k_cuda_runtime::CudaQueuedHtj2kCleanup impl j2k_cuda_runtime::CudaResidentDwt53Output impl j2k_cuda_runtime::CudaResidentDwt97Output +impl<'a> j2k_cuda_runtime::CudaExternalDeviceBufferViewMut<'a> impl j2k_cuda_runtime::CudaDeviceBufferView<'_, T> impl j2k_cuda_runtime::CudaDeviceBufferViewMut<'_, T> pub const fn j2k_cuda_runtime::CudaExternalHostOwner::bytes(&self) -> usize pub const fn j2k_cuda_runtime::CudaExternalHostReservation<'_>::external_live_bytes(&self) -> usize +pub const j2k_cuda_runtime::CudaClassicStatus::NAME: &'static str pub const j2k_cuda_runtime::CudaHtj2kEncodeStatus::NAME: &'static str pub const j2k_cuda_runtime::CudaHtj2kPacketizationBlock::NAME: &'static str pub const j2k_cuda_runtime::CudaHtj2kPacketizationStatus::NAME: &'static str @@ -2806,6 +2820,9 @@ pub const j2k_cuda_runtime::CudaJpegEntropyCheckpoint::NAME: &'static str pub const j2k_cuda_runtime::CudaJpegEntropyOverflowState::NAME: &'static str pub const j2k_cuda_runtime::CudaJpegEntropySyncState::NAME: &'static str pub const j2k_cuda_runtime::CudaJpegHuffmanTable::NAME: &'static str +pub enum j2k_cuda_runtime::CudaJ2kMlLayout +pub enum j2k_cuda_runtime::CudaJ2kMlNormalization +pub enum j2k_cuda_runtime::CudaJ2kMlSample pub enum j2k_cuda_runtime::CudaJpegBaselineEncodeFormat pub enum j2k_cuda_runtime::CudaJpegRgb8Sampling pub fn j2k_cuda_runtime::CudaBufferPool::best_fit_with_limits(j2k_cuda_runtime::CudaContext, j2k_cuda_runtime::CudaBufferPoolLimits) -> Self @@ -2815,9 +2832,12 @@ pub fn j2k_cuda_runtime::CudaBufferPool::upload_i16(&self, &[i16]) -> core::resu pub fn j2k_cuda_runtime::CudaBufferPool::upload_i16_pinned(&self, &[i16]) -> core::result::Result pub fn j2k_cuda_runtime::CudaBufferPool::with_limits(j2k_cuda_runtime::CudaContext, j2k_cuda_runtime::CudaBufferPoolLimits) -> Self pub fn j2k_cuda_runtime::CudaBufferPoolLimits::default() -> Self +pub fn j2k_cuda_runtime::CudaContext::allocate_classic_coefficients_with_pool(&self, usize, &j2k_cuda_runtime::CudaBufferPool) -> core::result::Result pub fn j2k_cuda_runtime::CudaContext::allocate_htj2k_codeblock_coefficients_with_pool(&self, &[j2k_cuda_runtime::CudaHtj2kCodeBlockJob], usize, &j2k_cuda_runtime::CudaBufferPool) -> core::result::Result pub fn j2k_cuda_runtime::CudaContext::begin_pinned_upload_operation(&self) -> core::result::Result, j2k_cuda_runtime::CudaError> pub fn j2k_cuda_runtime::CudaContext::copy_with_kernel(&self, &[u8]) -> core::result::Result +pub fn j2k_cuda_runtime::CudaContext::decode_classic_codeblocks_multi_with_resources_and_pool(&self, &j2k_cuda_runtime::CudaHtj2kDecodeResources, &[j2k_cuda_runtime::CudaClassicDecodeTarget<'_>], &j2k_cuda_runtime::CudaBufferPool, usize) -> core::result::Result, j2k_cuda_runtime::CudaError> +pub fn j2k_cuda_runtime::CudaContext::decode_classic_codeblocks_multi_with_resources_and_pool_timed(&self, &j2k_cuda_runtime::CudaHtj2kDecodeResources, &[j2k_cuda_runtime::CudaClassicDecodeTarget<'_>], &j2k_cuda_runtime::CudaBufferPool, usize, bool) -> core::result::Result<(alloc::vec::Vec, j2k_cuda_runtime::CudaClassicDecodeStageTimings), j2k_cuda_runtime::CudaError> pub fn j2k_cuda_runtime::CudaContext::decode_htj2k_codeblocks(&self, &[u8], &[j2k_cuda_runtime::CudaHtj2kCodeBlockJob], j2k_cuda_runtime::CudaHtj2kDecodeTables<'_>, usize) -> core::result::Result pub fn j2k_cuda_runtime::CudaContext::decode_htj2k_codeblocks_cleanup_dequantize_multi_with_resources_and_pool_timed(&self, &j2k_cuda_runtime::CudaHtj2kDecodeResources, &[j2k_cuda_runtime::CudaHtj2kCleanupTarget<'_>], &j2k_cuda_runtime::CudaBufferPool, bool) -> core::result::Result<(j2k_cuda_runtime::CudaExecutionStats, j2k_cuda_runtime::CudaHtj2kDecodeStageTimings), j2k_cuda_runtime::CudaError> pub fn j2k_cuda_runtime::CudaContext::decode_htj2k_codeblocks_cleanup_dequantize_multi_with_resources_and_pool_timed_and_live_host_bytes(&self, &j2k_cuda_runtime::CudaHtj2kDecodeResources, &[j2k_cuda_runtime::CudaHtj2kCleanupTarget<'_>], &j2k_cuda_runtime::CudaBufferPool, bool, usize) -> core::result::Result<(j2k_cuda_runtime::CudaExecutionStats, j2k_cuda_runtime::CudaHtj2kDecodeStageTimings), j2k_cuda_runtime::CudaError> @@ -2827,6 +2847,7 @@ pub fn j2k_cuda_runtime::CudaContext::decode_jpeg_rgb8_owned(&self, &j2k_cuda_ru pub fn j2k_cuda_runtime::CudaContext::decode_jpeg_rgb8_owned_into(&self, &j2k_cuda_runtime::CudaJpegRgb8DecodePlan<'_>, &j2k_cuda_runtime::CudaDeviceBuffer, usize) -> core::result::Result pub fn j2k_cuda_runtime::CudaContext::decode_jpeg_rgb8_owned_into_with_external_live(&self, &j2k_cuda_runtime::CudaJpegRgb8DecodePlan<'_>, &j2k_cuda_runtime::CudaDeviceBuffer, usize, usize) -> core::result::Result pub fn j2k_cuda_runtime::CudaContext::decode_jpeg_rgb8_owned_with_external_live(&self, &j2k_cuda_runtime::CudaJpegRgb8DecodePlan<'_>, usize) -> core::result::Result +pub fn j2k_cuda_runtime::CudaContext::device_ordinal(&self) -> usize pub fn j2k_cuda_runtime::CudaContext::diagnose_jpeg_420_entropy_self_sync(&self, &j2k_cuda_runtime::CudaJpegChunkedEntropyPlan<'_>) -> core::result::Result pub fn j2k_cuda_runtime::CudaContext::diagnose_jpeg_420_entropy_self_sync_with_external_live(&self, &j2k_cuda_runtime::CudaJpegChunkedEntropyPlan<'_>, usize) -> core::result::Result pub fn j2k_cuda_runtime::CudaContext::diagnose_jpeg_420_entropy_self_sync_with_pinned_upload_operation(&self, &j2k_cuda_runtime::CudaJpegChunkedEntropyPlan<'_>, usize, &j2k_cuda_runtime::CudaPinnedUploadOperationGuard<'_>) -> core::result::Result @@ -2867,6 +2888,7 @@ pub fn j2k_cuda_runtime::CudaContext::j2k_inverse_dwt_single_device_untimed_with pub fn j2k_cuda_runtime::CudaContext::j2k_inverse_dwt_single_device_with_pool(&self, &j2k_cuda_runtime::CudaDeviceBuffer, &j2k_cuda_runtime::CudaDeviceBuffer, &j2k_cuda_runtime::CudaDeviceBuffer, &j2k_cuda_runtime::CudaDeviceBuffer, j2k_cuda_runtime::CudaJ2kIdwtJob, &j2k_cuda_runtime::CudaBufferPool) -> core::result::Result pub fn j2k_cuda_runtime::CudaContext::j2k_inverse_dwt_single_output_bytes(&self, &j2k_cuda_runtime::CudaDeviceBuffer, &j2k_cuda_runtime::CudaDeviceBuffer, &j2k_cuda_runtime::CudaDeviceBuffer, &j2k_cuda_runtime::CudaDeviceBuffer, j2k_cuda_runtime::CudaJ2kIdwtJob) -> core::result::Result pub fn j2k_cuda_runtime::CudaContext::j2k_inverse_mct_device(&self, &j2k_cuda_runtime::CudaDeviceBuffer, &j2k_cuda_runtime::CudaDeviceBuffer, &j2k_cuda_runtime::CudaDeviceBuffer, j2k_cuda_runtime::CudaJ2kInverseMctJob) -> core::result::Result +pub fn j2k_cuda_runtime::CudaContext::j2k_ml_convert_into_external(&self, u64, usize, &mut j2k_cuda_runtime::CudaExternalDeviceBufferViewMut<'_>, j2k_cuda_runtime::CudaJ2kMlKernelConfig) -> core::result::Result<(), j2k_cuda_runtime::CudaError> pub fn j2k_cuda_runtime::CudaContext::j2k_quantize_subband(&self, &[f32], j2k_cuda_runtime::CudaJ2kQuantizeJob) -> core::result::Result pub fn j2k_cuda_runtime::CudaContext::j2k_quantize_subband_region_resident(&self, &j2k_cuda_runtime::CudaDeviceBuffer, j2k_cuda_runtime::CudaJ2kQuantizeSubbandRegionJob) -> core::result::Result pub fn j2k_cuda_runtime::CudaContext::j2k_quantize_subband_resident(&self, &j2k_cuda_runtime::CudaDeviceBuffer, usize, j2k_cuda_runtime::CudaJ2kQuantizeJob) -> core::result::Result @@ -2899,6 +2921,8 @@ pub fn j2k_cuda_runtime::CudaContext::upload_htj2k_decode_resources_with_tables( pub fn j2k_cuda_runtime::CudaContext::upload_htj2k_decode_resources_with_tables_and_pool(&self, &[u8], &j2k_cuda_runtime::CudaHtj2kDecodeTableResources, &j2k_cuda_runtime::CudaBufferPool) -> core::result::Result pub fn j2k_cuda_runtime::CudaContext::upload_htj2k_decode_table_resources(&self, j2k_cuda_runtime::CudaHtj2kDecodeTables<'_>) -> core::result::Result pub fn j2k_cuda_runtime::CudaContext::upload_htj2k_encode_resources(&self, j2k_cuda_runtime::CudaHtj2kEncodeTables<'_>) -> core::result::Result +pub fn j2k_cuda_runtime::CudaContext::upload_j2k_decode_payload(&self, &[u8]) -> core::result::Result +pub fn j2k_cuda_runtime::CudaContext::upload_j2k_decode_payload_with_pool(&self, &[u8], &j2k_cuda_runtime::CudaBufferPool) -> core::result::Result pub fn j2k_cuda_runtime::CudaContext::validate_jpeg_output_buffer_context(&self, &j2k_cuda_runtime::CudaDeviceBuffer) -> core::result::Result<(), j2k_cuda_runtime::CudaError> pub fn j2k_cuda_runtime::CudaContext::with_nvtx_range(&self, &str, impl core::ops::function::FnOnce() -> core::result::Result) -> core::result::Result pub fn j2k_cuda_runtime::CudaDeviceBuffer::typed_view(&self) -> core::result::Result, j2k_cuda_runtime::CudaError> @@ -2917,6 +2941,9 @@ pub fn j2k_cuda_runtime::CudaDwt97Output::execution(&self) -> j2k_cuda_runtime:: pub fn j2k_cuda_runtime::CudaDwt97Output::levels(&self) -> &[j2k_cuda_runtime::CudaDwt53LevelShape] pub fn j2k_cuda_runtime::CudaDwt97Output::ll_dimensions(&self) -> (u32, u32) pub fn j2k_cuda_runtime::CudaDwt97Output::transformed(&self) -> &[f32] +pub fn j2k_cuda_runtime::CudaExternalDeviceBufferViewMut<'a>::byte_len(&self) -> usize +pub fn j2k_cuda_runtime::CudaExternalDeviceBufferViewMut<'a>::context(&self) -> &j2k_cuda_runtime::CudaContext +pub fn j2k_cuda_runtime::CudaExternalDeviceBufferViewMut<'a>::device_ptr(&self) -> u64 pub fn j2k_cuda_runtime::CudaExternalHostOwner::context_live_bytes(&self) -> core::result::Result pub fn j2k_cuda_runtime::CudaExternalHostOwner::context_live_bytes_excluding(&self, usize) -> core::result::Result pub fn j2k_cuda_runtime::CudaExternalHostOwner::drop(&mut self) @@ -3067,6 +3094,36 @@ pub j2k_cuda_runtime::CudaBufferPoolTakeTrace::free_count_before: usize pub j2k_cuda_runtime::CudaBufferPoolTakeTrace::requested_len: usize pub j2k_cuda_runtime::CudaBufferPoolTakeTrace::reused: bool pub j2k_cuda_runtime::CudaBufferPoolTakeTrace::scanned_count: usize +pub j2k_cuda_runtime::CudaClassicCodeBlockJob::dequantization_step: f32 +pub j2k_cuda_runtime::CudaClassicCodeBlockJob::height: u32 +pub j2k_cuda_runtime::CudaClassicCodeBlockJob::missing_bitplanes: u32 +pub j2k_cuda_runtime::CudaClassicCodeBlockJob::number_of_coding_passes: u32 +pub j2k_cuda_runtime::CudaClassicCodeBlockJob::output_offset: u32 +pub j2k_cuda_runtime::CudaClassicCodeBlockJob::output_stride: u32 +pub j2k_cuda_runtime::CudaClassicCodeBlockJob::payload_len: u32 +pub j2k_cuda_runtime::CudaClassicCodeBlockJob::payload_offset: u64 +pub j2k_cuda_runtime::CudaClassicCodeBlockJob::segment_count: u32 +pub j2k_cuda_runtime::CudaClassicCodeBlockJob::segment_start: u32 +pub j2k_cuda_runtime::CudaClassicCodeBlockJob::strict: bool +pub j2k_cuda_runtime::CudaClassicCodeBlockJob::style_flags: u32 +pub j2k_cuda_runtime::CudaClassicCodeBlockJob::sub_band_type: u32 +pub j2k_cuda_runtime::CudaClassicCodeBlockJob::total_bitplanes: u32 +pub j2k_cuda_runtime::CudaClassicCodeBlockJob::width: u32 +pub j2k_cuda_runtime::CudaClassicDecodeStageTimings::job_upload_us: u128 +pub j2k_cuda_runtime::CudaClassicDecodeStageTimings::kernel_us: u128 +pub j2k_cuda_runtime::CudaClassicDecodeStageTimings::status_d2h_us: u128 +pub j2k_cuda_runtime::CudaClassicDecodeStageTimings::table_upload_us: u128 +pub j2k_cuda_runtime::CudaClassicDecodeTarget::coefficients: &'a j2k_cuda_runtime::CudaDeviceBuffer +pub j2k_cuda_runtime::CudaClassicDecodeTarget::jobs: &'a [j2k_cuda_runtime::CudaClassicCodeBlockJob] +pub j2k_cuda_runtime::CudaClassicDecodeTarget::output_words: usize +pub j2k_cuda_runtime::CudaClassicDecodeTarget::segments: &'a [j2k_cuda_runtime::CudaClassicSegment] +pub j2k_cuda_runtime::CudaClassicSegment::data_length: u32 +pub j2k_cuda_runtime::CudaClassicSegment::data_offset: u32 +pub j2k_cuda_runtime::CudaClassicSegment::end_coding_pass: u32 +pub j2k_cuda_runtime::CudaClassicSegment::start_coding_pass: u32 +pub j2k_cuda_runtime::CudaClassicSegment::use_arithmetic: bool +pub j2k_cuda_runtime::CudaClassicStatus::code: u32 +pub j2k_cuda_runtime::CudaClassicStatus::detail: u32 pub j2k_cuda_runtime::CudaDeviceBufferRange::len: usize pub j2k_cuda_runtime::CudaDeviceBufferRange::offset: usize pub j2k_cuda_runtime::CudaDwt53LevelShape::height: u32 @@ -3230,6 +3287,23 @@ pub j2k_cuda_runtime::CudaJ2kInverseMctJob::addend1: f32 pub j2k_cuda_runtime::CudaJ2kInverseMctJob::addend2: f32 pub j2k_cuda_runtime::CudaJ2kInverseMctJob::irreversible97: u32 pub j2k_cuda_runtime::CudaJ2kInverseMctJob::len: u32 +pub j2k_cuda_runtime::CudaJ2kMlKernelConfig::channels: u32 +pub j2k_cuda_runtime::CudaJ2kMlKernelConfig::destination_offset_elements: usize +pub j2k_cuda_runtime::CudaJ2kMlKernelConfig::height: u32 +pub j2k_cuda_runtime::CudaJ2kMlKernelConfig::layout: j2k_cuda_runtime::CudaJ2kMlLayout +pub j2k_cuda_runtime::CudaJ2kMlKernelConfig::normalization: j2k_cuda_runtime::CudaJ2kMlNormalization +pub j2k_cuda_runtime::CudaJ2kMlKernelConfig::sample: j2k_cuda_runtime::CudaJ2kMlSample +pub j2k_cuda_runtime::CudaJ2kMlKernelConfig::width: u32 +pub j2k_cuda_runtime::CudaJ2kMlLayout::ChannelsFirst +pub j2k_cuda_runtime::CudaJ2kMlLayout::ChannelsLast +pub j2k_cuda_runtime::CudaJ2kMlNormalization::Integer +pub j2k_cuda_runtime::CudaJ2kMlNormalization::MeanStd +pub j2k_cuda_runtime::CudaJ2kMlNormalization::MeanStd::mean: [f32; 4] +pub j2k_cuda_runtime::CudaJ2kMlNormalization::MeanStd::std: [f32; 4] +pub j2k_cuda_runtime::CudaJ2kMlNormalization::Raw +pub j2k_cuda_runtime::CudaJ2kMlNormalization::Unit +pub j2k_cuda_runtime::CudaJ2kMlSample::U16 +pub j2k_cuda_runtime::CudaJ2kMlSample::U8 pub j2k_cuda_runtime::CudaJ2kQuantizeJob::range_bits: u8 pub j2k_cuda_runtime::CudaJ2kQuantizeJob::reversible: bool pub j2k_cuda_runtime::CudaJ2kQuantizeJob::step_exponent: u16 @@ -3468,6 +3542,10 @@ pub j2k_cuda_runtime::CudaTranscodeReversible53Bands::low_width: usize pub struct j2k_cuda_runtime::CudaBufferPoolDiagnostics pub struct j2k_cuda_runtime::CudaBufferPoolLimits pub struct j2k_cuda_runtime::CudaBufferPoolTakeTrace +pub struct j2k_cuda_runtime::CudaClassicCodeBlockJob +pub struct j2k_cuda_runtime::CudaClassicDecodeStageTimings +pub struct j2k_cuda_runtime::CudaClassicDecodeTarget<'a> +pub struct j2k_cuda_runtime::CudaClassicSegment pub struct j2k_cuda_runtime::CudaDeviceBufferRange pub struct j2k_cuda_runtime::CudaDeviceBufferView<'a, T> pub struct j2k_cuda_runtime::CudaDeviceBufferViewMut<'a, T> @@ -3477,6 +3555,7 @@ pub struct j2k_cuda_runtime::CudaDwt97BatchGeometry pub struct j2k_cuda_runtime::CudaDwt97BatchStageTimings pub struct j2k_cuda_runtime::CudaDwt97BatchWithPoolRequest<'a> pub struct j2k_cuda_runtime::CudaDwt97Output +pub struct j2k_cuda_runtime::CudaExternalDeviceBufferViewMut<'a> pub struct j2k_cuda_runtime::CudaExternalHostOwner pub struct j2k_cuda_runtime::CudaExternalHostReservation<'a> pub struct j2k_cuda_runtime::CudaHtj2k97CodeblockBands @@ -3507,6 +3586,7 @@ pub struct j2k_cuda_runtime::CudaHtj2kPacketizationStageTimings pub struct j2k_cuda_runtime::CudaHtj2kPacketizedTile pub struct j2k_cuda_runtime::CudaJ2kDeinterleavedComponents pub struct j2k_cuda_runtime::CudaJ2kIdwtTarget<'a> +pub struct j2k_cuda_runtime::CudaJ2kMlKernelConfig pub struct j2k_cuda_runtime::CudaJ2kQuantizeJob pub struct j2k_cuda_runtime::CudaJ2kQuantizeSubbandRegionJob pub struct j2k_cuda_runtime::CudaJ2kQuantizedSubband @@ -3541,6 +3621,7 @@ pub unsafe fn j2k_cuda_runtime::CudaContext::j2k_inverse_dwt_batch_device_enqueu pub unsafe fn j2k_cuda_runtime::CudaContext::j2k_inverse_dwt_batch_sequence_enqueue_with_pool(&self, &[&[j2k_cuda_runtime::CudaJ2kIdwtTarget<'_>]], &j2k_cuda_runtime::CudaBufferPool) -> core::result::Result pub unsafe fn j2k_cuda_runtime::CudaContext::j2k_inverse_dwt_batch_sequence_enqueue_with_pool_and_live_host_bytes(&self, &[&[j2k_cuda_runtime::CudaJ2kIdwtTarget<'_>]], &j2k_cuda_runtime::CudaBufferPool, usize) -> core::result::Result pub unsafe fn j2k_cuda_runtime::CudaContext::submit_default_stream_named(&self, &str, impl core::ops::function::FnMut() -> core::result::Result) -> core::result::Result +pub unsafe fn j2k_cuda_runtime::CudaExternalDeviceBufferViewMut<'a>::from_raw_parts(&j2k_cuda_runtime::CudaContext, u64, usize, usize, &'a mut Owner) -> core::result::Result pub unsafe fn j2k_cuda_runtime::CudaQueuedExecution::release_pool_reuse_after_completion(&mut self) -> core::result::Result<(), j2k_cuda_runtime::CudaError> ``` diff --git a/docs/stable-api-1.0.md b/docs/stable-api-1.0.md index 178ca9af..2c6ee2fa 100644 --- a/docs/stable-api-1.0.md +++ b/docs/stable-api-1.0.md @@ -52,7 +52,7 @@ The generated 0.7 semver report compares the ordinary inventory with 0.6.2 and also records each package's complete hidden-inventory count and fingerprint. Every semver invocation collects both live passes, compares both committed companions, and requires exact ordinary added/removed fingerprints plus the -hidden count/fingerprint in `engineering/public-api-review-0.7.0.yml`. +hidden count/fingerprint in `engineering/public-api-review-0.7.1.yml`. Nonempty hidden inventories also require a package-specific hidden rationale. Consequently, additions and removals remain blocked until the snapshots, report, and review evidence are updated together and their diffs are reviewed. @@ -68,7 +68,7 @@ uses Rust `1.96` and does not accept the former `J2K_SEMVER_TOOLCHAIN` override. The snapshots record the published workspace's public items and the CLI exit-code contract expectations. Manual prose in this file must not duplicate that inventory. The published-baseline comparison belongs in the generated -[`0.7.0` reviewed API report](../engineering/reviewed-public-api-diff-0.7.0.md). +[`0.7.1` reviewed API report](../engineering/reviewed-public-api-diff-0.7.1.md). The ordinary and rustdoc-hidden artifacts were regenerated and verified after final source freeze and passed the exact-SHA local, hosted, and hardware gates. diff --git a/docs/stable-api-1.0.public-api.txt b/docs/stable-api-1.0.public-api.txt index 6e95b6df..6dd5c768 100644 --- a/docs/stable-api-1.0.public-api.txt +++ b/docs/stable-api-1.0.public-api.txt @@ -1054,6 +1054,14 @@ impl j2k_codec_math::dwt::Dwt53LinearTap pub const fn j2k_codec_math::dwt::Dwt53LinearTap::sample_index(self) -> usize pub const fn j2k_codec_math::dwt::Dwt53LinearTap::weight(self) -> f64 pub const fn j2k_codec_math::dwt::max_decomposition_levels(u32, u32) -> u8 +pub const j2k_codec_math::classic::MQ_QE_VALUES: [u32; 47] +pub const j2k_codec_math::classic::MQ_STATES: [j2k_codec_math::classic::ClassicMqState; 47] +pub const j2k_codec_math::classic::PACKED_MQ_TRANSITION_VALUES: [u32; 47] +pub const j2k_codec_math::classic::PACKED_SIGN_CONTEXT_LOOKUP: [u16; 256] +pub const j2k_codec_math::classic::SIGN_CONTEXT_LOOKUP: [(u8, u8); 256] +pub const j2k_codec_math::classic::ZERO_CTX_HH_LOOKUP: [u8; 256] +pub const j2k_codec_math::classic::ZERO_CTX_HL_LOOKUP: [u8; 256] +pub const j2k_codec_math::classic::ZERO_CTX_LL_LH_LOOKUP: [u8; 256] pub const j2k_codec_math::dwt::DWT53_MAX_HIGH_LINEAR_TAPS: usize pub const j2k_codec_math::dwt::DWT53_MAX_LINEAR_TAPS: usize pub const j2k_codec_math::dwt::DWT97_ALPHA_F32: f32 @@ -1113,6 +1121,10 @@ pub fn j2k_codec_math::dwt::Dwt53LinearRow::taps(&self) -> &[j2k_codec_math::dwt pub fn j2k_codec_math::dwt::linearized_dwt53_row(usize, j2k_codec_math::dwt::Dwt53Band, usize) -> core::option::Option pub fn j2k_codec_math::jpeg::CanonicalHuffmanError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub fn j2k_codec_math::jpeg::derive_canonical_huffman(&[u8; 16], usize) -> core::result::Result +pub j2k_codec_math::classic::ClassicMqState::nlps: u8 +pub j2k_codec_math::classic::ClassicMqState::nmps: u8 +pub j2k_codec_math::classic::ClassicMqState::qe: u32 +pub j2k_codec_math::classic::ClassicMqState::switch: bool pub j2k_codec_math::dwt::Dwt53Band::High pub j2k_codec_math::dwt::Dwt53Band::Low pub j2k_codec_math::jpeg::CanonicalHuffmanDerivation::huffcode: [u16; 256] @@ -1125,12 +1137,14 @@ pub j2k_codec_math::jpeg::CanonicalHuffmanError::BitsExceedTableCapacity pub j2k_codec_math::jpeg::CanonicalHuffmanError::BitsValuesLenMismatch pub j2k_codec_math::jpeg::CanonicalHuffmanError::CodeOverflow pub mod j2k_codec_math +pub mod j2k_codec_math::classic pub mod j2k_codec_math::dwt pub mod j2k_codec_math::generated pub mod j2k_codec_math::jpeg pub mod j2k_codec_math::jpeg::idct pub mod j2k_codec_math::jpeg::ycbcr pub mod j2k_codec_math::mct +pub struct j2k_codec_math::classic::ClassicMqState pub struct j2k_codec_math::dwt::Dwt53LinearRow pub struct j2k_codec_math::dwt::Dwt53LinearTap pub struct j2k_codec_math::jpeg::CanonicalHuffmanDerivation @@ -1804,6 +1818,7 @@ pub fn j2k_jpeg_metal::Decoder<'a>::into_inner(self) -> j2k_jpeg::decoder::Decod pub fn j2k_jpeg_metal::Decoder<'a>::new(&'a [u8]) -> core::result::Result pub fn j2k_jpeg_metal::JpegBaselineMetalEncodeTile<'a>::byte_offset(&self) -> usize pub fn j2k_jpeg_metal::JpegBaselineMetalEncodeTile<'a>::dimensions(&self) -> (u32, u32) +pub fn j2k_jpeg_metal::JpegBaselineMetalEncodeTile<'a>::from_resident(&'a j2k_metal_support::resident::ResidentMetalImage, (u32, u32)) -> Self pub fn j2k_jpeg_metal::JpegBaselineMetalEncodeTile<'a>::output_dimensions(&self) -> (u32, u32) pub fn j2k_jpeg_metal::JpegBaselineMetalEncodeTile<'a>::pitch_bytes(&self) -> usize pub fn j2k_jpeg_metal::JpegBaselineMetalEncodeTile<'a>::pixel_format(&self) -> j2k_core::pixel::PixelFormat @@ -1845,8 +1860,10 @@ pub fn j2k_jpeg_metal::MetalTextureTile::dimensions(&self) -> (u32, u32) pub fn j2k_jpeg_metal::MetalTextureTile::pixel_format(&self) -> j2k_core::pixel::PixelFormat pub fn j2k_jpeg_metal::Surface::as_bytes(&self) -> core::result::Result, j2k_jpeg_metal::Error> pub fn j2k_jpeg_metal::Surface::download_into(&self, &mut [u8], usize) -> core::result::Result<(), j2k_jpeg_metal::Error> +pub fn j2k_jpeg_metal::Surface::into_resident_metal_image(self) -> core::option::Option pub fn j2k_jpeg_metal::Surface::pitch_bytes(&self) -> usize pub fn j2k_jpeg_metal::Surface::residency(&self) -> j2k_core::accelerator::SurfaceResidency +pub fn j2k_jpeg_metal::Surface::resident_metal_image(&self) -> core::option::Option<&j2k_metal_support::resident::ResidentMetalImage> pub fn j2k_jpeg_metal::choose_viewport_surface_strategy(&j2k_jpeg_metal::ViewportWorkload, j2k_core::backend::BackendRequest) -> core::result::Result pub fn j2k_jpeg_metal::encode_jpeg_baseline_batch_from_metal_buffers(&[j2k_jpeg_metal::JpegBaselineMetalEncodeTile<'_>], j2k_jpeg::baseline_encode_contract::JpegEncodeOptions, &j2k_jpeg_metal::MetalBackendSession) -> core::result::Result, j2k_jpeg_metal::Error> pub fn j2k_jpeg_metal::encode_jpeg_baseline_from_metal_buffer(j2k_jpeg_metal::JpegBaselineMetalEncodeTile<'_>, j2k_jpeg::baseline_encode_contract::JpegEncodeOptions, &j2k_jpeg_metal::MetalBackendSession) -> core::result::Result @@ -1994,6 +2011,7 @@ pub fn j2k_metal::MetalEncodedJ2k::to_encoded_j2k(&self) -> core::result::Result pub fn j2k_metal::MetalEncodedJ2k::width(&self) -> u32 pub fn j2k_metal::MetalLosslessEncodeTile<'a>::byte_offset(self) -> usize pub fn j2k_metal::MetalLosslessEncodeTile<'a>::dimensions(self) -> (u32, u32) +pub fn j2k_metal::MetalLosslessEncodeTile<'a>::from_resident(&'a j2k_metal_support::resident::ResidentMetalImage, (u32, u32)) -> Self pub fn j2k_metal::MetalLosslessEncodeTile<'a>::output_dimensions(self) -> (u32, u32) pub fn j2k_metal::MetalLosslessEncodeTile<'a>::pitch_bytes(self) -> usize pub fn j2k_metal::MetalLosslessEncodeTile<'a>::pixel_format(self) -> j2k_core::pixel::PixelFormat @@ -2013,8 +2031,10 @@ pub fn j2k_metal::NativeBackendError::fmt(&self, &mut core::fmt::Formatter<'_>) pub fn j2k_metal::NativeBackendError::source(&self) -> core::option::Option<&(dyn core::error::Error + 'static)> pub fn j2k_metal::Surface::as_bytes(&self) -> core::result::Result, j2k_metal::Error> pub fn j2k_metal::Surface::download_into(&self, &mut [u8], usize) -> core::result::Result<(), j2k_metal::Error> +pub fn j2k_metal::Surface::into_resident_metal_image(self) -> core::option::Option pub fn j2k_metal::Surface::pitch_bytes(&self) -> usize pub fn j2k_metal::Surface::residency(&self) -> j2k_core::accelerator::SurfaceResidency +pub fn j2k_metal::Surface::resident_metal_image(&self) -> core::option::Option<&j2k_metal_support::resident::ResidentMetalImage> pub fn j2k_metal::submit_lossless_batch(j2k_metal::MetalLosslessEncodeBatchRequest<'_, '_>, &j2k::encode::contracts::J2kLosslessEncodeOptions, &j2k_metal::MetalBackendSession) -> core::result::Result pub fn j2k_metal::submit_lossless_batch_to_metal(j2k_metal::MetalLosslessEncodeBatchRequest<'_, '_>, &j2k::encode::contracts::J2kLosslessEncodeOptions, &j2k_metal::MetalBackendSession) -> core::result::Result pub fn j2k_metal::validate_lossless_roundtrip_on_metal(j2k::encode::samples::J2kLosslessSamples<'_>, &[u8]) -> core::result::Result<(), j2k_metal::Error> @@ -2439,6 +2459,7 @@ pub fn j2k_transcode::TranscodeStageError::source(&self) -> core::option::Option pub fn j2k_transcode::TranscodeStageProcessor::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub fn j2k_transcode::dct8x8_blocks_then_dwt53_float(&[[[f64; 8]; 8]], usize, usize, usize, usize) -> core::result::Result, j2k_transcode::DctTransformError> pub fn j2k_transcode::dct8x8_blocks_then_dwt97_float(&[[[f64; 8]; 8]], usize, usize, usize, usize) -> core::result::Result, j2k_transcode::DctTransformError> +pub fn j2k_transcode::dct8x8_blocks_then_dwt97_float_with_scratch(&[[[f64; 8]; 8]], usize, usize, usize, usize, &mut j2k_transcode::Dct97GridScratch) -> core::result::Result, j2k_transcode::DctTransformError> pub fn j2k_transcode::dct8x8_blocks_to_dwt53_float_linear(&[[[f64; 8]; 8]], usize, usize, usize, usize) -> core::result::Result, j2k_transcode::DctTransformError> pub fn j2k_transcode::htj2k97_subband_delta(j2k_transcode::Htj2k97CodeBlockOptions, j2k_types::J2kSubBandType) -> f64 pub fn j2k_transcode::htj2k97_subband_total_bitplanes(j2k_transcode::Htj2k97CodeBlockOptions, j2k_types::J2kSubBandType) -> u8 @@ -2681,6 +2702,7 @@ pub j2k_transcode::TranscodeStageProcessor::Hybrid pub j2k_transcode::TranscodeStageProcessor::Metal pub mod j2k_transcode pub struct j2k_transcode::CpuOnlyDctToWaveletStageAccelerator +pub struct j2k_transcode::Dct97GridScratch pub struct j2k_transcode::DctGridError pub struct j2k_transcode::DctGridI16ToHtj2k97CodeBlockBatch<'a, 'j> pub struct j2k_transcode::DctGridI16ToHtj2k97CodeBlockJob<'a> @@ -2812,15 +2834,33 @@ pub struct j2k_transcode_cuda::CudaRuntimeFailure #[non_exhaustive] pub enum j2k_metal_support::MetalSupportError #[non_exhaustive] pub struct j2k_metal_support::MetalRouteProfileLabels impl core::error::Error for j2k_metal_support::MetalSupportError +impl core::fmt::Debug for j2k_metal_support::ResidentMetalImage +impl core::fmt::Debug for j2k_metal_support::SubmittedMetalImages impl core::fmt::Display for j2k_metal_support::MetalCommandEncoderKind impl core::fmt::Display for j2k_metal_support::MetalSupportError +impl core::ops::drop::Drop for j2k_metal_support::SubmittedMetalImages +impl j2k_core::traits::DeviceSubmission for j2k_metal_support::SubmittedMetalImages +impl j2k_metal_support::MetalImageLayout impl j2k_metal_support::MetalPipelineLoader impl j2k_metal_support::MetalRouteProfileLabels impl j2k_metal_support::MetalSupportError +impl j2k_metal_support::ResidentMetalImage +impl j2k_metal_support::SubmittedMetalImages impl core::clone::Clone for j2k_metal_support::MetalRuntimeSession impl j2k_metal_support::MetalRuntimeSession +pub const fn j2k_metal_support::MetalImageLayout::byte_len(self) -> usize +pub const fn j2k_metal_support::MetalImageLayout::byte_offset(self) -> usize +pub const fn j2k_metal_support::MetalImageLayout::dimensions(self) -> (u32, u32) +pub const fn j2k_metal_support::MetalImageLayout::pitch_bytes(self) -> usize +pub const fn j2k_metal_support::MetalImageLayout::pixel_format(self) -> j2k_core::pixel::PixelFormat pub const fn j2k_metal_support::MetalRouteProfileLabels::new(&'static str, &'static str) -> Self pub const fn j2k_metal_support::MetalSupportError::is_unavailable(&self) -> bool +pub const fn j2k_metal_support::ResidentMetalImage::byte_len(&self) -> usize +pub const fn j2k_metal_support::ResidentMetalImage::byte_offset(&self) -> usize +pub const fn j2k_metal_support::ResidentMetalImage::dimensions(&self) -> (u32, u32) +pub const fn j2k_metal_support::ResidentMetalImage::layout(&self) -> j2k_metal_support::MetalImageLayout +pub const fn j2k_metal_support::ResidentMetalImage::pitch_bytes(&self) -> usize +pub const fn j2k_metal_support::ResidentMetalImage::pixel_format(&self) -> j2k_core::pixel::PixelFormat pub const fn j2k_metal_support::cpu_host_route() -> j2k_metal_support::MetalRouteProfileLabels pub const fn j2k_metal_support::metal_kernel_route() -> j2k_metal_support::MetalRouteProfileLabels pub const fn j2k_metal_support::metal_unavailable_route() -> j2k_metal_support::MetalRouteProfileLabels @@ -2830,6 +2870,7 @@ pub const fn j2k_metal_support::reject_explicit_metal_route(&'static str) -> j2k pub const fn j2k_metal_support::reject_unsupported_backend_route() -> j2k_metal_support::MetalRouteProfileLabels pub const fn j2k_metal_support::two_d_threads_per_group(u64, u64) -> metal::types::MTLSize pub fn j2k_metal_support::MetalCommandEncoderKind::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub fn j2k_metal_support::MetalImageLayout::new(usize, (u32, u32), usize, j2k_core::pixel::PixelFormat) -> core::result::Result pub fn j2k_metal_support::MetalPipelineLoader::library(&self) -> &metal::library::Library pub fn j2k_metal_support::MetalPipelineLoader::new(&metal::device::DeviceRef, &str) -> core::result::Result pub fn j2k_metal_support::MetalPipelineLoader::pipeline(&self, &str) -> core::result::Result @@ -2842,6 +2883,13 @@ pub fn j2k_metal_support::MetalRuntimeSession::runtime_initialized(&self) pub fn j2k_metal_support::MetalRuntimeSession::runtime_result(&self) -> core::option::Option<&core::result::Result> pub fn j2k_metal_support::MetalRuntimeSession::system_default() -> core::result::Result pub fn j2k_metal_support::MetalSupportError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub fn j2k_metal_support::ResidentMetalImage::device_registry_id(&self) -> u64 +pub fn j2k_metal_support::ResidentMetalImage::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub fn j2k_metal_support::ResidentMetalImage::validate_device(&self, &metal::device::DeviceRef) -> core::result::Result<(), j2k_metal_support::MetalSupportError> +pub fn j2k_metal_support::ResidentMetalImage::view(&self, j2k_metal_support::MetalImageLayout) -> core::result::Result +pub fn j2k_metal_support::SubmittedMetalImages::drop(&mut self) +pub fn j2k_metal_support::SubmittedMetalImages::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub fn j2k_metal_support::SubmittedMetalImages::wait(self) -> core::result::Result pub fn j2k_metal_support::checked_blit_command_encoder(&metal::commandbuffer::CommandBufferRef) -> core::result::Result pub fn j2k_metal_support::checked_command_buffer(&metal::commandqueue::CommandQueueRef) -> core::result::Result pub fn j2k_metal_support::checked_command_queue(&metal::device::DeviceRef) -> core::result::Result @@ -2902,6 +2950,11 @@ pub j2k_metal_support::MetalSupportError::CommandEncoderUnavailable::kind: j2k_m pub j2k_metal_support::MetalSupportError::CommandQueue pub j2k_metal_support::MetalSupportError::CommandQueue::message: alloc::string::String pub j2k_metal_support::MetalSupportError::CommandQueueUnavailable +pub j2k_metal_support::MetalSupportError::MetalImageDeviceMismatch +pub j2k_metal_support::MetalSupportError::MetalImageDeviceMismatch::image_registry_id: u64 +pub j2k_metal_support::MetalSupportError::MetalImageDeviceMismatch::requested_registry_id: u64 +pub j2k_metal_support::MetalSupportError::MetalImageLayout +pub j2k_metal_support::MetalSupportError::MetalImageLayout::reason: &'static str pub j2k_metal_support::MetalSupportError::MetalUnavailable pub j2k_metal_support::MetalSupportError::PipelineFunction pub j2k_metal_support::MetalSupportError::PipelineFunction::function_name: alloc::string::String @@ -2925,8 +2978,16 @@ pub j2k_metal_support::MetalSupportError::TextureDescriptorInvalid pub j2k_metal_support::MetalSupportError::TextureDescriptorInvalid::reason: &'static str pub j2k_metal_support::MetalSupportError::TextureDescriptorUnavailable pub mod j2k_metal_support +pub struct j2k_metal_support::MetalImageLayout pub struct j2k_metal_support::MetalPipelineLoader pub struct j2k_metal_support::MetalRuntimeSession +pub struct j2k_metal_support::ResidentMetalImage +pub struct j2k_metal_support::SubmittedMetalImages +pub type j2k_metal_support::SubmittedMetalImages::Error = j2k_metal_support::MetalSupportError +pub type j2k_metal_support::SubmittedMetalImages::Output = alloc::vec::Vec +pub unsafe fn j2k_metal_support::ResidentMetalImage::from_completed_buffer(metal::buffer::Buffer, j2k_metal_support::MetalImageLayout) -> core::result::Result +pub unsafe fn j2k_metal_support::ResidentMetalImage::raw_buffer(&self) -> &metal::buffer::Buffer +pub unsafe fn j2k_metal_support::SubmittedMetalImages::from_uncommitted(&metal::device::DeviceRef, metal::commandbuffer::CommandBuffer, alloc::vec::Vec<(metal::buffer::Buffer, j2k_metal_support::MetalImageLayout)>, alloc::vec::Vec) -> core::result::Result pub unsafe fn j2k_metal_support::checked_buffer_fill_bytes(&metal::buffer::Buffer, usize, usize, u8) -> core::result::Result<(), j2k_metal_support::MetalSupportError> pub unsafe fn j2k_metal_support::checked_buffer_read(&metal::buffer::Buffer, usize) -> core::result::Result pub unsafe fn j2k_metal_support::checked_buffer_read_vec(&metal::buffer::Buffer, usize, usize) -> core::result::Result, j2k_metal_support::MetalSupportError> @@ -3781,6 +3842,7 @@ pub fn j2k_cuda_runtime::CudaContext::allocate(&self, usize) -> core::result::Re pub fn j2k_cuda_runtime::CudaContext::best_fit_buffer_pool(&self) -> j2k_cuda_runtime::CudaBufferPool pub fn j2k_cuda_runtime::CudaContext::buffer_pool(&self) -> j2k_cuda_runtime::CudaBufferPool pub fn j2k_cuda_runtime::CudaContext::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub fn j2k_cuda_runtime::CudaContext::retain_primary(usize) -> core::result::Result pub fn j2k_cuda_runtime::CudaContext::synchronize(&self) -> core::result::Result<(), j2k_cuda_runtime::CudaError> pub fn j2k_cuda_runtime::CudaContext::system_default() -> core::result::Result pub fn j2k_cuda_runtime::CudaContext::upload(&self, &[u8]) -> core::result::Result diff --git a/docs/unsafe-audit.md b/docs/unsafe-audit.md index fb50a3e9..64cc16d3 100644 --- a/docs/unsafe-audit.md +++ b/docs/unsafe-audit.md @@ -12,20 +12,23 @@ moved, or removed. | `crates/j2k-core/src/accelerator.rs` | Shared GPU ABI marker trait and byte-view helpers for host/device struct transfers. | Implementers are plain-data GPU ABI values with stable layout and valid byte views. | Core API tests plus backend layout/parity tests for GPU ABI structs. | | `crates/j2k-core/src/backend.rs` | CPU feature detection and backend probing. | CPU intrinsics are called only behind matching architecture/configuration checks. | Cross-architecture CI matrix and backend feature tests. | | `crates/j2k-cuda-runtime/src/bytes/abi.rs` | Unsafe `GpuAbi` marker implementations for CUDA host/device records. | Every marked type is `repr(C)`, accepts every bit pattern, and has a compile-time field-offset walk proving its complete representation contains no padding; explicit reserved-tail fields are initialized and mirrored in device records without changing ABI sizes or offsets. | Safe byte-view tests, ABI size/offset tests, CUDA ABI repository policy, strict cuda-oxide builds, and runtime parity tests. | -| `crates/j2k-cuda-runtime/src/context/device.rs` | CUDA driver initialization, device discovery, and context creation. | Driver entry points receive correctly typed writable outputs; device zero is requested only after a positive device count; the created context is retained by its owner. | CUDA context tests with fake and real driver paths plus the strict GPU validation workflow. | +| `crates/j2k-cuda-runtime/src/context/creation.rs` | CUDA driver initialization, device discovery, owned-context creation, and primary-context retain cleanup. | Driver entry points receive correctly typed writable outputs; the ordinal is range-checked; owned and retained-primary handles have distinct exactly-once cleanup paths, including validation failure. | CUDA context identity/retain/release tests with real driver paths plus the strict GPU validation workflow. | | `crates/j2k-cuda-runtime/src/context/inner.rs` | Cached CUDA resource teardown, context destruction, and thread-safety marker implementations. | The context handle remains live while used; mutable caches are mutex-protected; poisoned contexts skip individual resource transitions and defer reclamation to context-wide destruction. | CUDA lifecycle/cache/drop tests, concurrent runtime tests, and the strict GPU validation workflow. | | `crates/j2k-cuda-runtime/src/context/kernel_cache.rs` | CUDA PTX module/function loading and cached opaque kernel handles. | PTX and symbol names are NUL-terminated, loaded handles belong to the owning context, the cache mutex coordinates handle sharing and eventual unload, and a lookup or rollback failure quarantines the context without exposing an uncertain module handle. | Pure module-lookup and failed-rollback regressions, CUDA kernel-cache/metadata tests, and the strict GPU validation workflow. | | `crates/j2k-cuda-runtime/src/context/operations.rs` | Current-context binding, gated driver-operation recovery, completion checks, and resource-state quarantine. | Every operation binds the owning context under one lifecycle gate; ordinary operation errors are recoverable only after context-wide completion succeeds; failed completion, resource ownership transitions, binding failures, and unwinding poison further use. | Pure lifecycle recovery/quarantine/concurrency tests, runtime behavior tests, and the strict GPU validation workflow. | +| `crates/j2k-cuda-runtime/src/context/pointer.rs` | CUDA driver pointer-to-context identity query for external-memory validation. | The query runs while the expected context is current and lifecycle-gated; its writable output has the exact `CUcontext` representation, and the returned driver handle must equal the owning context handle. | Foreign-context external-buffer rejection and strict CUDA hardware validation. | | `crates/j2k-cuda-runtime/src/context/pinned_host.rs` | Borrowed slice views and deallocation for owned CUDA-pinned upload staging. | The pointer covers the recorded allocation length, mutable views require exclusive access, and the allocation is freed exactly once through its owning context. | CUDA pinned-memory upload/reuse tests and the strict GPU validation workflow. | | `crates/j2k-cuda-runtime/src/cuda_oxide_simt_prelude.rs` | Shared cuda-oxide SIMT pointer prelude for device load, store, and pointer-offset helpers. | Device-kernel callers pass validated live buffers and launch-bounded indices; the helper functions centralize the raw pointer arithmetic used by generated CUDA-Oxide SIMT crates. | `xtask` repo lint requires every CUDA-Oxide SIMT source to include this prelude and rejects reintroduced direct helper pointer dereferences. | | `crates/j2k-cuda-runtime/src/cuda_oxide_copy_u8/simt/src/main.rs` | cuda-oxide device `CopyU8` kernel raw pointer copy. | Device pointers refer to live buffers of at least `len` bytes and each thread checks `index < len` before reading or writing one byte. | CUDA Oxide CopyU8 parity test when cuda-oxide PTX is generated and strict GPU validation workflow. | | `crates/j2k-cuda-runtime/src/cuda_oxide_j2k_decode_store/simt/src/main.rs` | cuda-oxide device J2K decode store and inverse-MCT kernels. | Host preflight validates every source/destination extent, allocation size, and `u32` linear index before driver work; partial outputs are zeroed before launch; device pointers then refer to live, launch-bounded component/output buffers or job-buffer device pointers. | Pure adversarial bounds tests, partial/zero-copy initialization tests, decode store/MCT parity and metadata tests when cuda-oxide PTX is generated, plus strict GPU validation workflow. | +| `crates/j2k-cuda-runtime/src/cuda_oxide_j2k_classic_decode/simt/src/main.rs` | cuda-oxide device classic JPEG 2000 Tier-1 decode kernel and per-block shared state. | Host preflight validates payload and segment partitions, pass/style limits, code-block geometry, output strides, and disjoint target ranges before launch; each block owns one validated code-block, its bounded global scratch slice, and its block-shared state, while status rows surface defensive device rejection before coefficient output. | Pure adversarial classic preflight and kernel-structure tests, native-versus-CUDA style/dimension parity on the NVIDIA runner, and fail-closed CUDA validation with strict CUDA-Oxide builds. | | `crates/j2k-cuda-runtime/src/cuda_oxide_j2k_dequantize/simt/src/main.rs` | cuda-oxide device J2K HTJ2K dequantize kernels. | Device pointers refer to live, launch-bounded coefficient buffers or job-buffer device pointers; each block reads only its own job and each thread strides inside that job's sample count. | HTJ2K dequantize parity and metadata tests when cuda-oxide PTX is generated plus strict GPU validation workflow. | | `crates/j2k-cuda-runtime/src/cuda_oxide_htj2k_decode/simt/src/main.rs` | cuda-oxide device HTJ2K cleanup/refinement decode kernels. | Device pointers refer to live payload, lookup-table, job, output, status, and timing buffers; per-thread stack scratch is bounded by the validated code-block width, height, and stride constants before raw pointer access or status writes. | HTJ2K decode metadata tests when cuda-oxide PTX is generated, focused strict runtime tests on a self-hosted CUDA runner, and HTJ2K CUDA smoke coverage. | | `crates/j2k-cuda-runtime/src/cuda_oxide_htj2k_encode/simt/src/main.rs` | cuda-oxide device HTJ2K cleanup/refinement codeblock encode kernels. | Device pointers refer to live coefficient, lookup-table, scratch-output, and status buffers; each block owns one codeblock, thread 0 exits on invalid shapes before stack-scratch encode work, and status rows surface unsupported/failure shapes without silent fallback. | HTJ2K encode metadata tests when cuda-oxide PTX is generated, focused strict runtime tests on a self-hosted CUDA runner, and HTJ2K codeblock parity tests. | | `crates/j2k-cuda-runtime/src/cuda_oxide_jpeg_decode/simt/src/main.rs` | cuda-oxide device baseline JPEG entropy decode and RGB8 output kernels for 4:2:0, 4:2:2, and 4:4:4 resident inputs. | Device pointers refer to live entropy, Huffman-table, checkpoint, output, and status buffers. Host preflight proves the exact sampling-derived MCU grid, a strict checkpoint partition from MCU zero, left-aligned bounded bit state, canonical role-correct tables, and a pitched last RGB byte within `u32`; the device repeats those bounds with checked arithmetic and stores a nonzero status on every defensive rejection before raw pointer reads or writes. | Pure adversarial plan/table/address tests, generated-kernel metadata and PTX assembly checks, exact-source 4:2:0/4:2:2/4:4:4 owned/caller-buffer parity on the NVIDIA runner, and GPU validation with `J2K_REQUIRE_CUDA_JPEG_HARDWARE_DECODE`. | | `crates/j2k-cuda-runtime/src/cuda_oxide_jpeg_encode/simt/src/main.rs` | cuda-oxide device baseline JPEG entropy encode kernels for single and same-buffer batch resident input tiles. | Host preflight validates input and entropy allocation ranges, disjoint batch outputs, sampling/MCU geometry, every kernel-side `u32` expression, nonzero quantizers, and canonical prefix-free non-all-ones Huffman codes before driver work. Device pointers then refer to live validated buffers, and status rows surface missing symbols or capacity exhaustion. | Pure adversarial layout/table/boundary tests, nonzero-offset single and batch round trips, generated-kernel metadata tests, and strict resident CUDA encode tests on a self-hosted runner. | | `crates/j2k-cuda-runtime/src/cuda_oxide_j2k_idwt/simt/src/main.rs` | cuda-oxide device J2K generic inverse-DWT kernels. | Before allocation or driver work, Rust validates coherent output/band rectangles, parity-derived band geometry, every input/output allocation, context ownership, alias rules, iteration bounds, and the maximum `u32` linear index. Device pointers then refer to live launch-bounded buffers retained through completion. | Pure full-job and adversarial geometry tests, queued ownership/alias tests, IDWT parity and metadata tests when cuda-oxide PTX is generated, plus strict GPU validation workflow. | +| `crates/j2k-cuda-runtime/src/cuda_oxide_j2k_ml/simt/src/main.rs` | cuda-oxide device interleaved-sample to Burn tensor conversion kernel. | Host preflight validates source/destination context identity, byte lengths, alignment, channel count, batch offset, normalization values, and launch size; each thread bounds-checks its source index and maps to one validated CHW/HWC destination element. | External-buffer bounds tests, direct CUDA U8/U16/F32 parity, batch/layout/normalization tests, and strict CUDA hardware validation. | | `crates/j2k-cuda-runtime/src/cuda_oxide_j2k_encode/simt/src/exports.rs` | cuda-oxide device J2K encode-stage kernel entry points for deinterleave, color transforms, DWT, quantization, HTJ2K compaction, and cleanup packetization. | Device pointers refer to live, launch-bounded sample/tile, scratch, compact-output, packet-output, metadata, status, and job buffers; forward-DWT host preflight caps decomposition levels and proves the last live sample index fits `u32`; every entry point exits or strides inside validated launch/job bounds before raw pointer access. | Forward-DWT boundary/degenerate/index tests, J2K encode-stage parity, HTJ2K compaction fixtures, packetization parity, generated-PTX metadata tests, and the strict GPU validation workflow. | | `crates/j2k-cuda-runtime/src/cuda_oxide_j2k_encode/simt/src/packetization.rs` | Raw status and packet-header writes shared by cuda-oxide HTJ2K packetization kernels. | Callers provide a live status row and output span for the current launch-bounded packet job; header/body lengths are checked against capacity before writes and only block-owned state is mutated. | HTJ2K packetization scalar-parity and overflow/status tests, generated-PTX metadata tests, and the strict GPU validation workflow. | | `crates/j2k-cuda-runtime/src/cuda_oxide_transcode/simt/src/exports.rs` | cuda-oxide device reversible 5/3 and irreversible 9/7 transcode kernel entry points, including batch, code-block quantize, and fused column-lift+quantize stages. | Device pointers refer to live, launch-bounded DCT block, spatial sample, row-band, quantized code-block, and output band buffers; every kernel exits out-of-range threads before raw pointer access, and cooperative row-lift synchronization has no divergent early return before barriers. | Transcode parity and generated-PTX metadata tests plus the strict GPU validation workflow. | @@ -42,15 +45,16 @@ moved, or removed. | `crates/j2k-cuda-runtime/src/j2k_decode/idwt.rs` | Unsafe asynchronous IDWT batch and sequence enqueue with pooled metadata retention. | Every band, output, and pool belongs to the launch context; full job geometry/allocation and alias validation completes before metadata upload; aggregation is checked and fallible; callers retain target allocations and exclude mutation/reuse until the queued handle is finished, dropped, or released after an actual context completion point. | Full-job boundary tests, IDWT ownership/source-policy tests, CUDA runtime tests, adapter host-surface tests, and the strict GPU validation workflow. | | `crates/j2k-cuda-runtime/src/j2k_decode/idwt/sequence.rs` | Unsafe multi-stage inverse-DWT enqueue with one pooled metadata upload. | All target buffers and the pool belong to the context and remain live, immutable, and unavailable for reuse until queued completion; same-stage outputs are disjoint from concurrent inputs, while validated dependencies may alias only across ordered stages. | Pure sequence context/alias regressions, two-stage runtime parity tests, queued-resource policy, and the strict GPU validation workflow. | | `crates/j2k-cuda-runtime/src/j2k_decode/idwt/tests.rs` | Test-only invocation of the unsafe queued IDWT sequence boundary. | The exercised jobs enqueue no device work, every buffer and pool belongs to the launch context, all resources remain live through `finish`, and the test establishes completion before release. | The ordered-stage alias regression itself plus the unsafe-audit path inventory. | -| `crates/j2k-cuda-runtime/src/memory.rs` | CUDA device/pinned memory allocation, copies, downloads, and typed buffer views. | Device ranges and context ownership are checked before address comparison; allocation/free ownership transitions quarantine the context on failure; copy failures recover only after completion; pinned staging wrappers and failed-free owners are retained instead of silently losing raw allocations. | CUDA lifecycle/memory tests, pure cross-context overlap and pinned-retention regressions, wrong-context tests, and surface downloads. | +| `crates/j2k-cuda-runtime/src/memory.rs` | CUDA device/pinned memory allocation, copies, downloads, typed buffer views, and the non-owning external-allocation bridge. | Device ranges and context ownership are checked before address comparison; the external view requires a lifetime-bound exclusive managed-owner borrow and validates non-null range, alignment, overflow, and actual driver context without assuming ownership; allocation/free transitions remain with the original owner. | CUDA lifecycle/memory tests, external-view foreign-context/non-ownership tests, wrong-context regressions, and strict CUDA hardware validation. | | `crates/j2k-cuda-runtime/src/memory/pinned_staging/operations/growth.rs` | Page-locked upload-staging allocation and transactional pool growth. | Requested lengths are capped and context-wide host headroom is reserved before allocation; successful CUDA allocation must return a non-null pointer; the operation gate serializes pool mutation; authority and active-pool accounting roll back transactionally, and failed or uncertain releases retain ownership instead of reusing or losing the allocation. | Context host-authority owner-first/pinned-first, reservation rollback, pinned-staging cap, checkout, retention, diagnostics, and real upload tests plus the strict GPU validation workflow. | | `crates/j2k-cuda-runtime/src/memory/pool.rs` | CUDA device-buffer pooling, deferred reuse, pooled uploads/downloads, and uninitialized host readback. | Pool ownership is context-checked; reuse holds defer recycling until completion; uncertain completion retains the pool and allocation ownership; host vector length is set only after an exact successful device copy. | CUDA pool/reuse tests, queued-resource structure policy, wrong-context regressions, and strict GPU validation workflow. | | `crates/j2k-cuda-runtime/src/memory/pool/readback.rs` | Fallible pooled-device readback into uninitialized `Vec` spare capacity. | Capacity is reserved before the copy, the destination spare slice has exactly the requested length, and `Vec::set_len` runs only after a successful device copy initialized that complete extent. | Pooled readback parity/error tests, allocation-failure coverage, and the unsafe-audit path inventory. | -| `crates/j2k-cuda-runtime/src/tests.rs` | Test-only fake CUDA driver table construction. | Fake FFI signatures match production driver types. | CUDA runtime unit tests compile and execute fake-driver paths. | +| `crates/j2k-cuda-runtime/src/tests.rs` | Test-only fake CUDA driver construction and external-buffer boundary invocation. | Fake FFI signatures match production driver types; real-device external views borrow live owned allocations exclusively and are dropped before the original owner is reused. | CUDA runtime unit tests, foreign-context rejection, batch-offset bounds, and non-ownership regressions. | | `crates/j2k-cuda-runtime/src/tests/pipeline.rs` | Test-only calls to asynchronous HTJ2K cleanup and IDWT enqueue APIs. | Every borrowed test allocation and session-private pool belongs to the launch context, remains live and unmodified while queued work is pending, and is retained through explicit synchronization or queued status completion. | Focused CUDA pipeline tests exercise queued IDWT batches, IDWT sequences, and empty HTJ2K cleanup resource ownership. | | `crates/j2k-cuda-runtime/src/transcode/types.rs` | CUDA transcode kernel parameter ABI marker implementation. | The marked parameter type is `repr(C)`, contains only CUDA scalar ABI fields, and matches the fused column-lift/quantize kernel signature. | CUDA transcode ABI/metadata tests and transform parity tests. | | `crates/j2k-compare/src/grok.rs` | Grok FFI comparison harness. | External decoder pointers are checked for null and output buffers are sized before copy. | Optional Grok/OpenJPEG parity tests. | | `crates/j2k-compare/src/openjpeg.rs` | OpenJPEG FFI comparison harness. | OpenJPEG stream/image lifetimes are paired with cleanup and component buffers are bounds-checked. | Optional Grok/OpenJPEG parity tests. | +| `crates/j2k-ml/src/cuda.rs` | Sole Burn bridge construction of a lifetime-bound view over CubeCL-managed CUDA memory. | CubeCL's managed-resource guard remains exclusively borrowed and live through same-primary-context J2K decode, validated cuda-oxide conversion, and completion; the external view never frees or escapes with Burn memory, and the tensor is registered only after completion. | CUDA direct U8/U16/F32, batch, normalization, default fusion, autodiff lifting, external-buffer ownership, and strict hardware tests. | | `crates/j2k-cuda/src/decoder.rs` | Adapter-side unsafe release of queued CUDA pool holds after synchronous downstream completion. | Session-private pools remain confined to the owning context; IDWT target owners stay live until context synchronization or a synchronously completed MCT/store launch proves all preceding default-stream work complete. | CUDA decoder structure/accounting tests, queued-resource source policy, host-surface tests, and strict GPU validation. | | `crates/j2k-cuda/src/decoder/resident/cleanup_dequant.rs` | Adapter calls to the unsafe HTJ2K-cleanup enqueue API. | Component work owns every coefficient target, decode resources and session pools stay live and context-matched, and queued handles are retained through status completion, explicit synchronization, or synchronously completed dependent work. | Resident decode unit tests, queued-resource source policy, host-surface tests, and strict GPU validation. | | `crates/j2k-cuda/src/decoder/resident/idwt.rs` | Adapter calls to unsafe IDWT enqueue APIs. | Component work owns every coefficient target, decode resources and session pools stay live and context-matched, and queued handles are retained through status completion, explicit synchronization, or synchronously completed dependent work. | Resident decode unit tests, queued-resource source policy, host-surface tests, and strict GPU validation. | @@ -61,12 +65,14 @@ moved, or removed. | `crates/j2k-metal/src/compute/direct_flattened.rs` | Flattened hybrid CPU-tier1 worker output pointers. | Work items are built from non-overlapping packed coefficient ranges, and each pointer is converted back to a mutable slice with the validated per-item output length before a single decode write. | MetalDirect hybrid batch and flattened CPU-tier1 tests. | | `crates/j2k-metal/src/compute/gpu_timing.rs` | Metal command-buffer GPU timestamp queries through Objective-C messages. | Timestamps are queried only for completed command buffers, duplicate buffers are ignored, and non-finite or inverted timestamp pairs are discarded. | Resident encode/decode profiling tests and GPU validation workflow. | | `crates/j2k-metal/src/encode/config.rs` | Resident Metal encode memory-budget probing and batch chunk policy. | Host memory probing writes into a correctly sized `u64` buffer and falls back when the sysctl probe is unavailable. | Metal encode config and resident chunking tests. | +| `crates/j2k-metal/src/encode/tests/resident_buffers.rs` | Test-only adoption of completed Metal buffers into immutable resident encode inputs. | Each shared test buffer is fully initialized, has no submitted or later writer, belongs to the encode session's device, and remains resident through synchronous encode completion. | Resident single/batch encode round-trip and cross-device rejection tests on the Metal release lane. | | `crates/j2k-metal/src/encode/tests/routing.rs` | Test-only readback proving hybrid encode uploads own their bytes. | The shared Metal-owned buffer is synchronously initialized, never submitted for GPU mutation, and read only after the caller storage has been overwritten and dropped. | The borrowed-input ownership regression itself plus the unsafe-audit path inventory. | | `crates/j2k-metal/src/encode/host_fallback.rs` | Checked owned host readback from completed Metal encode staging buffers. | Staging copies complete before readback, no later Metal writer targets the selected range, and the shared helper validates offset and length before copying bytes. | Metal encode, resident batch, and host-output tests. | | `crates/j2k-metal/src/encode/encoded.rs` | Construction and consuming handoff of raw Metal codestream buffers, plus owned readback of completed resident codestream bytes. | Raw construction validates ranges and requires prior writers and aliased mutation to be excluded; consuming handoff preserves those obligations across cloned handles; owned readback waits for production and checks the selected range before copying. | Encoded-codestream raw-parts/handoff tests, resident encode tests, and safe readback tests. | | `crates/j2k-metal/src/encode/types.rs` | Unsafe borrowed Metal input-tile constructor. | The buffer range is initialized, immutable, live for every submission, and allocated by the same Metal device as the session that consumes it. | Metal encode unit/device tests, strict Metal validation, example compilation, and the unsafe API contract. | | `crates/j2k-metal/src/profile_env.rs` | Metal profiling labels and OS signpost FFI shims. | Signpost end calls use only ids returned by the matching begin shim and invalid/null ids are discarded. | Metal profiling and encode/decode route tests. | -| `crates/j2k-metal/src/surface.rs` | Unsafe raw Metal surface handle access and owned download of completed decode surfaces. | Raw-handle callers complete prior writers and exclude aliased mutation; safe download checks offset/length and copies only after decode completion, so no safe borrowed host slice escapes. | Metal surface download, raw-handle device tests, device-output tests, and decode route tests. | +| `crates/j2k-metal/src/surface.rs` | Unsafe raw Metal surface handle access and owned single-surface downloads. | Raw-handle callers complete prior writers and exclude aliased mutation; safe download checks offset/length and copies only after decode completion, so no safe borrowed host slice escapes. | Metal surface download, raw-handle device tests, device-output tests, and decode route tests. | +| `crates/j2k-metal/src/surface/readback.rs` | One-allocation readback of a completed Metal surface batch. | Every surface is Metal-resident on the supplied device, source/destination offsets and aggregate size are checked, the single blit command completes before the staging buffer is copied, and no borrowed host view escapes. | `j2k-ml` Metal batch parity, route, and packed-transfer instrumentation tests plus strict Metal validation. | | `crates/j2k-metal/examples/resident_encode_buffer.rs` | Example construction of borrowed resident Metal encode tiles. | Buffers come from the same demonstrated session, are initialized before construction, remain immutable, and outlive the awaited batch. | Example/bench build gates and strict Clippy. | | `crates/j2k-metal/tests/device.rs` | Raw Metal surface/codestream constructor and consuming-handoff regression tests. | Test allocations have completed or have no writers; raw ranges are valid; tests retain no concurrently mutating alias. | The device integration test target on the Metal release lane. | | `crates/j2k-metal/tests/encode_auto_routing_benchmark.rs` | Benchmark-only borrowed resident Metal input tiles. | Inputs are allocated by the measured session, initialized before timing, immutable during both batches, and retained through completion. | Benchmark build gate and the explicit resident-codestream performance guard. | @@ -74,7 +80,12 @@ moved, or removed. | `crates/j2k-jpeg-metal/src/abi.rs` | Unsafe `GpuAbi` marker implementations for JPEG Metal shader parameter, checkpoint, and status structs. | Every marked type is `repr(C)`, `Copy`, accepts every bit pattern, and has a compile-time field-offset walk proving that its complete object representation contains no padding; explicit reserved fields match the Metal shader ABI. | Compile-time no-padding assertions, JPEG Metal ABI size/alignment/offset tests, benchmark gates, and Metal compile gates. | | `crates/j2k-jpeg-metal/src/buffers.rs` | JPEG Metal reusable scratch buffers and typed slice byte uploads. | The generic upload boundary accepts only `GpuAbi` values, so complete initialized, padding-free object representations are copied into capacity-checked Metal buffers. | JPEG Metal ABI policy, batch, viewport, and buffer reuse tests. | | `crates/j2k-jpeg-metal/src/encode.rs` | Unsafe borrowed Metal JPEG input-tile construction and raw buffer access. | The described range is initialized, immutable, live through synchronous encode completion, and usable by the consuming session's device; raw handles do not bypass those obligations. | JPEG Metal encode tests/benchmarks, API policy, and strict Metal validation. | -| `crates/j2k-jpeg-metal/src/surface.rs` | Unsafe raw buffer/texture access plus shared allocation gates for reusable outputs and derived tiles. | Prior writers complete; callers of raw access exclude overlapping CPU/GPU access; all safe writers/readers sharing an allocation retain one gate through command completion; private keepalive resources outlive every tile clone. | JPEG Metal raw-resource policy, reusable-output gate/poison tests, raw-handoff tests, and strict Metal suites. | +| `crates/j2k-jpeg-metal/src/encode/resident_tile.rs` | Safe JPEG encode-tile construction over an opaque resident image's audited raw read binding. | The borrowed resident owner keeps the same-device allocation live and logically immutable for the tile lifetime; the constructor copies only validated layout metadata and never exposes the raw handle. | Resident JPEG encode round-trip, cross-device rejection, API snapshot, and strict Metal validation. | +| `crates/j2k-jpeg-metal/src/surface.rs` | Unsafe raw buffer access for decoded surfaces, checked host readback, and resident-image conversion. | Prior writers complete; raw-handle callers exclude overlapping CPU/GPU access; safe readback holds the allocation's shared access gate; consuming resident conversion is limited to immutable resident storage rather than reusable batch outputs. | JPEG Metal raw-resource policy, surface readback/residency tests, raw-handoff tests, and strict Metal suites. | +| `crates/j2k-jpeg-metal/src/surface/batch_buffer.rs` | Unsafe raw access to a reusable batch output buffer and the shared allocation gate used by derived surfaces. | Raw-handle callers complete prior work and exclude all safe or aliased access; safe decode and readback paths retain and lock the same gate across the output owner and every derived surface. | Reusable buffer gate-sharing, poison, output-reuse, and batch decode tests plus the JPEG Metal raw-resource policy. | +| `crates/j2k-jpeg-metal/src/surface/batch_texture.rs` | Unsafe raw access to reusable batch output textures and allocation-set cloning. | Raw-handle callers complete prior work and exclude overlapping CPU/GPU access through every texture alias; slot clones and derived tiles retain the same allocation set and shared access gate through command completion. | Texture allocation/gate-sharing, slot-clone, poison, reuse, and batch decode tests plus strict Metal suites. | +| `crates/j2k-jpeg-metal/src/surface/resident_tile.rs` | Adoption and unsafe raw access for completed private JPEG output buffers behind an opaque resident image. | Construction occurs only after decode completion, validates the image layout and owning device, and retains private status/lookup keepalives; raw callers preserve logical immutability and synchronization for every cloned handle. | Private resident-tile conversion, clone/residency, raw-handoff, cross-device, and strict Metal tests. | +| `crates/j2k-jpeg-metal/src/surface/texture_tile.rs` | Unsafe raw access to a decoded Metal texture tile. | Decode completion precedes raw use; callers exclude overlapping GPU work or aliased access; every clone retains both the texture allocation and the shared access gate. | Texture-tile clone, residency, gate-sharing, readback, and strict Metal tests. | | `crates/j2k-jpeg-metal/src/tests/textures.rs` | Test-only raw texture/private-buffer inspection. | Decode completion precedes inspection and the tests submit no overlapping command or mutation through either alias. | JPEG Metal texture and private-output regression tests on the Metal release lane. | | `crates/j2k-jpeg-metal/src/tests/textures/residency.rs` | Test-only raw private-buffer inspection for texture-backed tile residency. | Decode completion precedes inspection; the retained tile keeps the allocation live, and the test submits no overlapping GPU work or CPU mutation through the raw alias. | JPEG Metal private-texture residency and tile-readback regression tests on the Metal release lane. | | `crates/j2k-jpeg-metal/tests/batch.rs` | Test-only raw Metal lookup on a CPU-backed surface. | A CPU surface has no Metal allocation, so there is no aliased GPU access to synchronize. | JPEG Metal batch integration tests. | @@ -94,8 +105,10 @@ moved, or removed. | `crates/j2k-metal-support/src/allocation.rs` | Raw Objective-C construction of Metal buffers, texture descriptors, and textures. | Checked products and device/repository caps precede dispatch; every retained pointer is checked for nil before a foreign owning handle is formed; uploads copy into Metal-owned storage and no borrowed no-copy constructor is exposed. | Pure cap/overflow/zero-sized-ABI tests, injected-nil tests, real Metal allocation/texture tests, and strict all-target Clippy. | | `crates/j2k-metal-support/src/buffer_access.rs` | Raw CPU access to shared Metal-buffer contents. | Typed ranges require padding-free `GpuAbi`, checked byte arithmetic, alignment, bounds, and CPU-visible storage; callers must prove command completion and exclusive access. | Checked read/write/fill regressions, nil/range/alignment tests, adapter alias-order policies, and real Metal tests. | | `crates/j2k-metal-support/src/pipeline.rs` | Raw Objective-C construction and error extraction for compile options, shader libraries, and compute pipelines. | Selector ABIs are fixed, Objective-C errors are copied into owned Rust text, and nil is rejected before any foreign owning handle is formed. | Injected-nil compile-option tests, shader/pipeline compile tests, and strict all-target Clippy. | +| `crates/j2k-metal-support/src/resident.rs` | Opaque immutable Metal image ownership, unsafe raw adoption/access, and completion-retaining image submissions. | Layout construction and adoption validate dimensions, pitch, overflow, allocation bounds, and device identity; safe clones and subviews expose no mutable handle; producers transfer exclusive outputs and every raw-bound input into a submission that retains them until successful completion or a blocking drop. | Layout/bounds/device regressions, input-retention and drop-before-wait tests, resident JPEG/J2K encode parity, unsafe API snapshots, and strict Metal validation. | | `crates/j2k-metal-support/src/runtime.rs` | Raw Objective-C construction of command queues, command buffers, and compute/blit encoders. | Each raw result is checked for nil before a foreign handle is formed; non-owning command/encoder results are retained into owned Rust handles before the creation autorelease pool can drain; completion status is validated before safe readback or reuse. | Injected-nil queue/buffer/encoder tests, an autorelease-pool escape regression, real command-resource tests, completion regressions, and strict all-target Clippy. | | `crates/j2k-metal-support/src/tests.rs` | Test-only padding-free ABI marker, injected nil pointers, and raw checked buffer-access calls. | The synthetic ABI type intentionally proves zero-sized rejection; nil pointers are never dereferenced or wrapped; real buffers are completed or never submitted and exclusively accessed during each checked read/write. | The Metal support tests themselves plus the unsafe-audit path inventory. | +| `crates/j2k-metal-support/src/tests/resident.rs` | Test-only resident-image adoption, raw read binding, completed readback, and submission construction. | Adopted inputs are synchronously initialized without writable aliases; outputs are fresh and exclusively written by their retained command; raw input bindings are included in submission keepalives; readback occurs only after successful completion. | Layout/bounds/device, input-retention, immutable output, and drop-before-wait regressions on the Metal release lane. | | `crates/j2k-transcode-metal/src/metal/buffers.rs` | Checked typed uploads and owned readback for Metal transcode staging buffers. | Uploads occur before submission with validated offsets and exclusive staging access; readbacks occur only after command completion and copy a checked initialized range into owned storage. | Metal transcode buffer, DCT53/DCT97 parity, and validation tests. | | `crates/j2k-transcode-metal/src/metal/runtime.rs` | Unsafe `GpuAbi` marker implementations for Metal transcode shader records. | Every marked type is `repr(C)`, `Copy`, contains only plain-data scalar fields, accepts every bit pattern, and matches its Metal shader declaration. | Metal transcode ABI/layout tests, shader compilation, and runtime parity validation. | | `crates/j2k-transcode-metal/tests/route_report.rs` | Test-only raw Metal codestream descriptor construction. | The fresh shared allocation has a valid selected range, no pending writer, remains immutable while described, and is consumed only by the route-report test. | Metal transcode route-report integration tests. | diff --git a/engineering/ai-codebase-audit-remediation-plan.md b/engineering/ai-codebase-audit-remediation-plan.md index 06fad2c7..0907f926 100644 --- a/engineering/ai-codebase-audit-remediation-plan.md +++ b/engineering/ai-codebase-audit-remediation-plan.md @@ -852,7 +852,7 @@ The rubric was checked against current primary or first-party sources on | REL-001 | P1 | complete | REC-001 | Local stale tag is absent and the final `0.7.0` changelog is dated 2026-07-12 without provisional Unreleased/staged markers | | BUILD-001 | P1 | complete | — | Host and CUDA-Oxide strict Clippy failures are fixed without allows, including removal of a stale platform-dependent lint expectation | | TEST-001 | P1 | complete | — | All 38 new ignores have exact dispositions | -| METAL-001 | P1 | complete | TEST-001 | Hosted compile and fail-closed runtime lanes; exact-SHA rerun belongs to FINAL-001 | +| METAL-001 | P1 | complete | TEST-001 | Fail-closed runtime inventory follows the final test-module paths, and release-mode scheduler counters clear ownership outside debug assertions; the full real-Metal release lane passes, with a clean exact-SHA rerun belonging to FINAL-001 | | POLICY-001 | P1 | complete | BUILD-001 | Public API scan and strict lane repaired | | CI-001 | P1 | complete | METAL-001 | Shared exact-SHA workflow verifier fails closed unless private vulnerability reporting is enabled | | PUB-001 | P1 | complete | CI-001, POLICY-001 | Candidate aggregate requires both ordinary and authoritative strict Clippy without replacing either gate | @@ -955,7 +955,7 @@ The rubric was checked against current primary or first-party sources on | PKG-001 | P1 | in progress | SEC-001 | Staged unpublished dependency closure is repaired and the clean package gate passed at `4c947c2b`; rerun after `3fd23cf2` and every later source commit | | CLONE-001 | P2 | complete | STR-008 through STR-015 | Repository-owned source-aware scanner passes after `59301ac4` across 1,192 staged Rust sources at 1.97% duplicated production lines; exact candidate verification still repeats the gate under FINAL-001 | | AUDIT-001 | P2 | complete | STR-010, COV-001 | Clone and panic quality gates use shared production-source classification, explicit reviewed ratchets, fixtures, and fail-closed repository policy | -| PERF-001 | P1 | in progress | STR-004 through STR-015 | Alternating `0e78229a`/candidate process medians pass on the source committed as `3fd23cf2`: direct -3.1%, resident buffer -6.7%, resident host -15.9%; exact committed-candidate repetition remains | +| PERF-001 | P1 | in progress | STR-004 through STR-015 | Alternating `0e78229a`/candidate Metal process medians pass on the source committed as `3fd23cf2`; the first exact CPU quick guard was invalidated by heavy concurrent host load and reported broad unrelated slowdowns, so an uncontended rerun against the immutable baseline remains without threshold or baseline changes | | PUB-002 | P1 | complete | PKG-001, CUDA-002 | Fail-closed canonical origin, exact remote annotated tag, Release, and crates.io preflight | | DOC-002 | P2 | complete | SEC-001 | Public/support/unsafe/API documentation is reconciled; warning-denied docs, downstream examples, and ordinary/final support gates pass, and this remains the sole execution ledger | | CONTACT-001 | P1 | blocked on maintainer action | DOC-002 | Publish and verify a working private vulnerability/conduct-reporting channel | @@ -2014,6 +2014,22 @@ tests, or begin another coverage tranche. Because it changes tracked policy, the candidate SHA must be advanced and the exact host evidence rerun once from the clean committed tree. +The first real-Metal candidate pass then exposed two release-only integration +defects. The fail-closed inventory still named three tests at their pre-split +module paths, so it correctly rejected the moved `routing` and +`stage_validation` tests even though the tests themselves remained present. +After those exact names were reconciled, four scheduler-ownership assertions +failed only in optimized test builds: the test counter reset lived inside +`debug_assert_eq!`, so release compilation removed the state transition. The +counter now performs `Cell::replace(0)` unconditionally and uses the debug +assertion only to validate the previous value. This changes no production +code, but keeps release-mode lifecycle evidence truthful. The four originally +failing optimized tests pass individually, the ignored inventory lists all 18 +required tests exactly, and the complete fail-closed real-Metal release command +passes across support, JPEG, J2K, transcode, facade, and ignored hardware +tests. These tracked corrections advance the candidate and require fresh +exact-SHA host and Metal coverage artifacts. + The first fresh NVIDIA build compiled all ten CUDA-Oxide kernel families, then strict Linux Clippy rejected a stale `cast_precision_loss` expectation in the transcode fixture builder. The helper already uses the lossless diff --git a/engineering/public-api-review-0.7.0.yml b/engineering/public-api-review-0.7.1.yml similarity index 73% rename from engineering/public-api-review-0.7.0.yml rename to engineering/public-api-review-0.7.1.yml index ab9421c3..dd9b9313 100644 --- a/engineering/public-api-review-0.7.0.yml +++ b/engineering/public-api-review-0.7.1.yml @@ -1,7 +1,7 @@ version: 2 baseline_tag: v0.6.2 baseline_version: 0.6.2 -candidate_version: 0.7.0 +candidate_version: "0.7.1" reviews: j2k: removed_fingerprint: "fnv1a64:6636444baca7f77c" @@ -19,10 +19,10 @@ reviews: hidden_rationale: "Reviewed the complete reachable rustdoc-hidden j2k-core inventory, including root re-export forms, batch collection internals, and GPU ABI implementations; the removed collector is replaced by fallible ordered collectors that preserve infrastructure failures." j2k-codec-math: removed_fingerprint: "none" - added_fingerprint: "fnv1a64:7309e987cbc1c09c" + added_fingerprint: "fnv1a64:2edcbda35cf5bab0" hidden_count: 0 hidden_fingerprint: "none" - rationale: "Reviewed the complete first-published j2k-codec-math API: it is a no_std, unsafe-free, allocation-free support crate containing shared codec constants, generated shader fragments, canonical Huffman validation, and fixed-capacity DWT geometry helpers, with no historical compatibility baseline." + rationale: "Reviewed the complete first-published j2k-codec-math API: it is a no_std, unsafe-free, allocation-free support crate containing shared codec constants, generated shader fragments, canonical Huffman validation, fixed-capacity DWT geometry helpers, and canonical classic JPEG 2000 MQ/context lookup tables used by the CUDA classic decoder, with no historical compatibility baseline." j2k-jpeg: removed_fingerprint: "fnv1a64:e9c8b4bcdfd4d07c" added_fingerprint: "fnv1a64:22365f198fcff3a3" @@ -39,18 +39,18 @@ reviews: hidden_rationale: "Reviewed the complete reachable rustdoc-hidden j2k-tilecodec inventory; codec and scratch implementations remain reachable while implementation-only module exposure is contracted, and all supported failures use the reviewed source-preserving error boundary." j2k-jpeg-metal: removed_fingerprint: "fnv1a64:7dba0ea76bbcfc83" - added_fingerprint: "fnv1a64:0b146f81fdb1b4c5" - hidden_count: 81 - hidden_fingerprint: "fnv1a64:79623e7d37b72c31" - rationale: "Reviewed the Metal JPEG adapter redesign: batch permutations contract to request-and-target APIs, Surface::as_bytes becomes a fallible Cow readback, raw Metal resource access becomes unsafe, and typed Metal support errors preserve synchronization and allocation failures." - hidden_rationale: "Reviewed the complete reachable rustdoc-hidden j2k-jpeg-metal inventory, including reusable output, viewport, submission, and raw-resource seams; resource access that depends on GPU completion is unsafe and the safe readback path is fallible." + added_fingerprint: "fnv1a64:2a1f63ef1ed19f54" + hidden_count: 83 + hidden_fingerprint: "fnv1a64:79227f0534bdbee0" + rationale: "Reviewed the Metal JPEG adapter redesign: batch permutations contract to request-and-target APIs, Surface::as_bytes becomes a fallible Cow readback, decoder surfaces expose an opaque resident image, safe encoders accept that resident image with device validation, raw Metal resource access remains unsafe, and typed Metal support errors preserve synchronization and allocation failures." + hidden_rationale: "Reviewed the complete reachable rustdoc-hidden j2k-jpeg-metal inventory, including reusable output, viewport, submission, and raw-resource seams. ResidentPrivateJpegTile now hands off ResidentMetalImage safely; its previous safe into_buffer escape is intentionally unsafe because callers must prove completion and exclusive ownership." j2k-metal: removed_fingerprint: "fnv1a64:cf808e7021f42efb" - added_fingerprint: "fnv1a64:bfba095818b4b3e8" - hidden_count: 286 - hidden_fingerprint: "fnv1a64:15deb070890f8330" - rationale: "Reviewed the Metal J2K adapter changes: decode and lossless encode permutations consolidate behind request and batch contracts, GPU-backed byte access becomes fallible, raw buffer construction and handoff become unsafe, large owners become move-only, and opaque native errors retain concrete sources." - hidden_rationale: "Reviewed the complete reachable rustdoc-hidden j2k-metal inventory, including prepared-plan cache, pooled resource, submission, and diagnostic surfaces; lifecycle-sensitive raw access is unsafe and source-preserving typed failures cover cache, accounting, and native boundaries." + added_fingerprint: "fnv1a64:030e13beed78dfc3" + hidden_count: 287 + hidden_fingerprint: "fnv1a64:79d7834b64cda3b0" + rationale: "Reviewed the Metal J2K adapter changes: decode and lossless encode permutations consolidate behind request and batch contracts, decoder surfaces expose an opaque resident image, safe encoders accept that resident image with device validation, GPU-backed byte access becomes fallible, raw buffer construction and handoff remain unsafe, large owners become move-only, and opaque native errors retain concrete sources." + hidden_rationale: "Reviewed the complete reachable rustdoc-hidden j2k-metal inventory, including prepared-plan cache, pooled resource, submission, resident-image integration, and diagnostic surfaces; lifecycle-sensitive raw access is unsafe and source-preserving typed failures cover cache, accounting, device identity, and native boundaries." j2k-jpeg-cuda: removed_fingerprint: "fnv1a64:b89441cc882c4788" added_fingerprint: "fnv1a64:c7feb31cdb60d809" @@ -61,16 +61,16 @@ reviews: j2k-cuda: removed_fingerprint: "fnv1a64:26bb93a098670693" added_fingerprint: "fnv1a64:b89582cdf65a54b1" - hidden_count: 165 - hidden_fingerprint: "fnv1a64:6276b4cc00532d7e" + hidden_count: 171 + hidden_fingerprint: "fnv1a64:171da25cdbd064de" rationale: "Reviewed the CUDA J2K adapter contraction: resident codestream ownership is split into metadata and a device-buffer owner, deprecated host outcome duplication is removed, encode-stage failures become typed, and opaque native and concrete CUDA runtime sources remain inspectable." - hidden_rationale: "Reviewed the complete reachable rustdoc-hidden j2k-cuda inventory, including resident decode, encode, profile, and packetization adapters; owner graphs are move-only where payload duplication was unsafe and lifecycle-sensitive runtime work remains session-bound." + hidden_rationale: "Reviewed the complete reachable rustdoc-hidden j2k-cuda inventory, including resident decode, encode, HT-versus-classic decode profiling, and packetization adapters; owner graphs are move-only where payload duplication was unsafe and lifecycle-sensitive runtime work remains session-bound." j2k-transcode: removed_fingerprint: "fnv1a64:59c18a5d5c568d82" - added_fingerprint: "fnv1a64:3634410eca693882" + added_fingerprint: "fnv1a64:5134f2653e8e74c1" hidden_count: 455 hidden_fingerprint: "fnv1a64:fdd61f6fdda4a9ab" - rationale: "Reviewed the transcode redesign: accelerator, transform, metrics, grid, allocation, resident-handoff, and HTJ2K encode failures become typed and source-preserving; large transform and packetization owners become move-only and implementation modules consolidate behind root contracts." + rationale: "Reviewed the transcode redesign: accelerator, transform, metrics, grid, allocation, resident-handoff, and HTJ2K encode failures become typed and source-preserving; large transform and packetization owners become move-only, implementation modules consolidate behind root contracts, and the DCT97 grid scratch owner is public so production callers can reuse allocation without depending on a private module." hidden_rationale: "Reviewed the complete reachable rustdoc-hidden j2k-transcode inventory, including resident-buffer handoffs and the opaque HTJ2K native encode source boundary; geometry, allocation, backend, and invariant failures remain distinguishable without string parsing." j2k-transcode-cuda: removed_fingerprint: "fnv1a64:bc3b3cb7278c3546" @@ -81,11 +81,11 @@ reviews: hidden_rationale: "Reviewed the complete reachable rustdoc-hidden j2k-transcode-cuda inventory; low-level accelerator and resident paths use the reviewed typed transcode error boundary and preserve detailed CUDA runtime sources." j2k-metal-support: removed_fingerprint: "fnv1a64:1e97dea61ab37693" - added_fingerprint: "fnv1a64:1813c38175e0bf26" - hidden_count: 10 - hidden_fingerprint: "fnv1a64:7289d93412564805" - rationale: "Reviewed the Metal support contraction: MetalDeviceSession and borrowed buffer slices are removed, MetalRuntimeSession owns lifecycle state, allocation and command creation are checked and fallible, and CPU/GPU synchronization-sensitive buffer reads, writes, and fills are explicitly unsafe." - hidden_rationale: "Reviewed the complete reachable rustdoc-hidden j2k-metal-support inventory; checked command, allocation, completion, and unsafe buffer operations expose typed MetalSupportError failures without returning synchronization-dependent borrowed storage." + added_fingerprint: "fnv1a64:f387ac03f02fd787" + hidden_count: 11 + hidden_fingerprint: "fnv1a64:671e50d4d121b419" + rationale: "Reviewed the Metal support contraction and ownership API: MetalDeviceSession and borrowed buffer slices are removed, MetalRuntimeSession owns lifecycle state, MetalImageLayout validates image bounds, ResidentMetalImage keeps buffers opaque and device-identified, SubmittedMetalImages retains inputs and outputs through completion, allocation and command creation are checked and fallible, and CPU/GPU synchronization-sensitive raw operations are explicitly unsafe." + hidden_rationale: "Reviewed the complete reachable rustdoc-hidden j2k-metal-support inventory; checked command, allocation, completion, and unsafe buffer operations expose typed MetalSupportError failures without returning synchronization-dependent borrowed storage. The hidden in-flight-buffer adoption constructor is unsafe and exists only for audited producers that transfer exclusive ownership into a retaining submission." j2k-transcode-metal: removed_fingerprint: "fnv1a64:cb7b5d9d32919719" added_fingerprint: "fnv1a64:c22ab3f78bbed4fc" @@ -109,11 +109,11 @@ reviews: hidden_rationale: "Reviewed the complete reachable rustdoc-hidden j2k-types inventory, including resident input validation, move-only large encode graphs, and typed stage failure classification shared by facade, native, Metal, and CUDA implementations." j2k-cuda-runtime: removed_fingerprint: "fnv1a64:de98d6fa72f8a1f1" - added_fingerprint: "fnv1a64:8fbf11477fdd930c" - hidden_count: 847 - hidden_fingerprint: "fnv1a64:4a17a6e4f32a7fbc" - rationale: "Reviewed the CUDA runtime contraction: ordinary root exports narrow to supported context, memory, execution, and error primitives; implementation-detail kernels move out of the documented surface, and completion, host allocation, invariant, and resource-release failures receive typed variants." - hidden_rationale: "Reviewed the complete reachable rustdoc-hidden j2k-cuda-runtime inventory, including asynchronous cleanup, IDWT, queued execution, pooled allocation, and packet ABI surfaces; enqueue and early-release operations requiring caller lifecycle proofs are unsafe and partial failures preserve both primary and cleanup errors." + added_fingerprint: "fnv1a64:c95260f698f6546d" + hidden_count: 918 + hidden_fingerprint: "fnv1a64:61e8d94589aa373a" + rationale: "Reviewed the CUDA runtime contraction: ordinary root exports narrow to supported context, memory, execution, and error primitives; the added classic JPEG 2000 decode ABI, pooled coefficient allocation, payload upload, and timed multi-target dispatch use the existing context and buffer-pool ownership model; implementation-detail kernels remain outside the documented surface, and completion, host allocation, invariant, and resource-release failures receive typed variants." + hidden_rationale: "Reviewed the complete reachable rustdoc-hidden j2k-cuda-runtime inventory, including asynchronous cleanup, classic and HT decode jobs, IDWT, queued execution, pooled allocation, and packet ABI surfaces; enqueue and early-release operations requiring caller lifecycle proofs are unsafe and partial failures preserve both primary and cleanup errors." j2k-profile: removed_fingerprint: "none" added_fingerprint: "none" diff --git a/engineering/reviewed-public-api-diff-0.7.0.md b/engineering/reviewed-public-api-diff-0.7.1.md similarity index 98% rename from engineering/reviewed-public-api-diff-0.7.0.md rename to engineering/reviewed-public-api-diff-0.7.1.md index 45867867..abfca714 100644 --- a/engineering/reviewed-public-api-diff-0.7.0.md +++ b/engineering/reviewed-public-api-diff-0.7.1.md @@ -1,37 +1,37 @@ -# Reviewed public API diff for j2k 0.7.0 +# Reviewed public API diff for j2k 0.7.1 -This report is generated by `cargo xtask semver --write-report`. Normal `cargo xtask semver` regenerates it in memory and fails if this committed file is stale. Every ordinary added/removed fingerprint and every full rustdoc-hidden candidate-inventory fingerprint requires an exact reviewed entry in `engineering/public-api-review-0.7.0.yml`; report regeneration never updates that review config. +This report is generated by `cargo xtask semver --write-report`. Normal `cargo xtask semver` regenerates it in memory and fails if this committed file is stale. Every ordinary added/removed fingerprint and every full rustdoc-hidden candidate-inventory fingerprint requires an exact reviewed entry in `engineering/public-api-review-0.7.1.yml`; report regeneration never updates that review config. - Baseline registry version: `0.6.2` - Baseline source snapshot: `v0.6.2` peeled to `55ee746e1b49f7309e4d030cc01a69d580173920` -- Candidate version: `0.7.0` +- Candidate version: `0.7.1` - Tool pins: Rust `1.96`, `cargo-semver-checks 0.48.0`, `cargo-public-api 0.52.0`, rustdoc `nightly-2026-06-28`, target `aarch64-apple-darwin` ## Summary | Package | Baseline | Candidate | Computed release type | Added | Removed/changed | Removed fingerprint | Added fingerprint | Rustdoc-hidden items | Hidden inventory fingerprint | | --- | --- | --- | --- | ---: | ---: | --- | --- | ---: | --- | -| `j2k` | `0.6.2` | `0.7.0` | `major` | 138 | 392 | `fnv1a64:6636444baca7f77c` | `fnv1a64:ff6625dda2d1913c` | 100 | `fnv1a64:0d7c7465a7899265` | -| `j2k-core` | `0.6.2` | `0.7.0` | `major` | 235 | 586 | `fnv1a64:77731319da5bb00b` | `fnv1a64:d8f97cd365d97513` | 142 | `fnv1a64:6ed51ee7bd843cdb` | -| `j2k-codec-math` | `new/unpublished` | `0.7.0` | `new` | 86 | 0 | `none` | `fnv1a64:7309e987cbc1c09c` | 0 | `none` | -| `j2k-jpeg` | `0.6.2` | `0.7.0` | `major` | 248 | 922 | `fnv1a64:e9c8b4bcdfd4d07c` | `fnv1a64:22365f198fcff3a3` | 550 | `fnv1a64:694878382ed6212c` | -| `j2k-tilecodec` | `0.6.2` | `0.7.0` | `major` | 5 | 35 | `fnv1a64:d88ac6fa29213015` | `fnv1a64:0df1685ea89ef064` | 32 | `fnv1a64:eb677842559d9b0e` | -| `j2k-jpeg-metal` | `0.6.2` | `0.7.0` | `major` | 62 | 130 | `fnv1a64:7dba0ea76bbcfc83` | `fnv1a64:0b146f81fdb1b4c5` | 81 | `fnv1a64:79623e7d37b72c31` | -| `j2k-metal` | `0.6.2` | `0.7.0` | `major` | 102 | 323 | `fnv1a64:cf808e7021f42efb` | `fnv1a64:bfba095818b4b3e8` | 286 | `fnv1a64:15deb070890f8330` | -| `j2k-jpeg-cuda` | `0.6.2` | `0.7.0` | `major` | 40 | 78 | `fnv1a64:b89441cc882c4788` | `fnv1a64:c7feb31cdb60d809` | 124 | `fnv1a64:b8ecbb08e772efc7` | -| `j2k-cuda` | `0.6.2` | `0.7.0` | `major` | 48 | 255 | `fnv1a64:26bb93a098670693` | `fnv1a64:b89582cdf65a54b1` | 165 | `fnv1a64:6276b4cc00532d7e` | -| `j2k-transcode` | `0.6.2` | `0.7.0` | `major` | 357 | 201 | `fnv1a64:59c18a5d5c568d82` | `fnv1a64:3634410eca693882` | 455 | `fnv1a64:fdd61f6fdda4a9ab` | -| `j2k-transcode-cuda` | `0.6.2` | `0.7.0` | `major` | 19 | 19 | `fnv1a64:bc3b3cb7278c3546` | `fnv1a64:a6bda71caf027368` | 17 | `fnv1a64:3e3e69d35c4a62a5` | -| `j2k-metal-support` | `0.6.2` | `0.7.0` | `major` | 76 | 22 | `fnv1a64:1e97dea61ab37693` | `fnv1a64:1813c38175e0bf26` | 10 | `fnv1a64:7289d93412564805` | -| `j2k-transcode-metal` | `0.6.2` | `0.7.0` | `major` | 55 | 14 | `fnv1a64:cb7b5d9d32919719` | `fnv1a64:c22ab3f78bbed4fc` | 49 | `fnv1a64:5bf8a22a33753a2d` | -| `j2k-native` | `0.6.2` | `0.7.0` | `major` | 136 | 499 | `fnv1a64:3a778df648bed7a0` | `fnv1a64:4cb163fa9f7597dc` | 535 | `fnv1a64:7c931c0e7c4eb27b` | -| `j2k-types` | `0.6.2` | `0.7.0` | `major` | 67 | 0 | `none` | `fnv1a64:4b4595dc5d8ff8ce` | 60 | `fnv1a64:bc3b26816db006bf` | -| `j2k-cuda-runtime` | `0.6.2` | `0.7.0` | `major` | 14 | 762 | `fnv1a64:de98d6fa72f8a1f1` | `fnv1a64:8fbf11477fdd930c` | 847 | `fnv1a64:4a17a6e4f32a7fbc` | -| `j2k-profile` | `0.6.2` | `0.7.0` | `major` | 0 | 0 | `none` | `none` | 117 | `fnv1a64:a1546f56ae38f2e3` | +| `j2k` | `0.6.2` | `0.7.1` | `major` | 138 | 392 | `fnv1a64:6636444baca7f77c` | `fnv1a64:ff6625dda2d1913c` | 100 | `fnv1a64:0d7c7465a7899265` | +| `j2k-core` | `0.6.2` | `0.7.1` | `major` | 235 | 586 | `fnv1a64:77731319da5bb00b` | `fnv1a64:d8f97cd365d97513` | 142 | `fnv1a64:6ed51ee7bd843cdb` | +| `j2k-codec-math` | `new/unpublished` | `0.7.1` | `new` | 100 | 0 | `none` | `fnv1a64:2edcbda35cf5bab0` | 0 | `none` | +| `j2k-jpeg` | `0.6.2` | `0.7.1` | `major` | 248 | 922 | `fnv1a64:e9c8b4bcdfd4d07c` | `fnv1a64:22365f198fcff3a3` | 550 | `fnv1a64:694878382ed6212c` | +| `j2k-tilecodec` | `0.6.2` | `0.7.1` | `major` | 5 | 35 | `fnv1a64:d88ac6fa29213015` | `fnv1a64:0df1685ea89ef064` | 32 | `fnv1a64:eb677842559d9b0e` | +| `j2k-jpeg-metal` | `0.6.2` | `0.7.1` | `major` | 65 | 130 | `fnv1a64:7dba0ea76bbcfc83` | `fnv1a64:2a1f63ef1ed19f54` | 83 | `fnv1a64:79227f0534bdbee0` | +| `j2k-metal` | `0.6.2` | `0.7.1` | `major` | 105 | 323 | `fnv1a64:cf808e7021f42efb` | `fnv1a64:030e13beed78dfc3` | 287 | `fnv1a64:79d7834b64cda3b0` | +| `j2k-jpeg-cuda` | `0.6.2` | `0.7.1` | `major` | 40 | 78 | `fnv1a64:b89441cc882c4788` | `fnv1a64:c7feb31cdb60d809` | 124 | `fnv1a64:b8ecbb08e772efc7` | +| `j2k-cuda` | `0.6.2` | `0.7.1` | `major` | 48 | 255 | `fnv1a64:26bb93a098670693` | `fnv1a64:b89582cdf65a54b1` | 171 | `fnv1a64:171da25cdbd064de` | +| `j2k-transcode` | `0.6.2` | `0.7.1` | `major` | 359 | 201 | `fnv1a64:59c18a5d5c568d82` | `fnv1a64:5134f2653e8e74c1` | 455 | `fnv1a64:fdd61f6fdda4a9ab` | +| `j2k-transcode-cuda` | `0.6.2` | `0.7.1` | `major` | 19 | 19 | `fnv1a64:bc3b3cb7278c3546` | `fnv1a64:a6bda71caf027368` | 17 | `fnv1a64:3e3e69d35c4a62a5` | +| `j2k-metal-support` | `0.6.2` | `0.7.1` | `major` | 115 | 22 | `fnv1a64:1e97dea61ab37693` | `fnv1a64:f387ac03f02fd787` | 11 | `fnv1a64:671e50d4d121b419` | +| `j2k-transcode-metal` | `0.6.2` | `0.7.1` | `major` | 55 | 14 | `fnv1a64:cb7b5d9d32919719` | `fnv1a64:c22ab3f78bbed4fc` | 49 | `fnv1a64:5bf8a22a33753a2d` | +| `j2k-native` | `0.6.2` | `0.7.1` | `major` | 136 | 499 | `fnv1a64:3a778df648bed7a0` | `fnv1a64:4cb163fa9f7597dc` | 535 | `fnv1a64:7c931c0e7c4eb27b` | +| `j2k-types` | `0.6.2` | `0.7.1` | `major` | 67 | 0 | `none` | `fnv1a64:4b4595dc5d8ff8ce` | 60 | `fnv1a64:bc3b26816db006bf` | +| `j2k-cuda-runtime` | `0.6.2` | `0.7.1` | `major` | 15 | 762 | `fnv1a64:de98d6fa72f8a1f1` | `fnv1a64:c95260f698f6546d` | 918 | `fnv1a64:61e8d94589aa373a` | +| `j2k-profile` | `0.6.2` | `0.7.1` | `major` | 0 | 0 | `none` | `none` | 117 | `fnv1a64:a1546f56ae38f2e3` | ## New packages without a 0.6.2 registry baseline -- `j2k-codec-math` `0.7.0`: 86 ordinary public API items, fingerprint `fnv1a64:7309e987cbc1c09c`; 0 rustdoc-hidden public API items, full-inventory fingerprint `none`. +- `j2k-codec-math` `0.7.1`: 100 ordinary public API items, fingerprint `fnv1a64:2edcbda35cf5bab0`; 0 rustdoc-hidden public API items, full-inventory fingerprint `none`. ## Published-package details @@ -2654,7 +2654,7 @@ pub j2k_tilecodec::TileCodecError::Malformed::source: std::io::error::Error ### `j2k-jpeg-metal` -Baseline items: 235. Candidate items: 167. Computed release type: `major`. Rustdoc-hidden candidate items: 81. Full hidden-inventory fingerprint: `fnv1a64:79623e7d37b72c31`. +Baseline items: 235. Candidate items: 170. Computed release type: `major`. Rustdoc-hidden candidate items: 83. Full hidden-inventory fingerprint: `fnv1a64:79227f0534bdbee0`. #### Removed or changed baseline API items @@ -2807,6 +2807,7 @@ pub fn j2k_jpeg_metal::Decoder<'a>::decode_request_to_device(&mut self, j2k_jpeg pub fn j2k_jpeg_metal::Decoder<'a>::from_view(j2k_jpeg::decoder::view::JpegView<'a>) -> core::result::Result pub fn j2k_jpeg_metal::JpegBaselineMetalEncodeTile<'a>::byte_offset(&self) -> usize pub fn j2k_jpeg_metal::JpegBaselineMetalEncodeTile<'a>::dimensions(&self) -> (u32, u32) +pub fn j2k_jpeg_metal::JpegBaselineMetalEncodeTile<'a>::from_resident(&'a j2k_metal_support::resident::ResidentMetalImage, (u32, u32)) -> Self pub fn j2k_jpeg_metal::JpegBaselineMetalEncodeTile<'a>::output_dimensions(&self) -> (u32, u32) pub fn j2k_jpeg_metal::JpegBaselineMetalEncodeTile<'a>::pitch_bytes(&self) -> usize pub fn j2k_jpeg_metal::JpegBaselineMetalEncodeTile<'a>::pixel_format(&self) -> j2k_core::pixel::PixelFormat @@ -2814,6 +2815,8 @@ pub fn j2k_jpeg_metal::JpegTileBatch::push_shared_tile_request(&mut self, alloc: pub fn j2k_jpeg_metal::JpegTileBatch::push_tile_request(&mut self, &[u8], j2k_jpeg_metal::MetalDecodeRequest) -> core::result::Result pub fn j2k_jpeg_metal::MetalTextureTile::clone(&self) -> Self pub fn j2k_jpeg_metal::Surface::as_bytes(&self) -> core::result::Result, j2k_jpeg_metal::Error> +pub fn j2k_jpeg_metal::Surface::into_resident_metal_image(self) -> core::option::Option +pub fn j2k_jpeg_metal::Surface::resident_metal_image(&self) -> core::option::Option<&j2k_metal_support::resident::ResidentMetalImage> pub fn j2k_jpeg_metal::choose_viewport_surface_strategy(&j2k_jpeg_metal::ViewportWorkload, j2k_core::backend::BackendRequest) -> core::result::Result pub fn j2k_jpeg_metal::encode_jpeg_baseline_batch_from_metal_buffers(&[j2k_jpeg_metal::JpegBaselineMetalEncodeTile<'_>], j2k_jpeg::baseline_encode_contract::JpegEncodeOptions, &j2k_jpeg_metal::MetalBackendSession) -> core::result::Result, j2k_jpeg_metal::Error> pub fn j2k_jpeg_metal::encode_jpeg_baseline_from_metal_buffer(j2k_jpeg_metal::JpegBaselineMetalEncodeTile<'_>, j2k_jpeg::baseline_encode_contract::JpegEncodeOptions, &j2k_jpeg_metal::MetalBackendSession) -> core::result::Result @@ -2860,7 +2863,7 @@ pub use j2k_jpeg_metal::SurfaceResidency ### `j2k-metal` -Baseline items: 399. Candidate items: 178. Computed release type: `major`. Rustdoc-hidden candidate items: 286. Full hidden-inventory fingerprint: `fnv1a64:15deb070890f8330`. +Baseline items: 399. Candidate items: 181. Computed release type: `major`. Rustdoc-hidden candidate items: 287. Full hidden-inventory fingerprint: `fnv1a64:79d7834b64cda3b0`. #### Removed or changed baseline API items @@ -3226,6 +3229,7 @@ pub fn j2k_metal::MetalEncodedJ2k::to_encoded_j2k(&self) -> core::result::Result pub fn j2k_metal::MetalEncodedJ2k::width(&self) -> u32 pub fn j2k_metal::MetalLosslessEncodeTile<'a>::byte_offset(self) -> usize pub fn j2k_metal::MetalLosslessEncodeTile<'a>::dimensions(self) -> (u32, u32) +pub fn j2k_metal::MetalLosslessEncodeTile<'a>::from_resident(&'a j2k_metal_support::resident::ResidentMetalImage, (u32, u32)) -> Self pub fn j2k_metal::MetalLosslessEncodeTile<'a>::output_dimensions(self) -> (u32, u32) pub fn j2k_metal::MetalLosslessEncodeTile<'a>::pitch_bytes(self) -> usize pub fn j2k_metal::MetalLosslessEncodeTile<'a>::pixel_format(self) -> j2k_core::pixel::PixelFormat @@ -3234,6 +3238,8 @@ pub fn j2k_metal::MetalTileBatch::push_tile_request(&mut self, &[u8], j2k_metal: pub fn j2k_metal::NativeBackendError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub fn j2k_metal::NativeBackendError::source(&self) -> core::option::Option<&(dyn core::error::Error + 'static)> pub fn j2k_metal::Surface::as_bytes(&self) -> core::result::Result, j2k_metal::Error> +pub fn j2k_metal::Surface::into_resident_metal_image(self) -> core::option::Option +pub fn j2k_metal::Surface::resident_metal_image(&self) -> core::option::Option<&j2k_metal_support::resident::ResidentMetalImage> pub fn j2k_metal::submit_lossless_batch(j2k_metal::MetalLosslessEncodeBatchRequest<'_, '_>, &j2k::encode::contracts::J2kLosslessEncodeOptions, &j2k_metal::MetalBackendSession) -> core::result::Result pub fn j2k_metal::submit_lossless_batch_to_metal(j2k_metal::MetalLosslessEncodeBatchRequest<'_, '_>, &j2k::encode::contracts::J2kLosslessEncodeOptions, &j2k_metal::MetalBackendSession) -> core::result::Result pub fn j2k_metal::validate_lossless_roundtrip_on_metal(j2k::encode::samples::J2kLosslessSamples<'_>, &[u8]) -> core::result::Result<(), j2k_metal::Error> @@ -3431,7 +3437,7 @@ pub struct j2k_jpeg_cuda::JpegBaselineCudaEncodeTile<'a> ### `j2k-cuda` -Baseline items: 330. Candidate items: 123. Computed release type: `major`. Rustdoc-hidden candidate items: 165. Full hidden-inventory fingerprint: `fnv1a64:6276b4cc00532d7e`. +Baseline items: 330. Candidate items: 123. Computed release type: `major`. Rustdoc-hidden candidate items: 171. Full hidden-inventory fingerprint: `fnv1a64:171da25cdbd064de`. #### Removed or changed baseline API items @@ -3748,7 +3754,7 @@ pub use j2k_cuda::SurfaceResidency ### `j2k-transcode` -Baseline items: 268. Candidate items: 424. Computed release type: `major`. Rustdoc-hidden candidate items: 455. Full hidden-inventory fingerprint: `fnv1a64:fdd61f6fdda4a9ab`. +Baseline items: 268. Candidate items: 426. Computed release type: `major`. Rustdoc-hidden candidate items: 455. Full hidden-inventory fingerprint: `fnv1a64:fdd61f6fdda4a9ab`. #### Removed or changed baseline API items @@ -4052,6 +4058,7 @@ pub fn j2k_transcode::TranscodeStageError::fmt(&self, &mut core::fmt::Formatter< pub fn j2k_transcode::TranscodeStageError::source(&self) -> core::option::Option<&(dyn core::error::Error + 'static)> pub fn j2k_transcode::dct8x8_blocks_then_dwt53_float(&[[[f64; 8]; 8]], usize, usize, usize, usize) -> core::result::Result, j2k_transcode::DctTransformError> pub fn j2k_transcode::dct8x8_blocks_then_dwt97_float(&[[[f64; 8]; 8]], usize, usize, usize, usize) -> core::result::Result, j2k_transcode::DctTransformError> +pub fn j2k_transcode::dct8x8_blocks_then_dwt97_float_with_scratch(&[[[f64; 8]; 8]], usize, usize, usize, usize, &mut j2k_transcode::Dct97GridScratch) -> core::result::Result, j2k_transcode::DctTransformError> pub fn j2k_transcode::dct8x8_blocks_to_dwt53_float_linear(&[[[f64; 8]; 8]], usize, usize, usize, usize) -> core::result::Result, j2k_transcode::DctTransformError> pub fn j2k_transcode::htj2k97_subband_delta(j2k_transcode::Htj2k97CodeBlockOptions, j2k_types::J2kSubBandType) -> f64 pub fn j2k_transcode::htj2k97_subband_total_bitplanes(j2k_transcode::Htj2k97CodeBlockOptions, j2k_types::J2kSubBandType) -> u8 @@ -4270,6 +4277,7 @@ pub j2k_transcode::TranscodeStageError::MemoryCapExceeded::cap: usize pub j2k_transcode::TranscodeStageError::MemoryCapExceeded::requested: usize pub j2k_transcode::TranscodeStageError::Unsupported(&'static str) pub struct j2k_transcode::CpuOnlyDctToWaveletStageAccelerator +pub struct j2k_transcode::Dct97GridScratch pub struct j2k_transcode::DctGridI16ToHtj2k97CodeBlockBatch<'a, 'j> pub struct j2k_transcode::DctGridI16ToHtj2k97CodeBlockJob<'a> pub struct j2k_transcode::DctGridToDwt53Job<'a> @@ -4372,7 +4380,7 @@ pub struct j2k_transcode_cuda::CudaRuntimeFailure ### `j2k-metal-support` -Baseline items: 69. Candidate items: 123. Computed release type: `major`. Rustdoc-hidden candidate items: 10. Full hidden-inventory fingerprint: `fnv1a64:7289d93412564805`. +Baseline items: 69. Candidate items: 162. Computed release type: `major`. Rustdoc-hidden candidate items: 11. Full hidden-inventory fingerprint: `fnv1a64:671e50d4d121b419`. #### Removed or changed baseline API items @@ -4407,10 +4415,29 @@ pub unsafe fn j2k_metal_support::buffer_contents_slice_mut core::clone::Clone for j2k_metal_support::MetalRuntimeSession impl j2k_metal_support::MetalRuntimeSession +pub const fn j2k_metal_support::MetalImageLayout::byte_len(self) -> usize +pub const fn j2k_metal_support::MetalImageLayout::byte_offset(self) -> usize +pub const fn j2k_metal_support::MetalImageLayout::dimensions(self) -> (u32, u32) +pub const fn j2k_metal_support::MetalImageLayout::pitch_bytes(self) -> usize +pub const fn j2k_metal_support::MetalImageLayout::pixel_format(self) -> j2k_core::pixel::PixelFormat +pub const fn j2k_metal_support::ResidentMetalImage::byte_len(&self) -> usize +pub const fn j2k_metal_support::ResidentMetalImage::byte_offset(&self) -> usize +pub const fn j2k_metal_support::ResidentMetalImage::dimensions(&self) -> (u32, u32) +pub const fn j2k_metal_support::ResidentMetalImage::layout(&self) -> j2k_metal_support::MetalImageLayout +pub const fn j2k_metal_support::ResidentMetalImage::pitch_bytes(&self) -> usize +pub const fn j2k_metal_support::ResidentMetalImage::pixel_format(&self) -> j2k_core::pixel::PixelFormat pub fn j2k_metal_support::MetalCommandEncoderKind::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub fn j2k_metal_support::MetalImageLayout::new(usize, (u32, u32), usize, j2k_core::pixel::PixelFormat) -> core::result::Result pub fn j2k_metal_support::MetalPipelineLoader::new(&metal::device::DeviceRef, &str) -> core::result::Result pub fn j2k_metal_support::MetalRuntimeSession::clone(&self) -> Self pub fn j2k_metal_support::MetalRuntimeSession::device(&self) -> &metal::device::DeviceRef @@ -4420,6 +4447,13 @@ pub fn j2k_metal_support::MetalRuntimeSession::new(metal::device::Device) pub fn j2k_metal_support::MetalRuntimeSession::runtime_initialized(&self) -> bool pub fn j2k_metal_support::MetalRuntimeSession::runtime_result(&self) -> core::option::Option<&core::result::Result> pub fn j2k_metal_support::MetalRuntimeSession::system_default() -> core::result::Result +pub fn j2k_metal_support::ResidentMetalImage::device_registry_id(&self) -> u64 +pub fn j2k_metal_support::ResidentMetalImage::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub fn j2k_metal_support::ResidentMetalImage::validate_device(&self, &metal::device::DeviceRef) -> core::result::Result<(), j2k_metal_support::MetalSupportError> +pub fn j2k_metal_support::ResidentMetalImage::view(&self, j2k_metal_support::MetalImageLayout) -> core::result::Result +pub fn j2k_metal_support::SubmittedMetalImages::drop(&mut self) +pub fn j2k_metal_support::SubmittedMetalImages::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub fn j2k_metal_support::SubmittedMetalImages::wait(self) -> core::result::Result pub fn j2k_metal_support::checked_blit_command_encoder(&metal::commandbuffer::CommandBufferRef) -> core::result::Result pub fn j2k_metal_support::checked_command_buffer(&metal::commandqueue::CommandQueueRef) -> core::result::Result pub fn j2k_metal_support::checked_command_queue(&metal::device::DeviceRef) -> core::result::Result @@ -4462,6 +4496,11 @@ pub j2k_metal_support::MetalSupportError::CommandEncoderCreation::kind: j2k_meta pub j2k_metal_support::MetalSupportError::CommandEncoderCreation::message: alloc::string::String pub j2k_metal_support::MetalSupportError::CommandEncoderUnavailable pub j2k_metal_support::MetalSupportError::CommandEncoderUnavailable::kind: j2k_metal_support::MetalCommandEncoderKind +pub j2k_metal_support::MetalSupportError::MetalImageDeviceMismatch +pub j2k_metal_support::MetalSupportError::MetalImageDeviceMismatch::image_registry_id: u64 +pub j2k_metal_support::MetalSupportError::MetalImageDeviceMismatch::requested_registry_id: u64 +pub j2k_metal_support::MetalSupportError::MetalImageLayout +pub j2k_metal_support::MetalSupportError::MetalImageLayout::reason: &'static str pub j2k_metal_support::MetalSupportError::TextureAllocation pub j2k_metal_support::MetalSupportError::TextureAllocation::message: alloc::string::String pub j2k_metal_support::MetalSupportError::TextureAllocationFailed @@ -4475,7 +4514,15 @@ pub j2k_metal_support::MetalSupportError::TextureAllocationTooLarge::requested: pub j2k_metal_support::MetalSupportError::TextureDescriptorInvalid pub j2k_metal_support::MetalSupportError::TextureDescriptorInvalid::reason: &'static str pub j2k_metal_support::MetalSupportError::TextureDescriptorUnavailable +pub struct j2k_metal_support::MetalImageLayout pub struct j2k_metal_support::MetalRuntimeSession +pub struct j2k_metal_support::ResidentMetalImage +pub struct j2k_metal_support::SubmittedMetalImages +pub type j2k_metal_support::SubmittedMetalImages::Error = j2k_metal_support::MetalSupportError +pub type j2k_metal_support::SubmittedMetalImages::Output = alloc::vec::Vec +pub unsafe fn j2k_metal_support::ResidentMetalImage::from_completed_buffer(metal::buffer::Buffer, j2k_metal_support::MetalImageLayout) -> core::result::Result +pub unsafe fn j2k_metal_support::ResidentMetalImage::raw_buffer(&self) -> &metal::buffer::Buffer +pub unsafe fn j2k_metal_support::SubmittedMetalImages::from_uncommitted(&metal::device::DeviceRef, metal::commandbuffer::CommandBuffer, alloc::vec::Vec<(metal::buffer::Buffer, j2k_metal_support::MetalImageLayout)>, alloc::vec::Vec) -> core::result::Result pub unsafe fn j2k_metal_support::checked_buffer_fill_bytes(&metal::buffer::Buffer, usize, usize, u8) -> core::result::Result<(), j2k_metal_support::MetalSupportError> pub unsafe fn j2k_metal_support::checked_buffer_read(&metal::buffer::Buffer, usize) -> core::result::Result pub unsafe fn j2k_metal_support::checked_buffer_read_vec(&metal::buffer::Buffer, usize, usize) -> core::result::Result, j2k_metal_support::MetalSupportError> @@ -5296,7 +5343,7 @@ pub type j2k_types::J2kEncodeStageResult = core::result::Result core::result::Result pub j2k_cuda_runtime::CudaError::CompletionFailed pub j2k_cuda_runtime::CudaError::CompletionFailed::completion: alloc::boxed::Box pub j2k_cuda_runtime::CudaError::CompletionFailed::primary: alloc::boxed::Box diff --git a/typos.toml b/typos.toml index 0b3ea194..c812914d 100644 --- a/typos.toml +++ b/typos.toml @@ -16,6 +16,7 @@ APPn = "APPn" flate = "flate" iy = "iy" OME = "OME" +Nd = "Nd" ois = "ois" Pn = "Pn" Psot = "Psot" diff --git a/xtask/src/benchmark_commands.rs b/xtask/src/benchmark_commands.rs index 5c7fff3b..f7e54dd0 100644 --- a/xtask/src/benchmark_commands.rs +++ b/xtask/src/benchmark_commands.rs @@ -51,6 +51,27 @@ pub(super) fn bench_build() -> Result<(), String> { "bench-libjpeg-turbo", "--no-run", ])?; + bench_build_accelerators()?; + run_cargo(&[ + "bench", + "-p", + "j2k-tilecodec", + "--bench", + "compare", + "--no-run", + ])?; + run_cargo(&[ + "bench", + "-p", + "j2k-transcode", + "--bench", + "dct53", + "--no-run", + ])?; + run_cargo(transcode_metal_bench_args()) +} + +fn bench_build_accelerators() -> Result<(), String> { run_cargo(&["bench", "-p", "j2k-jpeg-metal", "--no-run"])?; run_cargo(&[ "bench", @@ -95,20 +116,13 @@ pub(super) fn bench_build() -> Result<(), String> { run_cargo(&[ "bench", "-p", - "j2k-tilecodec", - "--bench", - "compare", - "--no-run", - ])?; - run_cargo(&[ - "bench", - "-p", - "j2k-transcode", + "j2k-ml", "--bench", - "dct53", + "tensor_decode", + "--features", + "cpu", "--no-run", - ])?; - run_cargo(transcode_metal_bench_args()) + ]) } fn transcode_metal_bench_args() -> &'static [&'static str] { diff --git a/xtask/src/benchmark_commands/tests.rs b/xtask/src/benchmark_commands/tests.rs index 00c862cf..f6a3b4ad 100644 --- a/xtask/src/benchmark_commands/tests.rs +++ b/xtask/src/benchmark_commands/tests.rs @@ -42,9 +42,10 @@ fn benchmark_build_and_signoff_execute_the_complete_fake_cargo_plan() { assert!(log.contains( "bench -p j2k-transcode-metal --bench dct97 --features bench-internals --no-run|" )); + assert!(log.contains("bench -p j2k-ml --bench tensor_decode --features cpu --no-run|")); assert!(log.contains("test -p j2k-compare --test in_process_parity -- --nocapture|")); assert!(log.contains("test -p j2k-jpeg --features bench-libjpeg-turbo --test libjpeg_turbo_compare -- --nocapture|")); - assert_eq!(log.lines().count(), 18); + assert_eq!(log.lines().count(), 19); } #[test] diff --git a/xtask/src/coverage/lane.rs b/xtask/src/coverage/lane.rs index 6af3bcdd..98144c88 100644 --- a/xtask/src/coverage/lane.rs +++ b/xtask/src/coverage/lane.rs @@ -332,7 +332,9 @@ mod tests { let args = package_coverage_args(&[], lane); for package in lane.coverage_packages() { assert!(args.windows(2).any(|pair| pair == ["-p", package])); - assert!(lane.owns_path(&format!("crates/{package}/src/lib.rs"))); + } + for prefix in lane.accelerator_source_prefixes() { + assert!(lane.owns_path(prefix)); } } } diff --git a/xtask/src/coverage/model.rs b/xtask/src/coverage/model.rs index d40ae032..0761bce6 100644 --- a/xtask/src/coverage/model.rs +++ b/xtask/src/coverage/model.rs @@ -33,6 +33,7 @@ const METAL_ACCELERATOR_LANE: AcceleratorLaneSpec = AcceleratorLaneSpec { accelerator_package("j2k-jpeg-metal", "crates/j2k-jpeg-metal/"), accelerator_package("j2k-metal", "crates/j2k-metal/"), accelerator_package("j2k-transcode-metal", "crates/j2k-transcode-metal/"), + accelerator_package("j2k-ml", "crates/j2k-ml/src/metal.rs"), ], }; @@ -42,6 +43,7 @@ const CUDA_ACCELERATOR_LANE: AcceleratorLaneSpec = AcceleratorLaneSpec { accelerator_package("j2k-jpeg-cuda", "crates/j2k-jpeg-cuda/"), accelerator_package("j2k-cuda", "crates/j2k-cuda/"), accelerator_package("j2k-transcode-cuda", "crates/j2k-transcode-cuda/"), + accelerator_package("j2k-ml", "crates/j2k-ml/src/cuda.rs"), ], }; @@ -134,6 +136,13 @@ impl CoverageLane { .map(|package| package.name) } + #[cfg(test)] + pub(super) fn accelerator_source_prefixes(self) -> impl Iterator { + self.accelerator_packages() + .iter() + .map(|package| package.source_prefix) + } + pub(super) fn includes_source(self, path: &str, role: SourceRole) -> bool { role.is_measurable() && self.owns_path(path) } diff --git a/xtask/src/cuda.rs b/xtask/src/cuda.rs index d55b5b3f..f4417000 100644 --- a/xtask/src/cuda.rs +++ b/xtask/src/cuda.rs @@ -45,6 +45,12 @@ const TRANSCODE_PARITY_TESTS: &[&str] = &[ "ycbcr_420_batch_transcodes_to_htj2k_with_explicit_cuda_97_codeblock_path", ]; +const ML_CUDA_TESTS: &[&str] = &[ + "direct_cuda_decode_reports_route_and_exact_pixels", + "direct_cuda_matches_portable_u16_float_batch_and_autodiff_lifting", + "retained_primary_context_matches_cubecl_device_context", +]; + struct CudaRuntimeSuite { label: &'static str, package: &'static str, @@ -114,6 +120,10 @@ const CUDA_CLIPPY_SUITES: &[CudaClippySuite] = &[ package: "j2k-cuda", features: "cuda-profiling", }, + CudaClippySuite { + package: "j2k-ml", + features: "cuda", + }, ]; struct ExactCudaSuite { @@ -125,6 +135,13 @@ struct ExactCudaSuite { } const EXACT_CUDA_SUITES: &[ExactCudaSuite] = &[ + ExactCudaSuite { + label: "Burn J2K CUDA direct tensor integration", + package: "j2k-ml", + features: "cuda", + test_targets: &["cuda"], + required_tests: ML_CUDA_TESTS, + }, ExactCudaSuite { label: "HTJ2K encode CUDA parity inventory", package: "j2k-cuda", diff --git a/xtask/src/cuda/tests.rs b/xtask/src/cuda/tests.rs index 6c74ed2e..3833dcaa 100644 --- a/xtask/src/cuda/tests.rs +++ b/xtask/src/cuda/tests.rs @@ -5,7 +5,7 @@ use super::{ reject_cuda_skip_markers, require_cuda_host, runtime_suite_args, successful_test_summaries, validate_complete_test_run, validate_cuda_device_probe, validate_exact_named_run, CUDA_CLIPPY_SUITES, CUDA_RUNTIME_SUITES, EXACT_CUDA_SUITES, HTJ2K_ENCODE_PARITY_TESTS, - TRANSCODE_PARITY_TESTS, + ML_CUDA_TESTS, TRANSCODE_PARITY_TESTS, }; #[cfg(unix)] @@ -26,10 +26,16 @@ fn exact_cuda_inventories_are_unique_and_have_audited_sizes() { .iter() .copied() .collect::>(); + let ml = ML_CUDA_TESTS + .iter() + .copied() + .collect::>(); assert_eq!(ht.len(), 7); assert_eq!(ht.len(), HTJ2K_ENCODE_PARITY_TESTS.len()); assert_eq!(transcode.len(), 8); assert_eq!(transcode.len(), TRANSCODE_PARITY_TESTS.len()); + assert_eq!(ml.len(), 3); + assert_eq!(ml.len(), ML_CUDA_TESTS.len()); } #[test] @@ -165,14 +171,17 @@ fn shell_lines(lines: &[&str], prefix: &str, suffix: &str) -> String { #[cfg(unix)] fn recording_cuda_cargo() -> RecordingProgram { + let ml_listed = shell_lines(ML_CUDA_TESTS, "", ": test"); let ht_listed = shell_lines(HTJ2K_ENCODE_PARITY_TESTS, "", ": test"); let transcode_listed = shell_lines(TRANSCODE_PARITY_TESTS, "", ": test"); let ht_passed = shell_lines(HTJ2K_ENCODE_PARITY_TESTS, "test ", " ... ok"); let transcode_passed = shell_lines(TRANSCODE_PARITY_TESTS, "test ", " ... ok"); + let ml_passed = shell_lines(ML_CUDA_TESTS, "test ", " ... ok"); let script = format!( r#"case "$*" in *"--list"*) case " $* " in + *" -p j2k-ml "*) {ml_listed} ;; *" -p j2k-cuda "*) {ht_listed} ;; *" -p j2k-transcode-cuda "*) {transcode_listed} ;; esac @@ -180,6 +189,10 @@ fn recording_cuda_cargo() -> RecordingProgram { ;; *" --test "*) case " $* " in + *" -p j2k-ml "*) + {ml_passed} + printf '%s\n' 'test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out' + ;; *" -p j2k-cuda "*) {ht_passed} printf '%s\n' 'test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out' @@ -218,7 +231,7 @@ fn cuda_release_executes_the_complete_hermetic_command_plan() { run_release_cuda("linux", "x86_64").expect("platform-independent CUDA release plan"); let log = cargo.log(); - assert_eq!(log.lines().count(), expected_runs * 16); + assert_eq!(log.lines().count(), expected_runs * 19); assert!(log.lines().all(|line| line.contains("RUST_TEST_THREADS=1"))); assert_eq!(device.log().lines().count(), expected_runs); } diff --git a/xtask/src/metal.rs b/xtask/src/metal.rs index cac320a7..c3d38e7e 100644 --- a/xtask/src/metal.rs +++ b/xtask/src/metal.rs @@ -19,6 +19,7 @@ const METAL_COMPILE_PACKAGES: &[&str] = &[ "j2k-jpeg-metal", "j2k-metal", "j2k-transcode-metal", + "j2k-ml", "j2k", ]; @@ -41,8 +42,14 @@ const J2K_METAL_REQUIRED_IGNORED_TESTS: &[&str] = &[ "direct::tests::ht_direct_plan_sub_band_decode_produces_nonzero_coefficients", "encode::tests::routing::auto_htj2k_padded_private_gray8_single_host_output_stays_cpu", "encode::tests::routing::auto_htj2k_padded_private_rgb8_single_host_output_stays_cpu", + "idwt::tests::metal_irreversible_idwt_perf_guard", ]; +// Capture requires an explicit output path and must never be folded into the +// unattended release run. It remains inventoried so a new ignored test cannot +// silently escape the fail-closed classification. +const METAL_OPTIONAL_IGNORED_TESTS: &[&str] = &["idwt::tests::metal_irreversible_idwt_gpu_capture"]; + struct MetalTestSuite { label: &'static str, package: &'static str, @@ -77,6 +84,12 @@ const METAL_TEST_SUITES: &[MetalTestSuite] = &[ required_test: "ycbcr_420_jpeg_transcodes_to_htj2k_with_explicit_metal_97_and_native_sampling", }, + MetalTestSuite { + label: "Burn J2K Metal tensor integration", + package: "j2k-ml", + minimum_passed: 4, + required_test: "strict_metal_staged_decode_reports_route_and_pixels", + }, MetalTestSuite { label: "J2K public facade", package: "j2k", @@ -144,7 +157,7 @@ fn run_release_metal() -> Result<(), String> { )?; } - let ignored_args = [ + let mut ignored_args = vec![ "test", "--release", "-p", @@ -154,6 +167,9 @@ fn run_release_metal() -> Result<(), String> { "--ignored", "--show-output", ]; + for optional_test in METAL_OPTIONAL_IGNORED_TESTS { + ignored_args.extend_from_slice(&["--skip", optional_test]); + } let output = run_cargo_captured( &ignored_args, METAL_RUNTIME_ENV, @@ -205,6 +221,7 @@ fn validate_required_ignored_inventory() -> Result<(), String> { let actual = listed_rust_tests(&output); let expected = J2K_METAL_REQUIRED_IGNORED_TESTS .iter() + .chain(METAL_OPTIONAL_IGNORED_TESTS) .map(|name| (*name).to_string()) .collect::>(); if actual == expected { diff --git a/xtask/src/metal/tests.rs b/xtask/src/metal/tests.rs index e3082832..6cfac026 100644 --- a/xtask/src/metal/tests.rs +++ b/xtask/src/metal/tests.rs @@ -6,12 +6,13 @@ use crate::test_command::RecordingProgram; use super::{ listed_rust_tests, metal_compile, passed_rust_tests, reject_skip_markers, release_metal, run_metal_compile, run_release_metal, runtime_suite_args, validate_exact_ignored_run, - J2K_METAL_REQUIRED_IGNORED_TESTS, + J2K_METAL_REQUIRED_IGNORED_TESTS, METAL_OPTIONAL_IGNORED_TESTS, }; fn recording_metal_cargo() -> RecordingProgram { let listed = J2K_METAL_REQUIRED_IGNORED_TESTS .iter() + .chain(METAL_OPTIONAL_IGNORED_TESTS) .map(|name| format!("printf '%s\\n' '{name}: test'")) .collect::>() .join("\n"); @@ -20,6 +21,7 @@ fn recording_metal_cargo() -> RecordingProgram { .map(|name| format!("printf '%s\\n' 'test {name} ... ok'")) .collect::>() .join("\n"); + let passed_count = J2K_METAL_REQUIRED_IGNORED_TESTS.len(); let script = format!( r#"case " $* " in *" --list "*) @@ -27,7 +29,7 @@ fn recording_metal_cargo() -> RecordingProgram { ;; *" --ignored "*) {passed} -printf '%s\n' 'test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out' +printf '%s\n' 'test result: ok. {passed_count} passed; 0 failed; 0 ignored; 0 measured; 0 filtered out' ;; *" -p j2k-metal-support "*) printf '%s\n' 'test tests::commit_and_wait_accepts_unlabeled_command_buffer ... ok' @@ -45,6 +47,10 @@ printf '%s\n' 'test result: ok. 20 passed; 0 failed; 0 ignored; 0 measured; 0 fi printf '%s\n' 'test encode::tests::stage_validation::metal_deinterleave_gray16_lossless_facade_dispatches_and_round_trips ... ok' printf '%s\n' 'test result: ok. 150 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out' ;; +*" -p j2k-ml "*) +printf '%s\n' 'test strict_metal_staged_decode_reports_route_and_pixels ... ok' +printf '%s\n' 'test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out' +;; *" -p j2k "*) printf '%s\n' 'test accelerator_facade_reports_requested_backend_after_all_required_stages_dispatch ... ok' printf '%s\n' 'test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out' @@ -70,15 +76,17 @@ fn metal_commands_execute_complete_hermetic_compile_and_release_plans() { } let log = recording.log(); - assert_eq!(log.lines().count(), 10); + assert_eq!(log.lines().count(), 11); assert!(log.contains("clippy --all-targets --all-features")); assert!(log.contains("test --release --all-features --lib --bins --tests")); assert!(log.contains("test --release --all-features --doc")); assert!(log.contains("--ignored --list")); assert!(log.contains("--ignored --show-output")); + assert!(log.contains("--skip idwt::tests::metal_irreversible_idwt_gpu_capture")); assert!(log.contains("-p j2k-metal-support")); assert!(log.contains("-p j2k-jpeg-metal")); assert!(log.contains("-p j2k-transcode-metal")); + assert!(log.contains("-p j2k-ml")); } #[test] @@ -117,12 +125,19 @@ fn exact_ignored_validation_rejects_zero_tests() { #[test] fn ignored_inventory_is_unique_and_has_expected_size() { - let unique = J2K_METAL_REQUIRED_IGNORED_TESTS + let required = J2K_METAL_REQUIRED_IGNORED_TESTS + .iter() + .copied() + .collect::>(); + let optional = METAL_OPTIONAL_IGNORED_TESTS .iter() .copied() .collect::>(); - assert_eq!(unique.len(), 18); - assert_eq!(unique.len(), J2K_METAL_REQUIRED_IGNORED_TESTS.len()); + assert_eq!(required.len(), 19); + assert_eq!(optional.len(), 1); + assert_eq!(required.len(), J2K_METAL_REQUIRED_IGNORED_TESTS.len()); + assert_eq!(optional.len(), METAL_OPTIONAL_IGNORED_TESTS.len()); + assert!(required.is_disjoint(&optional)); } #[test] diff --git a/xtask/src/release_commands/tests/orchestration.rs b/xtask/src/release_commands/tests/orchestration.rs index 810a997a..77633e21 100644 --- a/xtask/src/release_commands/tests/orchestration.rs +++ b/xtask/src/release_commands/tests/orchestration.rs @@ -72,12 +72,12 @@ fn release_integrity_publish_mode_accepts_hermetic_final_metadata() { } std::fs::write( release_root.join("Cargo.toml"), - "[workspace.package]\nversion = \"0.7.0\"\n", + "[workspace.package]\nversion = \"0.7.1\"\n", ) .expect("write workspace manifest fixture"); std::fs::write( release_root.join("CHANGELOG.md"), - "# Changelog\n\n## [0.7.0] - 2026-07-12\n", + "# Changelog\n\n## [0.7.1] - 2026-07-14\n", ) .expect("write finalized changelog fixture"); std::fs::write( diff --git a/xtask/src/semver.rs b/xtask/src/semver.rs index 1f41a335..94c973ae 100644 --- a/xtask/src/semver.rs +++ b/xtask/src/semver.rs @@ -24,8 +24,8 @@ const SEMVER_TOOLCHAIN: &str = "1.96"; const SEMVER_BASELINE_VERSION: &str = "0.6.2"; const SEMVER_BASELINE_TAG: &str = "v0.6.2"; const SEMVER_BASELINE_COMMIT: &str = "55ee746e1b49f7309e4d030cc01a69d580173920"; -const API_DIFF_REPORT: &str = "engineering/reviewed-public-api-diff-0.7.0.md"; -const API_REVIEW_CONFIG: &str = "engineering/public-api-review-0.7.0.yml"; +const API_DIFF_REPORT: &str = "engineering/reviewed-public-api-diff-0.7.1.md"; +const API_REVIEW_CONFIG: &str = "engineering/public-api-review-0.7.1.yml"; const SEMVER_BASELINE_PACKAGES: &[&str] = &[ "j2k", diff --git a/xtask/src/semver/tests/command_boundaries.rs b/xtask/src/semver/tests/command_boundaries.rs index 29aa7106..dc3cfc29 100644 --- a/xtask/src/semver/tests/command_boundaries.rs +++ b/xtask/src/semver/tests/command_boundaries.rs @@ -73,7 +73,7 @@ fn committed_candidate_semver_inputs_match_the_pinned_workspace_contract() { assert!(hidden.starts_with("# J2K 1.0 Rustdoc-Hidden Public API Snapshot")); let versions = workspace_package_versions().expect("workspace package versions"); - assert_eq!(versions.get("j2k").map(String::as_str), Some("0.7.0")); + assert_eq!(versions.get("j2k").map(String::as_str), Some("0.7.1")); assert!(versions.keys().collect::>().len() > 10); } diff --git a/xtask/tests/repo_lint_support/architecture_policy.rs b/xtask/tests/repo_lint_support/architecture_policy.rs index b9f4344a..9ca6cc8c 100644 --- a/xtask/tests/repo_lint_support/architecture_policy.rs +++ b/xtask/tests/repo_lint_support/architecture_policy.rs @@ -533,12 +533,14 @@ fn accidental_test_and_adapter_internals_stay_out_of_public_api() { ]), FilePatternCheck::new("crates/j2k-transcode/src/dct97_2d.rs") .named("j2k-transcode 9/7 scratch API") + .required(&[ + "pub struct Dct97GridScratch", + "pub fn dct8x8_blocks_then_dwt97_float_with_scratch", + ]) .forbidden(&[ "Dct97GridError", - "pub struct Dct97GridScratch", "pub fn max_abs_diff", "pub fn spatial_sample_capacity", - "pub fn dct8x8_blocks_then_dwt97_float_with_scratch", ]), FilePatternCheck::new("crates/j2k-transcode/src/jpeg_to_htj2k.rs") .named("j2k-transcode transcoder scratch API") @@ -552,6 +554,10 @@ fn accidental_test_and_adapter_internals_stay_out_of_public_api() { .forbidden(&["pub mod viewport;"]), FilePatternCheck::new("docs/stable-api-1.0.public-api.txt") .named("ordinary stable API snapshot") + .required(&[ + "pub struct j2k_transcode::Dct97GridScratch", + "pub fn j2k_transcode::dct8x8_blocks_then_dwt97_float_with_scratch", + ]) .forbidden(&[ "cuda_dwt53_output_to_j2k_for_test", "crate::adapter::DeviceBatchSummary", @@ -705,7 +711,6 @@ fn accidental_test_and_adapter_internals_stay_out_of_public_api() { "j2k_transcode::Dct53GridScratch::weight_row_capacity", "j2k_transcode::Dct97GridScratch::spatial_sample_capacity", "j2k_transcode::Dct53GridScratch", - "j2k_transcode::Dct97GridScratch", "j2k_transcode::Dwt53TwoDimensional::max_abs_diff", "j2k_transcode::Dwt97TwoDimensional::max_abs_diff", "j2k_transcode::Dct53GridError", @@ -714,7 +719,6 @@ fn accidental_test_and_adapter_internals_stay_out_of_public_api() { "j2k_transcode::dct8x8_to_dwt53_float_linear", "j2k_transcode::idct8x8_then_dwt53_float", "j2k_transcode::dct8x8_blocks_to_dwt53_float_linear_with_scratch", - "j2k_transcode::dct8x8_blocks_then_dwt97_float_with_scratch", "JpegToHtj2kTranscoder::dct_block_scratch_capacity", "JpegToHtj2kTranscoder::integer_idct_block_scratch_capacity", "pub fn j2k::J2kDecoder<'a>::parse(&'a [u8]) -> core::result::Result", diff --git a/xtask/tests/repo_lint_support/docs_and_workflows_policy/duplication_policy.rs b/xtask/tests/repo_lint_support/docs_and_workflows_policy/duplication_policy.rs index 8a618509..01a55e61 100644 --- a/xtask/tests/repo_lint_support/docs_and_workflows_policy/duplication_policy.rs +++ b/xtask/tests/repo_lint_support/docs_and_workflows_policy/duplication_policy.rs @@ -8,31 +8,7 @@ use crate::repo_lint_support::{ }; mod cache_identity; - -#[test] -fn mq_qe_table_is_shared_by_encoder_and_decoder() { - let root = repo_root(); - assert_file_pattern_checks( - root, - &[ - FilePatternCheck::new("crates/j2k-native/src/j2c/mq.rs") - .named("native MQ table module") - .required(&[ - "pub(crate) struct QeData", - "pub(crate) static QE_TABLE: [QeData; 47]", - "Shared MQ arithmetic-coder probability table", - ]), - FilePatternCheck::new("crates/j2k-native/src/j2c/arithmetic_decoder.rs") - .named("native arithmetic decoder") - .required(&["use super::mq::QE_TABLE;"]) - .forbidden(&["struct QeData", "static QE_TABLE"]), - FilePatternCheck::new("crates/j2k-native/src/j2c/arithmetic_encoder.rs") - .named("native arithmetic encoder") - .required(&["use super::mq::QE_TABLE;"]) - .forbidden(&["struct QeData", "static QE_TABLE"]), - ], - ); -} +mod classic_mq; #[test] fn component_plane_metadata_accessors_are_shared() { diff --git a/xtask/tests/repo_lint_support/docs_and_workflows_policy/duplication_policy/classic_mq.rs b/xtask/tests/repo_lint_support/docs_and_workflows_policy/duplication_policy/classic_mq.rs new file mode 100644 index 00000000..e0628489 --- /dev/null +++ b/xtask/tests/repo_lint_support/docs_and_workflows_policy/duplication_policy/classic_mq.rs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use crate::repo_lint_support::{assert_file_pattern_checks, repo_root, FilePatternCheck}; + +#[test] +fn mq_qe_table_is_shared_by_encoder_decoder_and_cuda() { + assert_file_pattern_checks( + repo_root(), + &[ + FilePatternCheck::new("crates/j2k-codec-math/src/classic.rs") + .named("shared classic Tier-1 table module") + .required(&[ + "pub struct ClassicMqState", + "pub const MQ_STATES: [ClassicMqState; 47]", + "pub const MQ_QE_VALUES: [u32; 47]", + "pub const PACKED_MQ_TRANSITION_VALUES: [u32; 47]", + ]), + FilePatternCheck::new("crates/j2k-native/src/j2c/mq.rs") + .named("native MQ table module") + .required(&["j2k_codec_math::classic::MQ_STATES as QE_TABLE"]) + .forbidden(&["struct QeData", "static QE_TABLE", "const QE_TABLE"]), + FilePatternCheck::new("crates/j2k-native/src/j2c/arithmetic_decoder.rs") + .named("native arithmetic decoder") + .required(&["use super::mq::QE_TABLE;"]) + .forbidden(&["struct QeData", "static QE_TABLE"]), + FilePatternCheck::new("crates/j2k-native/src/j2c/arithmetic_encoder.rs") + .named("native arithmetic encoder") + .required(&["use super::mq::QE_TABLE;"]) + .forbidden(&["struct QeData", "static QE_TABLE"]), + FilePatternCheck::new("crates/j2k-cuda-runtime/src/classic_decode.rs") + .named("CUDA classic Tier-1 table consumer") + .required(&[ + "MQ_QE_VALUES, PACKED_MQ_TRANSITION_VALUES, PACKED_SIGN_CONTEXT_LOOKUP", + "ZERO_CTX_HL_LOOKUP, ZERO_CTX_LL_LH_LOOKUP", + ]) + .forbidden(&["const MQ_QE_VALUES", "const PACKED_MQ_TRANSITION_VALUES"]), + ], + ); +} diff --git a/xtask/tests/repo_lint_support/docs_and_workflows_policy/stable_api_evidence.rs b/xtask/tests/repo_lint_support/docs_and_workflows_policy/stable_api_evidence.rs index 3374067b..9c38f23b 100644 --- a/xtask/tests/repo_lint_support/docs_and_workflows_policy/stable_api_evidence.rs +++ b/xtask/tests/repo_lint_support/docs_and_workflows_policy/stable_api_evidence.rs @@ -15,9 +15,9 @@ struct SummaryEvidence { #[test] fn reviewed_api_diff_artifacts_cover_every_ordinary_and_hidden_fingerprint() { let root = repo_root(); - let report = fs::read_to_string(root.join("engineering/reviewed-public-api-diff-0.7.0.md")) + let report = fs::read_to_string(root.join("engineering/reviewed-public-api-diff-0.7.1.md")) .expect("read reviewed API diff report"); - let config_source = fs::read_to_string(root.join("engineering/public-api-review-0.7.0.yml")) + let config_source = fs::read_to_string(root.join("engineering/public-api-review-0.7.1.yml")) .expect("read public API review config"); let config: serde_yaml_ng::Value = serde_yaml_ng::from_str(&config_source).expect("parse public API review config"); @@ -31,7 +31,7 @@ fn reviewed_api_diff_artifacts_cover_every_ordinary_and_hidden_fingerprint() { config .get("candidate_version") .and_then(serde_yaml_ng::Value::as_str), - Some("0.7.0") + Some("0.7.1") ); let reviews = config .get("reviews") @@ -55,7 +55,7 @@ fn reviewed_api_diff_artifacts_cover_every_ordinary_and_hidden_fingerprint() { ]), PatternCheck::new("reviewed API diff new-package classification", &report).required(&[ "## New packages without a 0.6.2 registry baseline", - "`j2k-codec-math` `0.7.0`", + "`j2k-codec-math` `0.7.1`", ]), ]); } diff --git a/xtask/tests/repo_lint_support/docs_and_workflows_policy/structural_ratchets.rs b/xtask/tests/repo_lint_support/docs_and_workflows_policy/structural_ratchets.rs index eab105db..87038543 100644 --- a/xtask/tests/repo_lint_support/docs_and_workflows_policy/structural_ratchets.rs +++ b/xtask/tests/repo_lint_support/docs_and_workflows_policy/structural_ratchets.rs @@ -97,10 +97,8 @@ const REPO_LINT_POLICY_LINE_LIMITS: &[(&str, usize)] = &[ "xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_decoder_policy/color_runtime.rs", 150, ), - ( - "xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_decoder_policy/resident_leaf_structure.rs", - 75, - ), + ("xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_decoder_policy/resident_leaf_structure.rs", 75), + ("xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_decoder_policy/resident_leaf_structure/classic.rs", 50), ( "xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_runtime_safety_policy.rs", 200, @@ -351,6 +349,7 @@ fn docs_and_workflows_policy_children_stay_split_by_responsibility() { ("structural_ratchets/lint_ratchets.rs", 150), ("duplication_policy.rs", 625), ("duplication_policy/cache_identity.rs", 100), + ("duplication_policy/classic_mq.rs", 75), ("encoder_architecture_policy.rs", 700), ("encoder_architecture_policy/native_contracts.rs", 150), ("decoder_fixture_policy.rs", 475), diff --git a/xtask/tests/repo_lint_support/gpu_adapter_policy.rs b/xtask/tests/repo_lint_support/gpu_adapter_policy.rs index 3b5dee66..854613b4 100644 --- a/xtask/tests/repo_lint_support/gpu_adapter_policy.rs +++ b/xtask/tests/repo_lint_support/gpu_adapter_policy.rs @@ -717,6 +717,7 @@ fn cuda_oxide_simt_helpers_use_shared_prelude() { ]); assert_pattern_checks(&[ PatternCheck::new("CUDA runtime SIMT prelude build dependency", &build_script).required(&[ + "cargo:rerun-if-changed=../j2k-codec-math/src/classic.rs", "cargo:rerun-if-changed=src/cuda_oxide_simt_prelude.rs", "stage_cuda_oxide_shared_prelude(context.out_dir);", "out_dir.join(\"cuda_oxide_simt_prelude.rs\")", @@ -804,14 +805,32 @@ fn backend_surfaces_use_core_metadata_and_residency() { "pub struct MetalBatchTextureOutput", ]), FilePatternCheck::new("crates/j2k-jpeg-metal/src/surface.rs") - .named("JPEG Metal surface module") + .named("JPEG Metal surface facade") .required(&[ "pub struct Surface", + "mod resident_tile;", + "mod batch_buffer;", + "mod batch_texture;", + "mod texture_tile;", + "pub use resident_tile::ResidentPrivateJpegTile;", + "pub use batch_buffer::MetalBatchOutputBuffer;", + "pub use batch_texture::MetalBatchTextureOutput;", + "pub use texture_tile::MetalTextureTile;", + ]) + .forbidden(&[ "pub struct MetalBatchOutputBuffer", "pub struct MetalBatchTextureOutput", "pub struct MetalTextureTile", "pub struct ResidentPrivateJpegTile", ]), + FilePatternCheck::new("crates/j2k-jpeg-metal/src/surface/resident_tile.rs") + .required(&["pub struct ResidentPrivateJpegTile"]), + FilePatternCheck::new("crates/j2k-jpeg-metal/src/surface/batch_buffer.rs") + .required(&["pub struct MetalBatchOutputBuffer"]), + FilePatternCheck::new("crates/j2k-jpeg-metal/src/surface/batch_texture.rs") + .required(&["pub struct MetalBatchTextureOutput"]), + FilePatternCheck::new("crates/j2k-jpeg-metal/src/surface/texture_tile.rs") + .required(&["pub struct MetalTextureTile"]), ], ); diff --git a/xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_decoder_policy/architecture.rs b/xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_decoder_policy/architecture.rs index 275f70d0..047c00a6 100644 --- a/xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_decoder_policy/architecture.rs +++ b/xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_decoder_policy/architecture.rs @@ -81,7 +81,7 @@ fn assert_resident_pipeline_ownership(sources: &CudaDecoderSources) { "fn decode_cuda_component_plan", "fn decode_cuda_component_subbands_with_resources", "fn finish_cuda_component_decode", - "fn cuda_code_block_job_from_plan_block", + "mod ht;", ]) .forbidden(&[ "fn run_component_cleanup_dequant_batches", diff --git a/xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_decoder_policy/color_runtime.rs b/xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_decoder_policy/color_runtime.rs index 085cccb2..c324f372 100644 --- a/xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_decoder_policy/color_runtime.rs +++ b/xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_decoder_policy/color_runtime.rs @@ -5,6 +5,10 @@ use super::{assert_pattern_checks, CudaDecoderSources, PatternCheck}; #[test] fn color_runtime_owns_store_and_queued_idwt_completion() { let sources = CudaDecoderSources::read(); + let finish = std::fs::read_to_string( + super::super::repo_root().join("crates/j2k-cuda/src/decoder/color_batch/finish.rs"), + ) + .expect("read CUDA color finish leaf"); assert_pattern_checks(&[ PatternCheck::new( "CUDA color surface orchestration ownership", @@ -15,8 +19,7 @@ fn color_runtime_owns_store_and_queued_idwt_completion() { "fn run_pending_color_idwt(", "fn finish_color_components(", "fn finalize_color_surface(", - "fn finish_color_cuda_resident_surface_with_component_work(", - "CudaQueuedIdwtBatch::resolve_optional_after_completed_work(", + "mod finish;", ]) .forbidden(&[ "pub(super) mod store;", @@ -28,6 +31,10 @@ fn color_runtime_owns_store_and_queued_idwt_completion() { "fn run_color_mct(", "fn dispatch_color_store(", ]), + PatternCheck::new("CUDA color finish ownership", &finish).required(&[ + "fn finish_color_cuda_resident_surface_with_component_work(", + "CudaQueuedIdwtBatch::resolve_optional_after_completed_work(", + ]), PatternCheck::new( "CUDA color transform and store ownership", &sources.color_store, @@ -92,10 +99,14 @@ fn color_runtime_owns_store_and_queued_idwt_completion() { "j2k_dequantize_htj2k_codeblocks_multi_device_with_pool_and_live_host_bytes(", ]), ]); - assert_owner_completion_contract(&sources); + assert!( + finish.lines().count() < 125, + "CUDA color finish leaf must stay focused" + ); + assert_owner_completion_contract(&sources, &finish); } -fn assert_owner_completion_contract(sources: &CudaDecoderSources) { +fn assert_owner_completion_contract(sources: &CudaDecoderSources, finish: &str) { assert_pattern_checks(&[ PatternCheck::new( "CUDA color batch host owner graph", @@ -122,7 +133,10 @@ fn assert_owner_completion_contract(sources: &CudaDecoderSources) { sources .color_batch .matches("CudaQueuedIdwtBatch::resolve_optional_after_completed_work(") - .count(), + .count() + + finish + .matches("CudaQueuedIdwtBatch::resolve_optional_after_completed_work(") + .count(), 2, "single and batch color decode must both resolve queued IDWT ownership" ); diff --git a/xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_decoder_policy/resident_leaf_structure.rs b/xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_decoder_policy/resident_leaf_structure.rs index 933b48da..1358dca6 100644 --- a/xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_decoder_policy/resident_leaf_structure.rs +++ b/xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_decoder_policy/resident_leaf_structure.rs @@ -6,6 +6,8 @@ use std::fs; use super::super::{assert_pattern_checks, repo_root, PatternCheck}; +mod classic; + #[test] fn resident_decode_shared_primitives_keep_downward_dependencies() { let root = repo_root(); @@ -47,7 +49,6 @@ fn resident_decode_shared_primitives_keep_downward_dependencies() { .required(&["checked_cuda_element_count("]) .forbidden(&["fn checked_color_store_area("]), ]); - let resident_sources = [&buffer, &cleanup, &component, &idwt, &surface]; let pooled_buffer_implementations = resident_sources .into_iter() diff --git a/xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_decoder_policy/resident_leaf_structure/classic.rs b/xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_decoder_policy/resident_leaf_structure/classic.rs new file mode 100644 index 00000000..331a19cb --- /dev/null +++ b/xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_decoder_policy/resident_leaf_structure/classic.rs @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use std::fs; + +use super::super::super::super::repo_root; + +#[test] +fn classic_and_ht_component_leaves_stay_focused() { + let root = repo_root(); + for (name, relative, limit) in [ + ( + "classic cleanup submission", + "crates/j2k-cuda/src/decoder/resident/cleanup_dequant/classic.rs", + 100, + ), + ( + "classic component planning", + "crates/j2k-cuda/src/decoder/resident/component/classic.rs", + 175, + ), + ( + "HT component job conversion", + "crates/j2k-cuda/src/decoder/resident/component/ht.rs", + 50, + ), + ] { + let source = fs::read_to_string(root.join(relative)) + .unwrap_or_else(|error| panic!("read {relative}: {error}")); + assert!( + source.lines().count() < limit, + "CUDA resident {name} leaf must stay focused" + ); + } +} diff --git a/xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_runtime_safety_policy/abi.rs b/xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_runtime_safety_policy/abi.rs index c32a6cf0..27342f86 100644 --- a/xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_runtime_safety_policy/abi.rs +++ b/xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_runtime_safety_policy/abi.rs @@ -13,6 +13,8 @@ fn cuda_gpu_abi_byte_views_require_compile_time_no_padding_proofs() { .expect("read CUDA byte-view facade"); let abi = fs::read_to_string(root.join("crates/j2k-cuda-runtime/src/bytes/abi.rs")) .expect("read CUDA GPU ABI proofs"); + let abi_tests = fs::read_to_string(root.join("crates/j2k-cuda-runtime/src/bytes/abi/tests.rs")) + .expect("read CUDA GPU ABI proof tests"); let reversible53 = fs::read_to_string(root.join("crates/j2k-cuda-runtime/src/transcode/reversible53.rs")) .expect("read CUDA reversible transcode upload"); @@ -21,6 +23,10 @@ fn cuda_gpu_abi_byte_views_require_compile_time_no_padding_proofs() { abi.lines().count() < 500, "CUDA GPU ABI proof ledger must remain focused" ); + assert!( + abi_tests.lines().count() < 100, + "CUDA GPU ABI proof tests must remain focused" + ); assert_pattern_checks(&[ PatternCheck::new("shared safe GPU byte-view contract", &core).required(&[ "object representation contains no internal or tail padding", @@ -46,6 +52,9 @@ fn cuda_gpu_abi_byte_views_require_compile_time_no_padding_proofs() { "CudaJ2kIdwtMultiKernelJob {", "CudaJ2kStoreRgb8MctBatchJob {", "reserved_tail: u32", + "mod tests;", + ]), + PatternCheck::new("CUDA no-padding proof tests", &abi_tests).required(&[ "explicit_tail_fields_preserve_cuda_host_abi_sizes_and_offsets", "explicit_cuda_tail_fields_are_part_of_safe_byte_views", ]), diff --git a/xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_runtime_safety_policy/allocation/resource_creation_contract.rs b/xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_runtime_safety_policy/allocation/resource_creation_contract.rs index 05913c49..754666e2 100644 --- a/xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_runtime_safety_policy/allocation/resource_creation_contract.rs +++ b/xtask/tests/repo_lint_support/gpu_adapter_policy/cuda_runtime_safety_policy/allocation/resource_creation_contract.rs @@ -11,7 +11,7 @@ pub(super) fn assert_policy(root: &Path) { }; let validation = read("crates/j2k-cuda-runtime/src/context/resource_creation.rs"); let memory = read("crates/j2k-cuda-runtime/src/memory.rs"); - let device = read("crates/j2k-cuda-runtime/src/context/device.rs"); + let context_creation = read("crates/j2k-cuda-runtime/src/context/creation.rs"); let events = read("crates/j2k-cuda-runtime/src/execution/events.rs"); let kernel_cache = read("crates/j2k-cuda-runtime/src/context/kernel_cache.rs"); let kernel_tests = read("crates/j2k-cuda-runtime/src/context/kernel_cache/tests.rs"); @@ -31,8 +31,9 @@ pub(super) fn assert_policy(root: &Path) { "cu_mem_alloc)(&raw mut ptr, len)", "validate_device_allocation(ptr, len)", ]), - PatternCheck::new("CUDA context promotion", &device).required(&[ + PatternCheck::new("CUDA context promotion", &context_creation).required(&[ "cu_ctx_create)(&raw mut context, 0, device)", + "cu_device_primary_ctx_retain)(&raw mut context, device)", "validate_resource_handle(", "CUDA returned a null context after successful creation", ]), diff --git a/xtask/tests/repo_lint_support/gpu_adapter_policy/jpeg_metal_plan_owner_policy.rs b/xtask/tests/repo_lint_support/gpu_adapter_policy/jpeg_metal_plan_owner_policy.rs index 0afc3e3e..6ed77ea9 100644 --- a/xtask/tests/repo_lint_support/gpu_adapter_policy/jpeg_metal_plan_owner_policy.rs +++ b/xtask/tests/repo_lint_support/gpu_adapter_policy/jpeg_metal_plan_owner_policy.rs @@ -72,6 +72,10 @@ impl PlanOwnerSources { "crates/j2k-jpeg-metal/src/batch/grouping.rs", "crates/j2k-jpeg-metal/src/lib.rs", "crates/j2k-jpeg-metal/src/surface.rs", + "crates/j2k-jpeg-metal/src/surface/batch_buffer.rs", + "crates/j2k-jpeg-metal/src/surface/batch_texture.rs", + "crates/j2k-jpeg-metal/src/surface/resident_tile.rs", + "crates/j2k-jpeg-metal/src/surface/texture_tile.rs", ], ), } diff --git a/xtask/tests/repo_lint_support/gpu_adapter_policy/metal_batch_allocation_policy.rs b/xtask/tests/repo_lint_support/gpu_adapter_policy/metal_batch_allocation_policy.rs index fc0cc982..b4e2150d 100644 --- a/xtask/tests/repo_lint_support/gpu_adapter_policy/metal_batch_allocation_policy.rs +++ b/xtask/tests/repo_lint_support/gpu_adapter_policy/metal_batch_allocation_policy.rs @@ -92,6 +92,10 @@ fn remediated_metal_batch_paths_do_not_regress_to_infallible_collections() { "crates/j2k-jpeg-metal/src/lib.rs", "crates/j2k-jpeg-metal/src/session.rs", "crates/j2k-jpeg-metal/src/surface.rs", + "crates/j2k-jpeg-metal/src/surface/batch_buffer.rs", + "crates/j2k-jpeg-metal/src/surface/batch_texture.rs", + "crates/j2k-jpeg-metal/src/surface/resident_tile.rs", + "crates/j2k-jpeg-metal/src/surface/texture_tile.rs", "crates/j2k-jpeg-metal/src/tile_batch.rs", "crates/j2k-jpeg-metal/src/compute/batch_entry.rs", "crates/j2k-jpeg-metal/src/compute/batch_plan.rs", @@ -115,6 +119,7 @@ fn remediated_metal_batch_paths_do_not_regress_to_infallible_collections() { "crates/j2k-metal/src/decoder/direct_paths.rs", "crates/j2k-metal/src/encode/batch.rs", "crates/j2k-metal/src/encode/packet_plan.rs", + "crates/j2k-metal/src/encode/resident_schedule.rs", "crates/j2k-metal/src/encode/resident_submit.rs", "crates/j2k-metal/src/encode/resident_wait.rs", "crates/j2k-metal/src/encode/submitted.rs", @@ -126,6 +131,7 @@ fn remediated_metal_batch_paths_do_not_regress_to_infallible_collections() { "crates/j2k-metal/src/compute/decode_dispatch/classic_cleanup/distinct_allocation.rs", "crates/j2k-metal/src/compute/decode_dispatch/ht_distinct.rs", "crates/j2k-metal/src/compute/direct_execute.rs", + "crates/j2k-metal/src/compute/direct_cpu.rs", "crates/j2k-metal/src/compute/direct_grayscale_execute.rs", "crates/j2k-metal/src/compute/direct_grayscale_execute/allocation.rs", "crates/j2k-metal/src/compute/direct_grayscale_execute/component_plane.rs", @@ -162,6 +168,7 @@ fn remediated_metal_batch_paths_do_not_regress_to_infallible_collections() { "HashMap::<", ".collect::>() .join("\n"); let prepared = read("crates/j2k-metal/src/compute/direct_plan_types/allocation.rs"); + let direct_cache = read("crates/j2k-metal/src/compute/direct_cache.rs"); let native = read("crates/j2k-native/src/direct_plan/allocation.rs"); assert_pattern_checks(&[ @@ -61,6 +62,9 @@ fn metal_prepared_plan_cache_uses_actual_separate_host_and_device_weights() { "job.segments.capacity()", "retained_allocation_bytes", ]), + PatternCheck::new("fallible CPU Tier-1 cache-hit copies", &direct_cache) + .required(&["drop(state)", "budget.try_vec("]) + .forbidden(&["entry.coefficients.to_vec()"]), ]); assert!( diff --git a/xtask/tests/repo_lint_support/gpu_adapter_policy/resident_encode_policy/error_preservation_policy.rs b/xtask/tests/repo_lint_support/gpu_adapter_policy/resident_encode_policy/error_preservation_policy.rs index 5d272e6d..05a96f11 100644 --- a/xtask/tests/repo_lint_support/gpu_adapter_policy/resident_encode_policy/error_preservation_policy.rs +++ b/xtask/tests/repo_lint_support/gpu_adapter_policy/resident_encode_policy/error_preservation_policy.rs @@ -14,10 +14,10 @@ fn resident_metal_batch_preparation_propagates_typed_errors_directly() { assert_pattern_checks(&[ PatternCheck::new("resident Metal batch preparation", &source) - .required(&["prepare_planned_resident_lossless_tiles_batch(chunk_planned, session)?"]) + .required(&["prepare_planned_resident_lossless_tiles_batch(planned, session)?"]) .forbidden(&[ - "resident {family_name} batch encode failed", - "prepare_planned_resident_lossless_tiles_batch(chunk_planned, session).map_err", + "prepare_planned_resident_lossless_tiles_batch(planned, session).map_err", + "source.to_string()", ]), ]); } diff --git a/xtask/tests/repo_lint_support/gpu_adapter_policy/resident_encode_policy/session_resource_policy.rs b/xtask/tests/repo_lint_support/gpu_adapter_policy/resident_encode_policy/session_resource_policy.rs index 9836032d..6da122fe 100644 --- a/xtask/tests/repo_lint_support/gpu_adapter_policy/resident_encode_policy/session_resource_policy.rs +++ b/xtask/tests/repo_lint_support/gpu_adapter_policy/resident_encode_policy/session_resource_policy.rs @@ -34,7 +34,7 @@ fn resident_cuda_encode_resources_are_session_owned_and_context_bound() { assert_pattern_checks(&[ PatternCheck::new("minimum CUDA context identity API", &runtime_context).required(&[ "pub fn is_same_context(&self, other: &Self) -> bool", - "Arc::ptr_eq(&self.inner, &other.inner)", + "self.inner.context == other.inner.context", "#[doc(hidden)]", ]), PatternCheck::new("session-owned HTJ2K encode resources", &session).required(&[ diff --git a/xtask/tests/repo_lint_support/j2k_ml_policy.rs b/xtask/tests/repo_lint_support/j2k_ml_policy.rs new file mode 100644 index 00000000..ec2eafca --- /dev/null +++ b/xtask/tests/repo_lint_support/j2k_ml_policy.rs @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use std::fs; + +use super::{ + assert_file_pattern_checks, assert_pattern_checks, repo_root, FilePatternCheck, PatternCheck, +}; + +#[test] +fn j2k_ml_stays_independent_experimental_and_explicitly_feature_gated() { + assert_file_pattern_checks( + repo_root(), + &[ + FilePatternCheck::new("crates/j2k-ml/Cargo.toml") + .named("j2k-ml manifest") + .required(&[ + "name = \"j2k-ml\"", + "publish = false", + "default = []", + "cpu = []", + "cuda = [", + "metal = [", + ]), + FilePatternCheck::new("crates/j2k-ml/README.md") + .named("j2k-ml independence notice") + .required(&[ + "independent integration", + "not an official Tracel or Burn crate", + ]), + ], + ); +} + +#[test] +fn j2k_ml_uses_a_portable_arm_linux_test_backend() { + assert_file_pattern_checks( + repo_root(), + &[ + FilePatternCheck::new("crates/j2k-ml/Cargo.toml") + .named("j2k-ml target-specific test backends") + .required(&[ + "target.'cfg(all(target_arch = \"aarch64\", target_os = \"linux\"))'.dev-dependencies", + "burn-ndarray = { workspace = true }", + "target.'cfg(not(all(target_arch = \"aarch64\", target_os = \"linux\")))'.dev-dependencies", + "burn-flex = { workspace = true }", + ]), + FilePatternCheck::new("docs/j2k-ml.md") + .named("j2k-ml ARM backend rationale") + .required(&[ + "Linux AArch64", + "https://github.com/sarah-quinones/gemm/issues/31", + "https://github.com/sarah-quinones/gemm/pull/43", + ]), + ], + ); +} + +#[test] +fn j2k_ml_accelerator_transfer_contracts_are_source_enforced() { + let root = repo_root(); + let cuda = + fs::read_to_string(root.join("crates/j2k-ml/src/cuda.rs")).expect("read j2k-ml CUDA route"); + let metal = fs::read_to_string(root.join("crates/j2k-ml/src/metal.rs")) + .expect("read j2k-ml Metal route"); + let readback = fs::read_to_string(root.join("crates/j2k-metal/src/surface/readback.rs")) + .expect("read packed Metal surface readback"); + + assert_pattern_checks(&[ + PatternCheck::new("j2k-ml CUDA direct allocation bridge", &cuda) + .required(&[ + "empty_device_contiguous_dtype", + ".get_resource(cube.handle.clone())", + "CudaExternalDeviceBufferViewMut::from_raw_parts(", + ".j2k_ml_convert_into_external(", + "register_tensor_handle(handle)", + ]) + .forbidden(&[ + "TensorData", + "copy_to_host(", + "copy_range_to_host(", + "Tensor::from_data(", + ]), + PatternCheck::new("j2k-ml Metal compact staged bridge", &metal) + .required(&[ + "download_surfaces_packed(session, &surface_refs)", + "integer_tensor_4_from_bytes::(", + ".cast(FloatDType::F32)", + "normalize_4(", + ]) + .forbidden(&["TensorData", "Vec", "Tensor::from_data("]), + PatternCheck::new("packed Metal batch readback", &readback).required(&[ + "checked_shared_buffer(session.device(), total)", + "checked_blit_command_encoder(&command)", + "commit_and_wait(&command)", + "checked_buffer_read_vec::(&staging, 0, total)", + ]), + ]); + + assert_eq!( + readback + .matches("checked_shared_buffer(session.device(), total)") + .count(), + 1, + "a Metal tensor batch must allocate exactly one packed staging buffer" + ); + assert_eq!( + readback.matches("commit_and_wait(&command)").count(), + 1, + "a Metal tensor batch must submit and complete exactly one packed readback command" + ); + assert_eq!( + readback + .matches("checked_buffer_read_vec::(&staging, 0, total)") + .count(), + 1, + "a Metal tensor batch must perform exactly one packed host read" + ); +} diff --git a/xtask/tests/repo_lint_support/jpeg_metal_resource_safety_policy.rs b/xtask/tests/repo_lint_support/jpeg_metal_resource_safety_policy.rs index 17616208..289df28d 100644 --- a/xtask/tests/repo_lint_support/jpeg_metal_resource_safety_policy.rs +++ b/xtask/tests/repo_lint_support/jpeg_metal_resource_safety_policy.rs @@ -11,8 +11,13 @@ use super::*; )] fn jpeg_metal_host_readback_aliases_require_unsafe_contracts() { let root = repo_root(); - let surface = fs::read_to_string(root.join("crates/j2k-jpeg-metal/src/surface.rs")) - .expect("read JPEG Metal surface module"); + let surface = read_source_files( + root, + &[ + "crates/j2k-jpeg-metal/src/surface.rs", + "crates/j2k-jpeg-metal/src/surface/batch_buffer.rs", + ], + ); let encode = fs::read_to_string(root.join("crates/j2k-jpeg-metal/src/encode.rs")) .expect("read JPEG Metal encode module"); let batch_entry = @@ -149,8 +154,13 @@ fn jpeg_metal_host_readback_aliases_require_unsafe_contracts() { #[test] fn jpeg_metal_private_texture_aliases_share_safe_write_ordering() { let root = repo_root(); - let surface = fs::read_to_string(root.join("crates/j2k-jpeg-metal/src/surface.rs")) - .expect("read JPEG Metal surface module"); + let surface = read_source_files( + root, + &[ + "crates/j2k-jpeg-metal/src/surface/batch_texture.rs", + "crates/j2k-jpeg-metal/src/surface/texture_tile.rs", + ], + ); let batch_entry = fs::read_to_string(root.join("crates/j2k-jpeg-metal/src/compute/batch_entry.rs")) .expect("read JPEG Metal batch decode entry module"); @@ -238,8 +248,10 @@ fn jpeg_metal_private_texture_aliases_share_safe_write_ordering() { #[test] fn jpeg_metal_resident_private_tile_hides_raw_keepalive_resources() { let root = repo_root(); - let surface = fs::read_to_string(root.join("crates/j2k-jpeg-metal/src/surface.rs")) - .expect("read JPEG Metal surface module"); + let surface = read_source_files( + root, + &["crates/j2k-jpeg-metal/src/surface/resident_tile.rs"], + ); let residency_tests = fs::read_to_string(root.join("crates/j2k-jpeg-metal/src/tests/textures/residency.rs")) .expect("read JPEG Metal texture residency tests"); @@ -256,9 +268,11 @@ fn jpeg_metal_resident_private_tile_hides_raw_keepalive_resources() { "pub fn dimensions(&self) -> (u32, u32)", "pub fn pixel_format(&self) -> PixelFormat", "pub fn pitch_bytes(&self) -> usize", + "pub fn resident_image(&self) -> &ResidentMetalImage", + "pub fn into_resident_image(self) -> ResidentMetalImage", "pub unsafe fn buffer(&self) -> &BufferRef", "pub(crate) fn buffer_trusted(&self) -> &BufferRef", - "pub fn into_buffer(self) -> Buffer", + "pub unsafe fn into_buffer(self) -> Buffer", "every clone of this tile", "No surviving tile offers safe host readback", "normal Metal synchronization remains each", @@ -272,6 +286,7 @@ fn jpeg_metal_resident_private_tile_hides_raw_keepalive_resources() { "pub status_buffer: Buffer", "pub command_buffer: CommandBuffer", "pub fn buffer(&self) -> &BufferRef", + "pub fn into_buffer(self) -> Buffer", ]), PatternCheck::new( "JPEG Metal resident private tile regressions", @@ -279,7 +294,8 @@ fn jpeg_metal_resident_private_tile_hides_raw_keepalive_resources() { ) .required(&[ "let raw_buffer = unsafe { tile.buffer() };", - "let handed_off = tile.clone().into_buffer();", + "let handed_off = tile.clone().into_resident_image();", + "let resident = tile.resident_image();", "assert_eq!(tile.dimensions(), (16, 16));", ]), ]); diff --git a/xtask/tests/repo_lint_support/metal_buffer_pool_policy.rs b/xtask/tests/repo_lint_support/metal_buffer_pool_policy.rs index d9b1e102..6ad3a557 100644 --- a/xtask/tests/repo_lint_support/metal_buffer_pool_policy.rs +++ b/xtask/tests/repo_lint_support/metal_buffer_pool_policy.rs @@ -74,7 +74,8 @@ fn metal_buffer_pools_use_flat_fallible_actual_byte_ledgers() { "BASE_PRIVATE_BUFFERS_PER_RESIDENT_BATCH: usize = 7", "CLASSIC_SPLIT_TOKEN_PRIVATE_BUFFERS_PER_BATCH: usize = 4", "DEFAULT_RESIDENT_PRIVATE_WORKING_SET_BUFFERS", - "RESIDENT_PRIVATE_POOL_BUFFER_LIMIT: usize = 4_096", + "fn private_pool_record_limit(working_set: usize)", + "private_pool_record_limit(DEFAULT_RESIDENT_PRIVATE_WORKING_SET_BUFFERS)", ]) .forbidden(&["Vec", "HashMap", "unsafe"]), PatternCheck::new("Metal default resident retention shape", &encode_config).required(&[ diff --git a/xtask/tests/repo_lint_support/metal_compute_structure_policy.rs b/xtask/tests/repo_lint_support/metal_compute_structure_policy.rs index f6498d00..1a7c123b 100644 --- a/xtask/tests/repo_lint_support/metal_compute_structure_policy.rs +++ b/xtask/tests/repo_lint_support/metal_compute_structure_policy.rs @@ -195,6 +195,7 @@ fn metal_compute_runtime_registry_is_split_from_compute_god_file() { ("compute/decode_dispatch/ht_distinct.rs", 250), ("compute/decode_dispatch/ht_subband.rs", 300), ("compute/decode_dispatch/idwt.rs", 550), + ("compute/decode_dispatch/idwt/irreversible.rs", 325), ("compute/decode_dispatch/mct.rs", 250), ("compute/decode_dispatch/store.rs", 400), ] { diff --git a/xtask/tests/repo_lint_support/mod.rs b/xtask/tests/repo_lint_support/mod.rs index f5af4b13..f4464450 100644 --- a/xtask/tests/repo_lint_support/mod.rs +++ b/xtask/tests/repo_lint_support/mod.rs @@ -26,6 +26,7 @@ pub(crate) mod j2k_container_allocation_policy; pub(crate) mod j2k_decode_structure_policy; pub(crate) mod j2k_encode_validation_policy; pub(crate) mod j2k_error_source_policy; +pub(crate) mod j2k_ml_policy; pub(crate) mod j2k_scratch_allocation_policy; pub(crate) mod jpeg_batch_allocation_policy; pub(crate) mod jpeg_dct_reemit_policy; diff --git a/xtask/tests/repo_lint_support/suppression_policy.rs b/xtask/tests/repo_lint_support/suppression_policy.rs index c65e17d4..b671b415 100644 --- a/xtask/tests/repo_lint_support/suppression_policy.rs +++ b/xtask/tests/repo_lint_support/suppression_policy.rs @@ -20,6 +20,8 @@ const REVIEWED_DEVICE_INCLUDE_FILES: &[&str] = &[ "crates/j2k-cuda-runtime/src/cuda_oxide_j2k_dequantize/simt/src/main.rs", "crates/j2k-cuda-runtime/src/cuda_oxide_j2k_encode/simt/src/main.rs", "crates/j2k-cuda-runtime/src/cuda_oxide_j2k_idwt/simt/src/main.rs", + "crates/j2k-cuda-runtime/src/cuda_oxide_j2k_classic_decode/simt/src/main.rs", + "crates/j2k-cuda-runtime/src/cuda_oxide_j2k_ml/simt/src/main.rs", "crates/j2k-cuda-runtime/src/cuda_oxide_jpeg_decode/simt/src/main.rs", "crates/j2k-cuda-runtime/src/cuda_oxide_jpeg_encode/simt/src/main.rs", "crates/j2k-cuda-runtime/src/cuda_oxide_transcode/simt/src/main.rs", diff --git a/xtask/tests/repo_lint_support/suppression_policy/manifest.rs b/xtask/tests/repo_lint_support/suppression_policy/manifest.rs index 41294c00..6ac820e3 100644 --- a/xtask/tests/repo_lint_support/suppression_policy/manifest.rs +++ b/xtask/tests/repo_lint_support/suppression_policy/manifest.rs @@ -16,6 +16,7 @@ const REVIEWED_SAFETY_ALLOWS: &[(&str, &str, &str)] = &[ ("crates/j2k-jpeg/Cargo.toml", "rust", "unsafe_code"), ("crates/j2k-metal-support/Cargo.toml", "rust", "unsafe_code"), ("crates/j2k-metal/Cargo.toml", "rust", "unsafe_code"), + ("crates/j2k-ml/Cargo.toml", "rust", "unsafe_code"), ]; // Non-safety API/documentation suppressions remain a ceiling: deleting one is @@ -87,6 +88,13 @@ const REVIEWED_ALLOW_CEILING: &[(&str, &str, &str)] = &[ "clippy", "must_use_candidate", ), + ("crates/j2k-ml/Cargo.toml", "clippy", "missing_errors_doc"), + ( + "crates/j2k-ml/Cargo.toml", + "clippy", + "module_name_repetitions", + ), + ("crates/j2k-ml/Cargo.toml", "clippy", "must_use_candidate"), ( "crates/j2k-test-support/Cargo.toml", "clippy", diff --git a/xtask/tests/repo_lint_support/suppression_policy/source.rs b/xtask/tests/repo_lint_support/suppression_policy/source.rs index 076202fe..10399999 100644 --- a/xtask/tests/repo_lint_support/suppression_policy/source.rs +++ b/xtask/tests/repo_lint_support/suppression_policy/source.rs @@ -6,6 +6,10 @@ use super::relative_path; use crate::repo_lint_support::{repo_root, rust_sources}; const REVIEWED_ALLOWS: &[(&str, &str)] = &[ + ( + "crates/j2k-ml/src/metal.rs", + "clippy::trivially_copy_pass_by_ref", + ), ( "crates/j2k-cuda-runtime/src/cuda_oxide_htj2k_encode/simt/src/main.rs", "clippy::manual_div_ceil", @@ -38,6 +42,14 @@ const REVIEWED_ALLOWS: &[(&str, &str)] = &[ "crates/j2k-cuda-runtime/src/cuda_oxide_j2k_idwt/simt/src/main.rs", "static_mut_refs", ), + ( + "crates/j2k-cuda-runtime/src/cuda_oxide_j2k_classic_decode/simt/src/main.rs", + "static_mut_refs", + ), + ( + "crates/j2k-cuda-runtime/src/cuda_oxide_j2k_ml/simt/src/main.rs", + "clippy::too_many_arguments", + ), ( "crates/j2k-cuda-runtime/src/cuda_oxide_jpeg_encode/simt/src/main.rs", "clippy::cast_possible_truncation", diff --git a/xtask/tests/repo_lint_support/workflow_policy.rs b/xtask/tests/repo_lint_support/workflow_policy.rs index d2a90612..e53fae1e 100644 --- a/xtask/tests/repo_lint_support/workflow_policy.rs +++ b/xtask/tests/repo_lint_support/workflow_policy.rs @@ -84,6 +84,25 @@ fn deny_paste_advisory_ignore_has_review_metadata() { ); } +#[test] +fn deny_bincode_advisory_ignore_and_license_exceptions_are_scoped() { + assert_file_pattern_checks( + repo_root(), + &[FilePatternCheck::new("deny.toml") + .named("deny.toml burn dependency policy") + .required(&[ + "RUSTSEC-2025-0141", + "Review-by: 2027-01-31", + "https://rustsec.org/advisories/RUSTSEC-2025-0141.html", + "https://github.com/tracel-ai/burn/tree/v0.21.0/crates/burn-core", + "{ crate = \"colored@3.1.1\", allow = [\"MPL-2.0\"] }", + "{ crate = \"hexf-parse@0.2.1\", allow = [\"CC0-1.0\"] }", + "{ crate = \"option-ext@0.2.0\", allow = [\"MPL-2.0\"] }", + "{ crate = \"xxhash-rust@0.8.16\", allow = [\"BSL-1.0\"] }", + ])], + ); +} + #[test] fn unsafe_audit_rows_include_invariants_and_regression_guards() { let audit = @@ -534,6 +553,7 @@ fn metal_xtask_owns_complete_compile_and_runtime_policy() { "RUST_TEST_THREADS", "J2K_GPU_TEST_SKIPPED", "J2K_METAL_REQUIRED_IGNORED_TESTS", + "METAL_OPTIONAL_IGNORED_TESTS", "fn run_metal_compile()", "fn run_release_metal()", "validate_required_ignored_inventory",