diff --git a/.github/workflows/packaging-lint.yml b/.github/workflows/packaging-lint.yml index b903f82ea..ccf74f43a 100644 --- a/.github/workflows/packaging-lint.yml +++ b/.github/workflows/packaging-lint.yml @@ -28,6 +28,10 @@ jobs: name: Windows installation policy (no app build) runs-on: windows-latest steps: + - name: Harden runner + uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4 + with: + egress-policy: audit - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 354fbd268..ffbe49afb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,10 @@ name: Release on: + # A nightly dry run on main: every release build except macOS, nothing signed or released + # (craft-common-actions release.yml `dry-run`). Finds release-only breakage before release day. + schedule: + - cron: "32 1 * * *" push: branches: [release] workflow_dispatch: @@ -29,6 +33,7 @@ jobs: # Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos. uses: storytold/craft-common-actions/.github/workflows/release.yml@main with: + dry-run: ${{ github.event_name == 'schedule' }} app-name: effectcraft app-title: EffectCraft version: ${{ inputs.version }} diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 5bff5cdb0..b463e79ea 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -129,6 +129,12 @@ configuration from these Apache-2.0 projects: - `crates/segment/src/mobilesam.rs` follows the MobileSAM, TinyViT and Segment Anything architectures and their Apache-2.0 reference code. +## Frozen matte regression fixture + +| Asset | Author | Source | Licence | +|---|---|---|---| +| `crates/engine/src/tests/fixtures/object-matte-0.22.ecproj` | lisn0 | Original work, synthetic project saved by EffectCraft 0.22.0 | MIT OR Apache-2.0 | + ## First-party brand marks The ArtCraft name, wordmark and logos are trademarks of the ArtCraft Team. They are not open source and are not diff --git a/Cargo.lock b/Cargo.lock index 37233ef58..79691772a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1089,15 +1089,24 @@ dependencies = [ "libc", ] +[[package]] +name = "craft-audio" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4#e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4" +dependencies = [ + "craft-media 0.1.0 (git+https://github.com/storytold/craft-libs?rev=e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4)", + "thiserror 2.0.21", +] + [[package]] name = "craft-docpath" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545#e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545" +source = "git+https://github.com/storytold/craft-libs?rev=e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4#e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4" [[package]] name = "craft-ecproj-format" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=ca1b5e2edb8568335c6c14f390307bd5beae8238#ca1b5e2edb8568335c6c14f390307bd5beae8238" +source = "git+https://github.com/storytold/craft-libs?rev=e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4#e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4" dependencies = [ "serde", "serde_json", @@ -1107,12 +1116,12 @@ dependencies = [ [[package]] name = "craft-fonts-build" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545#e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545" +source = "git+https://github.com/storytold/craft-libs?rev=e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4#e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4" [[package]] name = "craft-i18n" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545#e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545" +source = "git+https://github.com/storytold/craft-libs?rev=e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4#e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4" dependencies = [ "fixed_decimal", "icu_decimal", @@ -1132,6 +1141,14 @@ dependencies = [ "writeable", ] +[[package]] +name = "craft-media" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4#e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4" +dependencies = [ + "thiserror 2.0.21", +] + [[package]] name = "craft-media" version = "0.1.0" @@ -1143,7 +1160,7 @@ dependencies = [ [[package]] name = "craft-pdf" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545#e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545" +source = "git+https://github.com/storytold/craft-libs?rev=e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4#e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4" dependencies = [ "hayro-syntax", "thiserror 2.0.21", @@ -1152,7 +1169,7 @@ dependencies = [ [[package]] name = "craft-quit" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545#e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545" +source = "git+https://github.com/storytold/craft-libs?rev=e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4#e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4" dependencies = [ "dispatch2", "egui", @@ -1163,7 +1180,7 @@ dependencies = [ [[package]] name = "craft-ui" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545#e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545" +source = "git+https://github.com/storytold/craft-libs?rev=e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4#e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4" dependencies = [ "craft-fonts-build", "craft-i18n", @@ -1184,7 +1201,7 @@ dependencies = [ [[package]] name = "craft-update" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=1b34eba41e62138fc2438d2734523b302840b45d#1b34eba41e62138fc2438d2734523b302840b45d" +source = "git+https://github.com/storytold/craft-libs?rev=e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4#e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4" dependencies = [ "chrono", "dunce", @@ -1194,6 +1211,7 @@ dependencies = [ "image", "log", "plist", + "rustix 1.1.5", "semver", "serde", "serde_json", @@ -1207,6 +1225,16 @@ dependencies = [ "zip 9.1.3", ] +[[package]] +name = "craft-video" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4#e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4" +dependencies = [ + "craft-media 0.1.0 (git+https://github.com/storytold/craft-libs?rev=e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4)", + "rayon", + "thiserror 2.0.21", +] + [[package]] name = "crc32fast" version = "1.5.2" @@ -1420,7 +1448,7 @@ dependencies = [ [[package]] name = "effectcraft" -version = "0.23.1" +version = "0.24.1" dependencies = [ "chrono", "cpal", @@ -1448,7 +1476,7 @@ dependencies = [ [[package]] name = "effectcraft-aep" -version = "0.23.1" +version = "0.24.1" dependencies = [ "effectcraft-color", "effectcraft-effects", @@ -1462,7 +1490,7 @@ dependencies = [ [[package]] name = "effectcraft-automation" -version = "0.23.1" +version = "0.24.1" dependencies = [ "effectcraft-engine", "effectcraft-host", @@ -1474,16 +1502,9 @@ dependencies = [ "thiserror 2.0.21", ] -[[package]] -name = "effectcraft-av1enc" -version = "0.23.1" -dependencies = [ - "filmcraft-av1", -] - [[package]] name = "effectcraft-cli" -version = "0.23.1" +version = "0.24.1" dependencies = [ "cpu-time", "effectcraft-automation", @@ -1502,7 +1523,7 @@ dependencies = [ [[package]] name = "effectcraft-color" -version = "0.23.1" +version = "0.24.1" dependencies = [ "proptest", "serde", @@ -1510,7 +1531,7 @@ dependencies = [ [[package]] name = "effectcraft-effects" -version = "0.23.1" +version = "0.24.1" dependencies = [ "chrono", "effectcraft-color", @@ -1528,7 +1549,7 @@ dependencies = [ [[package]] name = "effectcraft-engine" -version = "0.23.1" +version = "0.24.1" dependencies = [ "chrono", "craft-docpath", @@ -1567,12 +1588,13 @@ dependencies = [ [[package]] name = "effectcraft-export" -version = "0.23.1" +version = "0.24.1" dependencies = [ - "effectcraft-av1enc", + "craft-audio", + "craft-media 0.1.0 (git+https://github.com/storytold/craft-libs?rev=e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4)", + "craft-video", "effectcraft-color", "effectcraft-effects", - "effectcraft-hevcenc", "effectcraft-keyframe", "effectcraft-media", "effectcraft-opusenc", @@ -1580,25 +1602,22 @@ dependencies = [ "effectcraft-raster", "effectcraft-render", "effectcraft-time", - "effectcraft-vp9enc", "exr", - "filmcraft-aac", - "filmcraft-h264enc", - "filmcraft-isobmff", - "filmcraft-opus", - "filmcraft-prores", + "filmcraft-av1", + "filmcraft-hevc", "filmcraft-vp9", "gif", "image", "log", "rayon", + "tempfile", "thiserror 2.0.21", "web-time", ] [[package]] name = "effectcraft-expr" -version = "0.23.1" +version = "0.24.1" dependencies = [ "boa_engine", "effectcraft-color", @@ -1615,7 +1634,7 @@ dependencies = [ [[package]] name = "effectcraft-geom" -version = "0.23.1" +version = "0.24.1" dependencies = [ "proptest", "serde", @@ -1623,7 +1642,7 @@ dependencies = [ [[package]] name = "effectcraft-gpu" -version = "0.23.1" +version = "0.24.1" dependencies = [ "bytemuck", "effectcraft-color", @@ -1646,7 +1665,7 @@ dependencies = [ [[package]] name = "effectcraft-helper-host" -version = "0.23.1" +version = "0.24.1" dependencies = [ "bytemuck", "effectcraft-effects", @@ -1655,16 +1674,9 @@ dependencies = [ "serde_json", ] -[[package]] -name = "effectcraft-hevcenc" -version = "0.23.1" -dependencies = [ - "filmcraft-hevc", -] - [[package]] name = "effectcraft-host" -version = "0.23.1" +version = "0.24.1" dependencies = [ "effectcraft-engine", "effectcraft-export", @@ -1691,7 +1703,7 @@ dependencies = [ [[package]] name = "effectcraft-interchange" -version = "0.23.1" +version = "0.24.1" dependencies = [ "effectcraft-color", "effectcraft-keyframe", @@ -1710,7 +1722,7 @@ dependencies = [ [[package]] name = "effectcraft-keyframe" -version = "0.23.1" +version = "0.24.1" dependencies = [ "effectcraft-color", "effectcraft-geom", @@ -1722,7 +1734,7 @@ dependencies = [ [[package]] name = "effectcraft-lottie" -version = "0.23.1" +version = "0.24.1" dependencies = [ "craft-docpath", "effectcraft-color", @@ -1740,9 +1752,11 @@ dependencies = [ [[package]] name = "effectcraft-media" -version = "0.23.1" +version = "0.24.1" dependencies = [ + "craft-audio", "craft-docpath", + "craft-media 0.1.0 (git+https://github.com/storytold/craft-libs?rev=e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4)", "effectcraft-color", "effectcraft-effects", "effectcraft-model", @@ -1754,12 +1768,9 @@ dependencies = [ "effectcraft-svg", "effectcraft-time", "exr", - "filmcraft-aac", "filmcraft-codecs", "filmcraft-color", "filmcraft-frame", - "filmcraft-isobmff", - "filmcraft-matroska", "filmcraft-media", "filmcraft-mpegts", "filmcraft-time", @@ -1768,12 +1779,13 @@ dependencies = [ "log", "rayon", "serde_json", + "tempfile", "thiserror 2.0.21", ] [[package]] name = "effectcraft-model" -version = "0.23.1" +version = "0.24.1" dependencies = [ "effectcraft-geom", "image", @@ -1786,14 +1798,14 @@ dependencies = [ [[package]] name = "effectcraft-opusenc" -version = "0.23.1" +version = "0.24.1" dependencies = [ - "filmcraft-opus", + "craft-audio", ] [[package]] name = "effectcraft-path" -version = "0.23.1" +version = "0.24.1" dependencies = [ "effectcraft-geom", "effectcraft-keyframe", @@ -1805,7 +1817,7 @@ dependencies = [ [[package]] name = "effectcraft-pdf" -version = "0.23.1" +version = "0.24.1" dependencies = [ "craft-pdf", "effectcraft-raster", @@ -1821,7 +1833,7 @@ dependencies = [ [[package]] name = "effectcraft-plugin" -version = "0.23.1" +version = "0.24.1" dependencies = [ "effectcraft-effects", "effectcraft-keyframe", @@ -1833,7 +1845,7 @@ dependencies = [ [[package]] name = "effectcraft-project" -version = "0.23.1" +version = "0.24.1" dependencies = [ "craft-ecproj-format", "effectcraft-color", @@ -1847,14 +1859,14 @@ dependencies = [ [[package]] name = "effectcraft-psd" -version = "0.23.1" +version = "0.24.1" dependencies = [ "miniz_oxide 0.9.1", ] [[package]] name = "effectcraft-raster" -version = "0.23.1" +version = "0.24.1" dependencies = [ "effectcraft-color", "effectcraft-geom", @@ -1864,7 +1876,7 @@ dependencies = [ [[package]] name = "effectcraft-render" -version = "0.23.1" +version = "0.24.1" dependencies = [ "effectcraft-color", "effectcraft-effects", @@ -1887,7 +1899,7 @@ dependencies = [ [[package]] name = "effectcraft-sam2" -version = "0.23.1" +version = "0.24.1" dependencies = [ "bytemuck", "candle-core", @@ -1902,7 +1914,7 @@ dependencies = [ [[package]] name = "effectcraft-script" -version = "0.23.1" +version = "0.24.1" dependencies = [ "boa_engine", "effectcraft-engine", @@ -1920,7 +1932,7 @@ dependencies = [ [[package]] name = "effectcraft-segment" -version = "0.23.1" +version = "0.24.1" dependencies = [ "miniz_oxide 0.9.1", "ndarray", @@ -1929,19 +1941,20 @@ dependencies = [ [[package]] name = "effectcraft-svg" -version = "0.23.1" +version = "0.24.1" dependencies = [ "effectcraft-geom", "effectcraft-path", "effectcraft-raster", "kurbo", + "log", "rayon", "roxmltree", ] [[package]] name = "effectcraft-text" -version = "0.23.1" +version = "0.24.1" dependencies = [ "effectcraft-keyframe", "harfrust", @@ -1953,7 +1966,7 @@ dependencies = [ [[package]] name = "effectcraft-time" -version = "0.23.1" +version = "0.24.1" dependencies = [ "proptest", "serde", @@ -1961,7 +1974,7 @@ dependencies = [ [[package]] name = "effectcraft-track" -version = "0.23.1" +version = "0.24.1" dependencies = [ "effectcraft-raster", "effectcraft-segment", @@ -1973,7 +1986,7 @@ dependencies = [ [[package]] name = "effectcraft-ui-egui" -version = "0.23.1" +version = "0.24.1" dependencies = [ "bytemuck", "chrono", @@ -2012,13 +2025,9 @@ dependencies = [ "web-time", ] -[[package]] -name = "effectcraft-vp9enc" -version = "0.23.1" - [[package]] name = "effectcraft-web" -version = "0.23.1" +version = "0.24.1" dependencies = [ "effectcraft-engine", "effectcraft-export", @@ -2483,7 +2492,7 @@ name = "filmcraft-bitstream" version = "0.12.0" source = "git+https://github.com/storytold/filmcraft?rev=d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5#d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" dependencies = [ - "craft-media", + "craft-media 0.1.0 (git+https://github.com/storytold/craft-libs?rev=f0ad6c1e1d297908915f4be315b0fcfc9be7b5d4)", ] [[package]] @@ -2584,16 +2593,6 @@ dependencies = [ "thiserror 2.0.21", ] -[[package]] -name = "filmcraft-h264enc" -version = "0.12.0" -source = "git+https://github.com/storytold/filmcraft?rev=d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5#d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" -dependencies = [ - "filmcraft-bitstream", - "rayon", - "thiserror 2.0.21", -] - [[package]] name = "filmcraft-hevc" version = "0.12.0" @@ -2626,7 +2625,7 @@ name = "filmcraft-isobmff" version = "0.12.0" source = "git+https://github.com/storytold/filmcraft?rev=d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5#d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" dependencies = [ - "craft-media", + "craft-media 0.1.0 (git+https://github.com/storytold/craft-libs?rev=f0ad6c1e1d297908915f4be315b0fcfc9be7b5d4)", ] [[package]] @@ -2634,7 +2633,7 @@ name = "filmcraft-matroska" version = "0.12.0" source = "git+https://github.com/storytold/filmcraft?rev=d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5#d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" dependencies = [ - "craft-media", + "craft-media 0.1.0 (git+https://github.com/storytold/craft-libs?rev=f0ad6c1e1d297908915f4be315b0fcfc9be7b5d4)", ] [[package]] @@ -3323,7 +3322,7 @@ checksum = "9f4d0e94ddd48749f06bbe4e5389fb9799a0c45bcaf00495042076ef05e3241a" [[package]] name = "hayro-jbig2" version = "0.3.0" -source = "git+https://github.com/storytold/craft-libs?rev=e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545#e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545" +source = "git+https://github.com/storytold/craft-libs?rev=e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4#e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4" dependencies = [ "hayro-ccitt", ] @@ -3337,7 +3336,7 @@ checksum = "c75ab947623ef4ccaa7acf0579edf7cbb5a73838e3839a7be73335e522f433a1" [[package]] name = "hayro-syntax" version = "0.7.2" -source = "git+https://github.com/storytold/craft-libs?rev=e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545#e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545" +source = "git+https://github.com/storytold/craft-libs?rev=e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4#e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4" dependencies = [ "hayro-jbig2", "hayro-jpeg2000", @@ -7835,7 +7834,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" [[package]] name = "xtask" -version = "0.23.1" +version = "0.24.1" dependencies = [ "chrono", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 6f96cb012..c812a3294 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,19 +4,19 @@ members = ["crates/*", "apps/*", "xtask"] default-members = ["crates/*", "apps/*"] [workspace.package] -version = "0.23.1" +version = "0.24.1" edition = "2024" license = "MIT OR Apache-2.0" rust-version = "1.95" repository = "https://github.com/storytold/effectcraft" [workspace.dependencies] -craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545", version = "=0.1.0", default-features = false, features = ["read"] } -craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545" } -craft-quit = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545" } -craft-ecproj-format = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "ca1b5e2edb8568335c6c14f390307bd5beae8238" } +craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4", version = "=0.1.0", default-features = false, features = ["read"] } +craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4" } +craft-quit = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4" } +craft-ecproj-format = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4" } # craft-update: app-specific pretend versions apply to startup and manual checks. -craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "1b34eba41e62138fc2438d2734523b302840b45d", features = ["egui"] } +craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4", features = ["egui"] } # internal effectcraft-time = { path = "crates/time" } effectcraft-geom = { path = "crates/geom" } @@ -41,7 +41,6 @@ effectcraft-interchange = { path = "crates/interchange" } effectcraft-pdf = { path = "crates/pdf" } effectcraft-psd = { path = "crates/psd" } effectcraft-aep = { path = "crates/aep" } -effectcraft-vp9enc = { path = "crates/vp9enc" } effectcraft-svg = { path = "crates/svg" } effectcraft-engine = { path = "crates/engine" } effectcraft-host = { path = "crates/host" } @@ -49,8 +48,6 @@ effectcraft-ui-egui = { path = "crates/ui-egui" } effectcraft-automation = { path = "crates/automation" } effectcraft-opusenc = { path = "crates/opusenc" } effectcraft-script = { path = "crates/script" } -effectcraft-hevcenc = { path = "crates/hevcenc" } -effectcraft-av1enc = { path = "crates/av1enc" } effectcraft-plugin = { path = "crates/plugin" } effectcraft-helper-host = { path = "crates/helper-host" } @@ -82,25 +79,23 @@ image = { version = "0.25", default-features = false, features = ["png", "jpeg"] same-file = "1.0" tempfile = "3.27" -# media codecs + containers: FilmCraft's pure-Rust crates, pinned to a revision (plan/adr/0001). +# codecs + containers shared by the Craft apps (crates/media, crates/export); keep all three on one rev. +craft-media = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4", default-features = false, features = ["isobmff", "matroska"] } +craft-audio = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4", default-features = false, features = ["aac-decode", "aac-encode", "opus-decode"] } +craft-video = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4", features = ["h264-encode", "prores", "hevc-encode", "vp9-encode", "av1", "threads"] } + +# footage decoding: FilmCraft's pure-Rust crates, pinned to a revision (plan/adr/0001). # Only `crates/media`, `crates/export` and `crates/interchange` may depend on these. filmcraft-codecs = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" } filmcraft-media = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" } filmcraft-frame = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" } filmcraft-color = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" } filmcraft-time = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" } -# encoders + muxer (crates/export) -filmcraft-h264enc = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" } -filmcraft-prores = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" } -filmcraft-aac = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" } -filmcraft-isobmff = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" } -# container headers read for AAC channel layouts (crates/media) -filmcraft-matroska = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" } +# MPEG TS / PS headers read for AAC channel layouts (crates/media); craft-media has no MPEG TS reader filmcraft-mpegts = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" } -# decoder used as the round-trip oracle of effectcraft-opusenc (crates/export dev-dependency) -filmcraft-opus = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" } +# decoders used as the round-trip oracles of craft-video's VP9, HEVC and AV1 encoders (crates/export +# dev-dependencies only); craft-video has no decoder for them filmcraft-vp9 = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" } -# decoders used as the round-trip oracles of effectcraft-hevcenc / effectcraft-av1enc (dev-dependencies only) filmcraft-hevc = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" } filmcraft-av1 = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" } # timeline interchange (EDL, FCP7 XML, FCPXML, OTIO) for Premiere Pro interop (crates/interchange only) @@ -117,7 +112,7 @@ half = "2.7" # ui # Shared frontend components; keep this dependency above the engine boundary. # Shared UI and the complete egui family select reviewed immutable commits below. -craft-ui = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545", features = ["serde"] } +craft-ui = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4", features = ["serde"] } # `default_fonts` and `monochrome_emoji_fonts` are explicit: the app's `FontDefinitions::default()` # and its `✔`/`▣` chrome glyphs need both, and kittest's own dev-dependencies do not propagate. egui = { version = "0.37", default-features = false, features = ["default_fonts", "monochrome_emoji_fonts"] } @@ -137,11 +132,6 @@ sys-locale = "0.3" # filmcraft-frame = { path = "../filmcraft/crates/frame" } # filmcraft-color = { path = "../filmcraft/crates/color" } # filmcraft-time = { path = "../filmcraft/crates/time" } -# filmcraft-h264enc = { path = "../filmcraft/crates/h264enc" } -# filmcraft-prores = { path = "../filmcraft/crates/prores" } -# filmcraft-aac = { path = "../filmcraft/crates/aac" } -# filmcraft-isobmff = { path = "../filmcraft/crates/isobmff" } -# filmcraft-matroska = { path = "../filmcraft/crates/matroska" } # filmcraft-mpegts = { path = "../filmcraft/crates/mpegts" } [workspace.lints.rust] @@ -200,50 +190,39 @@ opt-level = 3 [profile.dev.package.effectcraft-export] opt-level = 3 -[profile.dev.package.effectcraft-vp9enc] +# craft-libs codec hot loops. +[profile.dev.package.craft-video] opt-level = 3 -[profile.dev.package.effectcraft-hevcenc] +[profile.dev.package.craft-audio] opt-level = 3 -[profile.dev.package.effectcraft-av1enc] +[profile.dev.package.craft-media] opt-level = 3 # FilmCraft codec hot loops (same settings as FilmCraft's own workspace). [profile.dev.package.filmcraft-h264] opt-level = 3 -[profile.dev.package.filmcraft-h264enc] -opt-level = 3 - [profile.dev.package.filmcraft-hevc] opt-level = 3 [profile.dev.package.filmcraft-vp9] opt-level = 3 -[profile.dev.package.filmcraft-prores] -opt-level = 3 - [profile.dev.package.filmcraft-dnx] opt-level = 3 [profile.dev.package.filmcraft-av1] opt-level = 3 -[profile.dev.package.filmcraft-aac] -opt-level = 3 - -[profile.dev.package.filmcraft-opus] -opt-level = 3 - [profile.release] lto = "thin" codegen-units = 1 [patch.crates-io] -hayro-syntax = { git = "https://github.com/storytold/craft-libs", rev = "e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545", version = "=0.7.2" } -hayro-jbig2 = { git = "https://github.com/storytold/craft-libs", rev = "e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545", version = "=0.3.0" } +hayro-syntax = { git = "https://github.com/storytold/craft-libs", rev = "e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4", version = "=0.7.2" } +hayro-jbig2 = { git = "https://github.com/storytold/craft-libs", rev = "e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4", version = "=0.3.0" } ecolor = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" } emath = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" } epaint = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" } diff --git a/README.md b/README.md index 913f8aa06..57eddebdc 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ MP4, **WebM** (VP9 with inter frames and alpha, or AV1; Opus audio), PNG, JPEG, rendering with 3:2 pulldown, effect/solo/guide/depth overrides, crop, region of interest and resize, Render Settings and Output Module templates with defaults, post-render actions, storage overflow and render logs. The same queue runs from the command line. Every encoder is pure -Rust (FilmCraft's H.264, ProRes and AAC; EffectCraft's own VP9, AV1, HEVC and Opus); there is no +Rust (craft-libs' H.264, ProRes, HEVC, VP9, AV1 and AAC; EffectCraft's own Opus); there is no FFmpeg inside. **Lottie** goes both ways: File ▸ Export ▸ Lottie JSON… writes a composition (precomps, shape, diff --git a/crates/aep/src/container/effect_map.rs b/crates/aep/src/container/effect_map.rs index aea320395..49a53d2e7 100644 --- a/crates/aep/src/container/effect_map.rs +++ b/crates/aep/src/container/effect_map.rs @@ -1142,11 +1142,37 @@ pub const GLOW_MIDPOINT_MAX: f64 = 0.990_005_493_164_062_5; /// The key a popup item is matched by between the two programs: trimmed, lower case, and /// Set Matte's "Red Channel" ... "Alpha Channel" are EffectCraft's "Red" ... "Alpha" (904 E24). +/// +/// Lower case is Unicode's, not only ASCII's: After Effects stores popup items in its UI +/// language, so an item can start with a capital outside ASCII (French "Écran"), and its key must +/// be the same as for the same word in lower case. pub fn popup_key(item: &str) -> String { - let s = item.trim().to_ascii_lowercase(); + let s = item.trim().to_lowercase(); s.strip_suffix(" channel").map(str::to_string).unwrap_or(s) } +/// The English [`popup_key`] a non-English After Effects popup item name stands for, when this +/// table knows it; `None` otherwise. EffectCraft's own popup options are English only, so a +/// project saved by an After Effects set to another UI language never matches them by name and +/// the parameter falls back to its default (observed on a real "Niveaux" project, French After +/// Effects 2026 26.0.0x67: Levels' Channel and Clip to Output Black / White). Add an entry only +/// from a confirmed After Effects label, never a guess at a whole menu's translation. +pub fn popup_alias(key: &str) -> Option<&'static str> { + Some(match key { + // French: Levels' Channel popup (`ADBE Easy Levels2-0001`). "Alpha" is the same word in + // both languages, so it needs no entry. + "rvb" => "rgb", + "rouge" => "red", + "vert" => "green", + "bleu" => "blue", + // French: Levels' Clip to Output Black / White popups (`-0008`, `-0009`). + "activé" => "on", + "désactivé" => "off", + "désactivé pour la couleur 32 bpc" => "off for 32 bpc color", + _ => return None, + }) +} + /// The table row of an After Effects match name. pub fn by_match_name(match_name: &str) -> Option<&'static EffectMap> { EFFECTS.iter().find(|m| m.match_name == match_name) @@ -1164,3 +1190,17 @@ pub const HUE_SATURATION_MASTER: [&str; 3] = ["hue", "saturation", "lightness"]; /// The EffectCraft parameters Levels' RGB channel values (`Extra::Levels`) go to, in the order of /// the data (input black, input white, gamma, output black, output white: 512-516). pub const LEVELS_RGB: [&str; 5] = ["inBlack", "inWhite", "gamma", "outBlack", "outWhite"]; + +#[cfg(test)] +mod tests { + use super::*; + + /// A capital outside ASCII has the same key as its lower case; ASCII keys are unchanged. + #[test] + fn popup_key_lowers_non_ascii_capitals() { + assert_eq!(popup_key("ÉCRAN"), popup_key("écran")); + assert_eq!(popup_key(" Désactivé "), "désactivé"); + assert_eq!(popup_key("Off for 32 bpc Color"), "off for 32 bpc color"); + assert_eq!(popup_key("Red Channel"), "red"); + } +} diff --git a/crates/aep/src/import/effects.rs b/crates/aep/src/import/effects.rs index 86faac76a..01e704b53 100644 --- a/crates/aep/src/import/effects.rs +++ b/crates/aep/src/import/effects.rs @@ -35,7 +35,7 @@ //! import. Hue/Saturation's master values and Levels' values are arbitrary data, decoded by the //! reader (444-446, 512-516). -use crate::effect_map::{CRYPTOMATTE_OUTPUT, Conv, EFFECTS, EffectMap, Extra, HUE_SATURATION_MASTER, LEVELS_RGB, ParamMap, popup_key}; +use crate::effect_map::{CRYPTOMATTE_OUTPUT, Conv, EFFECTS, EffectMap, Extra, HUE_SATURATION_MASTER, LEVELS_RGB, ParamMap, popup_alias, popup_key}; use crate::{ CryptomatteData, CurvesData, DefinitionSource, Effect as AepEffect, EffectDefinitions, EffectParam, ExtractorChannels, HueSaturationData, Layer as AepLayer, LayerRef, LevelsData, MeshData, ParamKind, ParamValue, @@ -422,8 +422,8 @@ pub(crate) fn resolve_layer_refs( } } -/// The EffectCraft item of each file item (by name, ignoring case and separators); `None` when -/// the parameter here is not a popup. +/// The EffectCraft item of each file item (by name, ignoring case and separators, and a known +/// non-English name for it, [`popup_alias`]); `None` when the parameter here is not a popup. fn popup_mapping(file_items: &[String], ui: &ParamUi) -> Option>> { let ParamUi::Popup { options } = ui else { return None }; let norm = popup_key; @@ -434,7 +434,12 @@ fn popup_mapping(file_items: &[String], ui: &ParamUi) -> Option> if item.trim() == "(-" { return None; } - options.iter().position(|o| norm(o) == norm(item)).and_then(|i| u32::try_from(i).ok()) + let key = norm(item); + options + .iter() + .position(|o| norm(o) == key) + .or_else(|| popup_alias(&key).and_then(|a| options.iter().position(|o| norm(o) == a))) + .and_then(|i| u32::try_from(i).ok()) }) .collect(), ) diff --git a/crates/aep/src/import/props.rs b/crates/aep/src/import/props.rs index fd0a4f9fc..8c331f1cb 100644 --- a/crates/aep/src/import/props.rs +++ b/crates/aep/src/import/props.rs @@ -231,15 +231,21 @@ pub(crate) fn import_transform( } import_expression(&node, prop, col); } - if let Some(PropNode::Other(_)) = group.get("ADBE Orientation") { + if let Some(PropNode::Other(otst)) = group.get("ADBE Orientation") { match (layer.orientation(), target.get_mut("orientation")) { - (Some(o), Some(prop)) if !o.animated && !o.has_expression && sane(&Value::Vec3(o.degrees)) => prop.value = Value::Vec3(o.degrees), + (Some(o), Some(prop)) if o.animated || sane(&Value::Vec3(o.degrees)) => { + if o.animated { + import_orientation_keys(layer, layer_name, tm, prop, col); + } else { + prop.value = Value::Vec3(o.degrees); + } + // The expression is stored in the `otst`'s `tdbs` as on any property (#3087). + if let Some(node) = otst.child_list(b"tdbs").and_then(AepProp::from_chunk) { + import_expression(&node, prop, col); + } + } // A layer without an orientation (an ambient light) loses nothing with the default. (Some(o), None) if !o.animated && !o.has_expression && o.degrees == [0.0; 3] => {} - (Some(o), Some(prop)) if o.animated && !o.has_expression => import_orientation_keys(layer, layer_name, tm, prop, col), - (Some(o), Some(_)) if o.has_expression => { - col.lose(LossCategory::TransformProperties, format!("{layer_name}: ADBE Orientation expression (not decoded)")); - } _ => col.lose(LossCategory::TransformProperties, format!("{layer_name}: ADBE Orientation")), } } diff --git a/crates/aep/src/import/report.rs b/crates/aep/src/import/report.rs index 8b3599266..7675f3f09 100644 --- a/crates/aep/src/import/report.rs +++ b/crates/aep/src/import/report.rs @@ -98,8 +98,12 @@ pub struct Loss { pub category: LossCategory, /// How many occurrences (exact). pub count: usize, - /// Up to [`MAX_DETAILS`] example lines. + /// Example lines: up to [`MAX_DETAILS`], and for effects with no counterpart one more line per + /// effect (with its number of instances). pub details: Vec, + /// How many of the `count` occurrences no line of `details` describes. + #[serde(default)] + pub unlisted: usize, } /// What arrived in the project. @@ -288,7 +292,7 @@ const MAX_UNMAPPED_EFFECTS: usize = 100; impl Collector { /// Records one loss. pub fn lose(&mut self, category: LossCategory, detail: impl Into) { - let loss = self.losses.entry(category).or_insert_with(|| Loss { category, count: 0, details: Vec::new() }); + let loss = self.losses.entry(category).or_insert_with(|| Loss { category, count: 0, details: Vec::new(), unlisted: 0 }); loss.count = loss.count.saturating_add(1); if loss.details.len() < MAX_DETAILS { loss.details.push(detail.into()); @@ -299,7 +303,8 @@ impl Collector { /// made in [`Collector::finish`], one per effect with the number of instances, so that many /// other losses cannot hide which effects were dropped. pub fn lose_unmapped_effect(&mut self, match_name: &str, detail: String) { - let loss = self.losses.entry(LossCategory::Effects).or_insert_with(|| Loss { category: LossCategory::Effects, count: 0, details: Vec::new() }); + let loss = + self.losses.entry(LossCategory::Effects).or_insert_with(|| Loss { category: LossCategory::Effects, count: 0, details: Vec::new(), unlisted: 0 }); loss.count = loss.count.saturating_add(1); if let Some(seen) = self.unmapped_effects.get_mut(match_name) { seen.1 = seen.1.saturating_add(1); @@ -310,12 +315,22 @@ impl Collector { } pub fn finish(mut self) -> ImportReport { + // Every line describes one occurrence, except an unmapped effect's, which describes all + // its instances. + for loss in self.losses.values_mut() { + loss.unlisted = loss.count.saturating_sub(loss.details.len()); + } if let Some(loss) = self.losses.get_mut(&LossCategory::Effects) { - let named = std::mem::take(&mut self.unmapped_effects).into_values().map(|(first, n)| match n { + // The most used effects first. + let mut named: Vec<(String, usize)> = std::mem::take(&mut self.unmapped_effects).into_values().collect(); + named.sort_by_key(|n| std::cmp::Reverse(n.1)); + let described = named.iter().fold(0usize, |sum, (_, n)| sum.saturating_add(*n)); + loss.unlisted = loss.unlisted.saturating_sub(described); + let lines = named.into_iter().map(|(first, n)| match n { 1 => first, _ => format!("{first}, and {} more like it", n - 1), }); - loss.details.splice(0..0, named); + loss.details.splice(0..0, lines); } ImportReport { imported: self.imported, @@ -362,6 +377,25 @@ mod tests { // One instance stays the single line it was. assert_eq!(named("(Pseudo/)"), ["C: 'D' has the effect 'Dropdown' (Pseudo/)"]); assert!(effects.details.len() <= MAX_DETAILS + 2, "the examples stay bounded"); + // 20 parameter lines, the cylinders' line and the dropdown's describe 46; 10 parameters + // have no line. + assert_eq!(effects.unlisted, 10); + // The most used effect comes first. + assert!(effects.details[0].contains("(CC Cylinder)"), "{:?}", effects.details); + } + + /// #3365: each category says how many of its occurrences have no example line. + #[test] + fn a_loss_counts_the_occurrences_without_a_line() { + let mut col = Collector::default(); + for i in 0..27 { + col.lose(LossCategory::Masks, format!("mask {i}")); + } + col.lose(LossCategory::ShapeLayers, "one"); + let report = col.finish(); + let unlisted = |c| report.losses.iter().find(|l| l.category == c).map(|l| (l.details.len(), l.unlisted)); + assert_eq!(unlisted(LossCategory::Masks), Some((MAX_DETAILS, 7))); + assert_eq!(unlisted(LossCategory::ShapeLayers), Some((1, 0))); } /// A file with more than [`MAX_UNMAPPED_EFFECTS`] different unmapped effects names that many @@ -376,5 +410,6 @@ mod tests { let effects = report.losses.iter().find(|l| l.category == LossCategory::Effects).unwrap(); assert_eq!(effects.count, MAX_UNMAPPED_EFFECTS + 1, "every instance is counted"); assert_eq!(effects.details.len(), MAX_UNMAPPED_EFFECTS, "{} lines", effects.details.len()); + assert_eq!(effects.unlisted, 1, "the effect past the limit has no line"); } } diff --git a/crates/aep/src/import/shapes.rs b/crates/aep/src/import/shapes.rs index 345346a6f..b86ee7492 100644 --- a/crates/aep/src/import/shapes.rs +++ b/crates/aep/src/import/shapes.rs @@ -457,18 +457,22 @@ fn shift_copy_offset(p: &mut effectcraft_project::Property, cx: &mut Cx<'_, '_>) } } -/// Reports a property whose only sampled value is the default 1. +/// Reports a property whose only sampled value is the default 1. A record in the default state +/// holds After Effects' default (docs/aep-format.md 3.4, state), so it holds nothing to lose. fn not_default(g: &AepGroup<'_, '_>, match_name: &str, what: &str, cx: &mut Cx<'_, '_>) { if let Some(PropNode::Property(p)) = g.get(match_name) + && stored(&p) && (p.is_animated() || p.expression().is_some() || p.static_value().and_then(|v| v.first().copied()).is_some_and(|v| v != 1.0)) { cx.col.lose(LossCategory::ShapeLayers, format!("{}: {what} not imported", cx.what)); } } -/// Reports a property whose only sampled value is 0. +/// Reports a property whose only sampled value is 0 (a record in the default state, as above, +/// holds nothing to lose). fn not_zero(g: &AepGroup<'_, '_>, match_name: &str, what: &str, cx: &mut Cx<'_, '_>) { if let Some(PropNode::Property(p)) = g.get(match_name) + && stored(&p) && (p.is_animated() || p.expression().is_some() || p.static_value().and_then(|v| v.first().copied()).is_some_and(|v| v != 0.0)) { cx.col.lose(LossCategory::ShapeLayers, format!("{}: {what} not imported", cx.what)); @@ -490,3 +494,41 @@ fn unknown(g: &AepGroup<'_, '_>, known: &[&str], what: &str, cx: &mut Cx<'_, '_> } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::Chunk; + + /// A group holding `match_name` as a one-component property in state `tdsb` with value `v`. + fn group_with(match_name: &str, tdsb: u32, v: f64) -> Chunk<'static> { + let mut name = match_name.as_bytes().to_vec(); + name.resize(40, 0); + let mut tdb4 = vec![0u8; 124]; + tdb4[3] = 1; + let tdbs = Chunk::list( + *b"tdbs", + vec![Chunk::data(*b"tdsb", tdsb.to_be_bytes().to_vec()), Chunk::data(*b"tdb4", tdb4), Chunk::data(*b"cdat", v.to_be_bytes().to_vec())], + ); + Chunk::list(*b"tdgp", vec![Chunk::data(*b"tdmn", name), tdbs]) + } + + fn reported(match_name: &str, tdsb: u32, v: f64, check: fn(&AepGroup<'_, '_>, &str, &str, &mut Cx<'_, '_>)) -> usize { + let chunk = group_with(match_name, tdsb, v); + let g = AepGroup::from_chunk(&chunk).unwrap(); + let tm = TimeMap { timescale: 30, frame_duration: 1, rate: crate::import::time::frame_rate(30, 1).unwrap() }; + let (mut next, mut col) = (1, Collector::default()); + check(&g, match_name, "it", &mut Cx { what: "L", tm: &tm, ids: &mut Ids(&mut next), col: &mut col }); + col.finish().losses.iter().map(|l| l.count).sum() + } + + /// #3087: a guide rectangle's Shape Direction recorded in the default state (`tdsb` 3, holding + /// 2) is After Effects' default and is no loss; written (`tdsb` 1) with 2, it is one. + #[test] + fn a_default_state_record_is_no_loss() { + assert_eq!(reported("ADBE Vector Shape Direction", 3, 2.0, not_default), 0); + assert_eq!(reported("ADBE Vector Shape Direction", 1, 2.0, not_default), 1); + assert_eq!(reported("ADBE Vector Grad HiLite Length", 3, 5.0, not_zero), 0); + assert_eq!(reported("ADBE Vector Grad HiLite Length", 1, 5.0, not_zero), 1); + } +} diff --git a/crates/aep/tests/it/import/synthetic.rs b/crates/aep/tests/it/import/synthetic.rs index 8e4c28f16..a31a61679 100644 --- a/crates/aep/tests/it/import/synthetic.rs +++ b/crates/aep/tests/it/import/synthetic.rs @@ -842,3 +842,101 @@ fn a_default_state_colour_record_holds_the_value() { assert_eq!(colour(&default_state), colour(&written)); assert_ne!(colour(&default_state), colour(&unchanged), "the recorded colour, not the default"); } + +/// #3087: an expression on Orientation arrives with the stored value, as on any property; it was +/// dropped together with the value and reported as "not decoded". +#[test] +fn an_orientation_expression_is_imported_with_its_value() { + let bytes = mutate(&mut |c| { + if !c.is_list(b"otst") { + return; + } + for child in c.children_mut() { + if child.is_list(b"tdbs") { + let Body::List { children, .. } = &mut child.body else { continue }; + for k in children.iter_mut() { + if k.is(b"tdsb") { + set_data(k, 0, &1u32.to_be_bytes()); + } else if k.is(b"tdb4") { + set_data(k, 120, &[1]); + } + } + children.push(Chunk::data(*b"Utf8", b"[0, time * 90, 0]".to_vec())); + } else if child.is_list(b"otky") { + for k in child.children_mut() { + let degrees: Vec = [10.0f64, 20.0, 30.0].iter().flat_map(|d| d.to_be_bytes()).collect(); + set_data(k, 0, °rees); + } + } + } + }); + let (p, r) = run(&bytes, true); + assert_eq!(losses(&r.unwrap(), LossCategory::TransformProperties), Vec::::new()); + let mut seen = 0; + for item in p.items.values() { + let ItemKind::Comp(c) = &item.kind else { continue }; + for layer in &c.layers { + let Some(o) = layer.transform().and_then(|t| t.prop("orientation")).filter(|o| o.expr.is_some()) else { continue }; + assert_eq!(o.value, effectcraft_keyframe::Value::Vec3([10.0, 20.0, 30.0]), "{}", layer.name); + assert_eq!(o.expr.as_ref().map(|e| (e.text.as_str(), e.enabled)), Some(("[0, time * 90, 0]", true)), "{}", layer.name); + seen += 1; + } + } + assert!(seen > 0); +} + +/// `SYNTHETIC` with Levels' Channel and Clip to Output Black / White popups' item names (`pdnm`) +/// rewritten to the French labels a French-language After Effects stores there, keeping the same +/// item order so the index each popup is set to is unaffected. +fn with_french_levels_popups() -> Vec { + let (mut channel, mut clip) = (0, 0); + let bytes = mutate(&mut |c| { + if !c.is(b"Utf8") { + return; + } + if c.bytes() == Some(b"RGB|Red|Green|Blue|Alpha".as_slice()) { + *c = Chunk::data(*b"Utf8", b"RVB|Rouge|Vert|Bleu|Alpha".to_vec()); + channel += 1; + } else if c.bytes() == Some(b"On|Off|Off for 32 bpc Color".as_slice()) { + *c = Chunk::data(*b"Utf8", "Activé|Désactivé|Désactivé pour la couleur 32 bpc".as_bytes().to_vec()); + clip += 1; + } + }); + assert_eq!(channel, 2, "Levels' Channel pdnm"); + assert_eq!(clip, 4, "Levels' two Clip pdnm (Black and White), each stored twice"); + bytes +} + +/// A popup's item names in the file are matched to EffectCraft's own (English) ones by name +/// (`popup_key`): a project saved by a French After Effects stores "RVB", "Activé", "Désactivé +/// pour la couleur 32 bpc" for Levels' Channel and Clip to Output Black / White (observed in a +/// real "Niveaux" effect, After Effects 2026 26.0.0x67). Before `popup_alias`, none of these +/// matched EffectCraft's English-only options, so each came back "has no counterpart, default +/// used" and the real selection was silently lost. +#[test] +fn french_levels_popup_items_are_recognized() { + let (unchanged, r1) = run(SYNTHETIC, true); + let (french, r2) = run(&with_french_levels_popups(), true); + let (r1, r2) = (r1.unwrap(), r2.unwrap()); + let found = losses(&r2, LossCategory::Effects); + assert!(!found.iter().any(|d| d.contains("has no counterpart")), "{found:?}"); + for id in ["channel", "clipToOutputBlack", "clipToOutputWhite"] { + let want: Vec<_> = effect_values(&unchanged).into_iter().filter(|(p, _)| p == id).collect(); + let got: Vec<_> = effect_values(&french).into_iter().filter(|(p, _)| p == id).collect(); + assert_eq!(got, want, "{id}"); + } + assert_eq!(r1.report.imported.effects, r2.report.imported.effects); +} + +/// `popup_alias` must cover every item of a popup it claims to know, not only the one a first +/// report happened to show: Channel's French "Rouge", "Vert", "Bleu" ("Alpha" is the same word in +/// both languages) alongside the already-covered "RVB" (`effect_map.rs`, reviewed on #3432). +#[test] +fn french_levels_channel_aliases_cover_every_item() { + use effectcraft_aep::effect_map::popup_alias; + for (french, english) in [("rvb", "rgb"), ("rouge", "red"), ("vert", "green"), ("bleu", "blue")] { + assert_eq!(popup_alias(french), Some(english), "{french}"); + } + // "Alpha" needs no alias: the same word in both languages. + assert_eq!(popup_alias("alpha"), None); +} diff --git a/crates/automation/src/confine.rs b/crates/automation/src/confine.rs index 371085938..0407ba756 100644 --- a/crates/automation/src/confine.rs +++ b/crates/automation/src/confine.rs @@ -75,6 +75,9 @@ const REFUSED: &[&str] = &[ "library.preview", ]; +/// Most numbered folders `check_render_folder` inspects for one output; more are refused. +const MAX_NUMBERED_FOLDERS: usize = 4096; + /// Commands that read files named by these parameters. const READS: &[(&str, &[&str])] = &[ ("file.open", &["path"]), @@ -202,6 +205,80 @@ impl Confinement { if self.roots(access).any(|r| real.starts_with(r)) { Ok(real) } else { Err(denied("it is outside the automation roots")) } } + /// A render output's folder holds no symbolic link that leads outside the write roots. An + /// image sequence is written to numbered names (`out_[#####].png` → `out_00001.png`, and a + /// sequence output without `#` gets `_00001` too) that the check of its template never + /// resolved, and each frame file is created through whatever link already sits at its name, + /// truncating the file it points to. A link that can't be resolved counts as outside: the + /// render would create its target. When the frame number is in a folder name, the same goes for + /// each existing numbered folder and the folder it holds the frames in. + fn check_render_folder(&self, output: &str) -> Result<()> { + let real = self.check(output, Access::Write)?; + if let Some(dir) = real.parent() { + self.check_no_outward_links(dir, output)?; + } + // The frame number may sit in a folder name (`shots_[##]/f_[#####].png`): the folders the + // frames go to are the numbered ones that exist, which the template's own parent is not. + let (before, _, after) = effectcraft_engine::project::render_queue::sequence_parts(output); + let sep = ['/', '\\']; + let Some((dir_tail, rest)) = after.split_once(sep) else { return Ok(()) }; + let (parent, prefix) = before.rsplit_once(sep).unwrap_or(("", &before)); + // A parent that doesn't exist has no numbered folders; one that can't be listed might. + let scan = if parent.is_empty() { Path::new("/") } else { Path::new(parent) }; + let entries = match std::fs::read_dir(scan) { + Ok(entries) => entries, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()), + Err(e) => return Err(Error::BadArgs(format!("cannot render to `{output}`: its numbered folders in `{}` can't be checked: {e}", scan.display()))), + }; + let mut seen = 0; + for e in entries { + let e = + e.map_err(|e| Error::BadArgs(format!("cannot render to `{output}`: its numbered folders in `{}` can't be checked: {e}", scan.display())))?; + let name = e.file_name().to_string_lossy().into_owned(); + let Some(digits) = name.strip_prefix(prefix).and_then(|n| n.strip_suffix(dir_tail)) else { continue }; + if !digits.strip_prefix('-').unwrap_or(digits).bytes().all(|b| b.is_ascii_digit()) || digits.is_empty() { + continue; + } + seen += 1; + if seen > MAX_NUMBERED_FOLDERS { + return Err(Error::BadArgs(format!("cannot render to `{output}`: more than {MAX_NUMBERED_FOLDERS} numbered folders already exist to check"))); + } + // The numbered folder itself may be a link out; `check` resolves every existing part. + let frame_file = e.path().join(rest); + let frame_file = frame_file.to_string_lossy(); + self.check(&frame_file, Access::Write)?; + if let Some(dir) = Path::new(&*frame_file).parent().and_then(resolve) { + self.check_no_outward_links(&dir, output)?; + } + } + Ok(()) + } + + /// `dir` holds no symbolic link that leads outside the write roots. + fn check_no_outward_links(&self, dir: &Path, output: &str) -> Result<()> { + // A folder that does not exist yet holds no link; one that can't be listed might. + let cannot = |e: std::io::Error| Error::BadArgs(format!("cannot render to `{output}`: its folder `{}` can't be checked for links: {e}", dir.display())); + let entries = match std::fs::read_dir(dir) { + Ok(entries) => entries, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()), + Err(e) => return Err(cannot(e)), + }; + for e in entries { + let e = e.map_err(cannot)?; + if !e.file_type().map_err(cannot)?.is_symlink() { + continue; + } + let link = e.path(); + if !link.canonicalize().is_ok_and(|r| self.write.iter().any(|w| r.starts_with(w))) { + return Err(Error::BadArgs(format!( + "cannot render to `{output}`: its folder holds `{}`, a link that leads outside the automation write roots, where a numbered frame could be written", + link.display() + ))); + } + } + Ok(()) + } + /// Check engine command `id` with `params` before it runs. Returns what must still be looked /// up and passed to [`Confinement::check_lookup`] for commands that write implicitly. pub fn check_command(&self, id: &str, params: &Value) -> Result> { @@ -221,6 +298,12 @@ impl Confinement { for step in params.get("steps").and_then(Value::as_array).into_iter().flatten() { let Some(inner) = step.get("command").or_else(|| step.get("id")).and_then(Value::as_str) else { continue }; let inner_params = step.get("params").cloned().unwrap_or(Value::Null); + // A whole `params` given as a `$N` reference becomes an earlier step's result + // only as the batch runs, so none of its file paths would have been checked + // (`essential.templateInfo` returns a manifest naming any file at all). + if !matches!(inner_params, Value::Null | Value::Object(_)) { + return Err(Error::BadArgs(format!("`{inner}` in a batch: `params` must be an object while file access is confined"))); + } let named = keys_of(READS, inner).or_else(|| keys_of(WRITES, inner)).unwrap_or(&[]); let mixed = keys_of(MIXED_WRITES, inner).unwrap_or(&[]); // A step's `$N` reference is resolved only as the batch runs: never for a file. @@ -258,6 +341,9 @@ impl Confinement { Some(s) => { gave_a_file |= v.is_string(); self.check(s, access)?; + if QUEUES_AN_OUTPUT.contains(&id) || id.starts_with("renderQueue.") { + self.check_render_folder(s)?; + } } None => return Err(Error::BadArgs(format!("`{id}`: `{key}` must be a path"))), } @@ -321,11 +407,15 @@ impl Confinement { let items = found.get("items").or(Some(found)).and_then(Value::as_array).cloned().unwrap_or_default(); for it in items.iter().filter(|it| it.get("render").and_then(Value::as_bool) != Some(false)) { let extra = it.get("extraOutputs").and_then(Value::as_array).into_iter().flatten().map(|o| o.get("outputPath")); - for p in std::iter::once(it.get("outputPath")).chain(extra).chain([it.get("logPath")]).flatten() { + for p in std::iter::once(it.get("outputPath")).chain(extra).flatten() { if let Some(p) = p.as_str() { self.check(p, Access::Write)?; + self.check_render_folder(p)?; } } + if let Some(p) = it.get("logPath").and_then(Value::as_str) { + self.check(p, Access::Write)?; + } } Ok(()) } @@ -757,6 +847,112 @@ mod tests { ] { assert!(c.check_command("effect.plugins.install", ¶ms).is_err(), "{params}"); } + } + + #[test] + fn a_batch_step_whose_params_are_a_reference_is_refused() { + let base = std::env::temp_dir().join(format!("ec-confine-batch-ref-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&base); + let (read, write) = (base.join("in"), base.join("out")); + for d in [&read, &write] { + std::fs::create_dir_all(d).unwrap(); + } + let c = Confinement::new(std::slice::from_ref(&read), std::slice::from_ref(&write)).unwrap(); + let info = read.join("t.ectemplate").to_string_lossy().into_owned(); + // Step 2's params are step 1's result, which can name any `path`. + let steps = serde_json::json!({"steps": [ + {"command": "essential.templateInfo", "params": {"path": info}}, + {"command": "file.saveAs", "params": "$1"}, + ]}); + let e = c.check_command("engine.batch", &steps).unwrap_err().to_string(); + assert!(e.contains("must be an object"), "{e}"); + for params in [serde_json::json!("$1.manifest"), serde_json::json!(["$1"]), serde_json::json!(7)] { + let steps = serde_json::json!({"steps": [{"command": "file.saveAs", "params": params}]}); + assert!(c.check_command("engine.batch", &steps).is_err(), "{params}"); + } + // Object params (or none) are checked as before. + let saved = write.join("p.ecproj").to_string_lossy().into_owned(); + let steps = serde_json::json!({"steps": [{"command": "file.saveAs", "params": {"path": saved}}, {"command": "edit.undo"}]}); + assert!(c.check_command("engine.batch", &steps).is_ok()); + let _ = std::fs::remove_dir_all(&base); + } + + #[cfg(unix)] + #[test] + fn a_render_folder_with_a_link_out_of_the_write_roots_is_refused() { + let base = std::env::temp_dir().join(format!("ec-confine-seq-link-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&base); + let (read, write, outside) = (base.join("in"), base.join("out"), base.join("elsewhere")); + for d in [&read, &write, &outside] { + std::fs::create_dir_all(d).unwrap(); + } + let c = Confinement::new(std::slice::from_ref(&read), std::slice::from_ref(&write)).unwrap(); + let template = write.join("frames_[#####].png").to_string_lossy().into_owned(); + let add = serde_json::json!({"output": template}); + let queue = serde_json::json!({"items": [{"outputPath": template}]}); + assert!(c.check_command("renderQueue.add", &add).is_ok()); + assert!(c.check_lookup(&Lookup::RenderOutputs, &queue).is_ok()); + // A link inside the write root is fine. + std::os::unix::fs::symlink(write.join("kept.png"), write.join("frames_00002.png")).unwrap(); + std::fs::write(write.join("kept.png"), b"x").unwrap(); + assert!(c.check_lookup(&Lookup::RenderOutputs, &queue).is_ok()); + // Frame 1's name leads outside: rendering would truncate that file. + std::os::unix::fs::symlink(outside.join("victim"), write.join("frames_00001.png")).unwrap(); + assert!(c.check_command("renderQueue.add", &add).is_err()); + assert!(c.check_command("render.preRender", &add).is_err()); + let e = c.check_lookup(&Lookup::RenderOutputs, &queue).unwrap_err().to_string(); + assert!(e.contains("frames_00001.png"), "{e}"); + let _ = std::fs::remove_dir_all(&base); + } + + #[cfg(unix)] + #[test] + fn a_render_folder_that_cannot_be_listed_is_refused() { + use std::os::unix::fs::PermissionsExt; + let base = std::env::temp_dir().join(format!("ec-confine-seq-nolist-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&base); + let write = base.join("out"); + std::fs::create_dir_all(&write).unwrap(); + let c = Confinement::new(&[], std::slice::from_ref(&write)).unwrap(); + let template = write.join("frames_[#####].png").to_string_lossy().into_owned(); + let add = serde_json::json!({"output": template}); + // Writable and searchable but not listable: a link in it could not be seen. + std::fs::set_permissions(&write, std::fs::Permissions::from_mode(0o300)).unwrap(); + // A user who can list it anyway (root) sees no difference to test. + if std::fs::read_dir(&write).is_err() { + assert!(c.check_command("renderQueue.add", &add).is_err()); + } + std::fs::set_permissions(&write, std::fs::Permissions::from_mode(0o700)).unwrap(); + assert!(c.check_command("renderQueue.add", &add).is_ok()); + let _ = std::fs::remove_dir_all(&base); + } + + #[cfg(unix)] + #[test] + fn numbered_render_folders_with_links_out_of_the_write_roots_are_refused() { + let base = std::env::temp_dir().join(format!("ec-confine-seq-dir-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&base); + let (write, outside) = (base.join("out"), base.join("elsewhere")); + for d in [&write, &outside] { + std::fs::create_dir_all(d).unwrap(); + } + let c = Confinement::new(&[], std::slice::from_ref(&write)).unwrap(); + let template = write.join("shot_[###]/f_[#####].png").to_string_lossy().into_owned(); + let add = serde_json::json!({"output": template}); + assert!(c.check_command("renderQueue.add", &add).is_ok(), "no numbered folder exists yet"); + // A numbered folder inside the roots holding a link out is refused. + std::fs::create_dir(write.join("shot_001")).unwrap(); + assert!(c.check_command("renderQueue.add", &add).is_ok()); + std::os::unix::fs::symlink(outside.join("victim"), write.join("shot_001/f_00001.png")).unwrap(); + assert!(c.check_command("renderQueue.add", &add).is_err()); + std::fs::remove_file(write.join("shot_001/f_00001.png")).unwrap(); + // A numbered folder that is itself a link out is refused. + std::os::unix::fs::symlink(&outside, write.join("shot_002")).unwrap(); + assert!(c.check_command("render.preRender", &add).is_err()); + // Unrelated names are left alone. + std::fs::remove_file(write.join("shot_002")).unwrap(); + std::os::unix::fs::symlink(&outside, write.join("shot_notes")).unwrap(); + assert!(c.check_command("renderQueue.add", &add).is_ok()); let _ = std::fs::remove_dir_all(&base); } } diff --git a/crates/av1enc/Cargo.toml b/crates/av1enc/Cargo.toml deleted file mode 100644 index 2a4c7ddfe..000000000 --- a/crates/av1enc/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "effectcraft-av1enc" -description = "Clean-room pure-Rust AV1 encoder (Main profile, key and inter frames) for MP4/WebM export, from the AV1 Bitstream & Decoding Process Specification" -version.workspace = true -edition.workspace = true -license.workspace = true -rust-version.workspace = true - -[lints] -workspace = true - -[dependencies] - -[dev-dependencies] -filmcraft-av1 = { workspace = true } diff --git a/crates/av1enc/README.md b/crates/av1enc/README.md deleted file mode 100644 index dcc343693..000000000 --- a/crates/av1enc/README.md +++ /dev/null @@ -1,76 +0,0 @@ -# effectcraft-av1enc - -Clean-room, pure-Rust AV1 encoder for EffectCraft's MP4 / WebM export (layer L0, `std` only, -no `unsafe`, builds for `wasm32-unknown-unknown`). - -Output is decoded **bit-exactly** by libdav1d (`ffmpeg -c:v libdav1d`, checked in -`tests/it/ffmpeg_oracle.rs` when ffmpeg is installed) and by FilmCraft's spec-derived -`filmcraft-av1` decoder (`tests/it/roundtrip.rs`): every decoded frame equals the encoder's own -reconstruction, so prediction never drifts. - -## What is implemented - -- **Stream**: Main profile (`seq_profile` 0), 4:2:0, 8 or 10 bit, BT.709 colour description, - limited or full range. Sequence header with one operating point; `seq_level_idx` given or the - lowest Annex A level whose MaxPicSize / MaxH/VSize / MaxDisplayRate (and, in bitrate mode, Main - tier MaxBitrate) fit. Order hints, film grain, superres, CDEF, loop restoration, intra edge - filter, filter intra, inter-intra, masked / distance compound, warped motion and dual filter - are disabled in the sequence header. -- **Packets**: one temporal unit per frame as stored in MP4 / Matroska samples: OBUs with size - fields, no temporal delimiter; key frames start with the sequence header OBU, followed by one - `OBU_FRAME` (frame header + tile group). Every frame is shown, in input order. - `Encoder::av1c()` returns the `AV1CodecConfigurationRecord` (also the WebM `CodecPrivate`). -- **Frames**: a `KEY_FRAME` every `keyint` frames, `INTER_FRAME`s otherwise. Inter frames use - slot 0 (the previous reconstruction) for all seven references and refresh slot 0 only, so - `LAST_FRAME` is always the previous frame. CDFs adapt per symbol and are saved at the end of - every frame; inter frames load them from `LAST_FRAME` (`primary_ref_frame` = 0). One tile, - `TX_MODE_LARGEST`, `reduced_tx_set`, no segmentation, no delta q / lf. -- **Deblocking**: the normative loop filter (7.14) runs on the reconstruction; the frame level - (the same for both luma directions and both chroma planes, no deltas, sharpness 0) is searched - per frame for the lowest luma error. -- **Tiles**: the multi-symbol arithmetic encoder is the exact inverse of the spec's symbol - decoder (8.2), including the exit process padding; CDF adaptation as in 8.2.6. -- **Partitions**: 64x64 superblocks, `PARTITION_NONE` / `PARTITION_SPLIT` down to 8x8 chosen by - a rate-distortion cost (SSE + λ·estimated bits); blocks crossing the frame edge are split as - the syntax requires. 64x64 blocks are only used as inter skip blocks. -- **Intra**: DC, V, H, Paeth, smooth, smooth-V, smooth-H for luma and chroma, - ranked by SATD with a rate-distortion choice among the best three; the spec's edge preparation - and above-right / below-left availability. -- **Inter**: single reference (`LAST_FRAME`), `NEWMV` / `NEARESTMV` / `NEARMV` / `GLOBALMV` - chosen from the spec's motion vector prediction stack (7.10.2, mirrored exactly); motion search - from neighbour / co-located / parent candidates, a whole-sample diamond search and half- then - quarter-sample refinement (`allow_high_precision_mv` = 0); the normative 8-tap `EIGHTTAP` - prediction for luma and chroma; skip blocks; per-block intra / inter choice. -- **Residual**: one transform per plane and block (4x4 ... 32x32): for intra luma `DCT_DCT` or - the mode's preferred `ADST` combination (rate-distortion choice over the two best modes), the - mode-dependent type for intra chroma, `DCT_DCT` for inter blocks; dead-zone quantisation with the - spec's `Dc_Qlookup` / `Ac_Qlookup`; the normative dequantisation and inverse transforms in the - reconstruction loop; full coefficient coding (all_zero, tx type, eob_pt, eob_extra, - coeff_base_eob, coeff_base, coeff_br, dc_sign, Golomb) with the exact contexts. -- **Rate control**: `ConstantQ(base_q_idx)`; `Bitrate { kbps }` adapts `base_q_idx` per frame - towards the average target (key frames get a lower quantizer and a larger share of the budget of - their group of `keyint` frames, the inter frames the rest; the first key frame is searched in a - few passes, a later key frame that misses its budget by more than 19 % is coded again; the - deviation from the budget is paid back over 16 frames). - -## Limits - -- One tile: width and height up to 4096 and at most 2304 superblocks (4096x2304 samples). -- No CDEF or loop restoration, no compound prediction, no rectangular or 4xN blocks, no - directional intra modes other than V / H, no CfL or palette, no transform-size splits, so - compression is below mature encoders. -- 4:2:0 only (Main profile), no 12 bit. - -## Specification and provenance - -Implemented from the **AV1 Bitstream & Decoding Process Specification, version 1.0.0 with -Errata 1** (AOMediaCodec, 2019; the edition FilmCraft's decoder follows) and the **AV1 Codec -ISO Media File Format Binding** (v1.2.0) for `av1C`. - -The constant tables (default CDFs, scan orders, quantizer lookup tables, interpolation filter -taps, smooth weights, block / transform size tables, ...) and the decoder-side processes the -encoder must reproduce exactly (inverse transforms, intra and inter prediction rounding, context -derivations, motion vector prediction) are mirrored from FilmCraft's first-party, spec-derived -`filmcraft-av1` decoder (MIT OR Apache-2.0), which is also the round-trip oracle. No source of -libaom, dav1d, rav1e, SVT-AV1, ffmpeg or any other third-party encoder or decoder was consulted; -ffmpeg / libdav1d is used only as an external test oracle. diff --git a/crates/av1enc/src/bitw.rs b/crates/av1enc/src/bitw.rs deleted file mode 100644 index e4c8a1e69..000000000 --- a/crates/av1enc/src/bitw.rs +++ /dev/null @@ -1,87 +0,0 @@ -//! MSB-first bit writer for the OBU headers (spec 4.10 `f(n)` descriptors) and `leb128()`. - -#[derive(Default)] -pub(crate) struct BitWriter { - pub buf: Vec, - nbits: usize, -} - -impl BitWriter { - pub fn new() -> Self { - BitWriter::default() - } - - /// `f(n)`: writes the low `n` bits of `v`, most significant first. - pub fn f(&mut self, n: u32, v: u32) { - for i in (0..n).rev() { - self.bit((v >> i) & 1 != 0); - } - } - - pub fn bit(&mut self, b: bool) { - if self.nbits.is_multiple_of(8) { - self.buf.push(0); - } - if b { - let last = self.buf.len() - 1; - self.buf[last] |= 0x80 >> (self.nbits % 8); - } - self.nbits += 1; - } - - /// `byte_alignment()`: zero bits up to the next byte boundary. - pub fn byte_align(&mut self) { - while !self.nbits.is_multiple_of(8) { - self.bit(false); - } - } - - /// `trailing_bits()`: a one bit followed by zero bits up to the byte boundary. - pub fn trailing_bits(&mut self) { - self.bit(true); - self.byte_align(); - } - - pub fn into_bytes(self) -> Vec { - self.buf - } -} - -/// `leb128()` encoding of an OBU size. -pub(crate) fn leb128(mut v: usize, out: &mut Vec) { - loop { - let mut b = (v & 0x7f) as u8; - v >>= 7; - if v != 0 { - b |= 0x80; - } - out.push(b); - if v == 0 { - break; - } - } -} - -/// An OBU with `obu_has_size_field` = 1 and no extension header. -pub(crate) fn obu(obu_type: u8, payload: &[u8], out: &mut Vec) { - out.push((obu_type << 3) | 0x02); - leb128(payload.len(), out); - out.extend_from_slice(payload); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn bits_and_leb128() { - let mut w = BitWriter::new(); - w.f(3, 0b101); - w.f(6, 0b110011); - w.trailing_bits(); - assert_eq!(w.into_bytes(), vec![0b1011_1001, 0b1100_0000]); - let mut v = Vec::new(); - leb128(300, &mut v); - assert_eq!(v, vec![0xAC, 0x02]); - } -} diff --git a/crates/av1enc/src/cdf.rs b/crates/av1enc/src/cdf.rs deleted file mode 100644 index 777164d1a..000000000 --- a/crates/av1enc/src/cdf.rs +++ /dev/null @@ -1,172 +0,0 @@ -//! The adaptive CDFs this encoder codes with (the subset of spec 6.8.2 init_non_coeff_cdfs / -//! init_coeff_cdfs that its syntax uses). Layout as in filmcraft-av1: each innermost array -//! holds N cumulative values (the last is 32768) plus the adaptation counter. - -use crate::tables::{ - DEFAULT_ANGLE_DELTA_CDF, DEFAULT_COEFF_BASE_CDF, DEFAULT_COEFF_BASE_EOB_CDF, DEFAULT_COEFF_BR_CDF, DEFAULT_DC_SIGN_CDF, DEFAULT_DRL_MODE_CDF, - DEFAULT_EOB_EXTRA_CDF, DEFAULT_EOB_PT_16_CDF, DEFAULT_EOB_PT_32_CDF, DEFAULT_EOB_PT_64_CDF, DEFAULT_EOB_PT_128_CDF, DEFAULT_EOB_PT_256_CDF, - DEFAULT_EOB_PT_512_CDF, DEFAULT_EOB_PT_1024_CDF, DEFAULT_INTER_TX_TYPE_SET3_CDF, DEFAULT_INTRA_TX_TYPE_SET2_CDF, DEFAULT_IS_INTER_CDF, DEFAULT_MV_BIT_CDF, - DEFAULT_MV_CLASS_CDF, DEFAULT_MV_CLASS0_BIT_CDF, DEFAULT_MV_CLASS0_FR_CDF, DEFAULT_MV_FR_CDF, DEFAULT_MV_JOINT_CDF, DEFAULT_MV_SIGN_CDF, - DEFAULT_NEW_MV_CDF, DEFAULT_PARTITION_W8_CDF, DEFAULT_PARTITION_W16_CDF, DEFAULT_PARTITION_W32_CDF, DEFAULT_PARTITION_W64_CDF, DEFAULT_REF_MV_CDF, - DEFAULT_SINGLE_REF_CDF, DEFAULT_SKIP_CDF, DEFAULT_TXB_SKIP_CDF, DEFAULT_UV_MODE_CFL_ALLOWED_CDF, DEFAULT_UV_MODE_CFL_NOT_ALLOWED_CDF, DEFAULT_Y_MODE_CDF, - DEFAULT_ZERO_MV_CDF, -}; - -#[derive(Clone)] -pub(crate) struct Cdfs { - pub y_mode: [[u16; 14]; 4], - pub uv_mode_cfl_not_allowed: [[u16; 14]; 13], - pub uv_mode_cfl_allowed: [[u16; 15]; 13], - pub angle_delta: [[u16; 8]; 8], - pub partition_w8: [[u16; 5]; 4], - pub partition_w16: [[u16; 11]; 4], - pub partition_w32: [[u16; 11]; 4], - pub partition_w64: [[u16; 11]; 4], - pub skip: [[u16; 3]; 3], - pub is_inter: [[u16; 3]; 4], - pub single_ref: [[[u16; 3]; 6]; 3], - pub new_mv: [[u16; 3]; 6], - pub zero_mv: [[u16; 3]; 2], - pub ref_mv: [[u16; 3]; 6], - pub drl_mode: [[u16; 3]; 3], - pub mv_joint: [u16; 5], - pub mv_class: [[u16; 12]; 2], - pub mv_class0_bit: [[u16; 3]; 2], - pub mv_fr: [[u16; 5]; 2], - pub mv_class0_fr: [[[u16; 5]; 2]; 2], - pub mv_sign: [[u16; 3]; 2], - pub mv_bit: [[[u16; 3]; 10]; 2], - pub intra_tx_type_set2: [[[u16; 6]; 13]; 3], - pub inter_tx_type_set3: [[u16; 3]; 4], - pub txb_skip: [[[u16; 3]; 13]; 5], - pub eob_pt_16: [[[u16; 6]; 2]; 2], - pub eob_pt_32: [[[u16; 7]; 2]; 2], - pub eob_pt_64: [[[u16; 8]; 2]; 2], - pub eob_pt_128: [[[u16; 9]; 2]; 2], - pub eob_pt_256: [[[u16; 10]; 2]; 2], - pub eob_pt_512: [[u16; 11]; 2], - pub eob_pt_1024: [[u16; 12]; 2], - pub eob_extra: [[[[u16; 3]; 9]; 2]; 5], - pub dc_sign: [[[u16; 3]; 3]; 2], - pub coeff_base_eob: [[[[u16; 4]; 4]; 2]; 5], - pub coeff_base: [[[[u16; 5]; 42]; 2]; 5], - pub coeff_br: [[[[u16; 5]; 21]; 2]; 5], -} - -impl Cdfs { - /// init_non_coeff_cdfs( ) followed by init_coeff_cdfs( ) for `base_q_idx`. - pub fn new(base_q_idx: u32) -> Box { - let idx = match base_q_idx { - 0..=20 => 0, - 21..=60 => 1, - 61..=120 => 2, - _ => 3, - }; - Box::new(Cdfs { - y_mode: DEFAULT_Y_MODE_CDF, - uv_mode_cfl_not_allowed: DEFAULT_UV_MODE_CFL_NOT_ALLOWED_CDF, - uv_mode_cfl_allowed: DEFAULT_UV_MODE_CFL_ALLOWED_CDF, - angle_delta: DEFAULT_ANGLE_DELTA_CDF, - partition_w8: DEFAULT_PARTITION_W8_CDF, - partition_w16: DEFAULT_PARTITION_W16_CDF, - partition_w32: DEFAULT_PARTITION_W32_CDF, - partition_w64: DEFAULT_PARTITION_W64_CDF, - skip: DEFAULT_SKIP_CDF, - is_inter: DEFAULT_IS_INTER_CDF, - single_ref: DEFAULT_SINGLE_REF_CDF, - new_mv: DEFAULT_NEW_MV_CDF, - zero_mv: DEFAULT_ZERO_MV_CDF, - ref_mv: DEFAULT_REF_MV_CDF, - drl_mode: DEFAULT_DRL_MODE_CDF, - mv_joint: DEFAULT_MV_JOINT_CDF, - mv_class: DEFAULT_MV_CLASS_CDF, - mv_class0_bit: [DEFAULT_MV_CLASS0_BIT_CDF; 2], - mv_fr: DEFAULT_MV_FR_CDF, - mv_class0_fr: DEFAULT_MV_CLASS0_FR_CDF, - mv_sign: [DEFAULT_MV_SIGN_CDF; 2], - mv_bit: [DEFAULT_MV_BIT_CDF; 2], - intra_tx_type_set2: DEFAULT_INTRA_TX_TYPE_SET2_CDF, - inter_tx_type_set3: DEFAULT_INTER_TX_TYPE_SET3_CDF, - txb_skip: DEFAULT_TXB_SKIP_CDF[idx], - eob_pt_16: DEFAULT_EOB_PT_16_CDF[idx], - eob_pt_32: DEFAULT_EOB_PT_32_CDF[idx], - eob_pt_64: DEFAULT_EOB_PT_64_CDF[idx], - eob_pt_128: DEFAULT_EOB_PT_128_CDF[idx], - eob_pt_256: DEFAULT_EOB_PT_256_CDF[idx], - eob_pt_512: DEFAULT_EOB_PT_512_CDF[idx], - eob_pt_1024: DEFAULT_EOB_PT_1024_CDF[idx], - eob_extra: DEFAULT_EOB_EXTRA_CDF[idx], - dc_sign: DEFAULT_DC_SIGN_CDF[idx], - coeff_base_eob: DEFAULT_COEFF_BASE_EOB_CDF[idx], - coeff_base: DEFAULT_COEFF_BASE_CDF[idx], - coeff_br: DEFAULT_COEFF_BR_CDF[idx], - }) - } -} - -impl Cdfs { - /// The counter reset of load_cdfs( ): the last entry of every CDF array is set to 0. - pub fn clear_counts(&mut self) { - macro_rules! clr { - ($($f:ident),*) => { $( self.$f.clear_count(); )* }; - } - clr!( - y_mode, - uv_mode_cfl_not_allowed, - uv_mode_cfl_allowed, - angle_delta, - partition_w8, - partition_w16, - partition_w32, - partition_w64, - skip, - is_inter, - single_ref, - new_mv, - zero_mv, - ref_mv, - drl_mode, - mv_joint, - mv_class, - mv_class0_bit, - mv_fr, - mv_class0_fr, - mv_sign, - mv_bit, - intra_tx_type_set2, - inter_tx_type_set3, - txb_skip, - eob_pt_16, - eob_pt_32, - eob_pt_64, - eob_pt_128, - eob_pt_256, - eob_pt_512, - eob_pt_1024, - eob_extra, - dc_sign, - coeff_base_eob, - coeff_base, - coeff_br - ); - } -} - -/// Zeroes the adaptation counter (last element) of every innermost CDF array. -trait ClearCount { - fn clear_count(&mut self); -} - -impl ClearCount for [u16; N] { - fn clear_count(&mut self) { - self[N - 1] = 0; - } -} - -impl ClearCount for [T; M] { - fn clear_count(&mut self) { - for e in self.iter_mut() { - e.clear_count(); - } - } -} diff --git a/crates/av1enc/src/decide.rs b/crates/av1enc/src/decide.rs deleted file mode 100644 index b06852809..000000000 --- a/crates/av1enc/src/decide.rs +++ /dev/null @@ -1,1273 +0,0 @@ -//! Mode decision and reconstruction (not normative): partitions, intra modes, motion vectors, -//! transform coefficients. Every prediction and reconstruction step uses the normative -//! processes, so the reconstruction built here is exactly what a decoder produces; the syntax -//! is written afterwards by `writer.rs`. - -use crate::pred::{INTRA_MODES_USED, InterBlock, IntraEdge, Plane, edges, half_planes, predict_inter, predict_intra}; -use crate::tables::{ - AC_QLOOKUP, BLOCK_8X8, BLOCK_64X64, DC_PRED, DC_QLOOKUP, DCT_DCT, DEFAULT_SCAN_4X4, DEFAULT_SCAN_8X8, DEFAULT_SCAN_16X16, DEFAULT_SCAN_32X32, MODE_TO_TXFM, - NEWMV, NUM_4X4_BLOCKS_WIDE, PARTITION_SPLIT, PARTITION_SUBSIZE, TX_4X4, TX_8X8, TX_16X16, TX_32X32, TX_64X64, TX_SET_DCTONLY, TX_SET_INTER_3, - TX_SET_INTRA_2, TX_SIZE_SQR_UP, TX_TYPE_IN_SET_INTER, TX_TYPE_IN_SET_INTRA, -}; -use crate::transform::{forward_2d, inverse_2d}; - -/// Decisions for one coded block. -#[derive(Clone)] -pub(crate) struct Leaf { - pub bsize: usize, - pub is_inter: bool, - pub y_mode: usize, - pub uv_mode: usize, - pub tx_type_y: usize, - /// Motion vector [row, col] in 1/8 luma samples (inter blocks). - pub mv: [i32; 2], - pub skip: bool, - /// Quantised coefficients of the single transform block of each plane (raster order). - pub coefs: [Vec; 3], -} - -pub(crate) enum Node { - Out, - Leaf(Box), - Split(Box<[Node; 4]>), -} - -/// A reconstructed frame kept as a reference. -pub(crate) struct RefFrame { - pub planes: [Plane; 3], - /// Motion vectors chosen for the frame (per 4x4), used as search candidates. - pub mvs: Vec<[i32; 2]>, - /// The frame's loop filter level. - pub lf_level: u32, - /// The CDFs saved at the end of the frame (loaded by the next frame). - pub cdfs: Box, -} - -pub(crate) struct FrameGeom { - pub width: usize, - pub height: usize, - pub mi_cols: usize, - pub mi_rows: usize, - pub sb_cols: usize, - pub sb_rows: usize, -} - -impl FrameGeom { - pub fn new(width: usize, height: usize) -> FrameGeom { - let mi_cols = 2 * width.div_ceil(8); - let mi_rows = 2 * height.div_ceil(8); - FrameGeom { width, height, mi_cols, mi_rows, sb_cols: mi_cols.div_ceil(16), sb_rows: mi_rows.div_ceil(16) } - } - /// Padded plane size (whole superblocks). - pub fn padded(&self, plane: usize) -> (usize, usize) { - let s = (plane > 0) as usize; - ((self.sb_cols * 64) >> s, (self.sb_rows * 64) >> s) - } - /// Visible plane size. - pub fn visible(&self, plane: usize) -> (usize, usize) { - if plane == 0 { (self.width, self.height) } else { (self.width.div_ceil(2), self.height.div_ceil(2)) } - } -} - -pub(crate) fn tx_size_for(log2: u32) -> usize { - match log2 { - 2 => TX_4X4, - 3 => TX_8X8, - 4 => TX_16X16, - 5 => TX_32X32, - _ => TX_64X64, - } -} - -/// get_tx_set( ) for the square sizes used here with reduced_tx_set = 1. -pub(crate) fn tx_set(tx_sz: usize, is_inter: bool) -> usize { - let sqr_up = TX_SIZE_SQR_UP[tx_sz] as usize; - if sqr_up > TX_32X32 { - return TX_SET_DCTONLY; - } - if is_inter { - TX_SET_INTER_3 - } else if sqr_up == TX_32X32 { - TX_SET_DCTONLY - } else { - TX_SET_INTRA_2 - } -} - -pub(crate) fn tx_in_set(set: usize, is_inter: bool, tx_type: usize) -> bool { - if is_inter { TX_TYPE_IN_SET_INTER[set][tx_type] == 1 } else { TX_TYPE_IN_SET_INTRA[set][tx_type] == 1 } -} - -/// The chroma transform type of an intra block (compute_tx_type for plane > 0). -pub(crate) fn intra_uv_tx_type(uv_mode: usize, uv_tx: usize) -> usize { - let t = MODE_TO_TXFM[uv_mode] as usize; - if tx_in_set(tx_set(uv_tx, false), false, t) { t } else { DCT_DCT } -} - -/// Geometry and coding mode of one square transform block. -#[derive(Clone, Copy)] -struct TransformBlock { - plane: usize, - x: usize, - y: usize, - log2: u32, - tx_type: usize, - intra: bool, -} - -struct TxResult { - levels: Vec, - nonzero: bool, - dist: f64, - bits: f64, -} - -/// The output and scratch buffers of one reconstruction, each `n * n` entries long. -struct ReconBuffers<'a> { - deq: &'a mut [i32], - resid: &'a mut [i32], - recon: &'a mut [u16], -} - -/// Outcome of trying one transform block; the levels and the reconstruction stay in `TxScratch`. -#[derive(Clone, Copy)] -struct TxEval { - nonzero: bool, - dist: f64, - bits: f64, -} - -/// Buffers of the transform path, sized for the largest coded transform block (32x32). Only the -/// first `n * n` entries are used; they are reused across candidates instead of being allocated. -struct TxScratch { - res: [i32; 1024], - coef: [f32; 1024], - levels: [i32; 1024], - deq: [i32; 1024], - resid: [i32; 1024], - recon: [u16; 1024], -} - -impl Default for TxScratch { - fn default() -> Self { - TxScratch { res: [0; 1024], coef: [0.0; 1024], levels: [0; 1024], deq: [0; 1024], resid: [0; 1024], recon: [0; 1024] } - } -} - -/// Saved encoder state of a block area (reconstruction, BlockDecoded flags). -struct Snapshot { - planes: [Vec; 3], - /// BlockDecoded flags of the block area only (the only flags coding inside it can change). - decoded: [[[bool; 16]; 16]; 3], -} - -pub(crate) struct Decider<'a> { - pub g: &'a FrameGeom, - pub bd: u32, - pub intra_only: bool, - pub src: &'a [Plane; 3], - pub rec: [Plane; 3], - pub refr: Option<&'a RefFrame>, - decoded: Box<[[[bool; 35]; 35]; 3]>, - /// Decision motion vectors per 4x4 (search candidates). - pub mvs: Vec<[i32; 2]>, - dc_q: f32, - ac_q: f32, - dc_qi: i64, - ac_qi: i64, - /// Smallest magnitude that quantises to a non-zero level, `[dc, ac][intra]`. - dead_zone: [[f32; 2]; 2], - lambda: f64, - lambda_sad: f64, - /// Half-sample interpolations of the reference luma (visible area): (8, 0), (0, 8), (8, 8). - half: [Vec; 3], - /// Transform buffers, taken out while a block is being tried (see `code_tx`). - scratch: Option>, -} - -const MODE_BITS_INTRA: f64 = 3.0; -/// Dead-zone quantiser rounding offsets (below one half) for inter and intra blocks. -const INTER_RND: f32 = 0.30; -const INTRA_RND: f32 = 0.38; - -impl<'a> Decider<'a> { - pub fn new(g: &'a FrameGeom, bd: u32, qidx: u8, intra_only: bool, src: &'a [Plane; 3], refr: Option<&'a RefFrame>) -> Self { - let bdi = ((bd - 8) >> 1) as usize; - let dc_qi = DC_QLOOKUP[bdi][qidx as usize] as i64; - let ac_qi = AC_QLOOKUP[bdi][qidx as usize] as i64; - // Lagrangian multiplier in squared-sample units of the coded bit depth. - let step = ac_qi as f64 / 8.0; - let lambda = 0.25 * step * step; - let rec = std::array::from_fn(|p| { - let (w, h) = g.padded(p); - Plane::new(w, h) - }); - Decider { - g, - bd, - intra_only, - src, - rec, - refr, - decoded: Box::new([[[false; 35]; 35]; 3]), - mvs: vec![[0, 0]; g.mi_cols * g.mi_rows], - dc_q: dc_qi as f32, - ac_q: ac_qi as f32, - dc_qi, - ac_qi, - dead_zone: [dc_qi, ac_qi].map(|q| [INTER_RND, INTRA_RND].map(|rnd| zero_threshold(q as f32, rnd))), - lambda, - lambda_sad: lambda.sqrt(), - half: match refr { - Some(rf) => half_planes(&rf.planes[0], g.width, g.height, bd), - None => Default::default(), - }, - scratch: None, - } - } - - /// Decides one 64x64 superblock (reconstruction is written to `rec`). - pub fn superblock(&mut self, r: usize, c: usize) -> Node { - // clear_block_decoded_flags( r, c, sbSize4 ) - for plane in 0..3 { - let s = (plane > 0) as usize; - let sb_w4 = (self.g.mi_cols as isize - c as isize) >> s; - let sb_h4 = (self.g.mi_rows as isize - r as isize) >> s; - let bd = &mut self.decoded[plane]; - for y in -1..=((16 >> s) as isize) { - for x in -1..=((16 >> s) as isize) { - let v = if y < 0 && x < sb_w4 { true } else { x < 0 && y < sb_h4 }; - bd[(y + 1) as usize][(x + 1) as usize] = v; - } - } - bd[(16 >> s) + 1][0] = false; - } - self.partition(r, c, BLOCK_64X64, None).0 - } - - fn decoded_at(&self, plane: usize, y: isize, x: isize) -> bool { - let (yy, xx) = (y + 1, x + 1); - if yy < 0 || xx < 0 || yy > 34 || xx > 34 { - return false; - } - self.decoded[plane][yy as usize][xx as usize] - } - - /// Marks the transform block of `plane` covering the block at (r, c) as decoded. - fn mark_decoded(&mut self, plane: usize, r: usize, c: usize, n4: usize) { - let s = (plane > 0) as usize; - let by = (r & 15) >> s; - let bx = (c & 15) >> s; - let step = (n4 >> s).max(1); - for i in 0..step { - for j in 0..step { - if by + i + 1 < 35 && bx + j + 1 < 35 { - self.decoded[plane][by + i + 1][bx + j + 1] = true; - } - } - } - } - - /// The BlockDecoded cells `mark_decoded` can set for blocks inside (r, c, n4): rows and - /// columns `by + 1 .. by + 1 + step` / `bx + 1 ..` of `decoded[plane]`. - fn decoded_region(plane: usize, r: usize, c: usize, n4: usize) -> (usize, usize, usize) { - let s = (plane > 0) as usize; - ((r & 15) >> s, (c & 15) >> s, (n4 >> s).max(1)) - } - - fn snapshot(&self, r: usize, c: usize, n4: usize) -> Snapshot { - let planes = std::array::from_fn(|p| { - let s = (p > 0) as usize; - let (x0, y0, n) = ((c * 4) >> s, (r * 4) >> s, (n4 * 4) >> s); - let pl = &self.rec[p]; - let mut v = Vec::with_capacity(n * n); - for y in y0..y0 + n { - v.extend_from_slice(&pl.data[y * pl.stride + x0..y * pl.stride + x0 + n]); - } - v - }); - let mut decoded = [[[false; 16]; 16]; 3]; - for (p, d) in decoded.iter_mut().enumerate() { - let (by, bx, step) = Self::decoded_region(p, r, c, n4); - for i in 0..step { - d[i][..step].copy_from_slice(&self.decoded[p][by + i + 1][bx + 1..bx + 1 + step]); - } - } - Snapshot { planes, decoded } - } - - fn restore(&mut self, snap: &Snapshot, r: usize, c: usize, n4: usize) { - for p in 0..3 { - let s = (p > 0) as usize; - let (x0, y0, n) = ((c * 4) >> s, (r * 4) >> s, (n4 * 4) >> s); - let pl = &mut self.rec[p]; - for (i, y) in (y0..y0 + n).enumerate() { - pl.data[y * pl.stride + x0..y * pl.stride + x0 + n].copy_from_slice(&snap.planes[p][i * n..i * n + n]); - } - } - for p in 0..3 { - let (by, bx, step) = Self::decoded_region(p, r, c, n4); - for i in 0..step { - self.decoded[p][by + i + 1][bx + 1..bx + 1 + step].copy_from_slice(&snap.decoded[p][i][..step]); - } - } - } - - fn set_mvs(&mut self, r: usize, c: usize, n4: usize, mv: [i32; 2]) { - for y in r..(r + n4).min(self.g.mi_rows) { - for x in c..(c + n4).min(self.g.mi_cols) { - self.mvs[y * self.g.mi_cols + x] = mv; - } - } - } - - fn partition(&mut self, r: usize, c: usize, bsize: usize, parent_mv: Option<[i32; 2]>) -> (Node, f64) { - let g = self.g; - if r >= g.mi_rows || c >= g.mi_cols { - return (Node::Out, 0.0); - } - let n4 = NUM_4X4_BLOCKS_WIDE[bsize] as usize; - let half = n4 >> 1; - let has_rows = r + half < g.mi_rows; - let has_cols = c + half < g.mi_cols; - let can_none = has_rows && has_cols && (bsize != BLOCK_64X64 || !self.intra_only); - let can_split = bsize > BLOCK_8X8; - let sub = PARTITION_SUBSIZE[PARTITION_SPLIT][bsize] as usize; - if !can_none { - return self.split(r, c, sub, half, parent_mv); - } - let before = self.snapshot(r, c, n4); - let (leaf, cost_none) = self.leaf(r, c, bsize, parent_mv); - let cost_none = cost_none + self.lambda * 1.0; - if !can_split { - return (Node::Leaf(Box::new(leaf)), cost_none); - } - // a cheap skip block rarely gains from splitting - if leaf.skip && cost_none < self.lambda * 8.0 { - return (Node::Leaf(Box::new(leaf)), cost_none); - } - let after_none = self.snapshot(r, c, n4); - self.restore(&before, r, c, n4); - let pm = if leaf.is_inter { Some(leaf.mv) } else { parent_mv }; - let (node, cost_split) = self.split(r, c, sub, half, pm); - let cost_split = cost_split + self.lambda * 2.0; - if cost_split < cost_none { - (node, cost_split) - } else { - self.restore(&after_none, r, c, n4); - self.set_mvs(r, c, n4, if leaf.is_inter { leaf.mv } else { [0, 0] }); - (Node::Leaf(Box::new(leaf)), cost_none) - } - } - - fn split(&mut self, r: usize, c: usize, sub: usize, half: usize, pm: Option<[i32; 2]>) -> (Node, f64) { - let (n0, c0) = self.partition(r, c, sub, pm); - let (n1, c1) = self.partition(r, c + half, sub, pm); - let (n2, c2) = self.partition(r + half, c, sub, pm); - let (n3, c3) = self.partition(r + half, c + half, sub, pm); - (Node::Split(Box::new([n0, n1, n2, n3])), c0 + c1 + c2 + c3) - } - - /// Best coding of the block as a single (NONE partition) block. - fn leaf(&mut self, r: usize, c: usize, bsize: usize, parent_mv: Option<[i32; 2]>) -> (Leaf, f64) { - let n4 = NUM_4X4_BLOCKS_WIDE[bsize] as usize; - if self.intra_only { - let res = self.intra_leaf(r, c, bsize); - self.set_mvs(r, c, n4, [0, 0]); - return res; - } - let before = self.snapshot(r, c, n4); - let (inter, cost_inter) = self.inter_leaf(r, c, bsize, parent_mv); - if bsize == BLOCK_64X64 || inter.skip { - self.set_mvs(r, c, n4, inter.mv); - return (inter, cost_inter); - } - let after_inter = self.snapshot(r, c, n4); - self.restore(&before, r, c, n4); - let (intra, cost_intra) = self.intra_leaf(r, c, bsize); - if cost_intra < cost_inter { - self.set_mvs(r, c, n4, [0, 0]); - (intra, cost_intra) - } else { - self.restore(&after_inter, r, c, n4); - self.set_mvs(r, c, n4, inter.mv); - (inter, cost_inter) - } - } - - fn intra_leaf(&mut self, r: usize, c: usize, bsize: usize) -> (Leaf, f64) { - let n4 = NUM_4X4_BLOCKS_WIDE[bsize] as usize; - let log2 = (n4 * 4).trailing_zeros(); - let (sbr, sbc) = ((r & 15) as isize, (c & 15) as isize); - let bd = self.bd; - let mut pred = [0u16; 32 * 32]; - let mut best_pred = [0u16; 32 * 32]; - let mut best_levels = [0i32; 32 * 32]; - let mut best_recon = [0u16; 32 * 32]; - let n = 1usize << log2; - // luma - let e = IntraEdge { - x: c * 4, - y: r * 4, - have_left: c > 0, - have_above: r > 0, - have_above_right: self.decoded_at(0, sbr - 1, sbc + n4 as isize), - have_below_left: self.decoded_at(0, sbr + n4 as isize, sbc - 1), - log2, - max_x: (self.g.mi_cols * 4) as i32 - 1, - max_y: (self.g.mi_rows * 4) as i32 - 1, - bit_depth: bd, - }; - let ed = edges(&self.rec[0], &e); - // modes ranked by SATD, then a rate-distortion choice of mode and transform type among - // the best three - let mut ranked: [(f64, usize); INTRA_MODES_USED.len()] = INTRA_MODES_USED.map(|mode| { - predict_intra(&ed, &e, mode, &mut pred); - let sad = self.satd(0, c * 4, r * 4, n, &pred[..n * n]) as f64; - (sad + self.lambda_sad * if mode == DC_PRED { 1.0 } else { MODE_BITS_INTRA }, mode) - }); - ranked.sort_by(|a, b| a.0.total_cmp(&b.0)); - let tx_y = tx_size_for(log2); - let mut best: Option<(f64, usize, usize, TxEval, bool)> = None; - let mut sc = self.scratch.take().unwrap_or_default(); - for &(_, mode) in ranked.iter().take(3) { - predict_intra(&ed, &e, mode, &mut pred); - let mut types = [DCT_DCT; 2]; - let mut n_types = 1; - let alt = MODE_TO_TXFM[mode] as usize; - if alt != DCT_DCT && tx_in_set(tx_set(tx_y, false), false, alt) { - types[1] = alt; - n_types = 2; - } - for &t in &types[..n_types] { - let (res, has_recon) = self.try_tx(&mut sc, TransformBlock { plane: 0, x: c * 4, y: r * 4, log2, tx_type: t, intra: true }, &pred[..n * n]); - let type_bits = if !res.nonzero || tx_set(tx_y, false) == TX_SET_DCTONLY { - 0.0 - } else if t == DCT_DCT { - 1.0 - } else { - 2.0 - }; - let mode_bits = if mode == DC_PRED { 1.0 } else { MODE_BITS_INTRA }; - let cost = res.dist + self.lambda * (res.bits + type_bits + mode_bits); - if best.as_ref().is_none_or(|b| cost < b.0) { - best_pred[..n * n].copy_from_slice(&pred[..n * n]); - best_levels[..n * n].copy_from_slice(&sc.levels[..n * n]); - if has_recon { - best_recon[..n * n].copy_from_slice(&sc.recon[..n * n]); - } - best = Some((cost, mode, t, res, has_recon)); - } - } - } - self.scratch = Some(sc); - // Every ranked mode tries DCT_DCT, so `best` is always set; without a candidate the block - // can't be coded intra (an infinite cost keeps it from being chosen). - let Some((_, y_mode, tx_type_y, ty, has_recon)) = best else { - let leaf = - Leaf { bsize, is_inter: false, y_mode: DC_PRED, uv_mode: DC_PRED, tx_type_y: DCT_DCT, mv: [0, 0], skip: true, coefs: Default::default() }; - return (leaf, f64::INFINITY); - }; - self.put(0, c * 4, r * 4, n, if has_recon { &best_recon[..n * n] } else { &best_pred[..n * n] }); - let ty = TxResult { levels: best_levels[..n * n].to_vec(), nonzero: ty.nonzero, dist: ty.dist, bits: ty.bits }; - let tx_type_y = if ty.nonzero { tx_type_y } else { DCT_DCT }; - self.mark_decoded(0, r, c, n4); - // chroma (one mode for both planes) - let cn = n >> 1; - let clog2 = log2 - 1; - let (csbr, csbc) = (sbr >> 1, sbc >> 1); - let ce: [IntraEdge; 2] = std::array::from_fn(|i| IntraEdge { - x: c * 2, - y: r * 2, - have_left: c > 0, - have_above: r > 0, - have_above_right: self.decoded_at(1 + i, csbr - 1, csbc + (n4 >> 1) as isize), - have_below_left: self.decoded_at(1 + i, csbr + (n4 >> 1) as isize, csbc - 1), - log2: clog2, - max_x: (self.g.mi_cols * 2) as i32 - 1, - max_y: (self.g.mi_rows * 2) as i32 - 1, - bit_depth: bd, - }); - let ced = [edges(&self.rec[1], &ce[0]), edges(&self.rec[2], &ce[1])]; - let mut best_uv = (f64::MAX, DC_PRED); - for &mode in INTRA_MODES_USED.iter() { - let mut sad = 0u64; - for i in 0..2 { - predict_intra(&ced[i], &ce[i], mode, &mut pred); - sad += self.satd(1 + i, c * 2, r * 2, cn, &pred[..cn * cn]); - } - let cost = sad as f64 + self.lambda_sad * if mode == DC_PRED { 1.0 } else { 2.5 }; - if cost < best_uv.0 { - best_uv = (cost, mode); - } - } - let uv_mode = best_uv.1; - let uv_tx = tx_size_for(clog2); - let uv_type = intra_uv_tx_type(uv_mode, uv_tx); - let mut code_uv = |i: usize| { - predict_intra(&ced[i], &ce[i], uv_mode, &mut pred); - let t = self.code_tx(TransformBlock { plane: 1 + i, x: c * 2, y: r * 2, log2: clog2, tx_type: uv_type, intra: true }, &pred[..cn * cn]); - self.mark_decoded(1 + i, r, c, n4); - t - }; - let u = code_uv(0); - let v = code_uv(1); - let skip = !ty.nonzero && !u.nonzero && !v.nonzero; - let dist = ty.dist + u.dist + v.dist; - let bits = if skip { 1.0 } else { ty.bits + u.bits + v.bits + 1.0 } + MODE_BITS_INTRA + 2.0; - let leaf = Leaf { bsize, is_inter: false, y_mode, uv_mode, tx_type_y, mv: [0, 0], skip, coefs: [ty.levels, u.levels, v.levels] }; - (leaf, dist + self.lambda * bits) - } - - fn inter_leaf(&mut self, r: usize, c: usize, bsize: usize, parent_mv: Option<[i32; 2]>) -> (Leaf, f64) { - let n4 = NUM_4X4_BLOCKS_WIDE[bsize] as usize; - let n = n4 * 4; - let log2 = n.trailing_zeros(); - // Inter blocks are only decided with a reference frame; without one, code intra. - let Some(refr) = self.refr else { return self.intra_leaf(r, c, bsize) }; - let (mv, mv_bits) = self.motion_search(r, c, n, parent_mv); - let mut pred = vec![0u16; n * n]; - let mut tx = Vec::with_capacity(3); - let mut dist = 0.0; - for p in 0..3 { - let s = (p > 0) as usize; - let (vw, vh) = self.g.visible(p); - let pn = n >> s; - let (x, y) = ((c * 4) >> s, (r * 4) >> s); - predict_inter( - &refr.planes[p], - &InterBlock { last_x: vw as i32 - 1, last_y: vh as i32 - 1, x, y, width: pn, height: pn, mv, subsampling: s, bit_depth: self.bd }, - &mut pred[..pn * pn], - ); - if bsize == BLOCK_64X64 { - // coded as a skip block: the prediction is the reconstruction - dist += self.sse(p, x, y, pn, &pred[..pn * pn]); - self.put(p, x, y, pn, &pred[..pn * pn]); - } else { - let t = self.code_tx(TransformBlock { plane: p, x, y, log2: log2 - s as u32, tx_type: DCT_DCT, intra: false }, &pred[..pn * pn]); - dist += t.dist; - tx.push(t); - } - self.mark_decoded(p, r, c, n4); - } - let mode_bits = if mv == [0, 0] { 1.5 } else { 2.0 + mv_bits }; - if bsize == BLOCK_64X64 { - let leaf = Leaf { bsize, is_inter: true, y_mode: NEWMV, uv_mode: DC_PRED, tx_type_y: DCT_DCT, mv, skip: true, coefs: Default::default() }; - return (leaf, dist + self.lambda * (mode_bits + 1.0)); - } - let skip = tx.iter().all(|t| !t.nonzero); - let bits = mode_bits + if skip { 1.0 } else { 1.0 + tx.iter().map(|t| t.bits).sum::() }; - let [y, u, v]: [TxResult; 3] = match tx.try_into() { - Ok(planes) => planes, - // One transform block per plane was coded above. - Err(_) => { - return ( - Leaf { bsize, is_inter: true, y_mode: NEWMV, uv_mode: DC_PRED, tx_type_y: DCT_DCT, mv, skip: true, coefs: Default::default() }, - f64::INFINITY, - ); - } - }; - let leaf = Leaf { bsize, is_inter: true, y_mode: NEWMV, uv_mode: DC_PRED, tx_type_y: DCT_DCT, mv, skip, coefs: [y.levels, u.levels, v.levels] }; - (leaf, dist + self.lambda * bits) - } - - /// Full-sample motion search: candidate vectors then a shrinking diamond search. - /// Returns the vector and an estimate of its coding cost in bits. - fn motion_search(&self, r: usize, c: usize, n: usize, parent_mv: Option<[i32; 2]>) -> ([i32; 2], f64) { - let g = self.g; - let Some(refr) = self.refr else { return ([0, 0], 0.0) }; - let mi = |rr: usize, cc: usize| self.mvs[rr * g.mi_cols + cc]; - let mut pred_mv = [0, 0]; - let mut cands: Vec<[i32; 2]> = vec![[0, 0]]; - if c > 0 { - pred_mv = mi(r, c - 1); - cands.push(pred_mv); - } - if r > 0 { - cands.push(mi(r - 1, c)); - let cr = c + n / 4; - if cr < g.mi_cols { - cands.push(mi(r - 1, cr)); - } - } - if let Some(p) = parent_mv { - cands.push(p); - } - cands.push(refr.mvs[r * g.mi_cols + c]); - let (x0, y0) = ((c * 4) as i32, (r * 4) as i32); - let (w, h) = (g.width as i32, g.height as i32); - // keep the referenced block within 64 samples of the picture - let lim_x = (-x0 - n as i32 - 64, w - x0 + 64); - let lim_y = (-y0 - n as i32 - 64, h - y0 + 64); - let valid = |m: [i32; 2]| { - let (dy, dx) = (m[0] >> 3, m[1] >> 3); - dx >= lim_x.0 && dx <= lim_x.1 && dy >= lim_y.0 && dy <= lim_y.1 - }; - let lg = log2_table(); - let mv_bits = |m: [i32; 2]| -> f64 { - let comp = |d: i32| if d == 0 { 0.5 } else { 2.0 + 2.0 * log2_of(lg, (d.abs() / 8) as usize + 1) }; - comp(m[0] - pred_mv[0]) + comp(m[1] - pred_mv[1]) - }; - // `bound` is the cost to beat: the SAD stops early once the cost cannot be below it (the - // callers only accept a strictly lower cost, so the result is unchanged) - let cost = |m: [i32; 2], bound: f64| -> f64 { - let lb = self.lambda_sad * mv_bits(m); - self.sad_ref(&refr.planes[0], [x0 + (m[1] >> 3), y0 + (m[0] >> 3)], [c * 4, r * 4], n, [lb, bound]) as f64 + lb - }; - // positions already evaluated cannot beat `best` again (costs are deterministic) - let mut seen: Vec<[i32; 2]> = Vec::with_capacity(64); - let mut best = ([0, 0], f64::MAX); - for &m in &cands { - let m = [m[0] & !7, m[1] & !7]; - if !valid(m) || seen.contains(&m) { - continue; - } - seen.push(m); - let cst = cost(m, best.1); - if cst < best.1 { - best = (m, cst); - } - } - let mut step = 8 * if n >= 32 { - 16 - } else if n == 16 { - 8 - } else { - 4 - }; - while step >= 8 { - let mut moved = true; - let mut iters = 0; - while moved && iters < 16 { - moved = false; - iters += 1; - let center = best.0; - for d in [[-step, 0], [step, 0], [0, -step], [0, step]] { - let m = [center[0] + d[0], center[1] + d[1]]; - if !valid(m) || seen.contains(&m) { - continue; - } - seen.push(m); - let cst = cost(m, best.1); - if cst < best.1 { - best = (m, cst); - moved = true; - } - } - } - step >>= 1; - } - // half- then quarter-sample refinement (plus the exact neighbour candidates) - let mut buf = vec![0u16; n * n]; - let sub_cost = |m: [i32; 2], buf: &mut Vec, bound: f64| -> f64 { - self.search_block(m, c * 4, r * 4, n, buf); - let lb = self.lambda_sad * mv_bits(m); - self.sad_bounded(0, [c * 4, r * 4], n, buf, [lb, bound]) as f64 + lb - }; - let mut seen_sub: Vec<[i32; 2]> = Vec::with_capacity(24); - for m in cands { - if m[0] & 7 != 0 || m[1] & 7 != 0 { - let m = [m[0] & !1, m[1] & !1]; - if valid(m) && !seen_sub.contains(&m) { - seen_sub.push(m); - let cst = sub_cost(m, &mut buf, best.1); - if cst < best.1 { - best = (m, cst); - } - } - } - } - for step in [4, 2] { - let center = best.0; - for d in [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]] { - let m = [center[0] + d[0] * step, center[1] + d[1] * step]; - if !valid(m) || seen_sub.contains(&m) { - continue; - } - seen_sub.push(m); - let cst = sub_cost(m, &mut buf, best.1); - if cst < best.1 { - best = (m, cst); - } - } - } - (best.0, mv_bits(best.0)) - } - - /// Approximate luma prediction for motion search: exact at whole and half-sample - /// positions (precomputed planes), averaged neighbours at quarter-sample positions. - fn search_block(&self, mv: [i32; 2], x: usize, y: usize, n: usize, out: &mut [u16]) { - let Some(refr) = self.refr else { return }; - let (w, h) = (self.g.width as i32, self.g.height as i32); - // per axis: up to two (integer offset, half) taps - let axis = |v: i32| -> ([(i32, bool); 2], usize) { - let p = 2 * v; // 1/16 units - let (i, f) = (p >> 4, p & 15); - match f { - 0 => ([(i, false), (i, false)], 1), - 8 => ([(i, true), (i, true)], 1), - 4 => ([(i, false), (i, true)], 2), - _ => ([(i, true), (i + 1, false)], 2), - } - }; - let (ax, nx) = axis(mv[1]); - let (ay, ny) = axis(mv[0]); - let taps = nx.max(ny); - let fetch = |k: usize, xx: i32, yy: i32| -> u16 { - let (ox, hx) = ax[k.min(nx - 1)]; - let (oy, hy) = ay[k.min(ny - 1)]; - let sx = (xx + ox).clamp(0, w - 1) as usize; - let sy = (yy + oy).clamp(0, h - 1) as usize; - match (hx, hy) { - (false, false) => refr.planes[0].at(sx, sy), - (true, false) => self.half[0][sy * w as usize + sx], - (false, true) => self.half[1][sy * w as usize + sx], - (true, true) => self.half[2][sy * w as usize + sx], - } - }; - let vw = n.min(self.g.width.saturating_sub(x)); - let vh = n.min(self.g.height.saturating_sub(y)); - // fast path: every tap inside the picture - let (x0, y0) = (x as i32, y as i32); - let inside = (0..taps).all(|k| { - let (ox, _) = ax[k.min(nx - 1)]; - let (oy, _) = ay[k.min(ny - 1)]; - x0 + ox >= 0 && y0 + oy >= 0 && x0 + ox + vw as i32 <= w && y0 + oy + vh as i32 <= h - }); - if inside { - let src = |k: usize| -> (&[u16], usize, usize) { - let (ox, hx) = ax[k.min(nx - 1)]; - let (oy, hy) = ay[k.min(ny - 1)]; - let (sx, sy) = ((x0 + ox) as usize, (y0 + oy) as usize); - match (hx, hy) { - (false, false) => (&refr.planes[0].data, refr.planes[0].stride, sy * refr.planes[0].stride + sx), - (true, false) => (&self.half[0], w as usize, sy * w as usize + sx), - (false, true) => (&self.half[1], w as usize, sy * w as usize + sx), - (true, true) => (&self.half[2], w as usize, sy * w as usize + sx), - } - }; - let (a, sa, oa) = src(0); - if taps == 1 { - for i in 0..vh { - out[i * n..i * n + vw].copy_from_slice(&a[oa + i * sa..oa + i * sa + vw]); - } - } else { - let (b, sb, ob) = src(1); - for i in 0..vh { - let (ra, rb) = (&a[oa + i * sa..oa + i * sa + vw], &b[ob + i * sb..ob + i * sb + vw]); - for (o, (&p, &q)) in out[i * n..i * n + vw].iter_mut().zip(ra.iter().zip(rb)) { - *o = ((p as u32 + q as u32 + 1) >> 1) as u16; - } - } - } - return; - } - for i in 0..vh { - for j in 0..vw { - let (xx, yy) = (x0 + j as i32, y0 + i as i32); - out[i * n + j] = if taps == 1 { fetch(0, xx, yy) } else { ((fetch(0, xx, yy) as u32 + fetch(1, xx, yy) as u32 + 1) >> 1) as u16 }; - } - } - } - - /// SAD of the source block at (x, y) (luma) against the reference block at (rx, ry). - /// Stops after a row once `sad + lb >= bound` (the cost can no longer win). - fn sad_ref(&self, refp: &Plane, [rx, ry]: [i32; 2], [x, y]: [usize; 2], n: usize, [lb, bound]: [f64; 2]) -> u64 { - let (vw, vh) = (self.g.width, self.g.height); - let w = n.min(vw.saturating_sub(x)); - let h = n.min(vh.saturating_sub(y)); - let src = &self.src[0]; - let (lx, ly) = (vw as i32 - 1, vh as i32 - 1); - let mut sad = 0u64; - let inside = rx >= 0 && ry >= 0 && rx + w as i32 - 1 <= lx && ry + h as i32 - 1 <= ly; - for i in 0..h { - let s = &src.row(y + i)[x..x + w]; - let rr = refp.row((ry + i as i32).clamp(0, ly) as usize); - if inside { - let rrow = &rr[rx as usize..rx as usize + w]; - sad += s.iter().zip(rrow).map(|(&a, &b)| (a as i32 - b as i32).unsigned_abs()).sum::() as u64; - } else { - for j in 0..w { - let b = rr[(rx + j as i32).clamp(0, lx) as usize]; - sad += (s[j] as i32 - b as i32).unsigned_abs() as u64; - } - } - if sad as f64 + lb >= bound { - return sad; - } - } - sad - } - - /// SAD of a prediction against the source over the visible part of the block. Stops after a row - /// once `sad + lb >= bound` (the cost can no longer win). - fn sad_bounded(&self, p: usize, [x, y]: [usize; 2], n: usize, pred: &[u16], [lb, bound]: [f64; 2]) -> u64 { - let (vw, vh) = self.g.visible(p); - let w = n.min(vw.saturating_sub(x)); - let h = n.min(vh.saturating_sub(y)); - let src = &self.src[p]; - let mut s = 0u64; - for i in 0..h { - let row = &src.row(y + i)[x..x + w]; - s += row.iter().zip(&pred[i * n..i * n + w]).map(|(&a, &b)| (a as i32 - b as i32).unsigned_abs()).sum::() as u64; - if s as f64 + lb >= bound { - return s; - } - } - s - } - - /// Sum of absolute Hadamard-transformed differences (4x4 or 8x8 sub-blocks) of a prediction - /// against the source, scaled to about the size of the SAD. - fn satd(&self, p: usize, x: usize, y: usize, n: usize, pred: &[u16]) -> u64 { - if n >= 8 { self.satd_k::<8>(p, x, y, n, pred) } else { self.satd_k::<4>(p, x, y, n, pred) } - } - - /// `satd` with K x K Hadamard sub-blocks (fixed sizes so the butterflies vectorise). - fn satd_k(&self, p: usize, x: usize, y: usize, n: usize, pred: &[u16]) -> u64 { - let src = &self.src[p]; - let mut total = 0u64; - for by in (0..n).step_by(K) { - for bx in (0..n).step_by(K) { - // d[i] holds row i of the differences - let mut d = [[0i32; K]; K]; - for (i, di) in d.iter_mut().enumerate() { - let row = &src.row(y + by + i)[x + bx..x + bx + K]; - let pr = &pred[(by + i) * n + bx..(by + i) * n + bx + K]; - for j in 0..K { - di[j] = row[j] as i32 - pr[j] as i32; - } - } - // vertical butterflies across rows (all columns at once) - let mut h = 1; - while h < K { - for i0 in (0..K).step_by(2 * h) { - for i in i0..i0 + h { - let (a, b) = (d[i], d[i + h]); - for j in 0..K { - d[i][j] = a[j] + b[j]; - d[i + h][j] = a[j] - b[j]; - } - } - } - h *= 2; - } - // horizontal butterflies within each row - let mut s = 0u64; - for di in d.iter_mut() { - hadamard(di); - s += di.iter().map(|v| v.unsigned_abs() as u64).sum::(); - } - total += s / (K as u64 / 2); - } - } - total - } - - fn sse(&self, p: usize, x: usize, y: usize, n: usize, blk: &[u16]) -> f64 { - let (vw, vh) = self.g.visible(p); - let w = n.min(vw.saturating_sub(x)); - let h = n.min(vh.saturating_sub(y)); - let src = &self.src[p]; - let mut s = 0u64; - for i in 0..h { - let row = &src.row(y + i)[x..x + w]; - s += row - .iter() - .zip(&blk[i * n..i * n + w]) - .map(|(&a, &b)| { - let d = a as i64 - b as i64; - (d * d) as u64 - }) - .sum::(); - } - s as f64 - } - - fn put(&mut self, p: usize, x: usize, y: usize, n: usize, blk: &[u16]) { - let pl = &mut self.rec[p]; - for i in 0..n { - let o = (y + i) * pl.stride + x; - pl.data[o..o + n].copy_from_slice(&blk[i * n..i * n + n]); - } - } - - /// Transform, quantise and reconstruct one square transform block of `plane` at (x, y) - /// over the prediction `pred`; the reconstruction is written to `rec`. - fn code_tx(&mut self, block: TransformBlock, pred: &[u16]) -> TxResult { - let TransformBlock { plane: p, x, y, log2, .. } = block; - let mut sc = self.scratch.take().unwrap_or_default(); - let (res, has_recon) = self.try_tx(&mut sc, block, pred); - let n = 1usize << log2; - self.put(p, x, y, n, if has_recon { &sc.recon[..n * n] } else { pred }); - let levels = sc.levels[..n * n].to_vec(); - self.scratch = Some(sc); - TxResult { levels, nonzero: res.nonzero, dist: res.dist, bits: res.bits } - } - - /// Transform and quantise one block without writing it. The levels are left in `sc.levels`; - /// the flag tells whether `sc.recon` holds the reconstruction (false: it is the prediction). - fn try_tx(&self, sc: &mut TxScratch, block: TransformBlock, pred: &[u16]) -> (TxEval, bool) { - let TransformBlock { plane: p, x, y, log2, tx_type, intra } = block; - let n = 1usize << log2; - let nn = n * n; - let tx_sz = tx_size_for(log2); - let bd = self.bd; - let src = &self.src[p]; - let TxScratch { res, coef, levels, deq, resid, recon } = sc; - let (res, coef, levels) = (&mut res[..nn], &mut coef[..nn], &mut levels[..nn]); - for i in 0..n { - let row = &src.row(y + i)[x..x + n]; - for j in 0..n { - res[i * n + j] = row[j] as i32 - pred[i * n + j] as i32; - } - } - forward_2d(res, coef, n, tx_type); - // dead-zone quantiser: rounding offset below one half - let rnd = if intra { INTRA_RND } else { INTER_RND }; - let [dc_thr, ac_thr] = [self.dead_zone[0][intra as usize], self.dead_zone[1][intra as usize]]; - levels.fill(0); - let mut nonzero = false; - for (k, (&cf, l)) in coef.iter().zip(levels.iter_mut()).enumerate() { - let (q, thr) = if k == 0 { (self.dc_q, dc_thr) } else { (self.ac_q, ac_thr) }; - // dead zone: below the exact threshold the level is zero, no division needed - if cf.abs() < thr { - continue; - } - // the operand is >= 0, so truncation equals floor - let a = (cf.abs() / q + rnd) as i32; - if a != 0 { - *l = if cf < 0.0 { -a.min(1 << 16) } else { a.min(1 << 16) }; - nonzero = true; - } - } - let zero_dist = self.sse(p, x, y, n, pred); - if !nonzero { - return (TxEval { nonzero, dist: zero_dist, bits: 1.0 }, false); - } - let recon = &mut recon[..nn]; - self.reconstruct(levels, ReconBuffers { deq: &mut deq[..nn], resid: &mut resid[..nn], recon }, tx_sz, tx_type, pred, bd); - let dist = self.sse(p, x, y, n, recon); - let bits = estimate_bits(levels, tx_sz); - if dist + self.lambda * bits >= zero_dist + self.lambda { - levels.fill(0); - return (TxEval { nonzero: false, dist: zero_dist, bits: 1.0 }, false); - } - (TxEval { nonzero: true, dist, bits }, true) - } - - /// Dequantisation (7.12.3) and the inverse transform, added to the prediction; the result is - /// written to `recon`. `deq` and `resid` are scratch buffers of `n * n` entries. - fn reconstruct(&self, levels: &[i32], bufs: ReconBuffers, tx_sz: usize, tx_type: usize, pred: &[u16], bd: u32) { - let ReconBuffers { deq, resid, recon } = bufs; - let denom: i64 = if tx_sz == TX_32X32 { 2 } else { 1 }; - let lim = 1i64 << (7 + bd); - deq.fill(0); - for (k, (&l, d)) in levels.iter().zip(deq.iter_mut()).enumerate() { - if l == 0 { - continue; - } - let q = if k == 0 { self.dc_qi } else { self.ac_qi }; - let dq = l as i64 * q; - let mag = (dq.abs() & 0xFF_FFFF) / denom; - *d = (if dq < 0 { -mag } else { mag }).clamp(-lim, lim - 1) as i32; - } - inverse_2d(deq, resid, tx_sz, tx_type, bd); - let max = (1i32 << bd) - 1; - for ((o, &p), &r) in recon.iter_mut().zip(pred).zip(resid.iter()) { - *o = (p as i32 + r).clamp(0, max) as u16; - } - } -} - -/// Smallest `x >= 0` with `(x / q + rnd) as i32 >= 1` in f32 arithmetic. The expression is -/// monotone in `x`, so a binary search over the bit patterns finds the exact boundary. -fn zero_threshold(q: f32, rnd: f32) -> f32 { - let (mut lo, mut hi) = (0u32, f32::MAX.to_bits()); - while lo < hi { - let mid = lo + (hi - lo) / 2; - if (f32::from_bits(mid) / q + rnd) as i32 >= 1 { - hi = mid; - } else { - lo = mid + 1; - } - } - f32::from_bits(lo) -} - -const LOG2_TABLE_LEN: usize = 1025; - -/// `log2(i)` for `i < LOG2_TABLE_LEN`, computed with the same `f64::log2` call as the direct form. -fn log2_table() -> &'static [f64; LOG2_TABLE_LEN] { - static TABLE: std::sync::OnceLock<[f64; LOG2_TABLE_LEN]> = std::sync::OnceLock::new(); - TABLE.get_or_init(|| std::array::from_fn(|i| (i as f64).log2())) -} - -fn log2_of(table: &[f64; LOG2_TABLE_LEN], v: usize) -> f64 { - if v < LOG2_TABLE_LEN { table[v] } else { (v as f64).log2() } -} - -/// Unnormalised Walsh-Hadamard transform of 4 or 8 values, in place. -fn hadamard(v: &mut [i32]) { - let n = v.len(); - let mut h = 1; - while h < n { - for i in (0..n).step_by(2 * h) { - for j in i..i + h { - let (a, b) = (v[j], v[j + h]); - v[j] = a + b; - v[j + h] = a - b; - } - } - h *= 2; - } -} - -/// Rough coefficient coding cost in bits. -fn estimate_bits(levels: &[i32], tx_sz: usize) -> f64 { - let scan = scan_for(tx_sz); - let mut eob = 0; - for (i, &pos) in scan.iter().enumerate() { - if levels[pos as usize] != 0 { - eob = i + 1; - } - } - if eob == 0 { - return 1.0; - } - let lg = log2_table(); - let mut bits = 3.0 + log2_of(lg, eob); - for &pos in &scan[..eob] { - let a = levels[pos as usize].unsigned_abs(); - bits += match a { - 0 => 0.7, - 1 => 2.5, - _ => 3.5 + 1.6 * log2_of(lg, a as usize), - }; - } - bits -} - -pub(crate) fn scan_for(tx_sz: usize) -> &'static [u16] { - match tx_sz { - TX_4X4 => &DEFAULT_SCAN_4X4, - TX_8X8 => &DEFAULT_SCAN_8X8, - TX_16X16 => &DEFAULT_SCAN_16X16, - _ => &DEFAULT_SCAN_32X32, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::tables::{AC_QLOOKUP, DC_QLOOKUP}; - - /// The quantiser as it was before the dead-zone threshold and the truncation. - fn quant_ref(cf: f32, q: f32, rnd: f32) -> i32 { - (cf.abs() / q + rnd).floor() as i32 - } - - fn quant_new(cf: f32, q: f32, rnd: f32, thr: f32) -> i32 { - if cf.abs() < thr { - return 0; - } - (cf.abs() / q + rnd) as i32 - } - - #[test] - fn dead_zone_threshold_is_exact() { - let mut state = 0x1234_5678_9abc_def0u64; - let mut next = move || { - state ^= state << 13; - state ^= state >> 7; - state ^= state << 17; - state - }; - for table in AC_QLOOKUP.iter().chain(DC_QLOOKUP.iter()) { - for &qi in table.iter() { - let q = qi as f32; - for rnd in [INTER_RND, INTRA_RND] { - let thr = zero_threshold(q, rnd); - // the boundary itself and its neighbours - let b = thr.to_bits(); - for bits in b.saturating_sub(3)..=b + 3 { - let cf = f32::from_bits(bits); - assert_eq!(quant_ref(cf, q, rnd), quant_new(cf, q, rnd, thr), "q {q} rnd {rnd} cf {cf}"); - assert_eq!(quant_ref(-cf, q, rnd), quant_new(-cf, q, rnd, thr)); - } - for _ in 0..200 { - let cf = f32::from_bits((next() >> 32) as u32 & 0x7fff_ffff); - if cf.is_nan() { - continue; - } - assert_eq!(quant_ref(cf, q, rnd), quant_new(cf, q, rnd, thr)); - let cf = (next() % 100_000) as f32 / 100.0; - assert_eq!(quant_ref(cf, q, rnd), quant_new(cf, q, rnd, thr)); - } - } - } - } - } - - #[test] - fn log2_table_matches_direct_log2() { - let t = log2_table(); - for v in 1..3000usize { - assert_eq!(log2_of(t, v).to_bits(), (v as f64).log2().to_bits(), "{v}"); - } - } - - /// The SATD as it was before the fixed-size version: runtime block size, column gather. - fn satd_ref(src: &Plane, x: usize, y: usize, n: usize, pred: &[u16]) -> u64 { - let k = n.min(8); - let mut total = 0u64; - let mut d = [0i32; 64]; - for by in (0..n).step_by(k) { - for bx in (0..n).step_by(k) { - for i in 0..k { - let row = &src.row(y + by + i)[x + bx..x + bx + k]; - for j in 0..k { - d[i * k + j] = row[j] as i32 - pred[(by + i) * n + bx + j] as i32; - } - } - for i in 0..k { - hadamard(&mut d[i * k..i * k + k]); - } - let mut col = [0i32; 8]; - let mut s = 0u64; - for j in 0..k { - for i in 0..k { - col[i] = d[i * k + j]; - } - hadamard(&mut col[..k]); - s += col[..k].iter().map(|v| v.unsigned_abs() as u64).sum::(); - } - total += s / (k as u64 / 2); - } - } - total - } - - #[test] - fn satd_fixed_size_matches_runtime_size() { - let g = FrameGeom::new(100, 80); - let mut state = 0x9e37_79b9_7f4a_7c15u64; - let mut next = move || { - state ^= state << 13; - state ^= state >> 7; - state ^= state << 17; - state - }; - for bd in [8u32, 10] { - let max = (1u64 << bd) - 1; - let mut mk = |w: usize, h: usize| { - let mut p = Plane::new(w, h); - p.data.iter_mut().for_each(|v| *v = (next() % (max + 1)) as u16); - p - }; - let src = [mk(128, 128), mk(64, 64), mk(64, 64)]; - let dec = Decider::new(&g, bd, 100, true, &src, None); - for n in [4usize, 8, 16, 32] { - let pred: Vec = (0..n * n).map(|_| (next() % (max + 1)) as u16).collect(); - for (p, x, y) in [(0, 0, 0), (0, 32, 40), (1, 8, 4)] { - assert_eq!(dec.satd(p, x, y, n, &pred), satd_ref(&src[p], x, y, n, &pred), "n {n} plane {p}"); - } - } - } - } - - fn xorshift(seed: u64) -> impl FnMut() -> u64 { - let mut state = seed; - move || { - state ^= state << 13; - state ^= state >> 7; - state ^= state << 17; - state - } - } - - /// A snapshot keeps only the BlockDecoded cells of the block area. Coding inside the block - /// marks sub-blocks of every smaller size; restoring must give back the whole 3x35x35 array - /// as it was, for every block size of a 64x64 superblock and every aligned position. - #[test] - fn snapshot_restore_brings_back_every_blockdecoded_flag() { - let g = FrameGeom::new(256, 256); - let mut next = xorshift(0x2545_f491_4f6c_dd1d); - let src = [Plane::new(256, 256), Plane::new(128, 128), Plane::new(128, 128)]; - let mut dec = Decider::new(&g, 8, 100, true, &src, None); - for random_start in [false, true] { - for n4 in [1usize, 2, 4, 8, 16] { - for sb in [0usize, 16] { - for r in (sb..sb + 16).step_by(n4) { - for c in (sb..sb + 16).step_by(n4) { - for plane in dec.decoded.iter_mut() { - for row in plane.iter_mut() { - for v in row.iter_mut() { - *v = random_start && next() & 1 == 1; - } - } - } - let before = dec.decoded.clone(); - let snap = dec.snapshot(r, c, n4); - // every sub-block of every size inside the block, as coding marks them - let mut m = n4; - loop { - for rr in (r..r + n4).step_by(m) { - for cc in (c..c + n4).step_by(m) { - for p in 0..3 { - dec.mark_decoded(p, rr, cc, m); - } - } - } - if m == 1 { - break; - } - m /= 2; - } - dec.restore(&snap, r, c, n4); - assert!(dec.decoded == before, "n4 {n4} at ({r}, {c}), random start {random_start}"); - } - } - } - } - } - } - - /// The bounded SADs return the full SAD whenever the cost can still win (`sad + lb < bound`) - /// and otherwise a partial sum that already loses, so the callers' strict `<` decides the same. - #[test] - fn bounded_sad_stops_only_once_the_cost_cannot_win() { - let g = FrameGeom::new(100, 80); - let mut next = xorshift(0x9e37_79b9_7f4a_7c15); - let mut mk = |w: usize, h: usize| { - let mut p = Plane::new(w, h); - p.data.iter_mut().for_each(|v| *v = (next() % 256) as u16); - p - }; - let src = [mk(128, 128), mk(64, 64), mk(64, 64)]; - let refp = mk(128, 128); - let mut next = xorshift(0x1234_5678_9abc_def1); - let dec = Decider::new(&g, 8, 100, false, &src, None); - for n in [4usize, 8, 16, 32] { - for (x, y, rx, ry) in [(0usize, 0usize, 0i32, 0i32), (32, 40, 29, 43), (96, 72, 90, 70), (8, 8, -5, -3)] { - let full_ref = dec.sad_ref(&refp, [rx, ry], [x, y], n, [0.0, f64::INFINITY]); - let pred: Vec = (0..n * n).map(|_| (next() % 256) as u16).collect(); - let full_pred = dec.sad_bounded(0, [x, y], n, &pred, [0.0, f64::INFINITY]); - for _ in 0..50 { - let lb = (next() % 64) as f64 * 0.75; - let b_ref = (next() % (2 * full_ref + 2)) as f64; - let b_pred = (next() % (2 * full_pred + 2)) as f64; - let got_ref = dec.sad_ref(&refp, [rx, ry], [x, y], n, [lb, b_ref]); - let got_pred = dec.sad_bounded(0, [x, y], n, &pred, [lb, b_pred]); - for (got, full, bound) in [(got_ref, full_ref, b_ref), (got_pred, full_pred, b_pred)] { - if full as f64 + lb < bound { - assert_eq!(got, full, "n {n} at ({x}, {y}) bound {bound}"); - } else { - assert!(got as f64 + lb >= bound && got <= full, "n {n} at ({x}, {y}) bound {bound}"); - } - } - } - } - } - } -} diff --git a/crates/av1enc/src/lib.rs b/crates/av1enc/src/lib.rs deleted file mode 100644 index 2962833d9..000000000 --- a/crates/av1enc/src/lib.rs +++ /dev/null @@ -1,670 +0,0 @@ -//! Clean-room pure-Rust AV1 encoder (Main profile, 4:2:0, 8 and 10 bit), implemented from the -//! AV1 Bitstream & Decoding Process Specification v1.0.0 with Errata 1. -//! -//! Key frames and single-reference inter frames (LAST_FRAME = the previous frame), 64x64 -//! superblocks split down to 8x8 by rate-distortion decisions, intra modes DC / V / H / Paeth / -//! smooth, full-sample motion search with the spec's motion vector prediction, DCT / ADST -//! transforms up to 32x32, one tile per frame, in-loop filters off. See the crate README. -//! -//! The public API below is the contract `crates/export` codes against; keep it stable. - -#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] - -mod bitw; -mod cdf; -mod decide; -mod loopfilter; -mod pred; -mod symbol; -mod tables; -mod transform; -mod writer; - -use bitw::{BitWriter, obu}; -use decide::{Decider, FrameGeom, RefFrame}; -use loopfilter::{LfInfo, loop_filter}; -use pred::Plane; -use tables::AC_QLOOKUP; -use writer::TileWriter; - -/// Rate control. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RateControl { - /// Constant quantizer index (`base_q_idx`, 1–255; lower is better). - ConstantQ(u8), - /// Average bitrate target in kbit/s (frame-level quantizer adaptation). - Bitrate { kbps: u32 }, -} - -/// Encoder settings. Main profile (seq_profile 0), 4:2:0. -#[derive(Clone, Debug, PartialEq)] -pub struct EncoderConfig { - pub width: u32, - pub height: u32, - pub fps_num: u32, - pub fps_den: u32, - /// 8 or 10. - pub bit_depth: u8, - /// `seq_level_idx` (e.g. 8 = level 4.0); `None` picks the lowest level that fits. - pub level_idx: Option, - pub rate: RateControl, - /// Frames between key frames (1 = all key frames). - pub keyint: u32, - /// BT.709 limited range is signalled unless this is set. - pub full_range: bool, - /// The colour description of `color_config` (ITU-T H.273 code points); BT.709 by default. - pub color_primaries: u8, - pub transfer_characteristics: u8, - pub matrix_coefficients: u8, -} - -impl EncoderConfig { - pub fn new(width: u32, height: u32, fps_num: u32, fps_den: u32) -> Self { - EncoderConfig { - width, - height, - fps_num, - fps_den, - bit_depth: 8, - level_idx: None, - rate: RateControl::ConstantQ(100), - keyint: 60, - full_range: false, - color_primaries: 1, - transfer_characteristics: 1, - matrix_coefficients: 1, - } - } -} - -/// One input picture, 4:2:0, samples at the configured bit depth. -#[derive(Clone, Copy, Debug)] -pub struct Frame<'a> { - pub y: &'a [u16], - pub u: &'a [u16], - pub v: &'a [u16], - pub y_stride: usize, - pub uv_stride: usize, -} - -/// One temporal unit as stored in MP4 / WebM samples: OBUs with `obu_has_size_field` = 1 and -/// no temporal delimiter (key frames start with the sequence header OBU). -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Packet { - pub data: Vec, - pub keyframe: bool, -} - -#[derive(Debug, PartialEq, Eq)] -pub enum Error { - InvalidConfig(String), -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::InvalidConfig(s) => write!(f, "invalid AV1 encoder config: {s}"), - } - } -} - -impl std::error::Error for Error {} - -const OBU_SEQUENCE_HEADER: u8 = 1; -const OBU_FRAME: u8 = 6; - -/// Annex A level limits: (seq_level_idx, MaxPicSize, MaxHSize, MaxVSize, MaxDisplayRate, -/// MaxBitrate (Main tier, Mbit/s)). -const LEVELS: [(u8, u64, u32, u32, u64, u64); 14] = [ - (0, 147_456, 2048, 1152, 4_423_680, 1_500_000), - (1, 278_784, 2816, 1584, 8_363_520, 3_000_000), - (4, 665_856, 4352, 2448, 19_975_680, 6_000_000), - (5, 1_065_024, 5504, 3096, 31_950_720, 10_000_000), - (8, 2_359_296, 6144, 3456, 70_778_880, 12_000_000), - (9, 2_359_296, 6144, 3456, 141_557_760, 20_000_000), - (12, 8_912_896, 8192, 4352, 267_386_880, 30_000_000), - (13, 8_912_896, 8192, 4352, 534_773_760, 40_000_000), - (14, 8_912_896, 8192, 4352, 1_069_547_520, 60_000_000), - (15, 8_912_896, 8192, 4352, 1_069_547_520, 60_000_000), - (16, 35_651_584, 16384, 8704, 1_069_547_520, 60_000_000), - (17, 35_651_584, 16384, 8704, 2_139_095_040, 100_000_000), - (18, 35_651_584, 16384, 8704, 4_278_190_080, 160_000_000), - (19, 35_651_584, 16384, 8704, 4_278_190_080, 160_000_000), -]; - -/// The lowest level whose limits fit the stream (31 = no level limits when none does). -fn pick_level(cfg: &EncoderConfig) -> u8 { - let pic = cfg.width as u64 * cfg.height as u64; - let rate = pic * cfg.fps_num as u64 / cfg.fps_den.max(1) as u64; - let bitrate = match cfg.rate { - RateControl::Bitrate { kbps } => kbps as u64 * 1000, - RateControl::ConstantQ(_) => 0, - }; - for &(idx, max_pic, max_h, max_v, max_rate, max_br) in LEVELS.iter() { - if pic <= max_pic && cfg.width <= max_h && cfg.height <= max_v && rate <= max_rate && bitrate <= max_br { - return idx; - } - } - 31 -} - -/// Frame-level rate control state for `RateControl::Bitrate`. -struct RateState { - /// Bits per frame at the target rate. - frame_bits: f64, - /// Quantizer index for inter frames (continuous). - q: f64, - /// Key frame quantizer index minus the inter frame one. - key_off: f64, - /// Quantizer index steps per halving of the frame size, measured from consecutive inter - /// frames (the size/quantizer curve is much steeper than the default on noisy content). - slope: f64, - /// Quantizer index and size (in bits) of the previous inter frame. - prev_inter: Option<(f64, f64)>, - /// Accumulated (spent - budget) bits. - debt: f64, -} - -/// Quantizer index steps per halving of the frame size before any measurement. -const RC_SLOPE: f64 = 28.0; -/// Fraction of the quantizer step the size error asks for that is applied per frame. -const RC_GAIN: f64 = 0.8; -/// Frames over which an accumulated deviation from the budget is paid back. -const RC_PAYBACK: f64 = 16.0; - -impl RateState { - /// The nominal bit budget and the starting quantizer index of a key or inter frame. A key - /// frame gets a larger share, and the inter frames of the group get the rest, so that a whole - /// group of `keyint` frames adds up to `keyint * frame_bits`. - fn frame_budget(&self, keyint: u32, key: bool) -> (f64, f64) { - let keyint = keyint.max(1) as f64; - let boost = (1.0 + (keyint.clamp(1.0, 60.0) - 1.0).sqrt() * 0.75).min(0.5 * keyint + 0.5); - if key { - (self.frame_bits * boost, (self.q + self.key_off).clamp(1.0, 255.0)) - } else if keyint > 1.0 { - (self.frame_bits * (keyint - boost) / (keyint - 1.0), self.q) - } else { - (self.frame_bits, self.q) - } - } - - /// Adapts the quantizers to the size of the frame just coded at `q_used` against `target`. - fn update(&mut self, key: bool, first: bool, q_used: f64, bits: f64, target: f64) { - let err = (bits / target).log2().clamp(-1.5, 1.5); - if first && key { - // four passes ran: the key frame quantizer is known, the inter one follows from the offset - self.q = (q_used - self.key_off).clamp(1.0, 255.0); - } else if key { - self.key_off = q_used - self.q + RC_GAIN * self.slope * err; - } else { - if let Some((pq, pbits)) = self.prev_inter { - let (dq, dr) = (q_used - pq, (pbits / bits).log2()); - if dq.abs() >= 3.0 && dr.abs() >= 0.15 && dq * dr > 0.0 { - self.slope = (0.6 * self.slope + 0.4 * (dq / dr)).clamp(3.0, 45.0); - } - } - self.prev_inter = Some((q_used, bits)); - self.q = (q_used + RC_GAIN * self.slope * err).clamp(1.0, 255.0); - } - } -} - -/// Most passes on the first frame, and how far from the target (octaves, 3 %) its size may be -/// before it is accepted. -const RC_FIRST_PASSES: usize = 4; -const RC_FIRST_TOLERANCE: f64 = 0.04; -/// Quantizer index steps per halving of the size, for the first correction of the first frame -/// (the starting quantizer comes from the bits per sample; the first pass shows how far off it -/// is, and from there on the passes are placed by `next_probe_q`). -const RC_FIRST_SLOPE: f64 = 30.0; -/// The same for a key frame after the first: coded again when its size is more than 0.25 octaves -/// (19 %) off the target. -const RC_KEY_PASSES: usize = 3; -const RC_KEY_TOLERANCE: f64 = 0.25; -/// Octaves of frame size per octave of quantizer step assumed for the first correction of a frame. -const RC_PROBE_SLOPE: f64 = 1.0; -/// Largest change of the quantizer step (octaves) between two passes on one frame. -const RC_PROBE_MAX_STEP: f64 = 2.0; - -/// The quantizer index whose step size is closest to `2^x`. -fn q_for_step(bit_depth: u8, x: f64) -> u8 { - let table = &AC_QLOOKUP[((bit_depth - 8) / 2) as usize]; - let mut best = 1; - for q in 1..=255usize { - if ((table[q] as f64).log2() - x).abs() < ((table[best] as f64).log2() - x).abs() { - best = q; - } - } - best as u8 -} - -/// The next quantizer to try for a frame, from the `(quantizer, log2(size / target))` -/// pairs coded so far. The size follows the quantizer step (not the index, which is linear in the -/// step only below about 60) in a near log-log line: false position between the closest pair of -/// passes that enclose the target, otherwise a step from the latest one with the slope of the -/// last two. `None` when no untried quantizer lies between the enclosing passes. -fn next_probe_q(bit_depth: u8, tried: &[(u8, f64)]) -> Option { - let table = &AC_QLOOKUP[((bit_depth - 8) / 2) as usize]; - let x = |q: u8| (table[q as usize] as f64).log2(); - let over = tried.iter().filter(|t| t.1 > 0.0).max_by_key(|t| t.0); - let under = tried.iter().filter(|t| t.1 <= 0.0).min_by_key(|t| t.0); - if let (Some(&(q_lo, e_lo)), Some(&(q_hi, e_hi))) = (over, under) { - if q_hi - q_lo < 2 { - return None; - } - let next = x(q_lo) + (x(q_hi) - x(q_lo)) * e_lo / (e_lo - e_hi); - return Some(q_for_step(bit_depth, next).clamp(q_lo + 1, q_hi - 1)); - } - let &(q, err) = tried.last()?; - let mut slope = RC_PROBE_SLOPE; - if let [.., (q0, e0), (q1, e1)] = tried - && (x(*q1) - x(*q0)).abs() > 0.05 - && (e0 - e1) / (x(*q1) - x(*q0)) > 0.0 - { - slope = ((e0 - e1) / (x(*q1) - x(*q0))).clamp(0.25, 3.0); - } - Some(q_for_step(bit_depth, x(q) + (err / slope).clamp(-RC_PROBE_MAX_STEP, RC_PROBE_MAX_STEP))) -} - -/// AV1 encoder. Frames come out in input order (no hidden/reordered frames). -pub struct Encoder { - cfg: EncoderConfig, - geom: FrameGeom, - level: u8, - seq_obu: Vec, - frame_num: u64, - refr: Option, - rc: Option, -} - -impl Encoder { - pub fn new(cfg: EncoderConfig) -> Result { - if cfg.width == 0 || cfg.height == 0 { - return Err(Error::InvalidConfig("empty frame".into())); - } - if cfg.width > 4096 || cfg.height > 4096 { - return Err(Error::InvalidConfig(format!("{}x{} exceeds the single-tile limit (4096 samples per side)", cfg.width, cfg.height))); - } - if cfg.bit_depth != 8 && cfg.bit_depth != 10 { - return Err(Error::InvalidConfig(format!("bit depth {} (8 or 10 supported)", cfg.bit_depth))); - } - if cfg.fps_num == 0 || cfg.fps_den == 0 { - return Err(Error::InvalidConfig("frame rate".into())); - } - let geom = FrameGeom::new(cfg.width as usize, cfg.height as usize); - if geom.sb_cols * geom.sb_rows > 2304 { - return Err(Error::InvalidConfig(format!("{}x{} exceeds the single-tile area limit (MAX_TILE_AREA)", cfg.width, cfg.height))); - } - if let RateControl::Bitrate { kbps } = cfg.rate - && kbps == 0 - { - return Err(Error::InvalidConfig("zero bitrate".into())); - } - let level = cfg.level_idx.unwrap_or_else(|| pick_level(&cfg)); - if level > 31 { - return Err(Error::InvalidConfig(format!("seq_level_idx {level}"))); - } - let rc = match cfg.rate { - RateControl::ConstantQ(_) => None, - RateControl::Bitrate { kbps } => { - let frame_bits = kbps as f64 * 1000.0 * cfg.fps_den as f64 / cfg.fps_num as f64; - // initial guess from the bits per sample - let bpp = frame_bits / (cfg.width as f64 * cfg.height as f64); - let q = (110.0 - 40.0 * (bpp / 0.1).log2()).clamp(20.0, 230.0); - Some(RateState { frame_bits, q, key_off: -24.0, slope: RC_SLOPE, prev_inter: None, debt: 0.0 }) - } - }; - let mut enc = Encoder { cfg, geom, level, seq_obu: Vec::new(), frame_num: 0, refr: None, rc }; - let payload = enc.sequence_header_payload(); - obu(OBU_SEQUENCE_HEADER, &payload, &mut enc.seq_obu); - Ok(enc) - } - - pub fn config(&self) -> &EncoderConfig { - &self.cfg - } - - /// The sequence header OBU (with size field). - pub fn sequence_header_obu(&self) -> Vec { - self.seq_obu.clone() - } - - /// AV1CodecConfigurationRecord (`av1C` payload, also the WebM CodecPrivate). - pub fn av1c(&self) -> Vec { - let high = (self.cfg.bit_depth > 8) as u8; - let mut v = vec![ - 0x81, // marker (1), version (1) - self.level & 0x1f, // seq_profile 0, seq_level_idx_0 - (high << 6) | (1 << 3) | (1 << 2), // tier 0, high_bitdepth, twelve_bit 0, mono 0, ss_x 1, ss_y 1, csp 0 - 0, // no initial_presentation_delay - ]; - v.extend_from_slice(&self.seq_obu); - v - } - - /// sequence_header_obu( ) payload (5.5) including trailing bits. - fn sequence_header_payload(&self) -> Vec { - let c = &self.cfg; - let mut w = BitWriter::new(); - w.f(3, 0); // seq_profile - w.f(1, 0); // still_picture - w.f(1, 0); // reduced_still_picture_header - w.f(1, 0); // timing_info_present_flag - w.f(1, 0); // initial_display_delay_present_flag - w.f(5, 0); // operating_points_cnt_minus_1 - w.f(12, 0); // operating_point_idc[ 0 ] - w.f(5, self.level as u32); // seq_level_idx[ 0 ] - if self.level > 7 { - w.f(1, 0); // seq_tier[ 0 ] - } - let wbits = 32 - (c.width - 1).max(1).leading_zeros(); - let hbits = 32 - (c.height - 1).max(1).leading_zeros(); - w.f(4, wbits - 1); - w.f(4, hbits - 1); - w.f(wbits, c.width - 1); - w.f(hbits, c.height - 1); - w.f(1, 0); // frame_id_numbers_present_flag - w.f(1, 0); // use_128x128_superblock - w.f(1, 0); // enable_filter_intra - w.f(1, 0); // enable_intra_edge_filter - w.f(1, 0); // enable_interintra_compound - w.f(1, 0); // enable_masked_compound - w.f(1, 0); // enable_warped_motion - w.f(1, 0); // enable_dual_filter - w.f(1, 0); // enable_order_hint - w.f(1, 0); // seq_choose_screen_content_tools - w.f(1, 0); // seq_force_screen_content_tools - w.f(1, 0); // enable_superres - w.f(1, 0); // enable_cdef - w.f(1, 0); // enable_restoration - // color_config( ) - w.f(1, (c.bit_depth > 8) as u32); // high_bitdepth - w.f(1, 0); // mono_chrome - w.f(1, 1); // color_description_present_flag - w.f(8, c.color_primaries as u32); // color_primaries - w.f(8, c.transfer_characteristics as u32); // transfer_characteristics - w.f(8, c.matrix_coefficients as u32); // matrix_coefficients - w.f(1, c.full_range as u32); // color_range - w.f(2, 0); // chroma_sample_position: unknown - w.f(1, 0); // separate_uv_delta_q - w.f(1, 0); // film_grain_params_present - w.trailing_bits(); - w.into_bytes() - } - - /// uncompressed_header( ) (5.9) for a shown KEY_FRAME or an INTER_FRAME referencing slot 0. - fn frame_header(&self, w: &mut BitWriter, key: bool, qidx: u8, lf_level: u32) { - w.f(1, 0); // show_existing_frame - w.f(2, if key { 0 } else { 1 }); // frame_type - w.f(1, 1); // show_frame - if !key { - w.f(1, 0); // error_resilient_mode - } - w.f(1, 0); // disable_cdf_update - w.f(1, 0); // frame_size_override_flag - if !key { - w.f(3, 0); // primary_ref_frame: LAST_FRAME's saved CDFs - w.f(8, 1); // refresh_frame_flags: slot 0 becomes LAST for the next frame - for _ in 0..7 { - w.f(3, 0); // ref_frame_idx[ i ] - } - w.f(1, 0); // render_and_frame_size_different - w.f(1, 0); // allow_high_precision_mv - w.f(1, 0); // is_filter_switchable - w.f(2, 0); // interpolation_filter = EIGHTTAP - w.f(1, 0); // is_motion_mode_switchable - } else { - w.f(1, 0); // render_and_frame_size_different - } - w.f(1, 0); // disable_frame_end_update_cdf - // tile_info( ): uniform spacing, one tile - let g = &self.geom; - let tile_log2 = |blk: usize, target: usize| { - let mut k = 0; - while (blk << k) < target { - k += 1; - } - k - }; - w.f(1, 1); // uniform_tile_spacing_flag - if tile_log2(1, g.sb_cols.min(64)) > 0 { - w.f(1, 0); // increment_tile_cols_log2 - } - if tile_log2(1, g.sb_rows.min(64)) > 0 { - w.f(1, 0); // increment_tile_rows_log2 - } - // quantization_params( ) - w.f(8, qidx as u32); - w.f(1, 0); // DeltaQYDc - w.f(1, 0); // DeltaQUDc - w.f(1, 0); // DeltaQUAc - w.f(1, 0); // using_qmatrix - w.f(1, 0); // segmentation_enabled - w.f(1, 0); // delta_q_present (base_q_idx > 0) - // loop_filter_params( ): the same level for every plane and direction - w.f(6, lf_level); - w.f(6, lf_level); - if lf_level != 0 { - w.f(6, lf_level); - w.f(6, lf_level); - } - w.f(3, 0); // loop_filter_sharpness - w.f(1, 0); // loop_filter_delta_enabled - w.f(1, 0); // tx_mode_select (TX_MODE_LARGEST) - if !key { - w.f(1, 0); // reference_select - } - w.f(1, 1); // reduced_tx_set - if !key { - for _ in 0..7 { - w.f(1, 0); // is_global - } - } - } - - /// Copies a picture into superblock-padded planes (edge replication). - fn load_source(&self, frame: &Frame) -> [Plane; 3] { - let maxv = (1u16 << self.cfg.bit_depth) - 1; - std::array::from_fn(|p| { - let (vw, vh) = self.geom.visible(p); - let (pw, ph) = self.geom.padded(p); - let (data, stride) = match p { - 0 => (frame.y, frame.y_stride), - 1 => (frame.u, frame.uv_stride), - _ => (frame.v, frame.uv_stride), - }; - assert!(stride >= vw && data.len() >= stride * (vh - 1) + vw, "plane {p} is too small for {vw}x{vh}"); - let mut pl = Plane::new(pw, ph); - for y in 0..ph { - let sy = y.min(vh - 1); - let src = &data[sy * stride..sy * stride + vw]; - let row = &mut pl.data[y * pw..y * pw + pw]; - for (x, d) in row.iter_mut().enumerate() { - *d = src[x.min(vw - 1)].min(maxv); - } - } - pl - }) - } - - /// Codes one frame at `qidx`; returns the frame OBU and the reconstruction. - fn encode_frame(&self, src: &[Plane; 3], key: bool, qidx: u8) -> (Vec, RefFrame) { - let g = &self.geom; - let refr = if key { None } else { self.refr.as_ref() }; - let mut dec = Decider::new(g, self.cfg.bit_depth as u32, qidx, key, src, refr); - let mut tw = TileWriter::new(g, key, qidx, refr.map(|r| &*r.cdfs)); - for sbr in 0..g.sb_rows { - tw.start_sb_row(); - for sbc in 0..g.sb_cols { - let node = dec.superblock(sbr * 16, sbc * 16); - tw.superblock(sbr * 16, sbc * 16, &node); - } - } - let lf_level = self.pick_filter_level(&dec.rec, &tw.lf_info(), src, qidx); - loop_filter(&mut dec.rec, &tw.lf_info(), [lf_level; 4], self.cfg.bit_depth as u32, false, 1); - let (tile, cdfs) = tw.finish(); - let mut w = BitWriter::new(); - self.frame_header(&mut w, key, qidx, lf_level); - w.byte_align(); - // tile_group_obu( ): a single tile, no tile_start_and_end_present_flag - let mut payload = w.into_bytes(); - payload.extend_from_slice(&tile); - let mut out = Vec::with_capacity(payload.len() + 8); - obu(OBU_FRAME, &payload, &mut out); - let rf = RefFrame { planes: dec.rec, mvs: dec.mvs, lf_level, cdfs }; - (out, rf) - } - - /// The loop filter level minimising the luma error (searched around a guess from the - /// quantizer, on the luma of a subset of the superblock rows). - fn pick_filter_level(&self, rec: &[Plane; 3], info: &LfInfo, src: &[Plane; 3], qidx: u8) -> u32 { - let (w, h) = (self.geom.width, self.geom.height); - let sb_row_step = if self.geom.sb_rows >= 9 { 3 } else { 1 }; - let sse = |pl: &Plane| -> u64 { - let mut s = 0u64; - for y in 0..h { - if !(y >> 6).is_multiple_of(sb_row_step) { - continue; - } - for (a, b) in pl.row(y)[..w].iter().zip(&src[0].row(y)[..w]) { - let d = *a as i64 - *b as i64; - s += (d * d) as u64; - } - } - s - }; - let bd = self.cfg.bit_depth as u32; - let mut cache: Vec<(u32, u64)> = vec![(0, sse(&rec[0]))]; - // one working copy of the luma plane for all candidate levels - let mut planes = [rec[0].clone(), Plane::new(0, 0), Plane::new(0, 0)]; - let mut eval = |lvl: u32| -> u64 { - if let Some(&(_, e)) = cache.iter().find(|(l, _)| *l == lvl) { - return e; - } - planes[0].data.copy_from_slice(&rec[0].data); - loop_filter(&mut planes, info, [lvl; 4], bd, true, sb_row_step); - let e = sse(&planes[0]); - cache.push((lvl, e)); - e - }; - let guess = ((qidx as u32 * 3) / 16).clamp(2, 50); - let mut best = (0u32, eval(0)); - let mut step = (guess / 2).max(1); - let mut center = guess; - let e = eval(center); - if e < best.1 { - best = (center, e); - } - while step >= 2 { - for lvl in [center.saturating_sub(step), (center + step).min(63)] { - let e = eval(lvl); - if e < best.1 { - best = (lvl, e); - } - } - center = best.0.max(1); - step /= 2; - } - best.0 - } - - /// Encodes one frame. - pub fn encode(&mut self, frame: &Frame) -> Packet { - let key = self.frame_num.is_multiple_of(self.cfg.keyint.max(1) as u64); - let src = self.load_source(frame); - let (frame_obu, rf) = match self.cfg.rate { - RateControl::ConstantQ(q) => self.encode_frame(&src, key, q.max(1)), - RateControl::Bitrate { .. } => match self.rc.take() { - Some(mut rc) => { - let out = self.encode_rate_controlled(&src, key, &mut rc); - self.rc = Some(rc); - out - } - // The state is created with the encoder for bitrate mode; without it, encode at a - // middle quantizer rather than fail. - None => self.encode_frame(&src, key, 128), - }, - }; - self.refr = Some(rf); - self.frame_num += 1; - let mut data = Vec::with_capacity(frame_obu.len() + self.seq_obu.len()); - if key { - data.extend_from_slice(&self.seq_obu); - } - data.extend_from_slice(&frame_obu); - Packet { data, keyframe: key } - } - - fn encode_rate_controlled(&mut self, src: &[Plane; 3], key: bool, rc: &mut RateState) -> (Vec, RefFrame) { - let first = self.frame_num == 0; - let (budget, q_start) = rc.frame_budget(self.cfg.keyint, key); - // pay back the accumulated deviation over the next RC_PAYBACK frames - let target = (budget - rc.debt / RC_PAYBACK).clamp(budget * 0.35, budget * 2.0); - let mut q = q_start; - let mut best: Option<(Vec, RefFrame, f64, f64)> = None; - // The first frame is coded up to four times to find its quantizer (it fixes the - // quantizers of everything after it), a later key frame up to three times (it is a large - // part of the budget of its group). A frame is coded again only while its size is far from - // the target and an untried quantizer is left to try; other frames are coded once. - let (max_passes, tolerance) = if first { - (RC_FIRST_PASSES, RC_FIRST_TOLERANCE) - } else if key { - (RC_KEY_PASSES, RC_KEY_TOLERANCE) - } else { - (1, 0.0) - }; - let mut tried: Vec<(u8, f64)> = Vec::new(); - for _ in 0..max_passes { - let qi = q.round().clamp(1.0, 255.0) as u8; - if tried.iter().any(|t| t.0 == qi) { - break; - } - let (obu, rf) = self.encode_frame(src, key, qi); - let bits = obu.len() as f64 * 8.0; - let err = (bits / target).log2(); - tried.push((qi, err)); - if best.as_ref().is_none_or(|b| err.abs() < (b.2 / target).log2().abs()) { - best = Some((obu, rf, bits, qi as f64)); - } - if err.abs() <= tolerance { - break; - } - let next = if first && tried.len() == 1 { - Some((q + RC_FIRST_SLOPE * err).clamp(1.0, 255.0)) - } else { - next_probe_q(self.cfg.bit_depth, &tried).map(f64::from) - }; - match next { - Some(next) => q = next, - None => break, - } - } - // At least one pass ran, so `best` is set. - let Some((obu, rf, bits, q_used)) = best else { return self.encode_frame(src, key, q.round().clamp(1.0, 255.0) as u8) }; - rc.debt += bits - budget; - rc.update(key, first, q_used, bits, target); - (obu, rf) - } - - /// The loop filter level of the last encoded frame. For tests. - #[doc(hidden)] - pub fn last_loop_filter_level(&self) -> Option { - self.refr.as_ref().map(|r| r.lf_level) - } - - /// The reconstruction of the last encoded frame (visible area, planes Y, U, V), equal to - /// what a decoder outputs. For tests. - #[doc(hidden)] - pub fn last_reconstruction(&self) -> Option<[Vec; 3]> { - let rf = self.refr.as_ref()?; - Some(std::array::from_fn(|p| { - let (vw, vh) = self.geom.visible(p); - let pl = &rf.planes[p]; - let mut v = Vec::with_capacity(vw * vh); - for y in 0..vh { - v.extend_from_slice(&pl.row(y)[..vw]); - } - v - })) - } -} diff --git a/crates/av1enc/src/loopfilter.rs b/crates/av1enc/src/loopfilter.rs deleted file mode 100644 index cf350fbd2..000000000 --- a/crates/av1enc/src/loopfilter.rs +++ /dev/null @@ -1,209 +0,0 @@ -//! Deblocking loop filter (spec 7.14), mirrored from the decoder process so that the encoder's -//! references equal a decoder's. Segmentation and loop filter deltas are off and the sharpness -//! is 0 in the frames this encoder writes. - -use crate::pred::Plane; -use crate::tables::{MAX_LOOP_FILTER, NUM_4X4_BLOCKS_HIGH, NUM_4X4_BLOCKS_WIDE, SUBSAMPLED_SIZE, TX_HEIGHT, TX_WIDTH}; - -/// Mode info the loop filter reads (per 4x4 luma unit, row stride `mi_cols`), and the -/// transform size per 4x4 unit of each plane (`LoopfilterTxSizes`, row stride `mi_cols + 32`). -pub(crate) struct LfInfo<'a> { - pub mi_cols: usize, - pub mi_rows: usize, - pub width: usize, - pub height: usize, - pub mi_size: &'a [u8], - pub skip: &'a [bool], - pub is_inter: &'a [bool], - pub tx: [&'a [u8]; 3], -} - -/// Applies the loop filter with `loop_filter_level[ 0..4 ]` to `planes`. For level search -/// (`luma_only`), only plane 0 and only every `sb_row_step`-th superblock row are filtered. -pub(crate) fn loop_filter(planes: &mut [Plane; 3], info: &LfInfo, level: [u32; 4], bit_depth: u32, luma_only: bool, sb_row_step: usize) { - if level[0] == 0 && level[1] == 0 { - return; - } - let nplanes = if luma_only { 1 } else { 3 }; - for (plane, pl) in planes.iter_mut().enumerate().take(nplanes) { - if plane > 0 && level[1 + plane] == 0 { - continue; - } - for pass in 0..2 { - let step = if plane == 0 { 1 } else { 2 }; - let mut row = 0; - while row < info.mi_rows { - if !(row >> 4).is_multiple_of(sb_row_step) { - row += step; - continue; - } - let mut col = 0; - while col < info.mi_cols { - edge(pl, info, level, bit_depth, plane, pass, (row, col)); - col += step; - } - row += step; - } - } - } -} - -fn edge(pl: &mut Plane, info: &LfInfo, level: [u32; 4], bd: u32, plane: usize, pass: usize, position: (usize, usize)) { - let (row, col) = position; - let s = (plane > 0) as usize; - let (dx, dy) = if pass == 0 { (1usize, 0usize) } else { (0, 1) }; - let x = col * 4; - let y = row * 4; - let row = row | s; - let col = col | s; - let on_screen = !(x >= info.width || y >= info.height || (pass == 0 && x == 0) || (pass == 1 && y == 0)); - if !on_screen { - return; - } - let xp = x >> s; - let yp = y >> s; - let prev_row = row - (dy << s); - let prev_col = col - (dx << s); - let i = row * info.mi_cols + col; - let mi_size = info.mi_size[i] as usize; - let tx_stride = info.mi_cols + 32; - let tx_sz = info.tx[plane][(row >> s) * tx_stride + (col >> s)] as usize; - let plane_size = SUBSAMPLED_SIZE[mi_size][s][s] as usize; - let skip = info.skip[i]; - let is_intra = !info.is_inter[i]; - let prev_tx_sz = info.tx[plane][(prev_row >> s) * tx_stride + (prev_col >> s)] as usize; - let is_block_edge = if pass == 0 { - xp.is_multiple_of(4 * NUM_4X4_BLOCKS_WIDE[plane_size] as usize) - } else { - yp.is_multiple_of(4 * NUM_4X4_BLOCKS_HIGH[plane_size] as usize) - }; - let is_tx_edge = if pass == 0 { xp.is_multiple_of(TX_WIDTH[tx_sz] as usize) } else { yp.is_multiple_of(TX_HEIGHT[tx_sz] as usize) }; - let apply_filter = is_tx_edge && (is_block_edge || !skip || is_intra); - let base_size = if pass == 0 { TX_WIDTH[prev_tx_sz].min(TX_WIDTH[tx_sz]) } else { TX_HEIGHT[prev_tx_sz].min(TX_HEIGHT[tx_sz]) } as usize; - let filter_size = if plane == 0 { 16.min(base_size) } else { 8.min(base_size) }; - // 7.14.4 with no deltas, segmentation or sharpness: the same strength for every block - let lvl = level[if plane == 0 { pass } else { plane + 1 }].min(MAX_LOOP_FILTER as u32) as i32; - if !apply_filter || lvl == 0 { - return; - } - let limit = lvl.max(1); - let blimit = 2 * (lvl + 2) + limit; - let thresh = lvl >> 4; - let strength = FilterStrength { limit, blimit, thresh }; - for k in 0..4 { - sample_filter(pl, (xp + dy * k, yp + dx * k), plane, &strength, (dx as isize, dy as isize), filter_size, bd); - } -} - -/// Thresholds derived from the loop-filter level (spec 7.14.4). -struct FilterStrength { - limit: i32, - blimit: i32, - thresh: i32, -} - -#[inline] -fn sample_filter(pl: &mut Plane, position: (usize, usize), plane: usize, strength: &FilterStrength, direction: (isize, isize), filter_size: usize, bd: u32) { - let (x, y) = position; - let (dx, dy) = direction; - let FilterStrength { limit, blimit, thresh } = *strength; - let stride = pl.stride as isize; - let step = dy * stride + dx; - let base = y as isize * stride + x as isize; - let reach: isize = if filter_size >= 16 { 7 } else { 4 }; - let mut v = [0i32; 16]; - for k in -reach..reach { - v[(k + 8) as usize] = pl.data[(base + k * step) as usize] as i32; - } - let at = |k: isize| -> i32 { v[(k + 8) as usize] }; - let (q0, q1, q2, q3) = (at(0), at(1), at(2), at(3)); - let (p0, p1, p2, p3) = (at(-1), at(-2), at(-3), at(-4)); - let d = &mut pl.data; - let sh = bd - 8; - let thresh_bd = thresh << sh; - let hev = (p1 - p0).abs() > thresh_bd || (q1 - q0).abs() > thresh_bd; - let filter_len = if filter_size == 4 { - 4 - } else if plane != 0 { - 6 - } else if filter_size == 8 { - 8 - } else { - 16 - }; - let limit_bd = limit << sh; - let blimit_bd = blimit << sh; - let mut mask = (p1 - p0).abs() > limit_bd || (q1 - q0).abs() > limit_bd || (p0 - q0).abs() * 2 + (p1 - q1).abs() / 2 > blimit_bd; - if filter_len >= 6 { - mask |= (p2 - p1).abs() > limit_bd || (q2 - q1).abs() > limit_bd; - } - if filter_len >= 8 { - mask |= (p3 - p2).abs() > limit_bd || (q3 - q2).abs() > limit_bd; - } - if mask { - return; - } - let t_bd = 1 << sh; - let flat = if filter_size >= 8 { - let mut m = (p1 - p0).abs() > t_bd || (q1 - q0).abs() > t_bd || (p2 - p0).abs() > t_bd || (q2 - q0).abs() > t_bd; - if filter_len >= 8 { - m |= (p3 - p0).abs() > t_bd || (q3 - q0).abs() > t_bd; - } - !m - } else { - false - }; - let flat2 = if filter_size >= 16 { - let (q4, q5, q6, p4, p5, p6) = (at(4), at(5), at(6), at(-5), at(-6), at(-7)); - !((p6 - p0).abs() > t_bd - || (q6 - q0).abs() > t_bd - || (p5 - p0).abs() > t_bd - || (q5 - q0).abs() > t_bd - || (p4 - p0).abs() > t_bd - || (q4 - q0).abs() > t_bd) - } else { - false - }; - if filter_size == 4 || !flat { - // narrow filter (7.14.6.3) - let half = 1i32 << (bd - 1); - let c = |v: i32| v.clamp(-half, half - 1); - let off = 0x80 << sh; - let (ps1, ps0, qs0, qs1) = (p1 - off, p0 - off, q0 - off, q1 - off); - let mut filter = if hev { c(ps1 - qs1) } else { 0 }; - filter = c(filter + 3 * (qs0 - ps0)); - let filter1 = c(filter + 4) >> 3; - let filter2 = c(filter + 3) >> 3; - d[base as usize] = (c(qs0 - filter1) + off) as u16; - d[(base - step) as usize] = (c(ps0 + filter2) + off) as u16; - if !hev { - let f = (filter1 + 1) >> 1; - d[(base + step) as usize] = (c(qs1 - f) + off) as u16; - d[(base - 2 * step) as usize] = (c(ps1 + f) + off) as u16; - } - } else { - // wide filter (7.14.6.4) - let log2 = if filter_size == 8 || !flat2 { 3 } else { 4 }; - let n: isize = if log2 == 4 { - 6 - } else if plane == 0 { - 3 - } else { - 2 - }; - let n2: isize = if log2 == 3 && plane == 0 { 0 } else { 1 }; - let mut f = [0i32; 12]; - for i in -n..n { - let mut t = 0; - for j in -n..=n { - let p = (i + j).clamp(-(n + 1), n); - let tap = if j.abs() <= n2 { 2 } else { 1 }; - t += at(p) * tap; - } - f[(i + n) as usize] = (t + (1 << (log2 - 1))) >> log2; - } - for i in -n..n { - d[(base + i * step) as usize] = f[(i + n) as usize] as u16; - } - } -} diff --git a/crates/av1enc/src/pred.rs b/crates/av1enc/src/pred.rs deleted file mode 100644 index b899202f4..000000000 --- a/crates/av1enc/src/pred.rs +++ /dev/null @@ -1,445 +0,0 @@ -//! Prediction processes, mirrored from the spec (7.11.2 intra prediction for the non-directional -//! modes plus V_PRED / H_PRED without edge filtering, 7.11.3 block inter prediction without -//! reference scaling) so that the encoder predicts exactly like a decoder. - -use crate::tables::{ - DC_PRED, EIGHTTAP, H_PRED, PAETH_PRED, SM_WEIGHTS_TX_4X4, SM_WEIGHTS_TX_8X8, SM_WEIGHTS_TX_16X16, SM_WEIGHTS_TX_32X32, SMOOTH_H_PRED, SMOOTH_PRED, - SMOOTH_V_PRED, SUBPEL_FILTERS, V_PRED, -}; -use crate::transform::round2; - -/// A plane buffer (padded to whole superblocks). -#[derive(Clone)] -pub(crate) struct Plane { - pub data: Vec, - pub stride: usize, -} - -impl Plane { - pub fn new(w: usize, h: usize) -> Plane { - Plane { data: vec![0; w * h], stride: w } - } - #[inline(always)] - pub fn at(&self, x: usize, y: usize) -> u16 { - self.data[y * self.stride + x] - } - #[inline(always)] - pub fn row(&self, y: usize) -> &[u16] { - &self.data[y * self.stride..(y + 1) * self.stride] - } -} - -/// The intra modes this encoder uses (values are the spec's y / uv mode numbers). -pub(crate) const INTRA_MODES_USED: [usize; 7] = [DC_PRED, V_PRED, H_PRED, PAETH_PRED, SMOOTH_PRED, SMOOTH_V_PRED, SMOOTH_H_PRED]; - -/// Inputs of the intra prediction process. -pub(crate) struct IntraEdge { - pub x: usize, - pub y: usize, - pub have_left: bool, - pub have_above: bool, - pub have_above_right: bool, - pub have_below_left: bool, - pub log2: u32, - /// ((MiCols * MI_SIZE) >> subX) - 1 and the same for rows. - pub max_x: i32, - pub max_y: i32, - pub bit_depth: u32, -} - -/// Edge samples: `above[0..2n]`, `left[0..2n]` and the top-left corner. -pub(crate) struct Edges { - above: [i32; 64], - left: [i32; 64], - corner: i32, -} - -pub(crate) fn edges(pl: &Plane, e: &IntraEdge) -> Edges { - let w = 1usize << e.log2; - let h = w; - let bd = e.bit_depth; - let (x, y) = (e.x, e.y); - let n = w + h; - let mut above = [0i32; 64]; - let mut left = [0i32; 64]; - if !e.have_above && e.have_left { - let v = pl.at(x - 1, y) as i32; - above[..n].iter_mut().for_each(|a| *a = v); - } else if !e.have_above && !e.have_left { - let v = (1 << (bd - 1)) - 1; - above[..n].iter_mut().for_each(|a| *a = v); - } else { - let above_limit = e.max_x.min(x as i32 + if e.have_above_right { 2 * w as i32 } else { w as i32 } - 1); - let row = pl.row(y - 1); - for (i, a) in above[..n].iter_mut().enumerate() { - *a = row[above_limit.min(x as i32 + i as i32) as usize] as i32; - } - } - if !e.have_left && e.have_above { - let v = pl.at(x, y - 1) as i32; - left[..n].iter_mut().for_each(|a| *a = v); - } else if !e.have_left && !e.have_above { - let v = (1 << (bd - 1)) + 1; - left[..n].iter_mut().for_each(|a| *a = v); - } else { - let left_limit = e.max_y.min(y as i32 + if e.have_below_left { 2 * h as i32 } else { h as i32 } - 1); - for (i, l) in left[..n].iter_mut().enumerate() { - *l = pl.at(x - 1, left_limit.min(y as i32 + i as i32) as usize) as i32; - } - } - let corner = if e.have_above && e.have_left { - pl.at(x - 1, y - 1) as i32 - } else if e.have_above { - pl.at(x, y - 1) as i32 - } else if e.have_left { - pl.at(x - 1, y) as i32 - } else { - 1 << (bd - 1) - }; - Edges { above, left, corner } -} - -fn sm_weights(log2: u32) -> &'static [u8] { - match log2 { - 2 => &SM_WEIGHTS_TX_4X4, - 3 => &SM_WEIGHTS_TX_8X8, - 4 => &SM_WEIGHTS_TX_16X16, - _ => &SM_WEIGHTS_TX_32X32, - } -} - -/// Predicts a square `2^log2` block with `mode` into `pred` (row stride = block width). -pub(crate) fn predict_intra(ed: &Edges, e: &IntraEdge, mode: usize, pred: &mut [u16]) { - let log2 = e.log2; - let w = 1usize << log2; - let h = w; - let bd = e.bit_depth; - let (above, left) = (&ed.above, &ed.left); - match mode { - DC_PRED => { - let v = if e.have_left && e.have_above { - let sum: i32 = left[..h].iter().sum::() + above[..w].iter().sum::() + ((w + h) >> 1) as i32; - sum / (w + h) as i32 - } else if e.have_left { - (left[..h].iter().sum::() + (h >> 1) as i32) >> log2 - } else if e.have_above { - (above[..w].iter().sum::() + (w >> 1) as i32) >> log2 - } else { - 1 << (bd - 1) - }; - let v = v.clamp(0, (1 << bd) - 1) as u16; - pred[..w * h].iter_mut().for_each(|p| *p = v); - } - V_PRED => { - for i in 0..h { - for j in 0..w { - pred[i * w + j] = above[j] as u16; - } - } - } - H_PRED => { - for i in 0..h { - pred[i * w..i * w + w].iter_mut().for_each(|p| *p = left[i] as u16); - } - } - PAETH_PRED => { - let tl = ed.corner; - for i in 0..h { - for j in 0..w { - let a = above[j]; - let l = left[i]; - let base = a + l - tl; - let p_left = (base - l).abs(); - let p_top = (base - a).abs(); - let p_tl = (base - tl).abs(); - pred[i * w + j] = if p_left <= p_top && p_left <= p_tl { - l as u16 - } else if p_top <= p_tl { - a as u16 - } else { - tl as u16 - }; - } - } - } - _ => { - let wx = sm_weights(log2); - let wy = wx; - for i in 0..h { - for j in 0..w { - let v = if mode == SMOOTH_PRED { - let s = wy[i] as i32 * above[j] + (256 - wy[i] as i32) * left[h - 1] + wx[j] as i32 * left[i] + (256 - wx[j] as i32) * above[w - 1]; - round2(s, 9) - } else if mode == SMOOTH_V_PRED { - round2(wy[i] as i32 * above[j] + (256 - wy[i] as i32) * left[h - 1], 8) - } else { - round2(wx[j] as i32 * left[i] + (256 - wx[j] as i32) * above[w - 1], 8) - }; - pred[i * w + j] = v as u16; - } - } - } - } -} - -/// Block inter prediction (7.11.3.3 motion vector scaling without scaling, 7.11.3.4 block -/// inter prediction with the EIGHTTAP filter, single reference so InterRound1 = 11 and the -/// final rounding is the identity) of the `w x h` block at plane position (x, y) displaced by -/// `mv` (1/8 luma sample units, [row, col]). `last_x` / `last_y` are the largest sample -/// coordinates of the reference plane; the result goes to `pred` (row stride `w`). -pub(crate) struct InterBlock { - pub last_x: i32, - pub last_y: i32, - pub x: usize, - pub y: usize, - pub width: usize, - pub height: usize, - pub mv: [i32; 2], - pub subsampling: usize, - pub bit_depth: u32, -} - -pub(crate) fn predict_inter(refp: &Plane, block: &InterBlock, pred: &mut [u16]) { - let InterBlock { last_x, last_y, x, y, width: w, height: h, mv, subsampling: ss, bit_depth } = *block; - // positions in 1/16 sample units ((startX >> 6) & 15 is the filter phase) - let px = ((x as i32) << 4) + ((2 * mv[1]) >> ss); - let py = ((y as i32) << 4) + ((2 * mv[0]) >> ss); - let (fx, fy) = ((px & 15) as usize, (py & 15) as usize); - let (ix, iy) = (px >> 4, py >> 4); - let max = (1i32 << bit_depth) - 1; - if fx == 0 && fy == 0 { - // the filters' phase-0 kernel is a unit impulse: a clamped copy - for r in 0..h { - let ry = (iy + r as i32).clamp(0, last_y) as usize; - let row = refp.row(ry); - let out = &mut pred[r * w..r * w + w]; - let x0 = ix; - if x0 >= 0 && x0 + w as i32 - 1 <= last_x { - out.copy_from_slice(&row[x0 as usize..x0 as usize + w]); - } else { - for (c, o) in out.iter_mut().enumerate() { - *o = row[(x0 + c as i32).clamp(0, last_x) as usize]; - } - } - } - return; - } - let filt_h = if w <= 4 { 4 } else { EIGHTTAP }; - let filt_v = if h <= 4 { 4 } else { EIGHTTAP }; - let kh = &SUBPEL_FILTERS[filt_h][fx]; - let kv = &SUBPEL_FILTERS[filt_v][fy]; - let bx = ix - 3; - let x_inside = bx >= 0 && bx + w as i32 + 7 <= last_x + 1; - // horizontal pass of one reference row into `out` (Round2( sum, InterRound0 = 3 )) - let hfilter = |ry: usize, out: &mut [i32]| { - let row = refp.row(ry); - if x_inside { - let seg = &row[bx as usize..bx as usize + w + 7]; - for (c, o) in out.iter_mut().enumerate() { - let mut s = 0i32; - for t in 0..8 { - s += kh[t] as i32 * seg[c + t] as i32; - } - *o = round2(s, 3); - } - } else { - for (c, o) in out.iter_mut().enumerate() { - let mut s = 0i32; - for t in 0..8 { - s += kh[t] as i32 * row[(bx + c as i32 + t as i32).clamp(0, last_x) as usize] as i32; - } - *o = round2(s, 3); - } - } - }; - let mut inter = [0i32; (64 + 7) * 64]; - if fy == 0 { - // the vertical kernel is 128 at the centre: Round2( 128 * v, 11 ) = Round2( v, 4 ) - for r in 0..h { - let ry = (iy + r as i32).clamp(0, last_y) as usize; - hfilter(ry, &mut inter[..w]); - for (p, &v) in pred[r * w..r * w + w].iter_mut().zip(&inter[..w]) { - *p = round2(v, 4).clamp(0, max) as u16; - } - } - return; - } - if fx == 0 { - // the horizontal pass is exactly 16 * sample: Round2( 16 * sum, 11 ) = Round2( sum, 7 ) - for r in 0..h { - let rows: [&[u16]; 8] = std::array::from_fn(|t| refp.row((iy + r as i32 + t as i32 - 3).clamp(0, last_y) as usize)); - for c in 0..w { - let xx = if x_inside { (ix + c as i32) as usize } else { (ix + c as i32).clamp(0, last_x) as usize }; - let mut s = 0i32; - for t in 0..8 { - s += kv[t] as i32 * rows[t][xx] as i32; - } - pred[r * w + c] = round2(s, 7).clamp(0, max) as u16; - } - } - return; - } - let inter_h = h + 7; - for r in 0..inter_h { - let ry = (iy + r as i32 - 3).clamp(0, last_y) as usize; - hfilter(ry, &mut inter[r * w..r * w + w]); - } - for r in 0..h { - for c in 0..w { - let mut s = 0i32; - for t in 0..8 { - s += kv[t] as i32 * inter[(r + t) * w + c]; - } - pred[r * w + c] = round2(s, 11).clamp(0, max) as u16; - } - } -} - -/// The visible `w x h` area of a reference plane interpolated at the sub-sample phase -/// (`fx`, `fy`) (1/16 units) with the EIGHTTAP filter, as `predict_inter` computes it for -/// blocks wider and taller than 4. The reference for `half_planes`, which the encoder uses. -#[cfg(test)] -pub(crate) fn subpel_plane(refp: &Plane, w: usize, h: usize, fx: usize, fy: usize, bit_depth: u32) -> Vec { - let kh = &SUBPEL_FILTERS[EIGHTTAP][fx]; - let kv = &SUBPEL_FILTERS[EIGHTTAP][fy]; - let max = (1i32 << bit_depth) - 1; - let (lx, ly) = (w as i32 - 1, h as i32 - 1); - let mut inter = vec![0i32; w * h]; - for y in 0..h { - let row = refp.row(y); - for x in 0..w { - let mut s = 0i32; - for t in 0..8 { - s += kh[t] as i32 * row[(x as i32 + t as i32 - 3).clamp(0, lx) as usize] as i32; - } - inter[y * w + x] = round2(s, 3); - } - } - let mut out = vec![0u16; w * h]; - for y in 0..h { - for x in 0..w { - let mut s = 0i32; - for t in 0..8 { - s += kv[t] as i32 * inter[(y as i32 + t as i32 - 3).clamp(0, ly) as usize * w + x]; - } - out[y * w + x] = round2(s, 11).clamp(0, max) as u16; - } - } - out -} - -/// The three half-sample planes (8, 0), (0, 8), (8, 8) of `subpel_plane` in one pass: the -/// horizontal half-sample pass is shared by (8, 0) and (8, 8), and the phase-0 kernels reduce to -/// exact shifts (Round2( 128 * v, 11 ) = Round2( v, 4 ), Round2( 16 * s, 11 ) = Round2( s, 7 )), -/// so the result equals three `subpel_plane` calls bit for bit. -pub(crate) fn half_planes(refp: &Plane, w: usize, h: usize, bit_depth: u32) -> [Vec; 3] { - let k = &SUBPEL_FILTERS[EIGHTTAP][8]; - let max = (1i32 << bit_depth) - 1; - let (lx, ly) = (w as i32 - 1, h as i32 - 1); - // horizontal pass at phase 8: Round2( sum, 3 ) - let mut hp = vec![0i32; w * h]; - for y in 0..h { - let row = &refp.row(y)[..w]; - let out = &mut hp[y * w..y * w + w]; - let tap = |x: i32| -> i32 { - let mut s = 0i32; - for t in 0..8 { - s += k[t] as i32 * row[(x + t as i32 - 3).clamp(0, lx) as usize] as i32; - } - round2(s, 3) - }; - if w >= 8 { - for x in 0..3 { - out[x] = tap(x as i32); - } - for (x, o) in out.iter_mut().enumerate().take(w - 4).skip(3) { - let seg = &row[x - 3..x + 5]; - let mut s = 0i32; - for t in 0..8 { - s += k[t] as i32 * seg[t] as i32; - } - *o = round2(s, 3); - } - for x in w - 4..w { - out[x] = tap(x as i32); - } - } else { - for (x, o) in out.iter_mut().enumerate() { - *o = tap(x as i32); - } - } - } - let p80: Vec = hp.iter().map(|&v| round2(v, 4).clamp(0, max) as u16).collect(); - let mut p08 = vec![0u16; w * h]; - let mut p88 = vec![0u16; w * h]; - for y in 0..h { - let rows: [usize; 8] = std::array::from_fn(|t| (y as i32 + t as i32 - 3).clamp(0, ly) as usize); - let (o08, o88) = (&mut p08[y * w..y * w + w], &mut p88[y * w..y * w + w]); - for x in 0..w { - let (mut a, mut b) = (0i32, 0i32); - for t in 0..8 { - a += k[t] as i32 * refp.data[rows[t] * refp.stride + x] as i32; - b += k[t] as i32 * hp[rows[t] * w + x]; - } - o08[x] = round2(a, 7).clamp(0, max) as u16; - o88[x] = round2(b, 11).clamp(0, max) as u16; - } - } - [p80, p08, p88] -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Whole-plane interpolation equals block inter prediction at the same phase. - #[test] - fn subpel_plane_matches_block_prediction() { - let (w, h) = (37, 29); - let mut p = Plane::new(64, 64); - for (i, v) in p.data.iter_mut().enumerate() { - *v = ((i * 7919) % 256) as u16; - } - for (fx, fy) in [(8, 0), (0, 8), (8, 8), (4, 12)] { - let plane = subpel_plane(&p, w, h, fx, fy, 8); - for (bx, by) in [(0i32, 0i32), (8, 8), (24, 16), (-5, 20), (30, -3)] { - let mut pred = vec![0u16; 64]; - let mv = [by * 8 + fy as i32 / 2, bx * 8 + fx as i32 / 2]; - predict_inter( - &p, - &InterBlock { last_x: w as i32 - 1, last_y: h as i32 - 1, x: 0, y: 0, width: 8, height: 8, mv, subsampling: 0, bit_depth: 8 }, - &mut pred, - ); - for r in 0..8 { - for c in 0..8 { - let (x, y) = ((bx + c).clamp(0, w as i32 - 1), (by + r).clamp(0, h as i32 - 1)); - if bx + c == x && by + r == y { - assert_eq!(pred[(r * 8 + c) as usize], plane[y as usize * w + x as usize], "phase ({fx}, {fy}) at ({}, {})", bx + c, by + r); - } - } - } - } - } - } - - #[test] - fn half_planes_match_subpel_plane() { - for (w, h) in [(37usize, 29usize), (5, 3), (1, 1), (64, 64)] { - let mut p = Plane::new(64, 64); - for (i, v) in p.data.iter_mut().enumerate() { - *v = ((i * 7919) % 1024) as u16; - } - let hp = half_planes(&p, w, h, 10); - for (i, (fx, fy)) in [(8, 0), (0, 8), (8, 8)].into_iter().enumerate() { - assert_eq!(hp[i], subpel_plane(&p, w, h, fx, fy, 10), "{w}x{h} phase ({fx}, {fy})"); - } - } - } - - #[test] - fn phase_zero_filters_are_impulses() { - for f in SUBPEL_FILTERS.iter() { - assert_eq!(f[0], [0, 0, 0, 128, 0, 0, 0, 0]); - } - } -} diff --git a/crates/av1enc/src/symbol.rs b/crates/av1enc/src/symbol.rs deleted file mode 100644 index 579a51e20..000000000 --- a/crates/av1enc/src/symbol.rs +++ /dev/null @@ -1,267 +0,0 @@ -//! Multi-symbol arithmetic encoder: the exact inverse of the symbol decoder of spec 8.2. -//! -//! The decoder keeps `SymbolValue`, the bitwise complement of a 15-bit window into the tile data -//! minus the lower bounds of the symbols decoded so far, and selects symbol `k` when -//! `cur(k) <= SymbolValue < cur(k - 1)` with -//! `cur(k) = ((SymbolRange >> 8) * ((32768 - cdf[k]) >> 6) >> 1) + 4 * (N - k - 1)` and -//! `cur(-1) = SymbolRange`. The encoder therefore runs an ordinary carry-propagating range -//! coder over the complemented bit stream: coding symbol `k` adds `cur(k)` to `low` and sets the -//! range to `cur(k - 1) - cur(k)`; both are renormalised exactly like the decoder's range. -//! -//! At the end (spec 8.2.4 exit process) the decoder has consumed `S` renormalisation bits plus a -//! 15-bit window, and the bits from position `S` on must be a one followed by zeros. In the -//! complemented domain that window is `0b011_1111_1111_1111`, so the encoder picks the `S`-bit -//! prefix `D` with `D * 2^15 + 2^14 - 1` inside `[low, low + range)`, i.e. -//! `D = (low + 2^14) >> 15`, and writes `!D`, a one bit and zero padding. - -pub(crate) struct SymbolWriter { - /// Low end of the interval, holding `15 + cnt` bits not yet moved to `out` (plus carries). - low: u64, - rng: u32, - cnt: u32, - out: Vec, - pub disable_update: bool, -} - -impl SymbolWriter { - pub fn new(disable_update: bool) -> Self { - SymbolWriter { low: 0, rng: 1 << 15, cnt: 0, out: Vec::with_capacity(1 << 16), disable_update } - } - - /// Adds `k` to the bytes already output (carry propagation). - fn carry(&mut self, mut k: u64) { - let mut i = self.out.len(); - while k != 0 { - i -= 1; - let t = self.out[i] as u64 + k; - self.out[i] = t as u8; - k = t >> 8; - } - } - - #[inline(always)] - fn encode(&mut self, cdf: &[u16], n: usize, s: usize) { - let r8 = self.rng >> 8; - let cur = |k: usize| -> u32 { ((r8 * ((32768 - cdf[k] as u32) >> 6)) >> 1) + 4 * (n - k - 1) as u32 }; - let hi = if s == 0 { self.rng } else { cur(s - 1) }; - let lo = cur(s); - debug_assert!(hi > lo, "symbol {s} has an empty interval"); - self.low += lo as u64; - self.rng = hi - lo; - let d = 15 - (31 - self.rng.leading_zeros()); - if d == 0 { - return; - } - self.rng <<= d; - self.low <<= d; - self.cnt += d; - while self.cnt >= 8 { - let sh = 15 + self.cnt - 8; - let c = self.low >> sh; - self.low &= (1u64 << sh) - 1; - self.cnt -= 8; - self.carry(c >> 8); - self.out.push((c & 0xff) as u8); - } - } - - /// Codes `s` with the adaptive CDF `cdf` (N values plus the adaptation counter). - #[inline] - pub fn symbol(&mut self, s: usize, cdf: &mut [u16]) { - let n = cdf.len() - 1; - debug_assert!(s < n); - self.encode(cdf, n, s); - if !self.disable_update { - update_cdf(cdf, n, s); - } - } - - /// Codes `s` with a CDF that is not adapted. - pub fn symbol_fixed(&mut self, s: usize, cdf: &[u16]) { - let n = cdf.len() - 1; - self.encode(cdf, n, s); - } - - /// `read_bool()` counterpart. - pub fn bool(&mut self, b: bool) { - self.encode(&[1 << 14, 1 << 15, 0], 2, b as usize); - } - - /// `L(n)` counterpart (most significant bit first). - #[cfg(test)] - pub fn literal(&mut self, n: u32, v: u32) { - for i in (0..n).rev() { - self.bool((v >> i) & 1 != 0); - } - } - - /// Finishes the tile: returns its bytes (with the trailing one bit and zero padding). - pub fn finish(mut self) -> Vec { - let low = self.low + (1 << 14); - let mut tail = low >> 15; - self.carry(tail >> self.cnt); - tail &= (1u64 << self.cnt) - 1; - let mut bytes: Vec = self.out.iter().map(|b| !b).collect(); - let n = self.cnt + 1; // 1..=8 bits: !tail then the trailing one bit - let v = (((!tail) & ((1u64 << self.cnt) - 1)) << 1) | 1; - bytes.push((v << (8 - n)) as u8); - bytes - } -} - -/// CDF adaptation (spec 8.2.6, as in the decoder). -#[inline(always)] -pub(crate) fn update_cdf(cdf: &mut [u16], n: usize, symbol: usize) { - let count = cdf[n]; - let rate = 3 + (count > 15) as u32 + (count > 31) as u32 + (31 - (n as u32).leading_zeros()).min(2); - let mut tmp = 0u32; - for i in 0..n - 1 { - if i == symbol { - tmp = 1 << 15; - } - let c = cdf[i] as u32; - if tmp < c { - cdf[i] = (c - ((c - tmp) >> rate)) as u16; - } else { - cdf[i] = (c + ((tmp - c) >> rate)) as u16; - } - } - cdf[n] += (count < 32) as u16; -} - -#[cfg(test)] -mod tests { - use super::*; - - /// The spec's symbol decoder (8.2.2 - 8.2.4), for checking the encoder. - struct Dec<'a> { - data: &'a [u8], - pos: usize, - value: u32, - range: u32, - max_bits: i64, - } - - impl<'a> Dec<'a> { - fn new(data: &'a [u8]) -> Self { - let mut d = Dec { data, pos: 0, value: 0, range: 1 << 15, max_bits: 8 * data.len() as i64 - 15 }; - let nb = (data.len() * 8).min(15) as u32; - let buf = d.bits(nb); - d.value = ((1 << 15) - 1) ^ (buf << (15 - nb)); - d - } - fn bits(&mut self, n: u32) -> u32 { - let mut x = 0; - for _ in 0..n { - let b = if self.pos < self.data.len() * 8 { (self.data[self.pos >> 3] >> (7 - (self.pos & 7))) & 1 } else { 0 }; - self.pos += 1; - x = (x << 1) | b as u32; - } - x - } - fn symbol(&mut self, cdf: &mut [u16], adapt: bool) -> usize { - let n = cdf.len() - 1; - let mut cur = self.range; - let mut s = 0; - let mut prev; - loop { - prev = cur; - let f = (1u32 << 15) - cdf[s] as u32; - cur = (((self.range >> 8) * (f >> 6)) >> 1) + 4 * (n - s - 1) as u32; - if self.value >= cur { - break; - } - s += 1; - } - self.range = prev - cur; - self.value -= cur; - let b = 15 - (31 - self.range.leading_zeros()); - if b > 0 { - self.range <<= b; - let nb = (b as i64).min(self.max_bits.max(0)) as u32; - let nd = self.bits(nb) << (b - nb); - self.value = nd ^ (((self.value + 1) << b) - 1); - self.max_bits -= b as i64; - } - if adapt { - update_cdf(cdf, n, s); - } - s - } - /// Position of the trailing one bit (8.2.4). - fn trailing_position(&self) -> i64 { - let consumed = (self.pos as i64).min(self.data.len() as i64 * 8); - consumed - (15i64).min(self.max_bits + 15) - } - } - - #[test] - fn bools_only() { - for len in 1..200 { - let bits: Vec = (0..len).map(|i| (i * 7 + len) % 3 == 0).collect(); - let mut w = SymbolWriter::new(false); - for &b in &bits { - w.bool(b); - } - let data = w.finish(); - let mut d = Dec::new(&data); - for (i, &b) in bits.iter().enumerate() { - assert_eq!(d.symbol(&mut [1 << 14, 1 << 15, 0], false) == 1, b, "len {len} bit {i} data {data:?}"); - } - } - } - - #[test] - fn round_trip_random_symbols() { - let mut seed = 0x1234_5678u32; - let mut rnd = || { - seed ^= seed << 13; - seed ^= seed >> 17; - seed ^= seed << 5; - seed - }; - for trial in 0..300 { - let count = 1 + (rnd() % 3000) as usize; - let mut cdfs_e = vec![[4096u16, 11264, 19328, 32768, 0]; 4]; - let mut cdfs_d = cdfs_e.clone(); - let mut syms = Vec::new(); - let mut w = SymbolWriter::new(false); - for _ in 0..count { - let kind = rnd() % 3; - let ctx = (rnd() % 4) as usize; - // skewed symbols so that the range also gets small - let s = if rnd() % 4 == 0 { (rnd() % 4) as usize } else { 0 }; - match kind { - 0 => w.symbol(s, &mut cdfs_e[ctx]), - 1 => w.bool(s & 1 == 1), - _ => w.literal(5, s as u32 * 7), - } - syms.push((kind, ctx, s)); - } - let data = w.finish(); - let mut d = Dec::new(&data); - for (idx, &(kind, ctx, s)) in syms.iter().enumerate() { - let got = match kind { - 0 => d.symbol(&mut cdfs_d[ctx], true), - 1 => d.symbol(&mut [1 << 14, 1 << 15, 0], false), - _ => { - let mut v = 0; - for _ in 0..5 { - v = (v << 1) | d.symbol(&mut [1 << 14, 1 << 15, 0], false); - } - v / 7 - } - }; - let want = if kind == 1 { s & 1 } else { s }; - assert_eq!(got, want, "trial {trial} idx {idx} kind {kind} of {}", syms.len()); - } - let tp = d.trailing_position(); - assert!(tp >= 0 && (tp as usize) < data.len() * 8, "trial {trial}: trailing bit position {tp}"); - let tp = tp as usize; - assert_eq!((data[tp >> 3] >> (7 - (tp & 7))) & 1, 1, "trial {trial}: trailing one bit"); - for p in tp + 1..data.len() * 8 { - assert_eq!((data[p >> 3] >> (7 - (p & 7))) & 1, 0, "trial {trial}: padding"); - } - } - } -} diff --git a/crates/av1enc/src/tables.rs b/crates/av1enc/src/tables.rs deleted file mode 100644 index 3e333a519..000000000 --- a/crates/av1enc/src/tables.rs +++ /dev/null @@ -1,4805 +0,0 @@ -// Tables mirrored from filmcraft-av1's spec_tables.rs, which extracts them from the AV1 Bitstream & -// Decoding Process Specification v1.0.0 with Errata 1. Only the tables this encoder uses are kept. -#![allow(dead_code, clippy::unreadable_literal)] - -pub const ADST_ADST: usize = 3; -pub const ADST_DCT: usize = 1; -pub const ADST_FLIPADST: usize = 7; -pub const AFFINE: usize = 3; -pub const ALTREF2_FRAME: usize = 6; -pub const ALTREF_FRAME: usize = 7; -pub const ANGLE_STEP: usize = 3; -pub const BIDIR_COMP_REFERENCE: usize = 1; -pub const BILINEAR: usize = 3; -pub const BLOCK_128X128: usize = 15; -pub const BLOCK_128X64: usize = 14; -pub const BLOCK_16X16: usize = 6; -pub const BLOCK_16X32: usize = 7; -pub const BLOCK_16X4: usize = 17; -pub const BLOCK_16X64: usize = 20; -pub const BLOCK_16X8: usize = 5; -pub const BLOCK_32X16: usize = 8; -pub const BLOCK_32X32: usize = 9; -pub const BLOCK_32X64: usize = 10; -pub const BLOCK_32X8: usize = 19; -pub const BLOCK_4X16: usize = 16; -pub const BLOCK_4X4: usize = 0; -pub const BLOCK_4X8: usize = 1; -pub const BLOCK_64X128: usize = 13; -pub const BLOCK_64X16: usize = 21; -pub const BLOCK_64X32: usize = 11; -pub const BLOCK_64X64: usize = 12; -pub const BLOCK_8X16: usize = 4; -pub const BLOCK_8X32: usize = 18; -pub const BLOCK_8X4: usize = 2; -pub const BLOCK_8X8: usize = 3; -pub const BLOCK_INVALID: usize = 22; -pub const BLOCK_SIZES: usize = 22; -pub const BLOCK_SIZE_GROUPS: usize = 4; -pub const BR_CDF_SIZE: usize = 4; -pub const BUFFER_POOL_MAX_SIZE: usize = 10; -pub const BWDREF_FRAME: usize = 5; -pub const BWD_REFS: usize = 3; -pub const CFL_ALPHABET_SIZE: usize = 16; -pub const CFL_ALPHA_CONTEXTS: usize = 6; -pub const CFL_JOINT_SIGNS: usize = 8; -pub const CFL_SIGN_NEG: usize = 1; -pub const CFL_SIGN_POS: usize = 2; -pub const CFL_SIGN_ZERO: usize = 0; -pub const CLASS0_SIZE: usize = 2; -pub const COEFF_BASE_RANGE: usize = 12; -pub const COEFF_CDF_Q_CTXS: usize = 4; -pub const COMPOUND_AVERAGE: usize = 2; -pub const COMPOUND_DIFFWTD: usize = 1; -pub const COMPOUND_DISTANCE: usize = 4; -pub const COMPOUND_IDX_CONTEXTS: usize = 6; -pub const COMPOUND_INTRA: usize = 3; -pub const COMPOUND_MODES: usize = 8; -pub const COMPOUND_MODE_CONTEXTS: usize = 8; -pub const COMPOUND_REFERENCE: usize = 1; -pub const COMPOUND_TYPES: usize = 2; -pub const COMPOUND_WEDGE: usize = 0; -pub const COMP_GROUP_IDX_CONTEXTS: usize = 6; -pub const COMP_INTER_CONTEXTS: usize = 5; -pub const COMP_NEWMV_CTXS: usize = 5; -pub const COMP_REF_TYPE_CONTEXTS: usize = 5; -pub const CP_BT_2020: usize = 9; -pub const CP_BT_470_B_G: usize = 5; -pub const CP_BT_470_M: usize = 4; -pub const CP_BT_601: usize = 6; -pub const CP_BT_709: usize = 1; -pub const CP_EBU_3213: usize = 22; -pub const CP_GENERIC_FILM: usize = 8; -pub const CP_SMPTE_240: usize = 7; -pub const CP_SMPTE_431: usize = 11; -pub const CP_SMPTE_432: usize = 12; -pub const CP_UNSPECIFIED: usize = 2; -pub const CP_XYZ: usize = 10; -pub const CSP_COLOCATED: usize = 2; -pub const CSP_RESERVED: usize = 3; -pub const CSP_UNKNOWN: usize = 0; -pub const CSP_VERTICAL: usize = 1; -pub const D113_PRED: usize = 5; -pub const D135_PRED: usize = 4; -pub const D157_PRED: usize = 6; -pub const D203_PRED: usize = 7; -pub const D45_PRED: usize = 3; -pub const D67_PRED: usize = 8; -pub const DCT_ADST: usize = 2; -pub const DCT_DCT: usize = 0; -pub const DCT_FLIPADST: usize = 5; -pub const DC_PRED: usize = 0; -pub const DC_SIGN_CONTEXTS: usize = 3; -pub const DELTA_LF_SMALL: usize = 3; -pub const DELTA_Q_SMALL: usize = 3; -pub const DIRECTIONAL_MODES: usize = 8; -pub const DIV_LUT_BITS: usize = 8; -pub const DIV_LUT_NUM: usize = 257; -pub const DIV_LUT_PREC_BITS: usize = 14; -pub const DRL_MODE_CONTEXTS: usize = 3; -pub const EC_MIN_PROB: usize = 4; -pub const EC_PROB_SHIFT: usize = 6; -pub const EIGHTTAP: usize = 0; -pub const EIGHTTAP_SHARP: usize = 2; -pub const EIGHTTAP_SMOOTH: usize = 1; -pub const EOB_COEF_CONTEXTS: usize = 9; -pub const FILTER_BITS: usize = 7; -pub const FILTER_D157_PRED: usize = 3; -pub const FILTER_DC_PRED: usize = 0; -pub const FILTER_H_PRED: usize = 2; -pub const FILTER_PAETH_PRED: usize = 4; -pub const FILTER_V_PRED: usize = 1; -pub const FLIPADST_ADST: usize = 8; -pub const FLIPADST_DCT: usize = 4; -pub const FLIPADST_FLIPADST: usize = 6; -pub const FRAME_LF_COUNT: usize = 4; -pub const FWD_REFS: usize = 4; -pub const GLOBALMV: usize = 16; -pub const GLOBAL_GLOBALMV: usize = 24; -pub const GM_ABS_ALPHA_BITS: usize = 12; -pub const GM_ABS_TRANS_BITS: usize = 12; -pub const GM_ABS_TRANS_ONLY_BITS: usize = 9; -pub const GM_ALPHA_PREC_BITS: usize = 15; -pub const GM_TRANS_ONLY_PREC_BITS: usize = 3; -pub const GM_TRANS_PREC_BITS: usize = 6; -pub const GOLDEN_FRAME: usize = 4; -pub const H_ADST: usize = 13; -pub const H_DCT: usize = 11; -pub const H_FLIPADST: usize = 15; -pub const H_PRED: usize = 2; -pub const IDENTITY: usize = 0; -pub const IDTX: usize = 9; -pub const II_DC_PRED: usize = 0; -pub const II_H_PRED: usize = 2; -pub const II_SMOOTH_PRED: usize = 3; -pub const II_V_PRED: usize = 1; -pub const INTERINTRA_MODES: usize = 4; -pub const INTERP_FILTERS: usize = 3; -pub const INTERP_FILTER_CONTEXTS: usize = 16; -pub const INTER_FRAME: usize = 1; -pub const INTRABC_DELAY_PIXELS: usize = 256; -pub const INTRABC_DELAY_SB64: usize = 4; -pub const INTRA_EDGE_KERNELS: usize = 3; -pub const INTRA_EDGE_TAPS: usize = 5; -pub const INTRA_FILTER_MODES: usize = 5; -pub const INTRA_FILTER_SCALE_BITS: usize = 4; -pub const INTRA_FRAME: usize = 0; -pub const INTRA_MODES: usize = 13; -pub const INTRA_MODE_CONTEXTS: usize = 5; -pub const INTRA_ONLY_FRAME: usize = 2; -pub const IS_INTER_CONTEXTS: usize = 4; -pub const KEY_FRAME: usize = 0; -pub const LAST2_FRAME: usize = 2; -pub const LAST3_FRAME: usize = 3; -pub const LAST_FRAME: usize = 1; -pub const LEAST_SQUARES_SAMPLES_MAX: usize = 8; -pub const LEVEL_CONTEXTS: usize = 21; -pub const LOCALWARP: usize = 2; -pub const LS_MV_MAX: usize = 256; -pub const MASK_MASTER_SIZE: usize = 64; -pub const MAX_ANGLE_DELTA: usize = 3; -pub const MAX_FRAME_DISTANCE: usize = 31; -pub const MAX_LOOP_FILTER: usize = 63; -pub const MAX_OFFSET_HEIGHT: usize = 0; -pub const MAX_OFFSET_WIDTH: usize = 8; -pub const MAX_REF_MV_STACK_SIZE: usize = 8; -pub const MAX_SB_SIZE: usize = 128; -pub const MAX_SEGMENTS: usize = 8; -pub const MAX_TILE_AREA: usize = 9437184; -pub const MAX_TILE_COLS: usize = 64; -pub const MAX_TILE_ROWS: usize = 64; -pub const MAX_TILE_WIDTH: usize = 4096; -pub const MAX_TX_DEPTH: usize = 2; -pub const MAX_VARTX_DEPTH: usize = 2; -pub const MB_MODE_COUNT: usize = 17; -pub const MC_BT_2020_CL: usize = 10; -pub const MC_BT_2020_NCL: usize = 9; -pub const MC_BT_470_B_G: usize = 5; -pub const MC_BT_601: usize = 6; -pub const MC_BT_709: usize = 1; -pub const MC_CHROMAT_CL: usize = 13; -pub const MC_CHROMAT_NCL: usize = 12; -pub const MC_FCC: usize = 4; -pub const MC_ICTCP: usize = 14; -pub const MC_IDENTITY: usize = 0; -pub const MC_RESERVED_3: usize = 3; -pub const MC_SMPTE_2085: usize = 11; -pub const MC_SMPTE_240: usize = 7; -pub const MC_SMPTE_YCGCO: usize = 8; -pub const MC_UNSPECIFIED: usize = 2; -pub const METADATA_TYPE_HDR_CLL: usize = 1; -pub const METADATA_TYPE_HDR_MDCV: usize = 2; -pub const METADATA_TYPE_ITUT_T35: usize = 4; -pub const METADATA_TYPE_SCALABILITY: usize = 3; -pub const METADATA_TYPE_TIMECODE: usize = 5; -pub const MFMV_STACK_SIZE: usize = 3; -pub const MI_SIZE: usize = 4; -pub const MI_SIZE_LOG2: usize = 2; -pub const MOTION_MODES: usize = 3; -pub const MV_BORDER: usize = 128; -pub const MV_CLASSES: usize = 11; -pub const MV_CLASS_0: usize = 0; -pub const MV_CLASS_1: usize = 1; -pub const MV_CLASS_10: usize = 10; -pub const MV_CLASS_2: usize = 2; -pub const MV_CLASS_3: usize = 3; -pub const MV_CLASS_4: usize = 4; -pub const MV_CLASS_5: usize = 5; -pub const MV_CLASS_6: usize = 6; -pub const MV_CLASS_7: usize = 7; -pub const MV_CLASS_8: usize = 8; -pub const MV_CLASS_9: usize = 9; -pub const MV_CONTEXTS: usize = 2; -pub const MV_INTRABC_CONTEXT: usize = 1; -pub const MV_JOINTS: usize = 4; -pub const MV_JOINT_HNZVNZ: usize = 3; -pub const MV_JOINT_HNZVZ: usize = 1; -pub const MV_JOINT_HZVNZ: usize = 2; -pub const MV_JOINT_ZERO: usize = 0; -pub const MV_OFFSET_BITS: usize = 10; -pub const NEARESTMV: usize = 14; -pub const NEAREST_NEARESTMV: usize = 18; -pub const NEAREST_NEWMV: usize = 20; -pub const NEARMV: usize = 15; -pub const NEAR_NEARMV: usize = 19; -pub const NEAR_NEWMV: usize = 22; -pub const NEWMV: usize = 17; -pub const NEW_MV_CONTEXTS: usize = 6; -pub const NEW_NEARESTMV: usize = 21; -pub const NEW_NEARMV: usize = 23; -pub const NEW_NEWMV: usize = 25; -pub const NUM_BASE_LEVELS: usize = 2; -pub const NUM_REF_FRAMES: usize = 8; -pub const OBMC: usize = 1; -pub const OBU_FRAME: usize = 6; -pub const OBU_FRAME_HEADER: usize = 3; -pub const OBU_METADATA: usize = 5; -pub const OBU_PADDING: usize = 15; -pub const OBU_REDUNDANT_FRAME_HEADER: usize = 7; -pub const OBU_SEQUENCE_HEADER: usize = 1; -pub const OBU_TEMPORAL_DELIMITER: usize = 2; -pub const OBU_TILE_GROUP: usize = 4; -pub const OBU_TILE_LIST: usize = 8; -pub const ONLY_4X4: usize = 0; -pub const PAETH_PRED: usize = 12; -pub const PALETTE_BLOCK_SIZE_CONTEXTS: usize = 7; -pub const PALETTE_COLORS: usize = 8; -pub const PALETTE_COLOR_CONTEXTS: usize = 5; -pub const PALETTE_MAX_COLOR_CONTEXT_HASH: usize = 8; -pub const PALETTE_NUM_NEIGHBORS: usize = 3; -pub const PALETTE_SIZES: usize = 7; -pub const PALETTE_UV_MODE_CONTEXTS: usize = 2; -pub const PALETTE_Y_MODE_CONTEXTS: usize = 3; -pub const PARTITION_CONTEXTS: usize = 4; -pub const PARTITION_HORZ: usize = 1; -pub const PARTITION_HORZ_4: usize = 8; -pub const PARTITION_HORZ_A: usize = 4; -pub const PARTITION_HORZ_B: usize = 5; -pub const PARTITION_NONE: usize = 0; -pub const PARTITION_SPLIT: usize = 3; -pub const PARTITION_VERT: usize = 2; -pub const PARTITION_VERT_4: usize = 9; -pub const PARTITION_VERT_A: usize = 6; -pub const PARTITION_VERT_B: usize = 7; -pub const PLANE_TYPES: usize = 2; -pub const PRIMARY_REF_NONE: usize = 7; -pub const QM_TOTAL_SIZE: usize = 3344; -pub const REFMVS_LIMIT: usize = 4095; -pub const REFS_PER_FRAME: usize = 7; -pub const REF_CAT_LEVEL: usize = 640; -pub const REF_CONTEXTS: usize = 3; -pub const REF_MV_CONTEXTS: usize = 6; -pub const REF_SCALE_SHIFT: usize = 14; -pub const RESTORATION_TILESIZE_MAX: usize = 256; -pub const RESTORE_NONE: usize = 0; -pub const RESTORE_SGRPROJ: usize = 2; -pub const RESTORE_SWITCHABLE: usize = 3; -pub const RESTORE_WIENER: usize = 1; -pub const ROTZOOM: usize = 2; -pub const SCALABILITY_L1T2: usize = 0; -pub const SCALABILITY_L1T3: usize = 1; -pub const SCALABILITY_L2T1: usize = 2; -pub const SCALABILITY_L2T2: usize = 3; -pub const SCALABILITY_L2T3: usize = 4; -pub const SCALABILITY_L3T1: usize = 15; -pub const SCALABILITY_L3T2: usize = 16; -pub const SCALABILITY_L3T2_KEY: usize = 21; -pub const SCALABILITY_L3T2_KEY_SHIFT: usize = 25; -pub const SCALABILITY_L3T3: usize = 17; -pub const SCALABILITY_L3T3_KEY: usize = 22; -pub const SCALABILITY_L3T3_KEY_SHIFT: usize = 26; -pub const SCALABILITY_L4T5_KEY: usize = 23; -pub const SCALABILITY_L4T5_KEY_SHIFT: usize = 27; -pub const SCALABILITY_L4T7_KEY: usize = 24; -pub const SCALABILITY_L4T7_KEY_SHIFT: usize = 28; -pub const SCALABILITY_S2T1: usize = 5; -pub const SCALABILITY_S2T2: usize = 6; -pub const SCALABILITY_S2T3: usize = 7; -pub const SCALABILITY_S3T1: usize = 18; -pub const SCALABILITY_S3T2: usize = 19; -pub const SCALABILITY_S3T3: usize = 20; -pub const SCALABILITY_SS: usize = 14; -pub const SCALE_SUBPEL_BITS: usize = 10; -pub const SEGMENT_ID_CONTEXTS: usize = 3; -pub const SEGMENT_ID_PREDICTED_CONTEXTS: usize = 3; -pub const SEG_LVL_ALT_LF_Y_V: usize = 1; -pub const SEG_LVL_ALT_Q: usize = 0; -pub const SEG_LVL_GLOBALMV: usize = 7; -pub const SEG_LVL_MAX: usize = 8; -pub const SEG_LVL_REF_FRAME: usize = 5; -pub const SEG_LVL_SKIP: usize = 6; -pub const SELECT_INTEGER_MV: usize = 2; -pub const SELECT_SCREEN_CONTENT_TOOLS: usize = 2; -pub const SGRPROJ_MTABLE_BITS: usize = 20; -pub const SGRPROJ_PARAMS_BITS: usize = 4; -pub const SGRPROJ_PRJ_BITS: usize = 7; -pub const SGRPROJ_PRJ_SUBEXP_K: usize = 4; -pub const SGRPROJ_RECIP_BITS: usize = 12; -pub const SGRPROJ_RST_BITS: usize = 4; -pub const SGRPROJ_SGR_BITS: usize = 8; -pub const SIG_COEF_CONTEXTS: usize = 42; -pub const SIG_COEF_CONTEXTS_2D: usize = 26; -pub const SIG_COEF_CONTEXTS_EOB: usize = 4; -pub const SIG_REF_DIFF_OFFSET_NUM: usize = 5; -pub const SIMPLE: usize = 0; -pub const SINGLE_REFERENCE: usize = 0; -pub const SINGLE_REFS: usize = 7; -pub const SINPI_1_9: usize = 1321; -pub const SINPI_2_9: usize = 2482; -pub const SINPI_3_9: usize = 3344; -pub const SINPI_4_9: usize = 3803; -pub const SKIP_CONTEXTS: usize = 3; -pub const SKIP_MODE_CONTEXTS: usize = 3; -pub const SMOOTH_H_PRED: usize = 11; -pub const SMOOTH_PRED: usize = 9; -pub const SMOOTH_V_PRED: usize = 10; -pub const SUBPEL_BITS: usize = 4; -pub const SUBPEL_MASK: usize = 15; -pub const SUPERRES_DENOM_BITS: usize = 3; -pub const SUPERRES_DENOM_MIN: usize = 9; -pub const SUPERRES_EXTRA_BITS: usize = 8; -pub const SUPERRES_FILTER_BITS: usize = 6; -pub const SUPERRES_FILTER_OFFSET: usize = 3; -pub const SUPERRES_FILTER_SHIFTS: usize = 64; -pub const SUPERRES_FILTER_TAPS: usize = 8; -pub const SUPERRES_NUM: usize = 8; -pub const SUPERRES_SCALE_BITS: usize = 14; -pub const SUPERRES_SCALE_MASK: usize = 16383; -pub const SWITCHABLE: usize = 4; -pub const SWITCH_FRAME: usize = 3; -pub const TC_BT_1361: usize = 12; -pub const TC_BT_2020_10_BIT: usize = 14; -pub const TC_BT_2020_12_BIT: usize = 15; -pub const TC_BT_470_B_G: usize = 5; -pub const TC_BT_470_M: usize = 4; -pub const TC_BT_601: usize = 6; -pub const TC_BT_709: usize = 1; -pub const TC_HLG: usize = 18; -pub const TC_IEC_61966: usize = 11; -pub const TC_LINEAR: usize = 8; -pub const TC_LOG_100: usize = 9; -pub const TC_LOG_100_SQRT10: usize = 10; -pub const TC_RESERVED_0: usize = 0; -pub const TC_RESERVED_3: usize = 3; -pub const TC_SMPTE_2084: usize = 16; -pub const TC_SMPTE_240: usize = 7; -pub const TC_SMPTE_428: usize = 17; -pub const TC_SRGB: usize = 13; -pub const TC_UNSPECIFIED: usize = 2; -pub const TOTAL_REFS_PER_FRAME: usize = 8; -pub const TRANSLATION: usize = 1; -pub const TXB_SKIP_CONTEXTS: usize = 13; -pub const TXFM_PARTITION_CONTEXTS: usize = 21; -pub const TX_16X16: usize = 2; -pub const TX_16X32: usize = 9; -pub const TX_16X4: usize = 14; -pub const TX_16X64: usize = 17; -pub const TX_16X8: usize = 8; -pub const TX_32X16: usize = 10; -pub const TX_32X32: usize = 3; -pub const TX_32X64: usize = 11; -pub const TX_32X8: usize = 16; -pub const TX_4X16: usize = 13; -pub const TX_4X4: usize = 0; -pub const TX_4X8: usize = 5; -pub const TX_64X16: usize = 18; -pub const TX_64X32: usize = 12; -pub const TX_64X64: usize = 4; -pub const TX_8X16: usize = 7; -pub const TX_8X32: usize = 15; -pub const TX_8X4: usize = 6; -pub const TX_8X8: usize = 1; -pub const TX_CLASS_2D: usize = 0; -pub const TX_CLASS_HORIZ: usize = 1; -pub const TX_CLASS_VERT: usize = 2; -pub const TX_MODES: usize = 3; -pub const TX_MODE_LARGEST: usize = 1; -pub const TX_MODE_SELECT: usize = 2; -pub const TX_SET_DCTONLY: usize = 0; -pub const TX_SET_INTER_1: usize = 1; -pub const TX_SET_INTER_2: usize = 2; -pub const TX_SET_INTER_3: usize = 3; -pub const TX_SET_INTRA_1: usize = 1; -pub const TX_SET_INTRA_2: usize = 2; -pub const TX_SET_TYPES_INTER: usize = 4; -pub const TX_SET_TYPES_INTRA: usize = 3; -pub const TX_SIZES: usize = 5; -pub const TX_SIZES_ALL: usize = 19; -pub const TX_SIZE_CONTEXTS: usize = 3; -pub const TX_TYPES: usize = 16; -pub const UNIDIR_COMP_REFERENCE: usize = 0; -pub const UNIDIR_COMP_REFS: usize = 4; -pub const UNIFORM_45: usize = 0; -pub const UNIFORM_45_INV: usize = 1; -pub const UV_CFL_PRED: usize = 13; -pub const UV_INTRA_MODES_CFL_ALLOWED: usize = 14; -pub const UV_INTRA_MODES_CFL_NOT_ALLOWED: usize = 13; -pub const V_ADST: usize = 12; -pub const V_DCT: usize = 10; -pub const V_FLIPADST: usize = 14; -pub const V_PRED: usize = 1; -pub const WARPEDDIFF_PREC_BITS: usize = 10; -pub const WARPEDMODEL_NONDIAGAFFINE_CLAMP: usize = 8192; -pub const WARPEDMODEL_PREC_BITS: usize = 16; -pub const WARPEDMODEL_TRANS_CLAMP: usize = 8388608; -pub const WARPEDPIXEL_PREC_SHIFTS: usize = 64; -pub const WARP_PARAM_REDUCE_BITS: usize = 6; -pub const WEDGE_HORIZONTAL: usize = 0; -pub const WEDGE_OBLIQUE117: usize = 4; -pub const WEDGE_OBLIQUE153: usize = 5; -pub const WEDGE_OBLIQUE27: usize = 2; -pub const WEDGE_OBLIQUE63: usize = 3; -pub const WEDGE_TYPES: usize = 16; -pub const WEDGE_VERTICAL: usize = 1; -pub const WIENER_COEFFS: usize = 3; -pub const ZERO_MV_CONTEXTS: usize = 2; - -pub static AC_QLOOKUP: [[u16; 256]; 3] = [ - [ - 4, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, - 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, - 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, - 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 155, 158, 161, 164, 167, 170, 173, 176, 179, 182, 185, 188, 191, 194, 197, 200, 203, 207, - 211, 215, 219, 223, 227, 231, 235, 239, 243, 247, 251, 255, 260, 265, 270, 275, 280, 285, 290, 295, 300, 305, 311, 317, 323, 329, 335, 341, 347, 353, - 359, 366, 373, 380, 387, 394, 401, 408, 416, 424, 432, 440, 448, 456, 465, 474, 483, 492, 501, 510, 520, 530, 540, 550, 560, 571, 582, 593, 604, 615, - 627, 639, 651, 663, 676, 689, 702, 715, 729, 743, 757, 771, 786, 801, 816, 832, 848, 864, 881, 898, 915, 933, 951, 969, 988, 1007, 1026, 1046, 1066, - 1087, 1108, 1129, 1151, 1173, 1196, 1219, 1243, 1267, 1292, 1317, 1343, 1369, 1396, 1423, 1451, 1479, 1508, 1537, 1567, 1597, 1628, 1660, 1692, 1725, - 1759, 1793, 1828, - ], - [ - 4, 9, 11, 13, 16, 18, 21, 24, 27, 30, 33, 37, 40, 44, 48, 51, 55, 59, 63, 67, 71, 75, 79, 83, 88, 92, 96, 100, 105, 109, 114, 118, 122, 127, 131, 136, - 140, 145, 149, 154, 158, 163, 168, 172, 177, 181, 186, 190, 195, 199, 204, 208, 213, 217, 222, 226, 231, 235, 240, 244, 249, 253, 258, 262, 267, 271, - 275, 280, 284, 289, 293, 297, 302, 306, 311, 315, 319, 324, 328, 332, 337, 341, 345, 349, 354, 358, 362, 367, 371, 375, 379, 384, 388, 392, 396, 401, - 409, 417, 425, 433, 441, 449, 458, 466, 474, 482, 490, 498, 506, 514, 523, 531, 539, 547, 555, 563, 571, 579, 588, 596, 604, 616, 628, 640, 652, 664, - 676, 688, 700, 713, 725, 737, 749, 761, 773, 785, 797, 809, 825, 841, 857, 873, 889, 905, 922, 938, 954, 970, 986, 1002, 1018, 1038, 1058, 1078, 1098, - 1118, 1138, 1158, 1178, 1198, 1218, 1242, 1266, 1290, 1314, 1338, 1362, 1386, 1411, 1435, 1463, 1491, 1519, 1547, 1575, 1603, 1631, 1663, 1695, 1727, - 1759, 1791, 1823, 1859, 1895, 1931, 1967, 2003, 2039, 2079, 2119, 2159, 2199, 2239, 2283, 2327, 2371, 2415, 2459, 2507, 2555, 2603, 2651, 2703, 2755, - 2807, 2859, 2915, 2971, 3027, 3083, 3143, 3203, 3263, 3327, 3391, 3455, 3523, 3591, 3659, 3731, 3803, 3876, 3952, 4028, 4104, 4184, 4264, 4348, 4432, - 4516, 4604, 4692, 4784, 4876, 4972, 5068, 5168, 5268, 5372, 5476, 5584, 5692, 5804, 5916, 6032, 6148, 6268, 6388, 6512, 6640, 6768, 6900, 7036, 7172, - 7312, - ], - [ - 4, 13, 19, 27, 35, 44, 54, 64, 75, 87, 99, 112, 126, 139, 154, 168, 183, 199, 214, 230, 247, 263, 280, 297, 314, 331, 349, 366, 384, 402, 420, 438, - 456, 475, 493, 511, 530, 548, 567, 586, 604, 623, 642, 660, 679, 698, 716, 735, 753, 772, 791, 809, 828, 846, 865, 884, 902, 920, 939, 957, 976, 994, - 1012, 1030, 1049, 1067, 1085, 1103, 1121, 1139, 1157, 1175, 1193, 1211, 1229, 1246, 1264, 1282, 1299, 1317, 1335, 1352, 1370, 1387, 1405, 1422, 1440, - 1457, 1474, 1491, 1509, 1526, 1543, 1560, 1577, 1595, 1627, 1660, 1693, 1725, 1758, 1791, 1824, 1856, 1889, 1922, 1954, 1987, 2020, 2052, 2085, 2118, - 2150, 2183, 2216, 2248, 2281, 2313, 2346, 2378, 2411, 2459, 2508, 2556, 2605, 2653, 2701, 2750, 2798, 2847, 2895, 2943, 2992, 3040, 3088, 3137, 3185, - 3234, 3298, 3362, 3426, 3491, 3555, 3619, 3684, 3748, 3812, 3876, 3941, 4005, 4069, 4149, 4230, 4310, 4390, 4470, 4550, 4631, 4711, 4791, 4871, 4967, - 5064, 5160, 5256, 5352, 5448, 5544, 5641, 5737, 5849, 5961, 6073, 6185, 6297, 6410, 6522, 6650, 6778, 6906, 7034, 7162, 7290, 7435, 7579, 7723, 7867, - 8011, 8155, 8315, 8475, 8635, 8795, 8956, 9132, 9308, 9484, 9660, 9836, 10028, 10220, 10412, 10604, 10812, 11020, 11228, 11437, 11661, 11885, 12109, - 12333, 12573, 12813, 13053, 13309, 13565, 13821, 14093, 14365, 14637, 14925, 15213, 15502, 15806, 16110, 16414, 16734, 17054, 17390, 17726, 18062, - 18414, 18766, 19134, 19502, 19886, 20270, 20670, 21070, 21486, 21902, 22334, 22766, 23214, 23662, 24126, 24590, 25070, 25551, 26047, 26559, 27071, - 27599, 28143, 28687, 29247, - ], -]; - -pub static ADJUSTED_TX_SIZE: [u8; 19] = [0, 1, 2, 3, 3, 5, 6, 7, 8, 9, 10, 3, 3, 13, 14, 15, 16, 9, 10]; - -pub static COEFF_BASE_CTX_OFFSET: [[[u8; 5]; 5]; 19] = [ - [[0, 1, 6, 6, 0], [1, 6, 6, 21, 0], [6, 6, 21, 21, 0], [6, 21, 21, 21, 0], [0, 0, 0, 0, 0]], - [[0, 1, 6, 6, 21], [1, 6, 6, 21, 21], [6, 6, 21, 21, 21], [6, 21, 21, 21, 21], [21, 21, 21, 21, 21]], - [[0, 1, 6, 6, 21], [1, 6, 6, 21, 21], [6, 6, 21, 21, 21], [6, 21, 21, 21, 21], [21, 21, 21, 21, 21]], - [[0, 1, 6, 6, 21], [1, 6, 6, 21, 21], [6, 6, 21, 21, 21], [6, 21, 21, 21, 21], [21, 21, 21, 21, 21]], - [[0, 1, 6, 6, 21], [1, 6, 6, 21, 21], [6, 6, 21, 21, 21], [6, 21, 21, 21, 21], [21, 21, 21, 21, 21]], - [[0, 11, 11, 11, 0], [11, 11, 11, 11, 0], [6, 6, 21, 21, 0], [6, 21, 21, 21, 0], [21, 21, 21, 21, 0]], - [[0, 16, 6, 6, 21], [16, 16, 6, 21, 21], [16, 16, 21, 21, 21], [16, 16, 21, 21, 21], [0, 0, 0, 0, 0]], - [[0, 11, 11, 11, 11], [11, 11, 11, 11, 11], [6, 6, 21, 21, 21], [6, 21, 21, 21, 21], [21, 21, 21, 21, 21]], - [[0, 16, 6, 6, 21], [16, 16, 6, 21, 21], [16, 16, 21, 21, 21], [16, 16, 21, 21, 21], [16, 16, 21, 21, 21]], - [[0, 11, 11, 11, 11], [11, 11, 11, 11, 11], [6, 6, 21, 21, 21], [6, 21, 21, 21, 21], [21, 21, 21, 21, 21]], - [[0, 16, 6, 6, 21], [16, 16, 6, 21, 21], [16, 16, 21, 21, 21], [16, 16, 21, 21, 21], [16, 16, 21, 21, 21]], - [[0, 11, 11, 11, 11], [11, 11, 11, 11, 11], [6, 6, 21, 21, 21], [6, 21, 21, 21, 21], [21, 21, 21, 21, 21]], - [[0, 16, 6, 6, 21], [16, 16, 6, 21, 21], [16, 16, 21, 21, 21], [16, 16, 21, 21, 21], [16, 16, 21, 21, 21]], - [[0, 11, 11, 11, 0], [11, 11, 11, 11, 0], [6, 6, 21, 21, 0], [6, 21, 21, 21, 0], [21, 21, 21, 21, 0]], - [[0, 16, 6, 6, 21], [16, 16, 6, 21, 21], [16, 16, 21, 21, 21], [16, 16, 21, 21, 21], [0, 0, 0, 0, 0]], - [[0, 11, 11, 11, 11], [11, 11, 11, 11, 11], [6, 6, 21, 21, 21], [6, 21, 21, 21, 21], [21, 21, 21, 21, 21]], - [[0, 16, 6, 6, 21], [16, 16, 6, 21, 21], [16, 16, 21, 21, 21], [16, 16, 21, 21, 21], [16, 16, 21, 21, 21]], - [[0, 11, 11, 11, 11], [11, 11, 11, 11, 11], [6, 6, 21, 21, 21], [6, 21, 21, 21, 21], [21, 21, 21, 21, 21]], - [[0, 16, 6, 6, 21], [16, 16, 6, 21, 21], [16, 16, 21, 21, 21], [16, 16, 21, 21, 21], [16, 16, 21, 21, 21]], -]; - -pub static COEFF_BASE_POS_CTX_OFFSET: [u8; 3] = [26, 31, 36]; - -pub static COS128_LOOKUP: [u16; 65] = [ - 4096, 4095, 4091, 4085, 4076, 4065, 4052, 4036, 4017, 3996, 3973, 3948, 3920, 3889, 3857, 3822, 3784, 3745, 3703, 3659, 3612, 3564, 3513, 3461, 3406, 3349, - 3290, 3229, 3166, 3102, 3035, 2967, 2896, 2824, 2751, 2675, 2598, 2520, 2440, 2359, 2276, 2191, 2106, 2019, 1931, 1842, 1751, 1660, 1567, 1474, 1380, 1285, - 1189, 1092, 995, 897, 799, 700, 601, 501, 401, 301, 201, 101, 0, -]; - -pub static DC_QLOOKUP: [[u16; 256]; 3] = [ - [ - 4, 8, 8, 9, 10, 11, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 23, 24, 25, 26, 26, 27, 28, 29, 30, 31, 32, 32, 33, 34, 35, 36, 37, 38, 38, 39, - 40, 41, 42, 43, 43, 44, 45, 46, 47, 48, 48, 49, 50, 51, 52, 53, 53, 54, 55, 56, 57, 57, 58, 59, 60, 61, 62, 62, 63, 64, 65, 66, 66, 67, 68, 69, 70, 70, - 71, 72, 73, 74, 74, 75, 76, 77, 78, 78, 79, 80, 81, 81, 82, 83, 84, 85, 85, 87, 88, 90, 92, 93, 95, 96, 98, 99, 101, 102, 104, 105, 107, 108, 110, 111, - 113, 114, 116, 117, 118, 120, 121, 123, 125, 127, 129, 131, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 161, 164, 166, 169, 172, - 174, 177, 180, 182, 185, 187, 190, 192, 195, 199, 202, 205, 208, 211, 214, 217, 220, 223, 226, 230, 233, 237, 240, 243, 247, 250, 253, 257, 261, 265, - 269, 272, 276, 280, 284, 288, 292, 296, 300, 304, 309, 313, 317, 322, 326, 330, 335, 340, 344, 349, 354, 359, 364, 369, 374, 379, 384, 389, 395, 400, - 406, 411, 417, 423, 429, 435, 441, 447, 454, 461, 467, 475, 482, 489, 497, 505, 513, 522, 530, 539, 549, 559, 569, 579, 590, 602, 614, 626, 640, 654, - 668, 684, 700, 717, 736, 755, 775, 796, 819, 843, 869, 896, 925, 955, 988, 1022, 1058, 1098, 1139, 1184, 1232, 1282, 1336, - ], - [ - 4, 9, 10, 13, 15, 17, 20, 22, 25, 28, 31, 34, 37, 40, 43, 47, 50, 53, 57, 60, 64, 68, 71, 75, 78, 82, 86, 90, 93, 97, 101, 105, 109, 113, 116, 120, - 124, 128, 132, 136, 140, 143, 147, 151, 155, 159, 163, 166, 170, 174, 178, 182, 185, 189, 193, 197, 200, 204, 208, 212, 215, 219, 223, 226, 230, 233, - 237, 241, 244, 248, 251, 255, 259, 262, 266, 269, 273, 276, 280, 283, 287, 290, 293, 297, 300, 304, 307, 310, 314, 317, 321, 324, 327, 331, 334, 337, - 343, 350, 356, 362, 369, 375, 381, 387, 394, 400, 406, 412, 418, 424, 430, 436, 442, 448, 454, 460, 466, 472, 478, 484, 490, 499, 507, 516, 525, 533, - 542, 550, 559, 567, 576, 584, 592, 601, 609, 617, 625, 634, 644, 655, 666, 676, 687, 698, 708, 718, 729, 739, 749, 759, 770, 782, 795, 807, 819, 831, - 844, 856, 868, 880, 891, 906, 920, 933, 947, 961, 975, 988, 1001, 1015, 1030, 1045, 1061, 1076, 1090, 1105, 1120, 1137, 1153, 1170, 1186, 1202, 1218, - 1236, 1253, 1271, 1288, 1306, 1323, 1342, 1361, 1379, 1398, 1416, 1436, 1456, 1476, 1496, 1516, 1537, 1559, 1580, 1601, 1624, 1647, 1670, 1692, 1717, - 1741, 1766, 1791, 1817, 1844, 1871, 1900, 1929, 1958, 1990, 2021, 2054, 2088, 2123, 2159, 2197, 2236, 2276, 2319, 2363, 2410, 2458, 2508, 2561, 2616, - 2675, 2737, 2802, 2871, 2944, 3020, 3102, 3188, 3280, 3375, 3478, 3586, 3702, 3823, 3953, 4089, 4236, 4394, 4559, 4737, 4929, 5130, 5347, - ], - [ - 4, 12, 18, 25, 33, 41, 50, 60, 70, 80, 91, 103, 115, 127, 140, 153, 166, 180, 194, 208, 222, 237, 251, 266, 281, 296, 312, 327, 343, 358, 374, 390, - 405, 421, 437, 453, 469, 484, 500, 516, 532, 548, 564, 580, 596, 611, 627, 643, 659, 674, 690, 706, 721, 737, 752, 768, 783, 798, 814, 829, 844, 859, - 874, 889, 904, 919, 934, 949, 964, 978, 993, 1008, 1022, 1037, 1051, 1065, 1080, 1094, 1108, 1122, 1136, 1151, 1165, 1179, 1192, 1206, 1220, 1234, - 1248, 1261, 1275, 1288, 1302, 1315, 1329, 1342, 1368, 1393, 1419, 1444, 1469, 1494, 1519, 1544, 1569, 1594, 1618, 1643, 1668, 1692, 1717, 1741, 1765, - 1789, 1814, 1838, 1862, 1885, 1909, 1933, 1957, 1992, 2027, 2061, 2096, 2130, 2165, 2199, 2233, 2267, 2300, 2334, 2367, 2400, 2434, 2467, 2499, 2532, - 2575, 2618, 2661, 2704, 2746, 2788, 2830, 2872, 2913, 2954, 2995, 3036, 3076, 3127, 3177, 3226, 3275, 3324, 3373, 3421, 3469, 3517, 3565, 3621, 3677, - 3733, 3788, 3843, 3897, 3951, 4005, 4058, 4119, 4181, 4241, 4301, 4361, 4420, 4479, 4546, 4612, 4677, 4742, 4807, 4871, 4942, 5013, 5083, 5153, 5222, - 5291, 5367, 5442, 5517, 5591, 5665, 5745, 5825, 5905, 5984, 6063, 6149, 6234, 6319, 6404, 6495, 6587, 6678, 6769, 6867, 6966, 7064, 7163, 7269, 7376, - 7483, 7599, 7715, 7832, 7958, 8085, 8214, 8352, 8492, 8635, 8788, 8945, 9104, 9275, 9450, 9639, 9832, 10031, 10245, 10465, 10702, 10946, 11210, 11482, - 11776, 12081, 12409, 12750, 13118, 13501, 13913, 14343, 14807, 15290, 15812, 16356, 16943, 17575, 18237, 18949, 19718, 20521, 21387, - ], -]; - -pub static DEFAULT_ANGLE_DELTA_CDF: [[u16; 8]; 8] = [ - [2180, 5032, 7567, 22776, 26989, 30217, 32768, 0], - [2301, 5608, 8801, 23487, 26974, 30330, 32768, 0], - [3780, 11018, 13699, 19354, 23083, 31286, 32768, 0], - [4581, 11226, 15147, 17138, 21834, 28397, 32768, 0], - [1737, 10927, 14509, 19588, 22745, 28823, 32768, 0], - [2664, 10176, 12485, 17650, 21600, 30495, 32768, 0], - [2240, 11096, 15453, 20341, 22561, 28917, 32768, 0], - [3605, 10428, 12459, 17676, 21244, 30655, 32768, 0], -]; - -pub static DEFAULT_COEFF_BASE_CDF: [[[[[u16; 5]; 42]; 2]; 5]; 4] = [ - [ - [ - [ - [4034, 8930, 12727, 32768, 0], - [18082, 29741, 31877, 32768, 0], - [12596, 26124, 30493, 32768, 0], - [9446, 21118, 27005, 32768, 0], - [6308, 15141, 21279, 32768, 0], - [2463, 6357, 9783, 32768, 0], - [20667, 30546, 31929, 32768, 0], - [13043, 26123, 30134, 32768, 0], - [8151, 18757, 24778, 32768, 0], - [5255, 12839, 18632, 32768, 0], - [2820, 7206, 11161, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [15736, 27553, 30604, 32768, 0], - [11210, 23794, 28787, 32768, 0], - [5947, 13874, 19701, 32768, 0], - [4215, 9323, 13891, 32768, 0], - [2833, 6462, 10059, 32768, 0], - [19605, 30393, 31582, 32768, 0], - [13523, 26252, 30248, 32768, 0], - [8446, 18622, 24512, 32768, 0], - [3818, 10343, 15974, 32768, 0], - [1481, 4117, 6796, 32768, 0], - [22649, 31302, 32190, 32768, 0], - [14829, 27127, 30449, 32768, 0], - [8313, 17702, 23304, 32768, 0], - [3022, 8301, 12786, 32768, 0], - [1536, 4412, 7184, 32768, 0], - [22354, 29774, 31372, 32768, 0], - [14723, 25472, 29214, 32768, 0], - [6673, 13745, 18662, 32768, 0], - [2068, 5766, 9322, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - [ - [6302, 16444, 21761, 32768, 0], - [23040, 31538, 32475, 32768, 0], - [15196, 28452, 31496, 32768, 0], - [10020, 22946, 28514, 32768, 0], - [6533, 16862, 23501, 32768, 0], - [3538, 9816, 15076, 32768, 0], - [24444, 31875, 32525, 32768, 0], - [15881, 28924, 31635, 32768, 0], - [9922, 22873, 28466, 32768, 0], - [6527, 16966, 23691, 32768, 0], - [4114, 11303, 17220, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [20201, 30770, 32209, 32768, 0], - [14754, 28071, 31258, 32768, 0], - [8378, 20186, 26517, 32768, 0], - [5916, 15299, 21978, 32768, 0], - [4268, 11583, 17901, 32768, 0], - [24361, 32025, 32581, 32768, 0], - [18673, 30105, 31943, 32768, 0], - [10196, 22244, 27576, 32768, 0], - [5495, 14349, 20417, 32768, 0], - [2676, 7415, 11498, 32768, 0], - [24678, 31958, 32585, 32768, 0], - [18629, 29906, 31831, 32768, 0], - [9364, 20724, 26315, 32768, 0], - [4641, 12318, 18094, 32768, 0], - [2758, 7387, 11579, 32768, 0], - [25433, 31842, 32469, 32768, 0], - [18795, 29289, 31411, 32768, 0], - [7644, 17584, 23592, 32768, 0], - [3408, 9014, 15047, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - ], - [ - [ - [4536, 10072, 14001, 32768, 0], - [25459, 31416, 32206, 32768, 0], - [16605, 28048, 30818, 32768, 0], - [11008, 22857, 27719, 32768, 0], - [6915, 16268, 22315, 32768, 0], - [2625, 6812, 10537, 32768, 0], - [24257, 31788, 32499, 32768, 0], - [16880, 29454, 31879, 32768, 0], - [11958, 25054, 29778, 32768, 0], - [7916, 18718, 25084, 32768, 0], - [3383, 8777, 13446, 32768, 0], - [22720, 31603, 32393, 32768, 0], - [14960, 28125, 31335, 32768, 0], - [9731, 22210, 27928, 32768, 0], - [6304, 15832, 22277, 32768, 0], - [2910, 7818, 12166, 32768, 0], - [20375, 30627, 32131, 32768, 0], - [13904, 27284, 30887, 32768, 0], - [9368, 21558, 27144, 32768, 0], - [5937, 14966, 21119, 32768, 0], - [2667, 7225, 11319, 32768, 0], - [23970, 31470, 32378, 32768, 0], - [17173, 29734, 32018, 32768, 0], - [12795, 25441, 29965, 32768, 0], - [8981, 19680, 25893, 32768, 0], - [4728, 11372, 16902, 32768, 0], - [24287, 31797, 32439, 32768, 0], - [16703, 29145, 31696, 32768, 0], - [10833, 23554, 28725, 32768, 0], - [6468, 16566, 23057, 32768, 0], - [2415, 6562, 10278, 32768, 0], - [26610, 32395, 32659, 32768, 0], - [18590, 30498, 32117, 32768, 0], - [12420, 25756, 29950, 32768, 0], - [7639, 18746, 24710, 32768, 0], - [3001, 8086, 12347, 32768, 0], - [25076, 32064, 32580, 32768, 0], - [17946, 30128, 32028, 32768, 0], - [12024, 24985, 29378, 32768, 0], - [7517, 18390, 24304, 32768, 0], - [3243, 8781, 13331, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - [ - [6037, 16771, 21957, 32768, 0], - [24774, 31704, 32426, 32768, 0], - [16830, 28589, 31056, 32768, 0], - [10602, 22828, 27760, 32768, 0], - [6733, 16829, 23071, 32768, 0], - [3250, 8914, 13556, 32768, 0], - [25582, 32220, 32668, 32768, 0], - [18659, 30342, 32223, 32768, 0], - [12546, 26149, 30515, 32768, 0], - [8420, 20451, 26801, 32768, 0], - [4636, 12420, 18344, 32768, 0], - [27581, 32362, 32639, 32768, 0], - [18987, 30083, 31978, 32768, 0], - [11327, 24248, 29084, 32768, 0], - [7264, 17719, 24120, 32768, 0], - [3995, 10768, 16169, 32768, 0], - [25893, 31831, 32487, 32768, 0], - [16577, 28587, 31379, 32768, 0], - [10189, 22748, 28182, 32768, 0], - [6832, 17094, 23556, 32768, 0], - [3708, 10110, 15334, 32768, 0], - [25904, 32282, 32656, 32768, 0], - [19721, 30792, 32276, 32768, 0], - [12819, 26243, 30411, 32768, 0], - [8572, 20614, 26891, 32768, 0], - [5364, 14059, 20467, 32768, 0], - [26580, 32438, 32677, 32768, 0], - [20852, 31225, 32340, 32768, 0], - [12435, 25700, 29967, 32768, 0], - [8691, 20825, 26976, 32768, 0], - [4446, 12209, 17269, 32768, 0], - [27350, 32429, 32696, 32768, 0], - [21372, 30977, 32272, 32768, 0], - [12673, 25270, 29853, 32768, 0], - [9208, 20925, 26640, 32768, 0], - [5018, 13351, 18732, 32768, 0], - [27351, 32479, 32713, 32768, 0], - [21398, 31209, 32387, 32768, 0], - [12162, 25047, 29842, 32768, 0], - [7896, 18691, 25319, 32768, 0], - [4670, 12882, 18881, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - ], - [ - [ - [5487, 10460, 13708, 32768, 0], - [21597, 28303, 30674, 32768, 0], - [11037, 21953, 26476, 32768, 0], - [8147, 17962, 22952, 32768, 0], - [5242, 13061, 18532, 32768, 0], - [1889, 5208, 8182, 32768, 0], - [26774, 32133, 32590, 32768, 0], - [17844, 29564, 31767, 32768, 0], - [11690, 24438, 29171, 32768, 0], - [7542, 18215, 24459, 32768, 0], - [2993, 8050, 12319, 32768, 0], - [28023, 32328, 32591, 32768, 0], - [18651, 30126, 31954, 32768, 0], - [12164, 25146, 29589, 32768, 0], - [7762, 18530, 24771, 32768, 0], - [3492, 9183, 13920, 32768, 0], - [27591, 32008, 32491, 32768, 0], - [17149, 28853, 31510, 32768, 0], - [11485, 24003, 28860, 32768, 0], - [7697, 18086, 24210, 32768, 0], - [3075, 7999, 12218, 32768, 0], - [28268, 32482, 32654, 32768, 0], - [19631, 31051, 32404, 32768, 0], - [13860, 27260, 31020, 32768, 0], - [9605, 21613, 27594, 32768, 0], - [4876, 12162, 17908, 32768, 0], - [27248, 32316, 32576, 32768, 0], - [18955, 30457, 32075, 32768, 0], - [11824, 23997, 28795, 32768, 0], - [7346, 18196, 24647, 32768, 0], - [3403, 9247, 14111, 32768, 0], - [29711, 32655, 32735, 32768, 0], - [21169, 31394, 32417, 32768, 0], - [13487, 27198, 30957, 32768, 0], - [8828, 21683, 27614, 32768, 0], - [4270, 11451, 17038, 32768, 0], - [28708, 32578, 32731, 32768, 0], - [20120, 31241, 32482, 32768, 0], - [13692, 27550, 31321, 32768, 0], - [9418, 22514, 28439, 32768, 0], - [4999, 13283, 19462, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - [ - [5673, 14302, 19711, 32768, 0], - [26251, 30701, 31834, 32768, 0], - [12782, 23783, 27803, 32768, 0], - [9127, 20657, 25808, 32768, 0], - [6368, 16208, 21462, 32768, 0], - [2465, 7177, 10822, 32768, 0], - [29961, 32563, 32719, 32768, 0], - [18318, 29891, 31949, 32768, 0], - [11361, 24514, 29357, 32768, 0], - [7900, 19603, 25607, 32768, 0], - [4002, 10590, 15546, 32768, 0], - [29637, 32310, 32595, 32768, 0], - [18296, 29913, 31809, 32768, 0], - [10144, 21515, 26871, 32768, 0], - [5358, 14322, 20394, 32768, 0], - [3067, 8362, 13346, 32768, 0], - [28652, 32470, 32676, 32768, 0], - [17538, 30771, 32209, 32768, 0], - [13924, 26882, 30494, 32768, 0], - [10496, 22837, 27869, 32768, 0], - [7236, 16396, 21621, 32768, 0], - [30743, 32687, 32746, 32768, 0], - [23006, 31676, 32489, 32768, 0], - [14494, 27828, 31120, 32768, 0], - [10174, 22801, 28352, 32768, 0], - [6242, 15281, 21043, 32768, 0], - [25817, 32243, 32720, 32768, 0], - [18618, 31367, 32325, 32768, 0], - [13997, 28318, 31878, 32768, 0], - [12255, 26534, 31383, 32768, 0], - [9561, 21588, 28450, 32768, 0], - [28188, 32635, 32724, 32768, 0], - [22060, 32365, 32728, 32768, 0], - [18102, 30690, 32528, 32768, 0], - [14196, 28864, 31999, 32768, 0], - [12262, 25792, 30865, 32768, 0], - [24176, 32109, 32628, 32768, 0], - [18280, 29681, 31963, 32768, 0], - [10205, 23703, 29664, 32768, 0], - [7889, 20025, 27676, 32768, 0], - [6060, 16743, 23970, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - ], - [ - [ - [5141, 7096, 8260, 32768, 0], - [27186, 29022, 29789, 32768, 0], - [6668, 12568, 15682, 32768, 0], - [2172, 6181, 8638, 32768, 0], - [1126, 3379, 4531, 32768, 0], - [443, 1361, 2254, 32768, 0], - [26083, 31153, 32436, 32768, 0], - [13486, 24603, 28483, 32768, 0], - [6508, 14840, 19910, 32768, 0], - [3386, 8800, 13286, 32768, 0], - [1530, 4322, 7054, 32768, 0], - [29639, 32080, 32548, 32768, 0], - [15897, 27552, 30290, 32768, 0], - [8588, 20047, 25383, 32768, 0], - [4889, 13339, 19269, 32768, 0], - [2240, 6871, 10498, 32768, 0], - [28165, 32197, 32517, 32768, 0], - [20735, 30427, 31568, 32768, 0], - [14325, 24671, 27692, 32768, 0], - [5119, 12554, 17805, 32768, 0], - [1810, 5441, 8261, 32768, 0], - [31212, 32724, 32748, 32768, 0], - [23352, 31766, 32545, 32768, 0], - [14669, 27570, 31059, 32768, 0], - [8492, 20894, 27272, 32768, 0], - [3644, 10194, 15204, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - [ - [2461, 7013, 9371, 32768, 0], - [24749, 29600, 30986, 32768, 0], - [9466, 19037, 22417, 32768, 0], - [3584, 9280, 14400, 32768, 0], - [1505, 3929, 5433, 32768, 0], - [677, 1500, 2736, 32768, 0], - [23987, 30702, 32117, 32768, 0], - [13554, 24571, 29263, 32768, 0], - [6211, 14556, 21155, 32768, 0], - [3135, 10972, 15625, 32768, 0], - [2435, 7127, 11427, 32768, 0], - [31300, 32532, 32550, 32768, 0], - [14757, 30365, 31954, 32768, 0], - [4405, 11612, 18553, 32768, 0], - [580, 4132, 7322, 32768, 0], - [1695, 10169, 14124, 32768, 0], - [30008, 32282, 32591, 32768, 0], - [19244, 30108, 31748, 32768, 0], - [11180, 24158, 29555, 32768, 0], - [5650, 14972, 19209, 32768, 0], - [2114, 5109, 8456, 32768, 0], - [31856, 32716, 32748, 32768, 0], - [23012, 31664, 32572, 32768, 0], - [13694, 26656, 30636, 32768, 0], - [8142, 19508, 26093, 32768, 0], - [4253, 10955, 16724, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - ], - [ - [ - [601, 983, 1311, 32768, 0], - [18725, 23406, 28087, 32768, 0], - [5461, 8192, 10923, 32768, 0], - [3781, 15124, 21425, 32768, 0], - [2587, 7761, 12072, 32768, 0], - [106, 458, 810, 32768, 0], - [22282, 29710, 31894, 32768, 0], - [8508, 20926, 25984, 32768, 0], - [3726, 12713, 18083, 32768, 0], - [1620, 7112, 10893, 32768, 0], - [729, 2236, 3495, 32768, 0], - [30163, 32474, 32684, 32768, 0], - [18304, 30464, 32000, 32768, 0], - [11443, 26526, 29647, 32768, 0], - [6007, 15292, 21299, 32768, 0], - [2234, 6703, 8937, 32768, 0], - [30954, 32177, 32571, 32768, 0], - [17363, 29562, 31076, 32768, 0], - [9686, 22464, 27410, 32768, 0], - [8192, 16384, 21390, 32768, 0], - [1755, 8046, 11264, 32768, 0], - [31168, 32734, 32748, 32768, 0], - [22486, 31441, 32471, 32768, 0], - [12833, 25627, 29738, 32768, 0], - [6980, 17379, 23122, 32768, 0], - [3111, 8887, 13479, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - [ - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - ], - ], - [ - [ - [ - [6041, 11854, 15927, 32768, 0], - [20326, 30905, 32251, 32768, 0], - [14164, 26831, 30725, 32768, 0], - [9760, 20647, 26585, 32768, 0], - [6416, 14953, 21219, 32768, 0], - [2966, 7151, 10891, 32768, 0], - [23567, 31374, 32254, 32768, 0], - [14978, 27416, 30946, 32768, 0], - [9434, 20225, 26254, 32768, 0], - [6658, 14558, 20535, 32768, 0], - [3916, 8677, 12989, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [18088, 29545, 31587, 32768, 0], - [13062, 25843, 30073, 32768, 0], - [8940, 16827, 22251, 32768, 0], - [7654, 13220, 17973, 32768, 0], - [5733, 10316, 14456, 32768, 0], - [22879, 31388, 32114, 32768, 0], - [15215, 27993, 30955, 32768, 0], - [9397, 19445, 24978, 32768, 0], - [3442, 9813, 15344, 32768, 0], - [1368, 3936, 6532, 32768, 0], - [25494, 32033, 32406, 32768, 0], - [16772, 27963, 30718, 32768, 0], - [9419, 18165, 23260, 32768, 0], - [2677, 7501, 11797, 32768, 0], - [1516, 4344, 7170, 32768, 0], - [26556, 31454, 32101, 32768, 0], - [17128, 27035, 30108, 32768, 0], - [8324, 15344, 20249, 32768, 0], - [1903, 5696, 9469, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - [ - [8455, 19003, 24368, 32768, 0], - [23563, 32021, 32604, 32768, 0], - [16237, 29446, 31935, 32768, 0], - [10724, 23999, 29358, 32768, 0], - [6725, 17528, 24416, 32768, 0], - [3927, 10927, 16825, 32768, 0], - [26313, 32288, 32634, 32768, 0], - [17430, 30095, 32095, 32768, 0], - [11116, 24606, 29679, 32768, 0], - [7195, 18384, 25269, 32768, 0], - [4726, 12852, 19315, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [22822, 31648, 32483, 32768, 0], - [16724, 29633, 31929, 32768, 0], - [10261, 23033, 28725, 32768, 0], - [7029, 17840, 24528, 32768, 0], - [4867, 13886, 21502, 32768, 0], - [25298, 31892, 32491, 32768, 0], - [17809, 29330, 31512, 32768, 0], - [9668, 21329, 26579, 32768, 0], - [4774, 12956, 18976, 32768, 0], - [2322, 7030, 11540, 32768, 0], - [25472, 31920, 32543, 32768, 0], - [17957, 29387, 31632, 32768, 0], - [9196, 20593, 26400, 32768, 0], - [4680, 12705, 19202, 32768, 0], - [2917, 8456, 13436, 32768, 0], - [26471, 32059, 32574, 32768, 0], - [18458, 29783, 31909, 32768, 0], - [8400, 19464, 25956, 32768, 0], - [3812, 10973, 17206, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - ], - [ - [ - [6779, 13743, 17678, 32768, 0], - [24806, 31797, 32457, 32768, 0], - [17616, 29047, 31372, 32768, 0], - [11063, 23175, 28003, 32768, 0], - [6521, 16110, 22324, 32768, 0], - [2764, 7504, 11654, 32768, 0], - [25266, 32367, 32637, 32768, 0], - [19054, 30553, 32175, 32768, 0], - [12139, 25212, 29807, 32768, 0], - [7311, 18162, 24704, 32768, 0], - [3397, 9164, 14074, 32768, 0], - [25988, 32208, 32522, 32768, 0], - [16253, 28912, 31526, 32768, 0], - [9151, 21387, 27372, 32768, 0], - [5688, 14915, 21496, 32768, 0], - [2717, 7627, 12004, 32768, 0], - [23144, 31855, 32443, 32768, 0], - [16070, 28491, 31325, 32768, 0], - [8702, 20467, 26517, 32768, 0], - [5243, 13956, 20367, 32768, 0], - [2621, 7335, 11567, 32768, 0], - [26636, 32340, 32630, 32768, 0], - [19990, 31050, 32341, 32768, 0], - [13243, 26105, 30315, 32768, 0], - [8588, 19521, 25918, 32768, 0], - [4717, 11585, 17304, 32768, 0], - [25844, 32292, 32582, 32768, 0], - [19090, 30635, 32097, 32768, 0], - [11963, 24546, 28939, 32768, 0], - [6218, 16087, 22354, 32768, 0], - [2340, 6608, 10426, 32768, 0], - [28046, 32576, 32694, 32768, 0], - [21178, 31313, 32296, 32768, 0], - [13486, 26184, 29870, 32768, 0], - [7149, 17871, 23723, 32768, 0], - [2833, 7958, 12259, 32768, 0], - [27710, 32528, 32686, 32768, 0], - [20674, 31076, 32268, 32768, 0], - [12413, 24955, 29243, 32768, 0], - [6676, 16927, 23097, 32768, 0], - [2966, 8333, 12919, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - [ - [8639, 19339, 24429, 32768, 0], - [24404, 31837, 32525, 32768, 0], - [16997, 29425, 31784, 32768, 0], - [11253, 24234, 29149, 32768, 0], - [6751, 17394, 24028, 32768, 0], - [3490, 9830, 15191, 32768, 0], - [26283, 32471, 32714, 32768, 0], - [19599, 31168, 32442, 32768, 0], - [13146, 26954, 30893, 32768, 0], - [8214, 20588, 26890, 32768, 0], - [4699, 13081, 19300, 32768, 0], - [28212, 32458, 32669, 32768, 0], - [18594, 30316, 32100, 32768, 0], - [11219, 24408, 29234, 32768, 0], - [6865, 17656, 24149, 32768, 0], - [3678, 10362, 16006, 32768, 0], - [25825, 32136, 32616, 32768, 0], - [17313, 29853, 32021, 32768, 0], - [11197, 24471, 29472, 32768, 0], - [6947, 17781, 24405, 32768, 0], - [3768, 10660, 16261, 32768, 0], - [27352, 32500, 32706, 32768, 0], - [20850, 31468, 32469, 32768, 0], - [14021, 27707, 31133, 32768, 0], - [8964, 21748, 27838, 32768, 0], - [5437, 14665, 21187, 32768, 0], - [26304, 32492, 32698, 32768, 0], - [20409, 31380, 32385, 32768, 0], - [13682, 27222, 30632, 32768, 0], - [8974, 21236, 26685, 32768, 0], - [4234, 11665, 16934, 32768, 0], - [26273, 32357, 32711, 32768, 0], - [20672, 31242, 32441, 32768, 0], - [14172, 27254, 30902, 32768, 0], - [9870, 21898, 27275, 32768, 0], - [5164, 13506, 19270, 32768, 0], - [26725, 32459, 32728, 32768, 0], - [20991, 31442, 32527, 32768, 0], - [13071, 26434, 30811, 32768, 0], - [8184, 20090, 26742, 32768, 0], - [4803, 13255, 19895, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - ], - [ - [ - [7555, 14942, 18501, 32768, 0], - [24410, 31178, 32287, 32768, 0], - [14394, 26738, 30253, 32768, 0], - [8413, 19554, 25195, 32768, 0], - [4766, 12924, 18785, 32768, 0], - [2029, 5806, 9207, 32768, 0], - [26776, 32364, 32663, 32768, 0], - [18732, 29967, 31931, 32768, 0], - [11005, 23786, 28852, 32768, 0], - [6466, 16909, 23510, 32768, 0], - [3044, 8638, 13419, 32768, 0], - [29208, 32582, 32704, 32768, 0], - [20068, 30857, 32208, 32768, 0], - [12003, 25085, 29595, 32768, 0], - [6947, 17750, 24189, 32768, 0], - [3245, 9103, 14007, 32768, 0], - [27359, 32465, 32669, 32768, 0], - [19421, 30614, 32174, 32768, 0], - [11915, 25010, 29579, 32768, 0], - [6950, 17676, 24074, 32768, 0], - [3007, 8473, 13096, 32768, 0], - [29002, 32676, 32735, 32768, 0], - [22102, 31849, 32576, 32768, 0], - [14408, 28009, 31405, 32768, 0], - [9027, 21679, 27931, 32768, 0], - [4694, 12678, 18748, 32768, 0], - [28216, 32528, 32682, 32768, 0], - [20849, 31264, 32318, 32768, 0], - [12756, 25815, 29751, 32768, 0], - [7565, 18801, 24923, 32768, 0], - [3509, 9533, 14477, 32768, 0], - [30133, 32687, 32739, 32768, 0], - [23063, 31910, 32515, 32768, 0], - [14588, 28051, 31132, 32768, 0], - [9085, 21649, 27457, 32768, 0], - [4261, 11654, 17264, 32768, 0], - [29518, 32691, 32748, 32768, 0], - [22451, 31959, 32613, 32768, 0], - [14864, 28722, 31700, 32768, 0], - [9695, 22964, 28716, 32768, 0], - [4932, 13358, 19502, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - [ - [6465, 16958, 21688, 32768, 0], - [25199, 31514, 32360, 32768, 0], - [14774, 27149, 30607, 32768, 0], - [9257, 21438, 26972, 32768, 0], - [5723, 15183, 21882, 32768, 0], - [3150, 8879, 13731, 32768, 0], - [26989, 32262, 32682, 32768, 0], - [17396, 29937, 32085, 32768, 0], - [11387, 24901, 29784, 32768, 0], - [7289, 18821, 25548, 32768, 0], - [3734, 10577, 16086, 32768, 0], - [29728, 32501, 32695, 32768, 0], - [17431, 29701, 31903, 32768, 0], - [9921, 22826, 28300, 32768, 0], - [5896, 15434, 22068, 32768, 0], - [3430, 9646, 14757, 32768, 0], - [28614, 32511, 32705, 32768, 0], - [19364, 30638, 32263, 32768, 0], - [13129, 26254, 30402, 32768, 0], - [8754, 20484, 26440, 32768, 0], - [4378, 11607, 17110, 32768, 0], - [30292, 32671, 32744, 32768, 0], - [21780, 31603, 32501, 32768, 0], - [14314, 27829, 31291, 32768, 0], - [9611, 22327, 28263, 32768, 0], - [4890, 13087, 19065, 32768, 0], - [25862, 32567, 32733, 32768, 0], - [20794, 32050, 32567, 32768, 0], - [17243, 30625, 32254, 32768, 0], - [13283, 27628, 31474, 32768, 0], - [9669, 22532, 28918, 32768, 0], - [27435, 32697, 32748, 32768, 0], - [24922, 32390, 32714, 32768, 0], - [21449, 31504, 32536, 32768, 0], - [16392, 29729, 31832, 32768, 0], - [11692, 24884, 29076, 32768, 0], - [24193, 32290, 32735, 32768, 0], - [18909, 31104, 32563, 32768, 0], - [12236, 26841, 31403, 32768, 0], - [8171, 21840, 29082, 32768, 0], - [7224, 17280, 25275, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - ], - [ - [ - [3078, 6839, 9890, 32768, 0], - [13837, 20450, 24479, 32768, 0], - [5914, 14222, 19328, 32768, 0], - [3866, 10267, 14762, 32768, 0], - [2612, 7208, 11042, 32768, 0], - [1067, 2991, 4776, 32768, 0], - [25817, 31646, 32529, 32768, 0], - [13708, 26338, 30385, 32768, 0], - [7328, 18585, 24870, 32768, 0], - [4691, 13080, 19276, 32768, 0], - [1825, 5253, 8352, 32768, 0], - [29386, 32315, 32624, 32768, 0], - [17160, 29001, 31360, 32768, 0], - [9602, 21862, 27396, 32768, 0], - [5915, 15772, 22148, 32768, 0], - [2786, 7779, 12047, 32768, 0], - [29246, 32450, 32663, 32768, 0], - [18696, 29929, 31818, 32768, 0], - [10510, 23369, 28560, 32768, 0], - [6229, 16499, 23125, 32768, 0], - [2608, 7448, 11705, 32768, 0], - [30753, 32710, 32748, 32768, 0], - [21638, 31487, 32503, 32768, 0], - [12937, 26854, 30870, 32768, 0], - [8182, 20596, 26970, 32768, 0], - [3637, 10269, 15497, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - [ - [5244, 12150, 16906, 32768, 0], - [20486, 26858, 29701, 32768, 0], - [7756, 18317, 23735, 32768, 0], - [3452, 9256, 13146, 32768, 0], - [2020, 5206, 8229, 32768, 0], - [1801, 4993, 7903, 32768, 0], - [27051, 31858, 32531, 32768, 0], - [15988, 27531, 30619, 32768, 0], - [9188, 21484, 26719, 32768, 0], - [6273, 17186, 23800, 32768, 0], - [3108, 9355, 14764, 32768, 0], - [31076, 32520, 32680, 32768, 0], - [18119, 30037, 31850, 32768, 0], - [10244, 22969, 27472, 32768, 0], - [4692, 14077, 19273, 32768, 0], - [3694, 11677, 17556, 32768, 0], - [30060, 32581, 32720, 32768, 0], - [21011, 30775, 32120, 32768, 0], - [11931, 24820, 29289, 32768, 0], - [7119, 17662, 24356, 32768, 0], - [3833, 10706, 16304, 32768, 0], - [31954, 32731, 32748, 32768, 0], - [23913, 31724, 32489, 32768, 0], - [15520, 28060, 31286, 32768, 0], - [11517, 23008, 28571, 32768, 0], - [6193, 14508, 20629, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - ], - [ - [ - [1035, 2807, 4156, 32768, 0], - [13162, 18138, 20939, 32768, 0], - [2696, 6633, 8755, 32768, 0], - [1373, 4161, 6853, 32768, 0], - [1099, 2746, 4716, 32768, 0], - [340, 1021, 1599, 32768, 0], - [22826, 30419, 32135, 32768, 0], - [10395, 21762, 26942, 32768, 0], - [4726, 12407, 17361, 32768, 0], - [2447, 7080, 10593, 32768, 0], - [1227, 3717, 6011, 32768, 0], - [28156, 31424, 31934, 32768, 0], - [16915, 27754, 30373, 32768, 0], - [9148, 20990, 26431, 32768, 0], - [5950, 15515, 21148, 32768, 0], - [2492, 7327, 11526, 32768, 0], - [30602, 32477, 32670, 32768, 0], - [20026, 29955, 31568, 32768, 0], - [11220, 23628, 28105, 32768, 0], - [6652, 17019, 22973, 32768, 0], - [3064, 8536, 13043, 32768, 0], - [31769, 32724, 32748, 32768, 0], - [22230, 30887, 32373, 32768, 0], - [12234, 25079, 29731, 32768, 0], - [7326, 18816, 25353, 32768, 0], - [3933, 10907, 16616, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - [ - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - ], - ], - [ - [ - [ - [8896, 16227, 20630, 32768, 0], - [23629, 31782, 32527, 32768, 0], - [15173, 27755, 31321, 32768, 0], - [10158, 21233, 27382, 32768, 0], - [6420, 14857, 21558, 32768, 0], - [3269, 8155, 12646, 32768, 0], - [24835, 32009, 32496, 32768, 0], - [16509, 28421, 31579, 32768, 0], - [10957, 21514, 27418, 32768, 0], - [7881, 15930, 22096, 32768, 0], - [5388, 10960, 15918, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [20745, 30773, 32093, 32768, 0], - [15200, 27221, 30861, 32768, 0], - [13032, 20873, 25667, 32768, 0], - [12285, 18663, 23494, 32768, 0], - [11563, 17481, 21489, 32768, 0], - [26260, 31982, 32320, 32768, 0], - [15397, 28083, 31100, 32768, 0], - [9742, 19217, 24824, 32768, 0], - [3261, 9629, 15362, 32768, 0], - [1480, 4322, 7499, 32768, 0], - [27599, 32256, 32460, 32768, 0], - [16857, 27659, 30774, 32768, 0], - [9551, 18290, 23748, 32768, 0], - [3052, 8933, 14103, 32768, 0], - [2021, 5910, 9787, 32768, 0], - [29005, 32015, 32392, 32768, 0], - [17677, 27694, 30863, 32768, 0], - [9204, 17356, 23219, 32768, 0], - [2403, 7516, 12814, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - [ - [10808, 22056, 26896, 32768, 0], - [25739, 32313, 32676, 32768, 0], - [17288, 30203, 32221, 32768, 0], - [11359, 24878, 29896, 32768, 0], - [6949, 17767, 24893, 32768, 0], - [4287, 11796, 18071, 32768, 0], - [27880, 32521, 32705, 32768, 0], - [19038, 31004, 32414, 32768, 0], - [12564, 26345, 30768, 32768, 0], - [8269, 19947, 26779, 32768, 0], - [5674, 14657, 21674, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [25742, 32319, 32671, 32768, 0], - [19557, 31164, 32454, 32768, 0], - [13381, 26381, 30755, 32768, 0], - [10101, 21466, 26722, 32768, 0], - [9209, 19650, 26825, 32768, 0], - [27107, 31917, 32432, 32768, 0], - [18056, 28893, 31203, 32768, 0], - [10200, 21434, 26764, 32768, 0], - [4660, 12913, 19502, 32768, 0], - [2368, 6930, 12504, 32768, 0], - [26960, 32158, 32613, 32768, 0], - [18628, 30005, 32031, 32768, 0], - [10233, 22442, 28232, 32768, 0], - [5471, 14630, 21516, 32768, 0], - [3235, 10767, 17109, 32768, 0], - [27696, 32440, 32692, 32768, 0], - [20032, 31167, 32438, 32768, 0], - [8700, 21341, 28442, 32768, 0], - [5662, 14831, 21795, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - ], - [ - [ - [9704, 17294, 21132, 32768, 0], - [26762, 32278, 32633, 32768, 0], - [18382, 29620, 31819, 32768, 0], - [10891, 23475, 28723, 32768, 0], - [6358, 16583, 23309, 32768, 0], - [3248, 9118, 14141, 32768, 0], - [27204, 32573, 32699, 32768, 0], - [19818, 30824, 32329, 32768, 0], - [11772, 25120, 30041, 32768, 0], - [6995, 18033, 25039, 32768, 0], - [3752, 10442, 16098, 32768, 0], - [27222, 32256, 32559, 32768, 0], - [15356, 28399, 31475, 32768, 0], - [8821, 20635, 27057, 32768, 0], - [5511, 14404, 21239, 32768, 0], - [2935, 8222, 13051, 32768, 0], - [24875, 32120, 32529, 32768, 0], - [15233, 28265, 31445, 32768, 0], - [8605, 20570, 26932, 32768, 0], - [5431, 14413, 21196, 32768, 0], - [2994, 8341, 13223, 32768, 0], - [28201, 32604, 32700, 32768, 0], - [21041, 31446, 32456, 32768, 0], - [13221, 26213, 30475, 32768, 0], - [8255, 19385, 26037, 32768, 0], - [4930, 12585, 18830, 32768, 0], - [28768, 32448, 32627, 32768, 0], - [19705, 30561, 32021, 32768, 0], - [11572, 23589, 28220, 32768, 0], - [5532, 15034, 21446, 32768, 0], - [2460, 7150, 11456, 32768, 0], - [29874, 32619, 32699, 32768, 0], - [21621, 31071, 32201, 32768, 0], - [12511, 24747, 28992, 32768, 0], - [6281, 16395, 22748, 32768, 0], - [3246, 9278, 14497, 32768, 0], - [29715, 32625, 32712, 32768, 0], - [20958, 31011, 32283, 32768, 0], - [11233, 23671, 28806, 32768, 0], - [6012, 16128, 22868, 32768, 0], - [3427, 9851, 15414, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - [ - [11016, 22111, 26794, 32768, 0], - [25946, 32357, 32677, 32768, 0], - [17890, 30452, 32252, 32768, 0], - [11678, 25142, 29816, 32768, 0], - [6720, 17534, 24584, 32768, 0], - [4230, 11665, 17820, 32768, 0], - [28400, 32623, 32747, 32768, 0], - [21164, 31668, 32575, 32768, 0], - [13572, 27388, 31182, 32768, 0], - [8234, 20750, 27358, 32768, 0], - [5065, 14055, 20897, 32768, 0], - [28981, 32547, 32705, 32768, 0], - [18681, 30543, 32239, 32768, 0], - [10919, 24075, 29286, 32768, 0], - [6431, 17199, 24077, 32768, 0], - [3819, 10464, 16618, 32768, 0], - [26870, 32467, 32693, 32768, 0], - [19041, 30831, 32347, 32768, 0], - [11794, 25211, 30016, 32768, 0], - [6888, 18019, 24970, 32768, 0], - [4370, 12363, 18992, 32768, 0], - [29578, 32670, 32744, 32768, 0], - [23159, 32007, 32613, 32768, 0], - [15315, 28669, 31676, 32768, 0], - [9298, 22607, 28782, 32768, 0], - [6144, 15913, 22968, 32768, 0], - [28110, 32499, 32669, 32768, 0], - [21574, 30937, 32015, 32768, 0], - [12759, 24818, 28727, 32768, 0], - [6545, 16761, 23042, 32768, 0], - [3649, 10597, 16833, 32768, 0], - [28163, 32552, 32728, 32768, 0], - [22101, 31469, 32464, 32768, 0], - [13160, 25472, 30143, 32768, 0], - [7303, 18684, 25468, 32768, 0], - [5241, 13975, 20955, 32768, 0], - [28400, 32631, 32744, 32768, 0], - [22104, 31793, 32603, 32768, 0], - [13557, 26571, 30846, 32768, 0], - [7749, 19861, 26675, 32768, 0], - [4873, 14030, 21234, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - ], - [ - [ - [9800, 17635, 21073, 32768, 0], - [26153, 31885, 32527, 32768, 0], - [15038, 27852, 31006, 32768, 0], - [8718, 20564, 26486, 32768, 0], - [5128, 14076, 20514, 32768, 0], - [2636, 7566, 11925, 32768, 0], - [27551, 32504, 32701, 32768, 0], - [18310, 30054, 32100, 32768, 0], - [10211, 23420, 29082, 32768, 0], - [6222, 16876, 23916, 32768, 0], - [3462, 9954, 15498, 32768, 0], - [29991, 32633, 32721, 32768, 0], - [19883, 30751, 32201, 32768, 0], - [11141, 24184, 29285, 32768, 0], - [6420, 16940, 23774, 32768, 0], - [3392, 9753, 15118, 32768, 0], - [28465, 32616, 32712, 32768, 0], - [19850, 30702, 32244, 32768, 0], - [10983, 24024, 29223, 32768, 0], - [6294, 16770, 23582, 32768, 0], - [3244, 9283, 14509, 32768, 0], - [30023, 32717, 32748, 32768, 0], - [22940, 32032, 32626, 32768, 0], - [14282, 27928, 31473, 32768, 0], - [8562, 21327, 27914, 32768, 0], - [4846, 13393, 19919, 32768, 0], - [29981, 32590, 32695, 32768, 0], - [20465, 30963, 32166, 32768, 0], - [11479, 23579, 28195, 32768, 0], - [5916, 15648, 22073, 32768, 0], - [3031, 8605, 13398, 32768, 0], - [31146, 32691, 32739, 32768, 0], - [23106, 31724, 32444, 32768, 0], - [13783, 26738, 30439, 32768, 0], - [7852, 19468, 25807, 32768, 0], - [3860, 11124, 16853, 32768, 0], - [31014, 32724, 32748, 32768, 0], - [23629, 32109, 32628, 32768, 0], - [14747, 28115, 31403, 32768, 0], - [8545, 21242, 27478, 32768, 0], - [4574, 12781, 19067, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - [ - [9185, 19694, 24688, 32768, 0], - [26081, 31985, 32621, 32768, 0], - [16015, 29000, 31787, 32768, 0], - [10542, 23690, 29206, 32768, 0], - [6732, 17945, 24677, 32768, 0], - [3916, 11039, 16722, 32768, 0], - [28224, 32566, 32744, 32768, 0], - [19100, 31138, 32485, 32768, 0], - [12528, 26620, 30879, 32768, 0], - [7741, 20277, 26885, 32768, 0], - [4566, 12845, 18990, 32768, 0], - [29933, 32593, 32718, 32768, 0], - [17670, 30333, 32155, 32768, 0], - [10385, 23600, 28909, 32768, 0], - [6243, 16236, 22407, 32768, 0], - [3976, 10389, 16017, 32768, 0], - [28377, 32561, 32738, 32768, 0], - [19366, 31175, 32482, 32768, 0], - [13327, 27175, 31094, 32768, 0], - [8258, 20769, 27143, 32768, 0], - [4703, 13198, 19527, 32768, 0], - [31086, 32706, 32748, 32768, 0], - [22853, 31902, 32583, 32768, 0], - [14759, 28186, 31419, 32768, 0], - [9284, 22382, 28348, 32768, 0], - [5585, 15192, 21868, 32768, 0], - [28291, 32652, 32746, 32768, 0], - [19849, 32107, 32571, 32768, 0], - [14834, 26818, 29214, 32768, 0], - [10306, 22594, 28672, 32768, 0], - [6615, 17384, 23384, 32768, 0], - [28947, 32604, 32745, 32768, 0], - [25625, 32289, 32646, 32768, 0], - [18758, 28672, 31403, 32768, 0], - [10017, 23430, 28523, 32768, 0], - [6862, 15269, 22131, 32768, 0], - [23933, 32509, 32739, 32768, 0], - [19927, 31495, 32631, 32768, 0], - [11903, 26023, 30621, 32768, 0], - [7026, 20094, 27252, 32768, 0], - [5998, 18106, 24437, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - ], - [ - [ - [4456, 11274, 15533, 32768, 0], - [21219, 29079, 31616, 32768, 0], - [11173, 23774, 28567, 32768, 0], - [7282, 18293, 24263, 32768, 0], - [4890, 13286, 19115, 32768, 0], - [1890, 5508, 8659, 32768, 0], - [26651, 32136, 32647, 32768, 0], - [14630, 28254, 31455, 32768, 0], - [8716, 21287, 27395, 32768, 0], - [5615, 15331, 22008, 32768, 0], - [2675, 7700, 12150, 32768, 0], - [29954, 32526, 32690, 32768, 0], - [16126, 28982, 31633, 32768, 0], - [9030, 21361, 27352, 32768, 0], - [5411, 14793, 21271, 32768, 0], - [2943, 8422, 13163, 32768, 0], - [29539, 32601, 32730, 32768, 0], - [18125, 30385, 32201, 32768, 0], - [10422, 24090, 29468, 32768, 0], - [6468, 17487, 24438, 32768, 0], - [2970, 8653, 13531, 32768, 0], - [30912, 32715, 32748, 32768, 0], - [20666, 31373, 32497, 32768, 0], - [12509, 26640, 30917, 32768, 0], - [8058, 20629, 27290, 32768, 0], - [4231, 12006, 18052, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - [ - [10202, 20633, 25484, 32768, 0], - [27336, 31445, 32352, 32768, 0], - [12420, 24384, 28552, 32768, 0], - [7648, 18115, 23856, 32768, 0], - [5662, 14341, 19902, 32768, 0], - [3611, 10328, 15390, 32768, 0], - [30945, 32616, 32736, 32768, 0], - [18682, 30505, 32253, 32768, 0], - [11513, 25336, 30203, 32768, 0], - [7449, 19452, 26148, 32768, 0], - [4482, 13051, 18886, 32768, 0], - [32022, 32690, 32747, 32768, 0], - [18578, 30501, 32146, 32768, 0], - [11249, 23368, 28631, 32768, 0], - [5645, 16958, 22158, 32768, 0], - [5009, 11444, 16637, 32768, 0], - [31357, 32710, 32748, 32768, 0], - [21552, 31494, 32504, 32768, 0], - [13891, 27677, 31340, 32768, 0], - [9051, 22098, 28172, 32768, 0], - [5190, 13377, 19486, 32768, 0], - [32364, 32740, 32748, 32768, 0], - [24839, 31907, 32551, 32768, 0], - [17160, 28779, 31696, 32768, 0], - [12452, 24137, 29602, 32768, 0], - [6165, 15389, 22477, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - ], - [ - [ - [2575, 7281, 11077, 32768, 0], - [14002, 20866, 25402, 32768, 0], - [6343, 15056, 19658, 32768, 0], - [4474, 11858, 17041, 32768, 0], - [2865, 8299, 12534, 32768, 0], - [1344, 3949, 6391, 32768, 0], - [24720, 31239, 32459, 32768, 0], - [12585, 25356, 29968, 32768, 0], - [7181, 18246, 24444, 32768, 0], - [5025, 13667, 19885, 32768, 0], - [2521, 7304, 11605, 32768, 0], - [29908, 32252, 32584, 32768, 0], - [17421, 29156, 31575, 32768, 0], - [9889, 22188, 27782, 32768, 0], - [5878, 15647, 22123, 32768, 0], - [2814, 8665, 13323, 32768, 0], - [30183, 32568, 32713, 32768, 0], - [18528, 30195, 32049, 32768, 0], - [10982, 24606, 29657, 32768, 0], - [6957, 18165, 25231, 32768, 0], - [3508, 10118, 15468, 32768, 0], - [31761, 32736, 32748, 32768, 0], - [21041, 31328, 32546, 32768, 0], - [12568, 26732, 31166, 32768, 0], - [8052, 20720, 27733, 32768, 0], - [4336, 12192, 18396, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - [ - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - ], - ], - [ - [ - [ - [7062, 16472, 22319, 32768, 0], - [24538, 32261, 32674, 32768, 0], - [13675, 28041, 31779, 32768, 0], - [8590, 20674, 27631, 32768, 0], - [5685, 14675, 22013, 32768, 0], - [3655, 9898, 15731, 32768, 0], - [26493, 32418, 32658, 32768, 0], - [16376, 29342, 32090, 32768, 0], - [10594, 22649, 28970, 32768, 0], - [8176, 17170, 24303, 32768, 0], - [5605, 12694, 19139, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [23888, 31902, 32542, 32768, 0], - [18612, 29687, 31987, 32768, 0], - [16245, 24852, 29249, 32768, 0], - [15765, 22608, 27559, 32768, 0], - [19895, 24699, 27510, 32768, 0], - [28401, 32212, 32457, 32768, 0], - [15274, 27825, 30980, 32768, 0], - [9364, 18128, 24332, 32768, 0], - [2283, 8193, 15082, 32768, 0], - [1228, 3972, 7881, 32768, 0], - [29455, 32469, 32620, 32768, 0], - [17981, 28245, 31388, 32768, 0], - [10921, 20098, 26240, 32768, 0], - [3743, 11829, 18657, 32768, 0], - [2374, 9593, 15715, 32768, 0], - [31068, 32466, 32635, 32768, 0], - [20321, 29572, 31971, 32768, 0], - [10771, 20255, 27119, 32768, 0], - [2795, 10410, 17361, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - [ - [9320, 22102, 27840, 32768, 0], - [27057, 32464, 32724, 32768, 0], - [16331, 30268, 32309, 32768, 0], - [10319, 23935, 29720, 32768, 0], - [6189, 16448, 24106, 32768, 0], - [3589, 10884, 18808, 32768, 0], - [29026, 32624, 32748, 32768, 0], - [19226, 31507, 32587, 32768, 0], - [12692, 26921, 31203, 32768, 0], - [7049, 19532, 27635, 32768, 0], - [7727, 15669, 23252, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [28056, 32625, 32748, 32768, 0], - [22383, 32075, 32669, 32768, 0], - [15417, 27098, 31749, 32768, 0], - [18127, 26493, 27190, 32768, 0], - [5461, 16384, 21845, 32768, 0], - [27982, 32091, 32584, 32768, 0], - [19045, 29868, 31972, 32768, 0], - [10397, 22266, 27932, 32768, 0], - [5990, 13697, 21500, 32768, 0], - [1792, 6912, 15104, 32768, 0], - [28198, 32501, 32718, 32768, 0], - [21534, 31521, 32569, 32768, 0], - [11109, 25217, 30017, 32768, 0], - [5671, 15124, 26151, 32768, 0], - [4681, 14043, 18725, 32768, 0], - [28688, 32580, 32741, 32768, 0], - [22576, 32079, 32661, 32768, 0], - [10627, 22141, 28340, 32768, 0], - [9362, 14043, 28087, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - ], - [ - [ - [7754, 16948, 22142, 32768, 0], - [25670, 32330, 32691, 32768, 0], - [15663, 29225, 31994, 32768, 0], - [9878, 23288, 29158, 32768, 0], - [6419, 17088, 24336, 32768, 0], - [3859, 11003, 17039, 32768, 0], - [27562, 32595, 32725, 32768, 0], - [17575, 30588, 32399, 32768, 0], - [10819, 24838, 30309, 32768, 0], - [7124, 18686, 25916, 32768, 0], - [4479, 12688, 19340, 32768, 0], - [28385, 32476, 32673, 32768, 0], - [15306, 29005, 31938, 32768, 0], - [8937, 21615, 28322, 32768, 0], - [5982, 15603, 22786, 32768, 0], - [3620, 10267, 16136, 32768, 0], - [27280, 32464, 32667, 32768, 0], - [15607, 29160, 32004, 32768, 0], - [9091, 22135, 28740, 32768, 0], - [6232, 16632, 24020, 32768, 0], - [4047, 11377, 17672, 32768, 0], - [29220, 32630, 32718, 32768, 0], - [19650, 31220, 32462, 32768, 0], - [13050, 26312, 30827, 32768, 0], - [9228, 20870, 27468, 32768, 0], - [6146, 15149, 21971, 32768, 0], - [30169, 32481, 32623, 32768, 0], - [17212, 29311, 31554, 32768, 0], - [9911, 21311, 26882, 32768, 0], - [4487, 13314, 20372, 32768, 0], - [2570, 7772, 12889, 32768, 0], - [30924, 32613, 32708, 32768, 0], - [19490, 30206, 32107, 32768, 0], - [11232, 23998, 29276, 32768, 0], - [6769, 17955, 25035, 32768, 0], - [4398, 12623, 19214, 32768, 0], - [30609, 32627, 32722, 32768, 0], - [19370, 30582, 32287, 32768, 0], - [10457, 23619, 29409, 32768, 0], - [6443, 17637, 24834, 32768, 0], - [4645, 13236, 20106, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - [ - [8626, 20271, 26216, 32768, 0], - [26707, 32406, 32711, 32768, 0], - [16999, 30329, 32286, 32768, 0], - [11445, 25123, 30286, 32768, 0], - [6411, 18828, 25601, 32768, 0], - [6801, 12458, 20248, 32768, 0], - [29918, 32682, 32748, 32768, 0], - [20649, 31739, 32618, 32768, 0], - [12879, 27773, 31581, 32768, 0], - [7896, 21751, 28244, 32768, 0], - [5260, 14870, 23698, 32768, 0], - [29252, 32593, 32731, 32768, 0], - [17072, 30460, 32294, 32768, 0], - [10653, 24143, 29365, 32768, 0], - [6536, 17490, 23983, 32768, 0], - [4929, 13170, 20085, 32768, 0], - [28137, 32518, 32715, 32768, 0], - [18171, 30784, 32407, 32768, 0], - [11437, 25436, 30459, 32768, 0], - [7252, 18534, 26176, 32768, 0], - [4126, 13353, 20978, 32768, 0], - [31162, 32726, 32748, 32768, 0], - [23017, 32222, 32701, 32768, 0], - [15629, 29233, 32046, 32768, 0], - [9387, 22621, 29480, 32768, 0], - [6922, 17616, 25010, 32768, 0], - [28838, 32265, 32614, 32768, 0], - [19701, 30206, 31920, 32768, 0], - [11214, 22410, 27933, 32768, 0], - [5320, 14177, 23034, 32768, 0], - [5049, 12881, 17827, 32768, 0], - [27484, 32471, 32734, 32768, 0], - [21076, 31526, 32561, 32768, 0], - [12707, 26303, 31211, 32768, 0], - [8169, 21722, 28219, 32768, 0], - [6045, 19406, 27042, 32768, 0], - [27753, 32572, 32745, 32768, 0], - [20832, 31878, 32653, 32768, 0], - [13250, 27356, 31674, 32768, 0], - [7718, 21508, 29858, 32768, 0], - [7209, 18350, 25559, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - ], - [ - [ - [7876, 16901, 21741, 32768, 0], - [24001, 31898, 32625, 32768, 0], - [14529, 27959, 31451, 32768, 0], - [8273, 20818, 27258, 32768, 0], - [5278, 14673, 21510, 32768, 0], - [2983, 8843, 14039, 32768, 0], - [28016, 32574, 32732, 32768, 0], - [17471, 30306, 32301, 32768, 0], - [10224, 24063, 29728, 32768, 0], - [6602, 17954, 25052, 32768, 0], - [4002, 11585, 17759, 32768, 0], - [30190, 32634, 32739, 32768, 0], - [17497, 30282, 32270, 32768, 0], - [10229, 23729, 29538, 32768, 0], - [6344, 17211, 24440, 32768, 0], - [3849, 11189, 17108, 32768, 0], - [28570, 32583, 32726, 32768, 0], - [17521, 30161, 32238, 32768, 0], - [10153, 23565, 29378, 32768, 0], - [6455, 17341, 24443, 32768, 0], - [3907, 11042, 17024, 32768, 0], - [30689, 32715, 32748, 32768, 0], - [21546, 31840, 32610, 32768, 0], - [13547, 27581, 31459, 32768, 0], - [8912, 21757, 28309, 32768, 0], - [5548, 15080, 22046, 32768, 0], - [30783, 32540, 32685, 32768, 0], - [17540, 29528, 31668, 32768, 0], - [10160, 21468, 26783, 32768, 0], - [4724, 13393, 20054, 32768, 0], - [2702, 8174, 13102, 32768, 0], - [31648, 32686, 32742, 32768, 0], - [20954, 31094, 32337, 32768, 0], - [12420, 25698, 30179, 32768, 0], - [7304, 19320, 26248, 32768, 0], - [4366, 12261, 18864, 32768, 0], - [31581, 32723, 32748, 32768, 0], - [21373, 31586, 32525, 32768, 0], - [12744, 26625, 30885, 32768, 0], - [7431, 20322, 26950, 32768, 0], - [4692, 13323, 20111, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - [ - [7833, 18369, 24095, 32768, 0], - [26650, 32273, 32702, 32768, 0], - [16371, 29961, 32191, 32768, 0], - [11055, 24082, 29629, 32768, 0], - [6892, 18644, 25400, 32768, 0], - [5006, 13057, 19240, 32768, 0], - [29834, 32666, 32748, 32768, 0], - [19577, 31335, 32570, 32768, 0], - [12253, 26509, 31122, 32768, 0], - [7991, 20772, 27711, 32768, 0], - [5677, 15910, 23059, 32768, 0], - [30109, 32532, 32720, 32768, 0], - [16747, 30166, 32252, 32768, 0], - [10134, 23542, 29184, 32768, 0], - [5791, 16176, 23556, 32768, 0], - [4362, 10414, 17284, 32768, 0], - [29492, 32626, 32748, 32768, 0], - [19894, 31402, 32525, 32768, 0], - [12942, 27071, 30869, 32768, 0], - [8346, 21216, 27405, 32768, 0], - [6572, 17087, 23859, 32768, 0], - [32035, 32735, 32748, 32768, 0], - [22957, 31838, 32618, 32768, 0], - [14724, 28572, 31772, 32768, 0], - [10364, 23999, 29553, 32768, 0], - [7004, 18433, 25655, 32768, 0], - [27528, 32277, 32681, 32768, 0], - [16959, 31171, 32096, 32768, 0], - [10486, 23593, 27962, 32768, 0], - [8192, 16384, 23211, 32768, 0], - [8937, 17873, 20852, 32768, 0], - [27715, 32002, 32615, 32768, 0], - [15073, 29491, 31676, 32768, 0], - [11264, 24576, 28672, 32768, 0], - [2341, 18725, 23406, 32768, 0], - [7282, 18204, 25486, 32768, 0], - [28547, 32213, 32657, 32768, 0], - [20788, 29773, 32239, 32768, 0], - [6780, 21469, 30508, 32768, 0], - [5958, 14895, 23831, 32768, 0], - [16384, 21845, 27307, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - ], - [ - [ - [5992, 14304, 19765, 32768, 0], - [22612, 31238, 32456, 32768, 0], - [13456, 27162, 31087, 32768, 0], - [8001, 20062, 26504, 32768, 0], - [5168, 14105, 20764, 32768, 0], - [2632, 7771, 12385, 32768, 0], - [27034, 32344, 32709, 32768, 0], - [15850, 29415, 31997, 32768, 0], - [9494, 22776, 28841, 32768, 0], - [6151, 16830, 23969, 32768, 0], - [3461, 10039, 15722, 32768, 0], - [30134, 32569, 32731, 32768, 0], - [15638, 29422, 31945, 32768, 0], - [9150, 21865, 28218, 32768, 0], - [5647, 15719, 22676, 32768, 0], - [3402, 9772, 15477, 32768, 0], - [28530, 32586, 32735, 32768, 0], - [17139, 30298, 32292, 32768, 0], - [10200, 24039, 29685, 32768, 0], - [6419, 17674, 24786, 32768, 0], - [3544, 10225, 15824, 32768, 0], - [31333, 32726, 32748, 32768, 0], - [20618, 31487, 32544, 32768, 0], - [12901, 27217, 31232, 32768, 0], - [8624, 21734, 28171, 32768, 0], - [5104, 14191, 20748, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - [ - [11206, 21090, 26561, 32768, 0], - [28759, 32279, 32671, 32768, 0], - [14171, 27952, 31569, 32768, 0], - [9743, 22907, 29141, 32768, 0], - [6871, 17886, 24868, 32768, 0], - [4960, 13152, 19315, 32768, 0], - [31077, 32661, 32748, 32768, 0], - [19400, 31195, 32515, 32768, 0], - [12752, 26858, 31040, 32768, 0], - [8370, 22098, 28591, 32768, 0], - [5457, 15373, 22298, 32768, 0], - [31697, 32706, 32748, 32768, 0], - [17860, 30657, 32333, 32768, 0], - [12510, 24812, 29261, 32768, 0], - [6180, 19124, 24722, 32768, 0], - [5041, 13548, 17959, 32768, 0], - [31552, 32716, 32748, 32768, 0], - [21908, 31769, 32623, 32768, 0], - [14470, 28201, 31565, 32768, 0], - [9493, 22982, 28608, 32768, 0], - [6858, 17240, 24137, 32768, 0], - [32543, 32752, 32756, 32768, 0], - [24286, 32097, 32666, 32768, 0], - [15958, 29217, 32024, 32768, 0], - [10207, 24234, 29958, 32768, 0], - [6929, 18305, 25652, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - ], - [ - [ - [4137, 10847, 15682, 32768, 0], - [17824, 27001, 30058, 32768, 0], - [10204, 22796, 28291, 32768, 0], - [6076, 15935, 22125, 32768, 0], - [3852, 10937, 16816, 32768, 0], - [2252, 6324, 10131, 32768, 0], - [25840, 32016, 32662, 32768, 0], - [15109, 28268, 31531, 32768, 0], - [9385, 22231, 28340, 32768, 0], - [6082, 16672, 23479, 32768, 0], - [3318, 9427, 14681, 32768, 0], - [30594, 32574, 32718, 32768, 0], - [16836, 29552, 31859, 32768, 0], - [9556, 22542, 28356, 32768, 0], - [6305, 16725, 23540, 32768, 0], - [3376, 9895, 15184, 32768, 0], - [29383, 32617, 32745, 32768, 0], - [18891, 30809, 32401, 32768, 0], - [11688, 25942, 30687, 32768, 0], - [7468, 19469, 26651, 32768, 0], - [3909, 11358, 17012, 32768, 0], - [31564, 32736, 32748, 32768, 0], - [20906, 31611, 32600, 32768, 0], - [13191, 27621, 31537, 32768, 0], - [8768, 22029, 28676, 32768, 0], - [5079, 14109, 20906, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - [ - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - ], - ], -]; - -pub static DEFAULT_COEFF_BASE_EOB_CDF: [[[[[u16; 4]; 4]; 2]; 5]; 4] = [ - [ - [ - [[17837, 29055, 32768, 0], [29600, 31446, 32768, 0], [30844, 31878, 32768, 0], [24926, 28948, 32768, 0]], - [[21365, 30026, 32768, 0], [30512, 32423, 32768, 0], [31658, 32621, 32768, 0], [29630, 31881, 32768, 0]], - ], - [ - [[5717, 26477, 32768, 0], [30491, 31703, 32768, 0], [31550, 32158, 32768, 0], [29648, 31491, 32768, 0]], - [[12608, 27820, 32768, 0], [30680, 32225, 32768, 0], [30809, 32335, 32768, 0], [31299, 32423, 32768, 0]], - ], - [ - [[1786, 12612, 32768, 0], [30663, 31625, 32768, 0], [32339, 32468, 32768, 0], [31148, 31833, 32768, 0]], - [[18857, 23865, 32768, 0], [31428, 32428, 32768, 0], [31744, 32373, 32768, 0], [31775, 32526, 32768, 0]], - ], - [ - [[1787, 2532, 32768, 0], [30832, 31662, 32768, 0], [31824, 32682, 32768, 0], [32133, 32569, 32768, 0]], - [[13751, 22235, 32768, 0], [32089, 32409, 32768, 0], [27084, 27920, 32768, 0], [29291, 32594, 32768, 0]], - ], - [ - [[1725, 3449, 32768, 0], [31102, 31935, 32768, 0], [32457, 32613, 32768, 0], [32412, 32649, 32768, 0]], - [[10923, 21845, 32768, 0], [10923, 21845, 32768, 0], [10923, 21845, 32768, 0], [10923, 21845, 32768, 0]], - ], - ], - [ - [ - [[17560, 29888, 32768, 0], [29671, 31549, 32768, 0], [31007, 32056, 32768, 0], [27286, 30006, 32768, 0]], - [[26594, 31212, 32768, 0], [31208, 32582, 32768, 0], [31835, 32637, 32768, 0], [30595, 32206, 32768, 0]], - ], - [ - [[15239, 29932, 32768, 0], [31315, 32095, 32768, 0], [32130, 32434, 32768, 0], [30864, 31996, 32768, 0]], - [[26279, 30968, 32768, 0], [31142, 32495, 32768, 0], [31713, 32540, 32768, 0], [31929, 32594, 32768, 0]], - ], - [ - [[2644, 25198, 32768, 0], [32038, 32451, 32768, 0], [32639, 32695, 32768, 0], [32166, 32518, 32768, 0]], - [[17187, 27668, 32768, 0], [31714, 32550, 32768, 0], [32283, 32678, 32768, 0], [31930, 32563, 32768, 0]], - ], - [ - [[1044, 2257, 32768, 0], [30755, 31923, 32768, 0], [32208, 32693, 32768, 0], [32244, 32615, 32768, 0]], - [[21317, 26207, 32768, 0], [29133, 30868, 32768, 0], [29311, 31231, 32768, 0], [29657, 31087, 32768, 0]], - ], - [ - [[478, 1834, 32768, 0], [31005, 31987, 32768, 0], [32317, 32724, 32768, 0], [30865, 32648, 32768, 0]], - [[10923, 21845, 32768, 0], [10923, 21845, 32768, 0], [10923, 21845, 32768, 0], [10923, 21845, 32768, 0]], - ], - ], - [ - [ - [[20092, 30774, 32768, 0], [30695, 32020, 32768, 0], [31131, 32103, 32768, 0], [28666, 30870, 32768, 0]], - [[27258, 31095, 32768, 0], [31804, 32623, 32768, 0], [31763, 32528, 32768, 0], [31438, 32506, 32768, 0]], - ], - [ - [[18049, 30489, 32768, 0], [31706, 32286, 32768, 0], [32163, 32473, 32768, 0], [31550, 32184, 32768, 0]], - [[27116, 30842, 32768, 0], [31971, 32598, 32768, 0], [32088, 32576, 32768, 0], [32067, 32664, 32768, 0]], - ], - [ - [[12854, 29093, 32768, 0], [32272, 32558, 32768, 0], [32667, 32729, 32768, 0], [32306, 32585, 32768, 0]], - [[25476, 30366, 32768, 0], [32169, 32687, 32768, 0], [32479, 32689, 32768, 0], [31673, 32634, 32768, 0]], - ], - [ - [[2809, 19301, 32768, 0], [32205, 32622, 32768, 0], [32338, 32730, 32768, 0], [31786, 32616, 32768, 0]], - [[22737, 29105, 32768, 0], [30810, 32362, 32768, 0], [30014, 32627, 32768, 0], [30528, 32574, 32768, 0]], - ], - [ - [[935, 3382, 32768, 0], [30789, 31909, 32768, 0], [32466, 32756, 32768, 0], [30860, 32513, 32768, 0]], - [[10923, 21845, 32768, 0], [10923, 21845, 32768, 0], [10923, 21845, 32768, 0], [10923, 21845, 32768, 0]], - ], - ], - [ - [ - [[22497, 31198, 32768, 0], [31715, 32495, 32768, 0], [31606, 32337, 32768, 0], [30388, 31990, 32768, 0]], - [[27877, 31584, 32768, 0], [32170, 32728, 32768, 0], [32155, 32688, 32768, 0], [32219, 32702, 32768, 0]], - ], - [ - [[21457, 31043, 32768, 0], [31951, 32483, 32768, 0], [32153, 32562, 32768, 0], [31473, 32215, 32768, 0]], - [[27558, 31151, 32768, 0], [32020, 32640, 32768, 0], [32097, 32575, 32768, 0], [32242, 32719, 32768, 0]], - ], - [ - [[19980, 30591, 32768, 0], [32219, 32597, 32768, 0], [32581, 32706, 32768, 0], [31803, 32287, 32768, 0]], - [[26473, 30507, 32768, 0], [32431, 32723, 32768, 0], [32196, 32611, 32768, 0], [31588, 32528, 32768, 0]], - ], - [ - [[24647, 30463, 32768, 0], [32412, 32695, 32768, 0], [32468, 32720, 32768, 0], [31269, 32523, 32768, 0]], - [[28482, 31505, 32768, 0], [32152, 32701, 32768, 0], [31732, 32598, 32768, 0], [31767, 32712, 32768, 0]], - ], - [ - [[12358, 24977, 32768, 0], [31331, 32385, 32768, 0], [32634, 32756, 32768, 0], [30411, 32548, 32768, 0]], - [[10923, 21845, 32768, 0], [10923, 21845, 32768, 0], [10923, 21845, 32768, 0], [10923, 21845, 32768, 0]], - ], - ], -]; - -pub static DEFAULT_COEFF_BR_CDF: [[[[[u16; 5]; 21]; 2]; 5]; 4] = [ - [ - [ - [ - [14298, 20718, 24174, 32768, 0], - [12536, 19601, 23789, 32768, 0], - [8712, 15051, 19503, 32768, 0], - [6170, 11327, 15434, 32768, 0], - [4742, 8926, 12538, 32768, 0], - [3803, 7317, 10546, 32768, 0], - [1696, 3317, 4871, 32768, 0], - [14392, 19951, 22756, 32768, 0], - [15978, 23218, 26818, 32768, 0], - [12187, 19474, 23889, 32768, 0], - [9176, 15640, 20259, 32768, 0], - [7068, 12655, 17028, 32768, 0], - [5656, 10442, 14472, 32768, 0], - [2580, 4992, 7244, 32768, 0], - [12136, 18049, 21426, 32768, 0], - [13784, 20721, 24481, 32768, 0], - [10836, 17621, 21900, 32768, 0], - [8372, 14444, 18847, 32768, 0], - [6523, 11779, 16000, 32768, 0], - [5337, 9898, 13760, 32768, 0], - [3034, 5860, 8462, 32768, 0], - ], - [ - [15967, 22905, 26286, 32768, 0], - [13534, 20654, 24579, 32768, 0], - [9504, 16092, 20535, 32768, 0], - [6975, 12568, 16903, 32768, 0], - [5364, 10091, 14020, 32768, 0], - [4357, 8370, 11857, 32768, 0], - [2506, 4934, 7218, 32768, 0], - [23032, 28815, 30936, 32768, 0], - [19540, 26704, 29719, 32768, 0], - [15158, 22969, 27097, 32768, 0], - [11408, 18865, 23650, 32768, 0], - [8885, 15448, 20250, 32768, 0], - [7108, 12853, 17416, 32768, 0], - [4231, 8041, 11480, 32768, 0], - [19823, 26490, 29156, 32768, 0], - [18890, 25929, 28932, 32768, 0], - [15660, 23491, 27433, 32768, 0], - [12147, 19776, 24488, 32768, 0], - [9728, 16774, 21649, 32768, 0], - [7919, 14277, 19066, 32768, 0], - [5440, 10170, 14185, 32768, 0], - ], - ], - [ - [ - [14406, 20862, 24414, 32768, 0], - [11824, 18907, 23109, 32768, 0], - [8257, 14393, 18803, 32768, 0], - [5860, 10747, 14778, 32768, 0], - [4475, 8486, 11984, 32768, 0], - [3606, 6954, 10043, 32768, 0], - [1736, 3410, 5048, 32768, 0], - [14430, 20046, 22882, 32768, 0], - [15593, 22899, 26709, 32768, 0], - [12102, 19368, 23811, 32768, 0], - [9059, 15584, 20262, 32768, 0], - [6999, 12603, 17048, 32768, 0], - [5684, 10497, 14553, 32768, 0], - [2822, 5438, 7862, 32768, 0], - [15785, 21585, 24359, 32768, 0], - [18347, 25229, 28266, 32768, 0], - [14974, 22487, 26389, 32768, 0], - [11423, 18681, 23271, 32768, 0], - [8863, 15350, 20008, 32768, 0], - [7153, 12852, 17278, 32768, 0], - [3707, 7036, 9982, 32768, 0], - ], - [ - [15460, 21696, 25469, 32768, 0], - [12170, 19249, 23191, 32768, 0], - [8723, 15027, 19332, 32768, 0], - [6428, 11704, 15874, 32768, 0], - [4922, 9292, 13052, 32768, 0], - [4139, 7695, 11010, 32768, 0], - [2291, 4508, 6598, 32768, 0], - [19856, 26920, 29828, 32768, 0], - [17923, 25289, 28792, 32768, 0], - [14278, 21968, 26297, 32768, 0], - [10910, 18136, 22950, 32768, 0], - [8423, 14815, 19627, 32768, 0], - [6771, 12283, 16774, 32768, 0], - [4074, 7750, 11081, 32768, 0], - [19852, 26074, 28672, 32768, 0], - [19371, 26110, 28989, 32768, 0], - [16265, 23873, 27663, 32768, 0], - [12758, 20378, 24952, 32768, 0], - [10095, 17098, 21961, 32768, 0], - [8250, 14628, 19451, 32768, 0], - [5205, 9745, 13622, 32768, 0], - ], - ], - [ - [ - [10563, 16233, 19763, 32768, 0], - [9794, 16022, 19804, 32768, 0], - [6750, 11945, 15759, 32768, 0], - [4963, 9186, 12752, 32768, 0], - [3845, 7435, 10627, 32768, 0], - [3051, 6085, 8834, 32768, 0], - [1311, 2596, 3830, 32768, 0], - [11246, 16404, 19689, 32768, 0], - [12315, 18911, 22731, 32768, 0], - [10557, 17095, 21289, 32768, 0], - [8136, 14006, 18249, 32768, 0], - [6348, 11474, 15565, 32768, 0], - [5196, 9655, 13400, 32768, 0], - [2349, 4526, 6587, 32768, 0], - [13337, 18730, 21569, 32768, 0], - [19306, 26071, 28882, 32768, 0], - [15952, 23540, 27254, 32768, 0], - [12409, 19934, 24430, 32768, 0], - [9760, 16706, 21389, 32768, 0], - [8004, 14220, 18818, 32768, 0], - [4138, 7794, 10961, 32768, 0], - ], - [ - [10870, 16684, 20949, 32768, 0], - [9664, 15230, 18680, 32768, 0], - [6886, 12109, 15408, 32768, 0], - [4825, 8900, 12305, 32768, 0], - [3630, 7162, 10314, 32768, 0], - [3036, 6429, 9387, 32768, 0], - [1671, 3296, 4940, 32768, 0], - [13819, 19159, 23026, 32768, 0], - [11984, 19108, 23120, 32768, 0], - [10690, 17210, 21663, 32768, 0], - [7984, 14154, 18333, 32768, 0], - [6868, 12294, 16124, 32768, 0], - [5274, 8994, 12868, 32768, 0], - [2988, 5771, 8424, 32768, 0], - [19736, 26647, 29141, 32768, 0], - [18933, 26070, 28984, 32768, 0], - [15779, 23048, 27200, 32768, 0], - [12638, 20061, 24532, 32768, 0], - [10692, 17545, 22220, 32768, 0], - [9217, 15251, 20054, 32768, 0], - [5078, 9284, 12594, 32768, 0], - ], - ], - [ - [ - [2331, 3662, 5244, 32768, 0], - [2891, 4771, 6145, 32768, 0], - [4598, 7623, 9729, 32768, 0], - [3520, 6845, 9199, 32768, 0], - [3417, 6119, 9324, 32768, 0], - [2601, 5412, 7385, 32768, 0], - [600, 1173, 1744, 32768, 0], - [7672, 13286, 17469, 32768, 0], - [4232, 7792, 10793, 32768, 0], - [2915, 5317, 7397, 32768, 0], - [2318, 4356, 6152, 32768, 0], - [2127, 4000, 5554, 32768, 0], - [1850, 3478, 5275, 32768, 0], - [977, 1933, 2843, 32768, 0], - [18280, 24387, 27989, 32768, 0], - [15852, 22671, 26185, 32768, 0], - [13845, 20951, 24789, 32768, 0], - [11055, 17966, 22129, 32768, 0], - [9138, 15422, 19801, 32768, 0], - [7454, 13145, 17456, 32768, 0], - [3370, 6393, 9013, 32768, 0], - ], - [ - [5842, 9229, 10838, 32768, 0], - [2313, 3491, 4276, 32768, 0], - [2998, 6104, 7496, 32768, 0], - [2420, 7447, 9868, 32768, 0], - [3034, 8495, 10923, 32768, 0], - [4076, 8937, 10975, 32768, 0], - [1086, 2370, 3299, 32768, 0], - [9714, 17254, 20444, 32768, 0], - [8543, 13698, 17123, 32768, 0], - [4918, 9007, 11910, 32768, 0], - [4129, 7532, 10553, 32768, 0], - [2364, 5533, 8058, 32768, 0], - [1834, 3546, 5563, 32768, 0], - [1473, 2908, 4133, 32768, 0], - [15405, 21193, 25619, 32768, 0], - [15691, 21952, 26561, 32768, 0], - [12962, 19194, 24165, 32768, 0], - [10272, 17855, 22129, 32768, 0], - [8588, 15270, 20718, 32768, 0], - [8682, 14669, 19500, 32768, 0], - [4870, 9636, 13205, 32768, 0], - ], - ], - [ - [ - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - [ - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - ], - ], - [ - [ - [ - [14995, 21341, 24749, 32768, 0], - [13158, 20289, 24601, 32768, 0], - [8941, 15326, 19876, 32768, 0], - [6297, 11541, 15807, 32768, 0], - [4817, 9029, 12776, 32768, 0], - [3731, 7273, 10627, 32768, 0], - [1847, 3617, 5354, 32768, 0], - [14472, 19659, 22343, 32768, 0], - [16806, 24162, 27533, 32768, 0], - [12900, 20404, 24713, 32768, 0], - [9411, 16112, 20797, 32768, 0], - [7056, 12697, 17148, 32768, 0], - [5544, 10339, 14460, 32768, 0], - [2954, 5704, 8319, 32768, 0], - [12464, 18071, 21354, 32768, 0], - [15482, 22528, 26034, 32768, 0], - [12070, 19269, 23624, 32768, 0], - [8953, 15406, 20106, 32768, 0], - [7027, 12730, 17220, 32768, 0], - [5887, 10913, 15140, 32768, 0], - [3793, 7278, 10447, 32768, 0], - ], - [ - [15571, 22232, 25749, 32768, 0], - [14506, 21575, 25374, 32768, 0], - [10189, 17089, 21569, 32768, 0], - [7316, 13301, 17915, 32768, 0], - [5783, 10912, 15190, 32768, 0], - [4760, 9155, 13088, 32768, 0], - [2993, 5966, 8774, 32768, 0], - [23424, 28903, 30778, 32768, 0], - [20775, 27666, 30290, 32768, 0], - [16474, 24410, 28299, 32768, 0], - [12471, 20180, 24987, 32768, 0], - [9410, 16487, 21439, 32768, 0], - [7536, 13614, 18529, 32768, 0], - [5048, 9586, 13549, 32768, 0], - [21090, 27290, 29756, 32768, 0], - [20796, 27402, 30026, 32768, 0], - [17819, 25485, 28969, 32768, 0], - [13860, 21909, 26462, 32768, 0], - [11002, 18494, 23529, 32768, 0], - [8953, 15929, 20897, 32768, 0], - [6448, 11918, 16454, 32768, 0], - ], - ], - [ - [ - [15999, 22208, 25449, 32768, 0], - [13050, 19988, 24122, 32768, 0], - [8594, 14864, 19378, 32768, 0], - [6033, 11079, 15238, 32768, 0], - [4554, 8683, 12347, 32768, 0], - [3672, 7139, 10337, 32768, 0], - [1900, 3771, 5576, 32768, 0], - [15788, 21340, 23949, 32768, 0], - [16825, 24235, 27758, 32768, 0], - [12873, 20402, 24810, 32768, 0], - [9590, 16363, 21094, 32768, 0], - [7352, 13209, 17733, 32768, 0], - [5960, 10989, 15184, 32768, 0], - [3232, 6234, 9007, 32768, 0], - [15761, 20716, 23224, 32768, 0], - [19318, 25989, 28759, 32768, 0], - [15529, 23094, 26929, 32768, 0], - [11662, 18989, 23641, 32768, 0], - [8955, 15568, 20366, 32768, 0], - [7281, 13106, 17708, 32768, 0], - [4248, 8059, 11440, 32768, 0], - ], - [ - [14899, 21217, 24503, 32768, 0], - [13519, 20283, 24047, 32768, 0], - [9429, 15966, 20365, 32768, 0], - [6700, 12355, 16652, 32768, 0], - [5088, 9704, 13716, 32768, 0], - [4243, 8154, 11731, 32768, 0], - [2702, 5364, 7861, 32768, 0], - [22745, 28388, 30454, 32768, 0], - [20235, 27146, 29922, 32768, 0], - [15896, 23715, 27637, 32768, 0], - [11840, 19350, 24131, 32768, 0], - [9122, 15932, 20880, 32768, 0], - [7488, 13581, 18362, 32768, 0], - [5114, 9568, 13370, 32768, 0], - [20845, 26553, 28932, 32768, 0], - [20981, 27372, 29884, 32768, 0], - [17781, 25335, 28785, 32768, 0], - [13760, 21708, 26297, 32768, 0], - [10975, 18415, 23365, 32768, 0], - [9045, 15789, 20686, 32768, 0], - [6130, 11199, 15423, 32768, 0], - ], - ], - [ - [ - [13549, 19724, 23158, 32768, 0], - [11844, 18382, 22246, 32768, 0], - [7919, 13619, 17773, 32768, 0], - [5486, 10143, 13946, 32768, 0], - [4166, 7983, 11324, 32768, 0], - [3364, 6506, 9427, 32768, 0], - [1598, 3160, 4674, 32768, 0], - [15281, 20979, 23781, 32768, 0], - [14939, 22119, 25952, 32768, 0], - [11363, 18407, 22812, 32768, 0], - [8609, 14857, 19370, 32768, 0], - [6737, 12184, 16480, 32768, 0], - [5506, 10263, 14262, 32768, 0], - [2990, 5786, 8380, 32768, 0], - [20249, 25253, 27417, 32768, 0], - [21070, 27518, 30001, 32768, 0], - [16854, 24469, 28074, 32768, 0], - [12864, 20486, 25000, 32768, 0], - [9962, 16978, 21778, 32768, 0], - [8074, 14338, 19048, 32768, 0], - [4494, 8479, 11906, 32768, 0], - ], - [ - [13960, 19617, 22829, 32768, 0], - [11150, 17341, 21228, 32768, 0], - [7150, 12964, 17190, 32768, 0], - [5331, 10002, 13867, 32768, 0], - [4167, 7744, 11057, 32768, 0], - [3480, 6629, 9646, 32768, 0], - [1883, 3784, 5686, 32768, 0], - [18752, 25660, 28912, 32768, 0], - [16968, 24586, 28030, 32768, 0], - [13520, 21055, 25313, 32768, 0], - [10453, 17626, 22280, 32768, 0], - [8386, 14505, 19116, 32768, 0], - [6742, 12595, 17008, 32768, 0], - [4273, 8140, 11499, 32768, 0], - [22120, 27827, 30233, 32768, 0], - [20563, 27358, 29895, 32768, 0], - [17076, 24644, 28153, 32768, 0], - [13362, 20942, 25309, 32768, 0], - [10794, 17965, 22695, 32768, 0], - [9014, 15652, 20319, 32768, 0], - [5708, 10512, 14497, 32768, 0], - ], - ], - [ - [ - [5705, 10930, 15725, 32768, 0], - [7946, 12765, 16115, 32768, 0], - [6801, 12123, 16226, 32768, 0], - [5462, 10135, 14200, 32768, 0], - [4189, 8011, 11507, 32768, 0], - [3191, 6229, 9408, 32768, 0], - [1057, 2137, 3212, 32768, 0], - [10018, 17067, 21491, 32768, 0], - [7380, 12582, 16453, 32768, 0], - [6068, 10845, 14339, 32768, 0], - [5098, 9198, 12555, 32768, 0], - [4312, 8010, 11119, 32768, 0], - [3700, 6966, 9781, 32768, 0], - [1693, 3326, 4887, 32768, 0], - [18757, 24930, 27774, 32768, 0], - [17648, 24596, 27817, 32768, 0], - [14707, 22052, 26026, 32768, 0], - [11720, 18852, 23292, 32768, 0], - [9357, 15952, 20525, 32768, 0], - [7810, 13753, 18210, 32768, 0], - [3879, 7333, 10328, 32768, 0], - ], - [ - [8278, 13242, 15922, 32768, 0], - [10547, 15867, 18919, 32768, 0], - [9106, 15842, 20609, 32768, 0], - [6833, 13007, 17218, 32768, 0], - [4811, 9712, 13923, 32768, 0], - [3985, 7352, 11128, 32768, 0], - [1688, 3458, 5262, 32768, 0], - [12951, 21861, 26510, 32768, 0], - [9788, 16044, 20276, 32768, 0], - [6309, 11244, 14870, 32768, 0], - [5183, 9349, 12566, 32768, 0], - [4389, 8229, 11492, 32768, 0], - [3633, 6945, 10620, 32768, 0], - [3600, 6847, 9907, 32768, 0], - [21748, 28137, 30255, 32768, 0], - [19436, 26581, 29560, 32768, 0], - [16359, 24201, 27953, 32768, 0], - [13961, 21693, 25871, 32768, 0], - [11544, 18686, 23322, 32768, 0], - [9372, 16462, 20952, 32768, 0], - [6138, 11210, 15390, 32768, 0], - ], - ], - [ - [ - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - [ - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - ], - ], - [ - [ - [ - [16138, 22223, 25509, 32768, 0], - [15347, 22430, 26332, 32768, 0], - [9614, 16736, 21332, 32768, 0], - [6600, 12275, 16907, 32768, 0], - [4811, 9424, 13547, 32768, 0], - [3748, 7809, 11420, 32768, 0], - [2254, 4587, 6890, 32768, 0], - [15196, 20284, 23177, 32768, 0], - [18317, 25469, 28451, 32768, 0], - [13918, 21651, 25842, 32768, 0], - [10052, 17150, 21995, 32768, 0], - [7499, 13630, 18587, 32768, 0], - [6158, 11417, 16003, 32768, 0], - [4014, 7785, 11252, 32768, 0], - [15048, 21067, 24384, 32768, 0], - [18202, 25346, 28553, 32768, 0], - [14302, 22019, 26356, 32768, 0], - [10839, 18139, 23166, 32768, 0], - [8715, 15744, 20806, 32768, 0], - [7536, 13576, 18544, 32768, 0], - [5413, 10335, 14498, 32768, 0], - ], - [ - [17394, 24501, 27895, 32768, 0], - [15889, 23420, 27185, 32768, 0], - [11561, 19133, 23870, 32768, 0], - [8285, 14812, 19844, 32768, 0], - [6496, 12043, 16550, 32768, 0], - [4771, 9574, 13677, 32768, 0], - [3603, 6830, 10144, 32768, 0], - [21656, 27704, 30200, 32768, 0], - [21324, 27915, 30511, 32768, 0], - [17327, 25336, 28997, 32768, 0], - [13417, 21381, 26033, 32768, 0], - [10132, 17425, 22338, 32768, 0], - [8580, 15016, 19633, 32768, 0], - [5694, 11477, 16411, 32768, 0], - [24116, 29780, 31450, 32768, 0], - [23853, 29695, 31591, 32768, 0], - [20085, 27614, 30428, 32768, 0], - [15326, 24335, 28575, 32768, 0], - [11814, 19472, 24810, 32768, 0], - [10221, 18611, 24767, 32768, 0], - [7689, 14558, 20321, 32768, 0], - ], - ], - [ - [ - [16214, 22380, 25770, 32768, 0], - [14213, 21304, 25295, 32768, 0], - [9213, 15823, 20455, 32768, 0], - [6395, 11758, 16139, 32768, 0], - [4779, 9187, 13066, 32768, 0], - [3821, 7501, 10953, 32768, 0], - [2293, 4567, 6795, 32768, 0], - [15859, 21283, 23820, 32768, 0], - [18404, 25602, 28726, 32768, 0], - [14325, 21980, 26206, 32768, 0], - [10669, 17937, 22720, 32768, 0], - [8297, 14642, 19447, 32768, 0], - [6746, 12389, 16893, 32768, 0], - [4324, 8251, 11770, 32768, 0], - [16532, 21631, 24475, 32768, 0], - [20667, 27150, 29668, 32768, 0], - [16728, 24510, 28175, 32768, 0], - [12861, 20645, 25332, 32768, 0], - [10076, 17361, 22417, 32768, 0], - [8395, 14940, 19963, 32768, 0], - [5731, 10683, 14912, 32768, 0], - ], - [ - [14433, 21155, 24938, 32768, 0], - [14658, 21716, 25545, 32768, 0], - [9923, 16824, 21557, 32768, 0], - [6982, 13052, 17721, 32768, 0], - [5419, 10503, 15050, 32768, 0], - [4852, 9162, 13014, 32768, 0], - [3271, 6395, 9630, 32768, 0], - [22210, 27833, 30109, 32768, 0], - [20750, 27368, 29821, 32768, 0], - [16894, 24828, 28573, 32768, 0], - [13247, 21276, 25757, 32768, 0], - [10038, 17265, 22563, 32768, 0], - [8587, 14947, 20327, 32768, 0], - [5645, 11371, 15252, 32768, 0], - [22027, 27526, 29714, 32768, 0], - [23098, 29146, 31221, 32768, 0], - [19886, 27341, 30272, 32768, 0], - [15609, 23747, 28046, 32768, 0], - [11993, 20065, 24939, 32768, 0], - [9637, 18267, 23671, 32768, 0], - [7625, 13801, 19144, 32768, 0], - ], - ], - [ - [ - [14438, 20798, 24089, 32768, 0], - [12621, 19203, 23097, 32768, 0], - [8177, 14125, 18402, 32768, 0], - [5674, 10501, 14456, 32768, 0], - [4236, 8239, 11733, 32768, 0], - [3447, 6750, 9806, 32768, 0], - [1986, 3950, 5864, 32768, 0], - [16208, 22099, 24930, 32768, 0], - [16537, 24025, 27585, 32768, 0], - [12780, 20381, 24867, 32768, 0], - [9767, 16612, 21416, 32768, 0], - [7686, 13738, 18398, 32768, 0], - [6333, 11614, 15964, 32768, 0], - [3941, 7571, 10836, 32768, 0], - [22819, 27422, 29202, 32768, 0], - [22224, 28514, 30721, 32768, 0], - [17660, 25433, 28913, 32768, 0], - [13574, 21482, 26002, 32768, 0], - [10629, 17977, 22938, 32768, 0], - [8612, 15298, 20265, 32768, 0], - [5607, 10491, 14596, 32768, 0], - ], - [ - [13569, 19800, 23206, 32768, 0], - [13128, 19924, 23869, 32768, 0], - [8329, 14841, 19403, 32768, 0], - [6130, 10976, 15057, 32768, 0], - [4682, 8839, 12518, 32768, 0], - [3656, 7409, 10588, 32768, 0], - [2577, 5099, 7412, 32768, 0], - [22427, 28684, 30585, 32768, 0], - [20913, 27750, 30139, 32768, 0], - [15840, 24109, 27834, 32768, 0], - [12308, 20029, 24569, 32768, 0], - [10216, 16785, 21458, 32768, 0], - [8309, 14203, 19113, 32768, 0], - [6043, 11168, 15307, 32768, 0], - [23166, 28901, 30998, 32768, 0], - [21899, 28405, 30751, 32768, 0], - [18413, 26091, 29443, 32768, 0], - [15233, 23114, 27352, 32768, 0], - [12683, 20472, 25288, 32768, 0], - [10702, 18259, 23409, 32768, 0], - [8125, 14464, 19226, 32768, 0], - ], - ], - [ - [ - [9040, 14786, 18360, 32768, 0], - [9979, 15718, 19415, 32768, 0], - [7913, 13918, 18311, 32768, 0], - [5859, 10889, 15184, 32768, 0], - [4593, 8677, 12510, 32768, 0], - [3820, 7396, 10791, 32768, 0], - [1730, 3471, 5192, 32768, 0], - [11803, 18365, 22709, 32768, 0], - [11419, 18058, 22225, 32768, 0], - [9418, 15774, 20243, 32768, 0], - [7539, 13325, 17657, 32768, 0], - [6233, 11317, 15384, 32768, 0], - [5137, 9656, 13545, 32768, 0], - [2977, 5774, 8349, 32768, 0], - [21207, 27246, 29640, 32768, 0], - [19547, 26578, 29497, 32768, 0], - [16169, 23871, 27690, 32768, 0], - [12820, 20458, 25018, 32768, 0], - [10224, 17332, 22214, 32768, 0], - [8526, 15048, 19884, 32768, 0], - [5037, 9410, 13118, 32768, 0], - ], - [ - [12339, 17329, 20140, 32768, 0], - [13505, 19895, 23225, 32768, 0], - [9847, 16944, 21564, 32768, 0], - [7280, 13256, 18348, 32768, 0], - [4712, 10009, 14454, 32768, 0], - [4361, 7914, 12477, 32768, 0], - [2870, 5628, 7995, 32768, 0], - [20061, 25504, 28526, 32768, 0], - [15235, 22878, 26145, 32768, 0], - [12985, 19958, 24155, 32768, 0], - [9782, 16641, 21403, 32768, 0], - [9456, 16360, 20760, 32768, 0], - [6855, 12940, 18557, 32768, 0], - [5661, 10564, 15002, 32768, 0], - [25656, 30602, 31894, 32768, 0], - [22570, 29107, 31092, 32768, 0], - [18917, 26423, 29541, 32768, 0], - [15940, 23649, 27754, 32768, 0], - [12803, 20581, 25219, 32768, 0], - [11082, 18695, 23376, 32768, 0], - [7939, 14373, 19005, 32768, 0], - ], - ], - [ - [ - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - [ - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - ], - ], - [ - [ - [ - [18315, 24289, 27551, 32768, 0], - [16854, 24068, 27835, 32768, 0], - [10140, 17927, 23173, 32768, 0], - [6722, 12982, 18267, 32768, 0], - [4661, 9826, 14706, 32768, 0], - [3832, 8165, 12294, 32768, 0], - [2795, 6098, 9245, 32768, 0], - [17145, 23326, 26672, 32768, 0], - [20733, 27680, 30308, 32768, 0], - [16032, 24461, 28546, 32768, 0], - [11653, 20093, 25081, 32768, 0], - [9290, 16429, 22086, 32768, 0], - [7796, 14598, 19982, 32768, 0], - [6502, 12378, 17441, 32768, 0], - [21681, 27732, 30320, 32768, 0], - [22389, 29044, 31261, 32768, 0], - [19027, 26731, 30087, 32768, 0], - [14739, 23755, 28624, 32768, 0], - [11358, 20778, 25511, 32768, 0], - [10995, 18073, 24190, 32768, 0], - [9162, 14990, 20617, 32768, 0], - ], - [ - [21425, 27952, 30388, 32768, 0], - [18062, 25838, 29034, 32768, 0], - [11956, 19881, 24808, 32768, 0], - [7718, 15000, 20980, 32768, 0], - [5702, 11254, 16143, 32768, 0], - [4898, 9088, 16864, 32768, 0], - [3679, 6776, 11907, 32768, 0], - [23294, 30160, 31663, 32768, 0], - [24397, 29896, 31836, 32768, 0], - [19245, 27128, 30593, 32768, 0], - [13202, 19825, 26404, 32768, 0], - [11578, 19297, 23957, 32768, 0], - [8073, 13297, 21370, 32768, 0], - [5461, 10923, 19745, 32768, 0], - [27367, 30521, 31934, 32768, 0], - [24904, 30671, 31940, 32768, 0], - [23075, 28460, 31299, 32768, 0], - [14400, 23658, 30417, 32768, 0], - [13885, 23882, 28325, 32768, 0], - [14746, 22938, 27853, 32768, 0], - [5461, 16384, 27307, 32768, 0], - ], - ], - [ - [ - [18274, 24813, 27890, 32768, 0], - [15537, 23149, 27003, 32768, 0], - [9449, 16740, 21827, 32768, 0], - [6700, 12498, 17261, 32768, 0], - [4988, 9866, 14198, 32768, 0], - [4236, 8147, 11902, 32768, 0], - [2867, 5860, 8654, 32768, 0], - [17124, 23171, 26101, 32768, 0], - [20396, 27477, 30148, 32768, 0], - [16573, 24629, 28492, 32768, 0], - [12749, 20846, 25674, 32768, 0], - [10233, 17878, 22818, 32768, 0], - [8525, 15332, 20363, 32768, 0], - [6283, 11632, 16255, 32768, 0], - [20466, 26511, 29286, 32768, 0], - [23059, 29174, 31191, 32768, 0], - [19481, 27263, 30241, 32768, 0], - [15458, 23631, 28137, 32768, 0], - [12416, 20608, 25693, 32768, 0], - [10261, 18011, 23261, 32768, 0], - [8016, 14655, 19666, 32768, 0], - ], - [ - [17616, 24586, 28112, 32768, 0], - [15809, 23299, 27155, 32768, 0], - [10767, 18890, 23793, 32768, 0], - [7727, 14255, 18865, 32768, 0], - [6129, 11926, 16882, 32768, 0], - [4482, 9704, 14861, 32768, 0], - [3277, 7452, 11522, 32768, 0], - [22956, 28551, 30730, 32768, 0], - [22724, 28937, 30961, 32768, 0], - [18467, 26324, 29580, 32768, 0], - [13234, 20713, 25649, 32768, 0], - [11181, 17592, 22481, 32768, 0], - [8291, 18358, 24576, 32768, 0], - [7568, 11881, 14984, 32768, 0], - [24948, 29001, 31147, 32768, 0], - [25674, 30619, 32151, 32768, 0], - [20841, 26793, 29603, 32768, 0], - [14669, 24356, 28666, 32768, 0], - [11334, 23593, 28219, 32768, 0], - [8922, 14762, 22873, 32768, 0], - [8301, 13544, 20535, 32768, 0], - ], - ], - [ - [ - [17113, 23733, 27081, 32768, 0], - [14139, 21406, 25452, 32768, 0], - [8552, 15002, 19776, 32768, 0], - [5871, 11120, 15378, 32768, 0], - [4455, 8616, 12253, 32768, 0], - [3469, 6910, 10386, 32768, 0], - [2255, 4553, 6782, 32768, 0], - [18224, 24376, 27053, 32768, 0], - [19290, 26710, 29614, 32768, 0], - [14936, 22991, 27184, 32768, 0], - [11238, 18951, 23762, 32768, 0], - [8786, 15617, 20588, 32768, 0], - [7317, 13228, 18003, 32768, 0], - [5101, 9512, 13493, 32768, 0], - [22639, 28222, 30210, 32768, 0], - [23216, 29331, 31307, 32768, 0], - [19075, 26762, 29895, 32768, 0], - [15014, 23113, 27457, 32768, 0], - [11938, 19857, 24752, 32768, 0], - [9942, 17280, 22282, 32768, 0], - [7167, 13144, 17752, 32768, 0], - ], - [ - [15820, 22738, 26488, 32768, 0], - [13530, 20885, 25216, 32768, 0], - [8395, 15530, 20452, 32768, 0], - [6574, 12321, 16380, 32768, 0], - [5353, 10419, 14568, 32768, 0], - [4613, 8446, 12381, 32768, 0], - [3440, 7158, 9903, 32768, 0], - [24247, 29051, 31224, 32768, 0], - [22118, 28058, 30369, 32768, 0], - [16498, 24768, 28389, 32768, 0], - [12920, 21175, 26137, 32768, 0], - [10730, 18619, 25352, 32768, 0], - [10187, 16279, 22791, 32768, 0], - [9310, 14631, 22127, 32768, 0], - [24970, 30558, 32057, 32768, 0], - [24801, 29942, 31698, 32768, 0], - [22432, 28453, 30855, 32768, 0], - [19054, 25680, 29580, 32768, 0], - [14392, 23036, 28109, 32768, 0], - [12495, 20947, 26650, 32768, 0], - [12442, 20326, 26214, 32768, 0], - ], - ], - [ - [ - [12162, 18785, 22648, 32768, 0], - [12749, 19697, 23806, 32768, 0], - [8580, 15297, 20346, 32768, 0], - [6169, 11749, 16543, 32768, 0], - [4836, 9391, 13448, 32768, 0], - [3821, 7711, 11613, 32768, 0], - [2228, 4601, 7070, 32768, 0], - [16319, 24725, 28280, 32768, 0], - [15698, 23277, 27168, 32768, 0], - [12726, 20368, 25047, 32768, 0], - [9912, 17015, 21976, 32768, 0], - [7888, 14220, 19179, 32768, 0], - [6777, 12284, 17018, 32768, 0], - [4492, 8590, 12252, 32768, 0], - [23249, 28904, 30947, 32768, 0], - [21050, 27908, 30512, 32768, 0], - [17440, 25340, 28949, 32768, 0], - [14059, 22018, 26541, 32768, 0], - [11288, 18903, 23898, 32768, 0], - [9411, 16342, 21428, 32768, 0], - [6278, 11588, 15944, 32768, 0], - ], - [ - [13981, 20067, 23226, 32768, 0], - [16922, 23580, 26783, 32768, 0], - [11005, 19039, 24487, 32768, 0], - [7389, 14218, 19798, 32768, 0], - [5598, 11505, 17206, 32768, 0], - [6090, 11213, 15659, 32768, 0], - [3820, 7371, 10119, 32768, 0], - [21082, 26925, 29675, 32768, 0], - [21262, 28627, 31128, 32768, 0], - [18392, 26454, 30437, 32768, 0], - [14870, 22910, 27096, 32768, 0], - [12620, 19484, 24908, 32768, 0], - [9290, 16553, 22802, 32768, 0], - [6668, 14288, 20004, 32768, 0], - [27704, 31055, 31949, 32768, 0], - [24709, 29978, 31788, 32768, 0], - [21668, 29264, 31657, 32768, 0], - [18295, 26968, 30074, 32768, 0], - [16399, 24422, 29313, 32768, 0], - [14347, 23026, 28104, 32768, 0], - [12370, 19806, 24477, 32768, 0], - ], - ], - [ - [ - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - [ - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - [8192, 16384, 24576, 32768, 0], - ], - ], - ], -]; - -pub static DEFAULT_DC_SIGN_CDF: [[[[u16; 3]; 3]; 2]; 4] = [ - [[[16000, 32768, 0], [13056, 32768, 0], [18816, 32768, 0]], [[15232, 32768, 0], [12928, 32768, 0], [17280, 32768, 0]]], - [[[16000, 32768, 0], [13056, 32768, 0], [18816, 32768, 0]], [[15232, 32768, 0], [12928, 32768, 0], [17280, 32768, 0]]], - [[[16000, 32768, 0], [13056, 32768, 0], [18816, 32768, 0]], [[15232, 32768, 0], [12928, 32768, 0], [17280, 32768, 0]]], - [[[16000, 32768, 0], [13056, 32768, 0], [18816, 32768, 0]], [[15232, 32768, 0], [12928, 32768, 0], [17280, 32768, 0]]], -]; - -pub static DEFAULT_DRL_MODE_CDF: [[u16; 3]; 3] = [[13104, 32768, 0], [24560, 32768, 0], [18945, 32768, 0]]; - -pub static DEFAULT_EOB_EXTRA_CDF: [[[[[u16; 3]; 9]; 2]; 5]; 4] = [ - [ - [ - [ - [16961, 32768, 0], - [17223, 32768, 0], - [7621, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - [ - [19069, 32768, 0], - [22525, 32768, 0], - [13377, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - ], - [ - [ - [20401, 32768, 0], - [17025, 32768, 0], - [12845, 32768, 0], - [12873, 32768, 0], - [14094, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - [ - [20681, 32768, 0], - [20701, 32768, 0], - [15250, 32768, 0], - [15017, 32768, 0], - [14928, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - ], - [ - [ - [23905, 32768, 0], - [17194, 32768, 0], - [16170, 32768, 0], - [17695, 32768, 0], - [13826, 32768, 0], - [15810, 32768, 0], - [12036, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - [ - [23959, 32768, 0], - [20799, 32768, 0], - [19021, 32768, 0], - [16203, 32768, 0], - [17886, 32768, 0], - [14144, 32768, 0], - [12010, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - ], - [ - [ - [27399, 32768, 0], - [16327, 32768, 0], - [18071, 32768, 0], - [19584, 32768, 0], - [20721, 32768, 0], - [18432, 32768, 0], - [19560, 32768, 0], - [10150, 32768, 0], - [8805, 32768, 0], - ], - [ - [24932, 32768, 0], - [20833, 32768, 0], - [12027, 32768, 0], - [16670, 32768, 0], - [19914, 32768, 0], - [15106, 32768, 0], - [17662, 32768, 0], - [13783, 32768, 0], - [28756, 32768, 0], - ], - ], - [ - [ - [23406, 32768, 0], - [21845, 32768, 0], - [18432, 32768, 0], - [16384, 32768, 0], - [17096, 32768, 0], - [12561, 32768, 0], - [17320, 32768, 0], - [22395, 32768, 0], - [21370, 32768, 0], - ], - [ - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - ], - ], - [ - [ - [ - [17471, 32768, 0], - [20223, 32768, 0], - [11357, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - [ - [20335, 32768, 0], - [21667, 32768, 0], - [14818, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - ], - [ - [ - [20430, 32768, 0], - [20662, 32768, 0], - [15367, 32768, 0], - [16970, 32768, 0], - [14657, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - [ - [22117, 32768, 0], - [22028, 32768, 0], - [18650, 32768, 0], - [16042, 32768, 0], - [15885, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - ], - [ - [ - [22409, 32768, 0], - [21012, 32768, 0], - [15650, 32768, 0], - [17395, 32768, 0], - [15469, 32768, 0], - [20205, 32768, 0], - [19511, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - [ - [24220, 32768, 0], - [22480, 32768, 0], - [17737, 32768, 0], - [18916, 32768, 0], - [19268, 32768, 0], - [18412, 32768, 0], - [18844, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - ], - [ - [ - [25991, 32768, 0], - [20314, 32768, 0], - [17731, 32768, 0], - [19678, 32768, 0], - [18649, 32768, 0], - [17307, 32768, 0], - [21798, 32768, 0], - [17549, 32768, 0], - [15630, 32768, 0], - ], - [ - [26585, 32768, 0], - [21469, 32768, 0], - [20432, 32768, 0], - [17735, 32768, 0], - [19280, 32768, 0], - [15235, 32768, 0], - [20297, 32768, 0], - [22471, 32768, 0], - [28997, 32768, 0], - ], - ], - [ - [ - [26605, 32768, 0], - [11304, 32768, 0], - [16726, 32768, 0], - [16560, 32768, 0], - [20866, 32768, 0], - [23524, 32768, 0], - [19878, 32768, 0], - [13469, 32768, 0], - [23084, 32768, 0], - ], - [ - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - ], - ], - [ - [ - [ - [18983, 32768, 0], - [20512, 32768, 0], - [14885, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - [ - [20090, 32768, 0], - [19444, 32768, 0], - [17286, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - ], - [ - [ - [19139, 32768, 0], - [21487, 32768, 0], - [18959, 32768, 0], - [20910, 32768, 0], - [19089, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - [ - [20536, 32768, 0], - [20664, 32768, 0], - [20625, 32768, 0], - [19123, 32768, 0], - [14862, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - ], - [ - [ - [19833, 32768, 0], - [21502, 32768, 0], - [17485, 32768, 0], - [20267, 32768, 0], - [18353, 32768, 0], - [23329, 32768, 0], - [21478, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - [ - [22041, 32768, 0], - [23434, 32768, 0], - [20001, 32768, 0], - [20554, 32768, 0], - [20951, 32768, 0], - [20145, 32768, 0], - [15562, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - ], - [ - [ - [23312, 32768, 0], - [21607, 32768, 0], - [16526, 32768, 0], - [18957, 32768, 0], - [18034, 32768, 0], - [18934, 32768, 0], - [24247, 32768, 0], - [16921, 32768, 0], - [17080, 32768, 0], - ], - [ - [26579, 32768, 0], - [24910, 32768, 0], - [18637, 32768, 0], - [19800, 32768, 0], - [20388, 32768, 0], - [9887, 32768, 0], - [15642, 32768, 0], - [30198, 32768, 0], - [24721, 32768, 0], - ], - ], - [ - [ - [26998, 32768, 0], - [16737, 32768, 0], - [17838, 32768, 0], - [18922, 32768, 0], - [19515, 32768, 0], - [18636, 32768, 0], - [17333, 32768, 0], - [15776, 32768, 0], - [22658, 32768, 0], - ], - [ - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - ], - ], - [ - [ - [ - [20177, 32768, 0], - [20789, 32768, 0], - [20262, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - [ - [21416, 32768, 0], - [20855, 32768, 0], - [23410, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - ], - [ - [ - [20238, 32768, 0], - [21057, 32768, 0], - [19159, 32768, 0], - [22337, 32768, 0], - [20159, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - [ - [20125, 32768, 0], - [20559, 32768, 0], - [21707, 32768, 0], - [22296, 32768, 0], - [17333, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - ], - [ - [ - [19941, 32768, 0], - [20527, 32768, 0], - [21470, 32768, 0], - [22487, 32768, 0], - [19558, 32768, 0], - [22354, 32768, 0], - [20331, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - [ - [22752, 32768, 0], - [25006, 32768, 0], - [22075, 32768, 0], - [21576, 32768, 0], - [17740, 32768, 0], - [21690, 32768, 0], - [19211, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - ], - [ - [ - [21442, 32768, 0], - [22358, 32768, 0], - [18503, 32768, 0], - [20291, 32768, 0], - [19945, 32768, 0], - [21294, 32768, 0], - [21178, 32768, 0], - [19400, 32768, 0], - [10556, 32768, 0], - ], - [ - [24648, 32768, 0], - [24949, 32768, 0], - [20708, 32768, 0], - [23905, 32768, 0], - [20501, 32768, 0], - [9558, 32768, 0], - [9423, 32768, 0], - [30365, 32768, 0], - [19253, 32768, 0], - ], - ], - [ - [ - [26064, 32768, 0], - [22098, 32768, 0], - [19613, 32768, 0], - [20525, 32768, 0], - [17595, 32768, 0], - [16618, 32768, 0], - [20497, 32768, 0], - [18989, 32768, 0], - [15513, 32768, 0], - ], - [ - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - ], - ], -]; - -pub static DEFAULT_EOB_PT_1024_CDF: [[[u16; 12]; 2]; 4] = [ - [[393, 421, 751, 1623, 3160, 6352, 13345, 18047, 22571, 25830, 32768, 0], [1865, 1988, 2930, 4242, 10533, 16538, 21354, 27255, 28546, 31784, 32768, 0]], - [[696, 948, 3145, 5702, 9706, 13217, 17851, 21856, 25692, 28034, 32768, 0], [2672, 3591, 9330, 17084, 22725, 24284, 26527, 28027, 28377, 30876, 32768, 0]], - [ - [2784, 3831, 7041, 10521, 14847, 18844, 23155, 26682, 29229, 31045, 32768, 0], - [9577, 12466, 17739, 20750, 22061, 23215, 24601, 25483, 25843, 32056, 32768, 0], - ], - [ - [6698, 8334, 11961, 15762, 20186, 23862, 27434, 29326, 31082, 32050, 32768, 0], - [20569, 22426, 25569, 26859, 28053, 28913, 29486, 29724, 29807, 32570, 32768, 0], - ], -]; - -pub static DEFAULT_EOB_PT_128_CDF: [[[[u16; 9]; 2]; 2]; 4] = [ - [ - [[219, 482, 1140, 2091, 3680, 6028, 12586, 32768, 0], [371, 699, 1254, 4830, 9479, 12562, 17497, 32768, 0]], - [[5245, 7456, 12880, 15852, 20033, 23932, 27608, 32768, 0], [2054, 3472, 5869, 14232, 18242, 20590, 26752, 32768, 0]], - ], - [ - [[685, 933, 1488, 2714, 4766, 8562, 19254, 32768, 0], [217, 352, 618, 2303, 5261, 9969, 17472, 32768, 0]], - [[8045, 11200, 15497, 19595, 23948, 27408, 30938, 32768, 0], [2310, 4160, 7471, 14997, 17931, 20768, 30240, 32768, 0]], - ], - [ - [[1366, 1738, 2527, 5016, 9355, 15797, 24643, 32768, 0], [354, 558, 944, 2760, 7287, 14037, 21779, 32768, 0]], - [[13627, 16246, 20173, 24429, 27948, 30415, 31863, 32768, 0], [6275, 9889, 14769, 23164, 27988, 30493, 32272, 32768, 0]], - ], - [ - [[3472, 4885, 7489, 12481, 18517, 24536, 29635, 32768, 0], [886, 1731, 3271, 8469, 15569, 22126, 28383, 32768, 0]], - [[24313, 26062, 28385, 30107, 31217, 31898, 32345, 32768, 0], [9165, 13282, 21150, 30286, 31894, 32571, 32712, 32768, 0]], - ], -]; - -pub static DEFAULT_EOB_PT_16_CDF: [[[[u16; 6]; 2]; 2]; 4] = [ - [[[840, 1039, 1980, 4895, 32768, 0], [370, 671, 1883, 4471, 32768, 0]], [[3247, 4950, 9688, 14563, 32768, 0], [1904, 3354, 7763, 14647, 32768, 0]]], - [[[2125, 2551, 5165, 8946, 32768, 0], [513, 765, 1859, 6339, 32768, 0]], [[7637, 9498, 14259, 19108, 32768, 0], [2497, 4096, 8866, 16993, 32768, 0]]], - [[[4016, 4897, 8881, 14968, 32768, 0], [716, 1105, 2646, 10056, 32768, 0]], [[11139, 13270, 18241, 23566, 32768, 0], [3192, 5032, 10297, 19755, 32768, 0]]], - [ - [[6708, 8958, 14746, 22133, 32768, 0], [1222, 2074, 4783, 15410, 32768, 0]], - [[19575, 21766, 26044, 29709, 32768, 0], [7297, 10767, 19273, 28194, 32768, 0]], - ], -]; - -pub static DEFAULT_EOB_PT_256_CDF: [[[[u16; 10]; 2]; 2]; 4] = [ - [ - [[310, 584, 1887, 3589, 6168, 8611, 11352, 15652, 32768, 0], [998, 1850, 2998, 5604, 17341, 19888, 22899, 25583, 32768, 0]], - [[2520, 3240, 5952, 8870, 12577, 17558, 19954, 24168, 32768, 0], [2203, 4130, 7435, 10739, 20652, 23681, 25609, 27261, 32768, 0]], - ], - [ - [[1448, 2109, 4151, 6263, 9329, 13260, 17944, 23300, 32768, 0], [399, 1019, 1749, 3038, 10444, 15546, 22739, 27294, 32768, 0]], - [[6402, 8148, 12623, 15072, 18728, 22847, 26447, 29377, 32768, 0], [1674, 3252, 5734, 10159, 22397, 23802, 24821, 30940, 32768, 0]], - ], - [ - [[3089, 3920, 6038, 9460, 14266, 19881, 25766, 29176, 32768, 0], [1084, 2358, 3488, 5122, 11483, 18103, 26023, 29799, 32768, 0]], - [[11514, 13794, 17480, 20754, 24361, 27378, 29492, 31277, 32768, 0], [6571, 9610, 15516, 21826, 29092, 30829, 31842, 32708, 32768, 0]], - ], - [ - [[5348, 7113, 11820, 15924, 22106, 26777, 30334, 31757, 32768, 0], [2453, 4474, 6307, 8777, 16474, 22975, 29000, 31547, 32768, 0]], - [[23110, 24597, 27140, 28894, 30167, 30927, 31392, 32094, 32768, 0], [9998, 17661, 25178, 28097, 31308, 32038, 32403, 32695, 32768, 0]], - ], -]; - -pub static DEFAULT_EOB_PT_32_CDF: [[[[u16; 7]; 2]; 2]; 4] = [ - [ - [[400, 520, 977, 2102, 6542, 32768, 0], [210, 405, 1315, 3326, 7537, 32768, 0]], - [[2636, 4273, 7588, 11794, 20401, 32768, 0], [1786, 3179, 6902, 11357, 19054, 32768, 0]], - ], - [ - [[989, 1249, 2019, 4151, 10785, 32768, 0], [313, 441, 1099, 2917, 8562, 32768, 0]], - [[8394, 10352, 13932, 18855, 26014, 32768, 0], [2578, 4124, 8181, 13670, 24234, 32768, 0]], - ], - [ - [[2515, 3003, 4452, 8162, 16041, 32768, 0], [574, 821, 1836, 5089, 13128, 32768, 0]], - [[13468, 16303, 20361, 25105, 29281, 32768, 0], [3542, 5502, 10415, 16760, 25644, 32768, 0]], - ], - [ - [[4617, 5709, 8446, 13584, 23135, 32768, 0], [1156, 1702, 3675, 9274, 20539, 32768, 0]], - [[22086, 24282, 27010, 29770, 31743, 32768, 0], [7699, 10897, 20891, 26926, 31628, 32768, 0]], - ], -]; - -pub static DEFAULT_EOB_PT_512_CDF: [[[u16; 11]; 2]; 4] = [ - [[641, 983, 3707, 5430, 10234, 14958, 18788, 23412, 26061, 32768, 0], [5095, 6446, 9996, 13354, 16017, 17986, 20919, 26129, 29140, 32768, 0]], - [[1230, 2278, 5035, 7776, 11871, 15346, 19590, 24584, 28749, 32768, 0], [7265, 9979, 15819, 19250, 21780, 23846, 26478, 28396, 31811, 32768, 0]], - [[2624, 3936, 6480, 9686, 13979, 17726, 23267, 28410, 31078, 32768, 0], [12015, 14769, 19588, 22052, 24222, 25812, 27300, 29219, 32114, 32768, 0]], - [[5927, 7809, 10923, 14597, 19439, 24135, 28456, 31142, 32060, 32768, 0], [21093, 23043, 25742, 27658, 29097, 29716, 30073, 30820, 31956, 32768, 0]], -]; - -pub static DEFAULT_EOB_PT_64_CDF: [[[[u16; 8]; 2]; 2]; 4] = [ - [ - [[329, 498, 1101, 1784, 3265, 7758, 32768, 0], [335, 730, 1459, 5494, 8755, 12997, 32768, 0]], - [[3505, 5304, 10086, 13814, 17684, 23370, 32768, 0], [1563, 2700, 4876, 10911, 14706, 22480, 32768, 0]], - ], - [ - [[1260, 1446, 2253, 3712, 6652, 13369, 32768, 0], [401, 605, 1029, 2563, 5845, 12626, 32768, 0]], - [[8609, 10612, 14624, 18714, 22614, 29024, 32768, 0], [1923, 3127, 5867, 9703, 14277, 27100, 32768, 0]], - ], - [ - [[2374, 2772, 4583, 7276, 12288, 19706, 32768, 0], [497, 810, 1315, 3000, 7004, 15641, 32768, 0]], - [[15050, 17126, 21410, 24886, 28156, 30726, 32768, 0], [4034, 6290, 10235, 14982, 21214, 28491, 32768, 0]], - ], - [ - [[6307, 7541, 12060, 16358, 22553, 27865, 32768, 0], [1289, 2320, 3971, 7926, 14153, 24291, 32768, 0]], - [[24212, 25708, 28268, 30035, 31307, 32049, 32768, 0], [8726, 12378, 19409, 26450, 30038, 32462, 32768, 0]], - ], -]; - -pub static DEFAULT_INTER_TX_TYPE_SET3_CDF: [[u16; 3]; 4] = [[16384, 32768, 0], [4167, 32768, 0], [1998, 32768, 0], [748, 32768, 0]]; - -pub static DEFAULT_INTRA_FRAME_Y_MODE_CDF: [[[u16; 14]; 5]; 5] = [ - [ - [15588, 17027, 19338, 20218, 20682, 21110, 21825, 23244, 24189, 28165, 29093, 30466, 32768, 0], - [12016, 18066, 19516, 20303, 20719, 21444, 21888, 23032, 24434, 28658, 30172, 31409, 32768, 0], - [10052, 10771, 22296, 22788, 23055, 23239, 24133, 25620, 26160, 29336, 29929, 31567, 32768, 0], - [14091, 15406, 16442, 18808, 19136, 19546, 19998, 22096, 24746, 29585, 30958, 32462, 32768, 0], - [12122, 13265, 15603, 16501, 18609, 20033, 22391, 25583, 26437, 30261, 31073, 32475, 32768, 0], - ], - [ - [10023, 19585, 20848, 21440, 21832, 22760, 23089, 24023, 25381, 29014, 30482, 31436, 32768, 0], - [5983, 24099, 24560, 24886, 25066, 25795, 25913, 26423, 27610, 29905, 31276, 31794, 32768, 0], - [7444, 12781, 20177, 20728, 21077, 21607, 22170, 23405, 24469, 27915, 29090, 30492, 32768, 0], - [8537, 14689, 15432, 17087, 17408, 18172, 18408, 19825, 24649, 29153, 31096, 32210, 32768, 0], - [7543, 14231, 15496, 16195, 17905, 20717, 21984, 24516, 26001, 29675, 30981, 31994, 32768, 0], - ], - [ - [12613, 13591, 21383, 22004, 22312, 22577, 23401, 25055, 25729, 29538, 30305, 32077, 32768, 0], - [9687, 13470, 18506, 19230, 19604, 20147, 20695, 22062, 23219, 27743, 29211, 30907, 32768, 0], - [6183, 6505, 26024, 26252, 26366, 26434, 27082, 28354, 28555, 30467, 30794, 32086, 32768, 0], - [10718, 11734, 14954, 17224, 17565, 17924, 18561, 21523, 23878, 28975, 30287, 32252, 32768, 0], - [9194, 9858, 16501, 17263, 18424, 19171, 21563, 25961, 26561, 30072, 30737, 32463, 32768, 0], - ], - [ - [12602, 14399, 15488, 18381, 18778, 19315, 19724, 21419, 25060, 29696, 30917, 32409, 32768, 0], - [8203, 13821, 14524, 17105, 17439, 18131, 18404, 19468, 25225, 29485, 31158, 32342, 32768, 0], - [8451, 9731, 15004, 17643, 18012, 18425, 19070, 21538, 24605, 29118, 30078, 32018, 32768, 0], - [7714, 9048, 9516, 16667, 16817, 16994, 17153, 18767, 26743, 30389, 31536, 32528, 32768, 0], - [8843, 10280, 11496, 15317, 16652, 17943, 19108, 22718, 25769, 29953, 30983, 32485, 32768, 0], - ], - [ - [12578, 13671, 15979, 16834, 19075, 20913, 22989, 25449, 26219, 30214, 31150, 32477, 32768, 0], - [9563, 13626, 15080, 15892, 17756, 20863, 22207, 24236, 25380, 29653, 31143, 32277, 32768, 0], - [8356, 8901, 17616, 18256, 19350, 20106, 22598, 25947, 26466, 29900, 30523, 32261, 32768, 0], - [10835, 11815, 13124, 16042, 17018, 18039, 18947, 22753, 24615, 29489, 30883, 32482, 32768, 0], - [7618, 8288, 9859, 10509, 15386, 18657, 22903, 28776, 29180, 31355, 31802, 32593, 32768, 0], - ], -]; - -pub static DEFAULT_INTRA_TX_TYPE_SET2_CDF: [[[u16; 6]; 13]; 3] = [ - [ - [6554, 13107, 19661, 26214, 32768, 0], - [6554, 13107, 19661, 26214, 32768, 0], - [6554, 13107, 19661, 26214, 32768, 0], - [6554, 13107, 19661, 26214, 32768, 0], - [6554, 13107, 19661, 26214, 32768, 0], - [6554, 13107, 19661, 26214, 32768, 0], - [6554, 13107, 19661, 26214, 32768, 0], - [6554, 13107, 19661, 26214, 32768, 0], - [6554, 13107, 19661, 26214, 32768, 0], - [6554, 13107, 19661, 26214, 32768, 0], - [6554, 13107, 19661, 26214, 32768, 0], - [6554, 13107, 19661, 26214, 32768, 0], - [6554, 13107, 19661, 26214, 32768, 0], - ], - [ - [6554, 13107, 19661, 26214, 32768, 0], - [6554, 13107, 19661, 26214, 32768, 0], - [6554, 13107, 19661, 26214, 32768, 0], - [6554, 13107, 19661, 26214, 32768, 0], - [6554, 13107, 19661, 26214, 32768, 0], - [6554, 13107, 19661, 26214, 32768, 0], - [6554, 13107, 19661, 26214, 32768, 0], - [6554, 13107, 19661, 26214, 32768, 0], - [6554, 13107, 19661, 26214, 32768, 0], - [6554, 13107, 19661, 26214, 32768, 0], - [6554, 13107, 19661, 26214, 32768, 0], - [6554, 13107, 19661, 26214, 32768, 0], - [6554, 13107, 19661, 26214, 32768, 0], - ], - [ - [1127, 12814, 22772, 27483, 32768, 0], - [145, 6761, 11980, 26667, 32768, 0], - [362, 5887, 11678, 16725, 32768, 0], - [385, 15213, 18587, 30693, 32768, 0], - [25, 2914, 23134, 27903, 32768, 0], - [60, 4470, 11749, 23991, 32768, 0], - [37, 3332, 14511, 21448, 32768, 0], - [157, 6320, 13036, 17439, 32768, 0], - [119, 6719, 12906, 29396, 32768, 0], - [47, 5537, 12576, 21499, 32768, 0], - [269, 6076, 11258, 23115, 32768, 0], - [83, 5615, 12001, 17228, 32768, 0], - [1968, 5556, 12023, 18547, 32768, 0], - ], -]; - -pub static DEFAULT_IS_INTER_CDF: [[u16; 3]; 4] = [[806, 32768, 0], [16662, 32768, 0], [20186, 32768, 0], [26538, 32768, 0]]; - -pub static DEFAULT_MV_BIT_CDF: [[u16; 3]; 10] = [ - [17408, 32768, 0], - [17920, 32768, 0], - [18944, 32768, 0], - [20480, 32768, 0], - [22528, 32768, 0], - [24576, 32768, 0], - [28672, 32768, 0], - [29952, 32768, 0], - [29952, 32768, 0], - [30720, 32768, 0], -]; - -pub static DEFAULT_MV_CLASS0_BIT_CDF: [u16; 3] = [27648, 32768, 0]; - -pub static DEFAULT_MV_CLASS0_FR_CDF: [[[u16; 5]; 2]; 2] = - [[[16384, 24576, 26624, 32768, 0], [12288, 21248, 24128, 32768, 0]], [[16384, 24576, 26624, 32768, 0], [12288, 21248, 24128, 32768, 0]]]; - -pub static DEFAULT_MV_CLASS0_HP_CDF: [u16; 3] = [20480, 32768, 0]; - -pub static DEFAULT_MV_CLASS_CDF: [[u16; 12]; 2] = [ - [28672, 30976, 31858, 32320, 32551, 32656, 32740, 32757, 32762, 32767, 32768, 0], - [28672, 30976, 31858, 32320, 32551, 32656, 32740, 32757, 32762, 32767, 32768, 0], -]; - -pub static DEFAULT_MV_FR_CDF: [[u16; 5]; 2] = [[8192, 17408, 21248, 32768, 0], [8192, 17408, 21248, 32768, 0]]; - -pub static DEFAULT_MV_HP_CDF: [u16; 3] = [16384, 32768, 0]; - -pub static DEFAULT_MV_JOINT_CDF: [u16; 5] = [4096, 11264, 19328, 32768, 0]; - -pub static DEFAULT_MV_SIGN_CDF: [u16; 3] = [16384, 32768, 0]; - -pub static DEFAULT_NEW_MV_CDF: [[u16; 3]; 6] = [[24035, 32768, 0], [16630, 32768, 0], [15339, 32768, 0], [8386, 32768, 0], [12222, 32768, 0], [4676, 32768, 0]]; - -pub static DEFAULT_PARTITION_W16_CDF: [[u16; 11]; 4] = [ - [15597, 20929, 24571, 26706, 27664, 28821, 29601, 30571, 31902, 32768, 0], - [7925, 11043, 16785, 22470, 23971, 25043, 26651, 28701, 29834, 32768, 0], - [5414, 13269, 15111, 20488, 22360, 24500, 25537, 26336, 32117, 32768, 0], - [2662, 6362, 8614, 20860, 23053, 24778, 26436, 27829, 31171, 32768, 0], -]; - -pub static DEFAULT_PARTITION_W32_CDF: [[u16; 11]; 4] = [ - [18462, 20920, 23124, 27647, 28227, 29049, 29519, 30178, 31544, 32768, 0], - [7689, 9060, 12056, 24992, 25660, 26182, 26951, 28041, 29052, 32768, 0], - [6015, 9009, 10062, 24544, 25409, 26545, 27071, 27526, 32047, 32768, 0], - [1394, 2208, 2796, 28614, 29061, 29466, 29840, 30185, 31899, 32768, 0], -]; - -pub static DEFAULT_PARTITION_W64_CDF: [[u16; 11]; 4] = [ - [20137, 21547, 23078, 29566, 29837, 30261, 30524, 30892, 31724, 32768, 0], - [6732, 7490, 9497, 27944, 28250, 28515, 28969, 29630, 30104, 32768, 0], - [5945, 7663, 8348, 28683, 29117, 29749, 30064, 30298, 32238, 32768, 0], - [870, 1212, 1487, 31198, 31394, 31574, 31743, 31881, 32332, 32768, 0], -]; - -pub static DEFAULT_PARTITION_W8_CDF: [[u16; 5]; 4] = - [[19132, 25510, 30392, 32768, 0], [13928, 19855, 28540, 32768, 0], [12522, 23679, 28629, 32768, 0], [9896, 18783, 25853, 32768, 0]]; - -pub static DEFAULT_REF_MV_CDF: [[u16; 3]; 6] = - [[23974, 32768, 0], [24188, 32768, 0], [17848, 32768, 0], [28622, 32768, 0], [24312, 32768, 0], [19923, 32768, 0]]; - -pub static DEFAULT_SCAN_16X16: [u16; 256] = [ - 0, 1, 16, 32, 17, 2, 3, 18, 33, 48, 64, 49, 34, 19, 4, 5, 20, 35, 50, 65, 80, 96, 81, 66, 51, 36, 21, 6, 7, 22, 37, 52, 67, 82, 97, 112, 128, 113, 98, 83, - 68, 53, 38, 23, 8, 9, 24, 39, 54, 69, 84, 99, 114, 129, 144, 160, 145, 130, 115, 100, 85, 70, 55, 40, 25, 10, 11, 26, 41, 56, 71, 86, 101, 116, 131, 146, - 161, 176, 192, 177, 162, 147, 132, 117, 102, 87, 72, 57, 42, 27, 12, 13, 28, 43, 58, 73, 88, 103, 118, 133, 148, 163, 178, 193, 208, 224, 209, 194, 179, - 164, 149, 134, 119, 104, 89, 74, 59, 44, 29, 14, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225, 240, 241, 226, 211, 196, 181, 166, - 151, 136, 121, 106, 91, 76, 61, 46, 31, 47, 62, 77, 92, 107, 122, 137, 152, 167, 182, 197, 212, 227, 242, 243, 228, 213, 198, 183, 168, 153, 138, 123, 108, - 93, 78, 63, 79, 94, 109, 124, 139, 154, 169, 184, 199, 214, 229, 244, 245, 230, 215, 200, 185, 170, 155, 140, 125, 110, 95, 111, 126, 141, 156, 171, 186, - 201, 216, 231, 246, 247, 232, 217, 202, 187, 172, 157, 142, 127, 143, 158, 173, 188, 203, 218, 233, 248, 249, 234, 219, 204, 189, 174, 159, 175, 190, 205, - 220, 235, 250, 251, 236, 221, 206, 191, 207, 222, 237, 252, 253, 238, 223, 239, 254, 255, -]; - -pub static DEFAULT_SCAN_32X32: [u16; 1024] = [ - 0, 1, 32, 64, 33, 2, 3, 34, 65, 96, 128, 97, 66, 35, 4, 5, 36, 67, 98, 129, 160, 192, 161, 130, 99, 68, 37, 6, 7, 38, 69, 100, 131, 162, 193, 224, 256, - 225, 194, 163, 132, 101, 70, 39, 8, 9, 40, 71, 102, 133, 164, 195, 226, 257, 288, 320, 289, 258, 227, 196, 165, 134, 103, 72, 41, 10, 11, 42, 73, 104, 135, - 166, 197, 228, 259, 290, 321, 352, 384, 353, 322, 291, 260, 229, 198, 167, 136, 105, 74, 43, 12, 13, 44, 75, 106, 137, 168, 199, 230, 261, 292, 323, 354, - 385, 416, 448, 417, 386, 355, 324, 293, 262, 231, 200, 169, 138, 107, 76, 45, 14, 15, 46, 77, 108, 139, 170, 201, 232, 263, 294, 325, 356, 387, 418, 449, - 480, 512, 481, 450, 419, 388, 357, 326, 295, 264, 233, 202, 171, 140, 109, 78, 47, 16, 17, 48, 79, 110, 141, 172, 203, 234, 265, 296, 327, 358, 389, 420, - 451, 482, 513, 544, 576, 545, 514, 483, 452, 421, 390, 359, 328, 297, 266, 235, 204, 173, 142, 111, 80, 49, 18, 19, 50, 81, 112, 143, 174, 205, 236, 267, - 298, 329, 360, 391, 422, 453, 484, 515, 546, 577, 608, 640, 609, 578, 547, 516, 485, 454, 423, 392, 361, 330, 299, 268, 237, 206, 175, 144, 113, 82, 51, - 20, 21, 52, 83, 114, 145, 176, 207, 238, 269, 300, 331, 362, 393, 424, 455, 486, 517, 548, 579, 610, 641, 672, 704, 673, 642, 611, 580, 549, 518, 487, 456, - 425, 394, 363, 332, 301, 270, 239, 208, 177, 146, 115, 84, 53, 22, 23, 54, 85, 116, 147, 178, 209, 240, 271, 302, 333, 364, 395, 426, 457, 488, 519, 550, - 581, 612, 643, 674, 705, 736, 768, 737, 706, 675, 644, 613, 582, 551, 520, 489, 458, 427, 396, 365, 334, 303, 272, 241, 210, 179, 148, 117, 86, 55, 24, 25, - 56, 87, 118, 149, 180, 211, 242, 273, 304, 335, 366, 397, 428, 459, 490, 521, 552, 583, 614, 645, 676, 707, 738, 769, 800, 832, 801, 770, 739, 708, 677, - 646, 615, 584, 553, 522, 491, 460, 429, 398, 367, 336, 305, 274, 243, 212, 181, 150, 119, 88, 57, 26, 27, 58, 89, 120, 151, 182, 213, 244, 275, 306, 337, - 368, 399, 430, 461, 492, 523, 554, 585, 616, 647, 678, 709, 740, 771, 802, 833, 864, 896, 865, 834, 803, 772, 741, 710, 679, 648, 617, 586, 555, 524, 493, - 462, 431, 400, 369, 338, 307, 276, 245, 214, 183, 152, 121, 90, 59, 28, 29, 60, 91, 122, 153, 184, 215, 246, 277, 308, 339, 370, 401, 432, 463, 494, 525, - 556, 587, 618, 649, 680, 711, 742, 773, 804, 835, 866, 897, 928, 960, 929, 898, 867, 836, 805, 774, 743, 712, 681, 650, 619, 588, 557, 526, 495, 464, 433, - 402, 371, 340, 309, 278, 247, 216, 185, 154, 123, 92, 61, 30, 31, 62, 93, 124, 155, 186, 217, 248, 279, 310, 341, 372, 403, 434, 465, 496, 527, 558, 589, - 620, 651, 682, 713, 744, 775, 806, 837, 868, 899, 930, 961, 992, 993, 962, 931, 900, 869, 838, 807, 776, 745, 714, 683, 652, 621, 590, 559, 528, 497, 466, - 435, 404, 373, 342, 311, 280, 249, 218, 187, 156, 125, 94, 63, 95, 126, 157, 188, 219, 250, 281, 312, 343, 374, 405, 436, 467, 498, 529, 560, 591, 622, - 653, 684, 715, 746, 777, 808, 839, 870, 901, 932, 963, 994, 995, 964, 933, 902, 871, 840, 809, 778, 747, 716, 685, 654, 623, 592, 561, 530, 499, 468, 437, - 406, 375, 344, 313, 282, 251, 220, 189, 158, 127, 159, 190, 221, 252, 283, 314, 345, 376, 407, 438, 469, 500, 531, 562, 593, 624, 655, 686, 717, 748, 779, - 810, 841, 872, 903, 934, 965, 996, 997, 966, 935, 904, 873, 842, 811, 780, 749, 718, 687, 656, 625, 594, 563, 532, 501, 470, 439, 408, 377, 346, 315, 284, - 253, 222, 191, 223, 254, 285, 316, 347, 378, 409, 440, 471, 502, 533, 564, 595, 626, 657, 688, 719, 750, 781, 812, 843, 874, 905, 936, 967, 998, 999, 968, - 937, 906, 875, 844, 813, 782, 751, 720, 689, 658, 627, 596, 565, 534, 503, 472, 441, 410, 379, 348, 317, 286, 255, 287, 318, 349, 380, 411, 442, 473, 504, - 535, 566, 597, 628, 659, 690, 721, 752, 783, 814, 845, 876, 907, 938, 969, 1000, 1001, 970, 939, 908, 877, 846, 815, 784, 753, 722, 691, 660, 629, 598, - 567, 536, 505, 474, 443, 412, 381, 350, 319, 351, 382, 413, 444, 475, 506, 537, 568, 599, 630, 661, 692, 723, 754, 785, 816, 847, 878, 909, 940, 971, 1002, - 1003, 972, 941, 910, 879, 848, 817, 786, 755, 724, 693, 662, 631, 600, 569, 538, 507, 476, 445, 414, 383, 415, 446, 477, 508, 539, 570, 601, 632, 663, 694, - 725, 756, 787, 818, 849, 880, 911, 942, 973, 1004, 1005, 974, 943, 912, 881, 850, 819, 788, 757, 726, 695, 664, 633, 602, 571, 540, 509, 478, 447, 479, - 510, 541, 572, 603, 634, 665, 696, 727, 758, 789, 820, 851, 882, 913, 944, 975, 1006, 1007, 976, 945, 914, 883, 852, 821, 790, 759, 728, 697, 666, 635, - 604, 573, 542, 511, 543, 574, 605, 636, 667, 698, 729, 760, 791, 822, 853, 884, 915, 946, 977, 1008, 1009, 978, 947, 916, 885, 854, 823, 792, 761, 730, - 699, 668, 637, 606, 575, 607, 638, 669, 700, 731, 762, 793, 824, 855, 886, 917, 948, 979, 1010, 1011, 980, 949, 918, 887, 856, 825, 794, 763, 732, 701, - 670, 639, 671, 702, 733, 764, 795, 826, 857, 888, 919, 950, 981, 1012, 1013, 982, 951, 920, 889, 858, 827, 796, 765, 734, 703, 735, 766, 797, 828, 859, - 890, 921, 952, 983, 1014, 1015, 984, 953, 922, 891, 860, 829, 798, 767, 799, 830, 861, 892, 923, 954, 985, 1016, 1017, 986, 955, 924, 893, 862, 831, 863, - 894, 925, 956, 987, 1018, 1019, 988, 957, 926, 895, 927, 958, 989, 1020, 1021, 990, 959, 991, 1022, 1023, -]; - -pub static DEFAULT_SCAN_4X4: [u16; 16] = [0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15]; - -pub static DEFAULT_SCAN_8X8: [u16; 64] = [ - 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, - 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63, -]; - -pub static DEFAULT_SINGLE_REF_CDF: [[[u16; 3]; 6]; 3] = [ - [[4897, 32768, 0], [1555, 32768, 0], [4236, 32768, 0], [8650, 32768, 0], [904, 32768, 0], [1444, 32768, 0]], - [[16973, 32768, 0], [16751, 32768, 0], [19647, 32768, 0], [24773, 32768, 0], [11014, 32768, 0], [15087, 32768, 0]], - [[29744, 32768, 0], [30279, 32768, 0], [31194, 32768, 0], [31895, 32768, 0], [26875, 32768, 0], [30304, 32768, 0]], -]; - -pub static DEFAULT_SKIP_CDF: [[u16; 3]; 3] = [[31671, 32768, 0], [16515, 32768, 0], [4576, 32768, 0]]; - -pub static DEFAULT_TXB_SKIP_CDF: [[[[u16; 3]; 13]; 5]; 4] = [ - [ - [ - [31849, 32768, 0], - [5892, 32768, 0], - [12112, 32768, 0], - [21935, 32768, 0], - [20289, 32768, 0], - [27473, 32768, 0], - [32487, 32768, 0], - [7654, 32768, 0], - [19473, 32768, 0], - [29984, 32768, 0], - [9961, 32768, 0], - [30242, 32768, 0], - [32117, 32768, 0], - ], - [ - [31548, 32768, 0], - [1549, 32768, 0], - [10130, 32768, 0], - [16656, 32768, 0], - [18591, 32768, 0], - [26308, 32768, 0], - [32537, 32768, 0], - [5403, 32768, 0], - [18096, 32768, 0], - [30003, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - [ - [29957, 32768, 0], - [5391, 32768, 0], - [18039, 32768, 0], - [23566, 32768, 0], - [22431, 32768, 0], - [25822, 32768, 0], - [32197, 32768, 0], - [3778, 32768, 0], - [15336, 32768, 0], - [28981, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - [ - [17920, 32768, 0], - [1818, 32768, 0], - [7282, 32768, 0], - [25273, 32768, 0], - [10923, 32768, 0], - [31554, 32768, 0], - [32624, 32768, 0], - [1366, 32768, 0], - [15628, 32768, 0], - [30462, 32768, 0], - [146, 32768, 0], - [5132, 32768, 0], - [31657, 32768, 0], - ], - [ - [6308, 32768, 0], - [117, 32768, 0], - [1638, 32768, 0], - [2161, 32768, 0], - [16384, 32768, 0], - [10923, 32768, 0], - [30247, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - ], - [ - [ - [30371, 32768, 0], - [7570, 32768, 0], - [13155, 32768, 0], - [20751, 32768, 0], - [20969, 32768, 0], - [27067, 32768, 0], - [32013, 32768, 0], - [5495, 32768, 0], - [17942, 32768, 0], - [28280, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - [ - [31782, 32768, 0], - [1836, 32768, 0], - [10689, 32768, 0], - [17604, 32768, 0], - [21622, 32768, 0], - [27518, 32768, 0], - [32399, 32768, 0], - [4419, 32768, 0], - [16294, 32768, 0], - [28345, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - [ - [31901, 32768, 0], - [10311, 32768, 0], - [18047, 32768, 0], - [24806, 32768, 0], - [23288, 32768, 0], - [27914, 32768, 0], - [32296, 32768, 0], - [4215, 32768, 0], - [15756, 32768, 0], - [28341, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - [ - [26726, 32768, 0], - [1045, 32768, 0], - [11703, 32768, 0], - [20590, 32768, 0], - [18554, 32768, 0], - [25970, 32768, 0], - [31938, 32768, 0], - [5583, 32768, 0], - [21313, 32768, 0], - [29390, 32768, 0], - [641, 32768, 0], - [22265, 32768, 0], - [31452, 32768, 0], - ], - [ - [26584, 32768, 0], - [188, 32768, 0], - [8847, 32768, 0], - [24519, 32768, 0], - [22938, 32768, 0], - [30583, 32768, 0], - [32608, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - ], - [ - [ - [29614, 32768, 0], - [9068, 32768, 0], - [12924, 32768, 0], - [19538, 32768, 0], - [17737, 32768, 0], - [24619, 32768, 0], - [30642, 32768, 0], - [4119, 32768, 0], - [16026, 32768, 0], - [25657, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - [ - [31957, 32768, 0], - [3230, 32768, 0], - [11153, 32768, 0], - [18123, 32768, 0], - [20143, 32768, 0], - [26536, 32768, 0], - [31986, 32768, 0], - [3050, 32768, 0], - [14603, 32768, 0], - [25155, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - [ - [32363, 32768, 0], - [10692, 32768, 0], - [19090, 32768, 0], - [24357, 32768, 0], - [24442, 32768, 0], - [28312, 32768, 0], - [32169, 32768, 0], - [3648, 32768, 0], - [15690, 32768, 0], - [26815, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - [ - [30669, 32768, 0], - [3832, 32768, 0], - [11663, 32768, 0], - [18889, 32768, 0], - [19782, 32768, 0], - [23313, 32768, 0], - [31330, 32768, 0], - [5124, 32768, 0], - [18719, 32768, 0], - [28468, 32768, 0], - [3082, 32768, 0], - [20982, 32768, 0], - [29443, 32768, 0], - ], - [ - [28573, 32768, 0], - [3183, 32768, 0], - [17802, 32768, 0], - [25977, 32768, 0], - [26677, 32768, 0], - [27832, 32768, 0], - [32387, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - ], - [ - [ - [26887, 32768, 0], - [6729, 32768, 0], - [10361, 32768, 0], - [17442, 32768, 0], - [15045, 32768, 0], - [22478, 32768, 0], - [29072, 32768, 0], - [2713, 32768, 0], - [11861, 32768, 0], - [20773, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - [ - [31903, 32768, 0], - [2044, 32768, 0], - [7528, 32768, 0], - [14618, 32768, 0], - [16182, 32768, 0], - [24168, 32768, 0], - [31037, 32768, 0], - [2786, 32768, 0], - [11194, 32768, 0], - [20155, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - [ - [32510, 32768, 0], - [8430, 32768, 0], - [17318, 32768, 0], - [24154, 32768, 0], - [23674, 32768, 0], - [28789, 32768, 0], - [32139, 32768, 0], - [3440, 32768, 0], - [13117, 32768, 0], - [22702, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - [ - [31671, 32768, 0], - [2056, 32768, 0], - [11746, 32768, 0], - [16852, 32768, 0], - [18635, 32768, 0], - [24715, 32768, 0], - [31484, 32768, 0], - [4656, 32768, 0], - [16074, 32768, 0], - [24704, 32768, 0], - [1806, 32768, 0], - [14645, 32768, 0], - [25336, 32768, 0], - ], - [ - [31539, 32768, 0], - [8433, 32768, 0], - [20576, 32768, 0], - [27904, 32768, 0], - [27852, 32768, 0], - [30026, 32768, 0], - [32441, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - [16384, 32768, 0], - ], - ], -]; - -pub static DEFAULT_UV_MODE_CFL_ALLOWED_CDF: [[u16; 15]; 13] = [ - [10407, 11208, 12900, 13181, 13823, 14175, 14899, 15656, 15986, 20086, 20995, 22455, 24212, 32768, 0], - [4532, 19780, 20057, 20215, 20428, 21071, 21199, 21451, 22099, 24228, 24693, 27032, 29472, 32768, 0], - [5273, 5379, 20177, 20270, 20385, 20439, 20949, 21695, 21774, 23138, 24256, 24703, 26679, 32768, 0], - [6740, 7167, 7662, 14152, 14536, 14785, 15034, 16741, 18371, 21520, 22206, 23389, 24182, 32768, 0], - [4987, 5368, 5928, 6068, 19114, 20315, 21857, 22253, 22411, 24911, 25380, 26027, 26376, 32768, 0], - [5370, 6889, 7247, 7393, 9498, 21114, 21402, 21753, 21981, 24780, 25386, 26517, 27176, 32768, 0], - [4816, 4961, 7204, 7326, 8765, 8930, 20169, 20682, 20803, 23188, 23763, 24455, 24940, 32768, 0], - [6608, 6740, 8529, 9049, 9257, 9356, 9735, 18827, 19059, 22336, 23204, 23964, 24793, 32768, 0], - [5998, 7419, 7781, 8933, 9255, 9549, 9753, 10417, 18898, 22494, 23139, 24764, 25989, 32768, 0], - [10660, 11298, 12550, 12957, 13322, 13624, 14040, 15004, 15534, 20714, 21789, 23443, 24861, 32768, 0], - [10522, 11530, 12552, 12963, 13378, 13779, 14245, 15235, 15902, 20102, 22696, 23774, 25838, 32768, 0], - [10099, 10691, 12639, 13049, 13386, 13665, 14125, 15163, 15636, 19676, 20474, 23519, 25208, 32768, 0], - [3144, 5087, 7382, 7504, 7593, 7690, 7801, 8064, 8232, 9248, 9875, 10521, 29048, 32768, 0], -]; - -pub static DEFAULT_UV_MODE_CFL_NOT_ALLOWED_CDF: [[u16; 14]; 13] = [ - [22631, 24152, 25378, 25661, 25986, 26520, 27055, 27923, 28244, 30059, 30941, 31961, 32768, 0], - [9513, 26881, 26973, 27046, 27118, 27664, 27739, 27824, 28359, 29505, 29800, 31796, 32768, 0], - [9845, 9915, 28663, 28704, 28757, 28780, 29198, 29822, 29854, 30764, 31777, 32029, 32768, 0], - [13639, 13897, 14171, 25331, 25606, 25727, 25953, 27148, 28577, 30612, 31355, 32493, 32768, 0], - [9764, 9835, 9930, 9954, 25386, 27053, 27958, 28148, 28243, 31101, 31744, 32363, 32768, 0], - [11825, 13589, 13677, 13720, 15048, 29213, 29301, 29458, 29711, 31161, 31441, 32550, 32768, 0], - [14175, 14399, 16608, 16821, 17718, 17775, 28551, 30200, 30245, 31837, 32342, 32667, 32768, 0], - [12885, 13038, 14978, 15590, 15673, 15748, 16176, 29128, 29267, 30643, 31961, 32461, 32768, 0], - [12026, 13661, 13874, 15305, 15490, 15726, 15995, 16273, 28443, 30388, 30767, 32416, 32768, 0], - [19052, 19840, 20579, 20916, 21150, 21467, 21885, 22719, 23174, 28861, 30379, 32175, 32768, 0], - [18627, 19649, 20974, 21219, 21492, 21816, 22199, 23119, 23527, 27053, 31397, 32148, 32768, 0], - [17026, 19004, 19997, 20339, 20586, 21103, 21349, 21907, 22482, 25896, 26541, 31819, 32768, 0], - [12124, 13759, 14959, 14992, 15007, 15051, 15078, 15166, 15255, 15753, 16039, 16606, 32768, 0], -]; - -pub static DEFAULT_Y_MODE_CDF: [[u16; 14]; 4] = [ - [22801, 23489, 24293, 24756, 25601, 26123, 26606, 27418, 27945, 29228, 29685, 30349, 32768, 0], - [18673, 19845, 22631, 23318, 23950, 24649, 25527, 27364, 28152, 29701, 29984, 30852, 32768, 0], - [19770, 20979, 23396, 23939, 24241, 24654, 25136, 27073, 27830, 29360, 29730, 30659, 32768, 0], - [20155, 21301, 22838, 23178, 23261, 23533, 23703, 24804, 25352, 26575, 27016, 28049, 32768, 0], -]; - -pub static DEFAULT_ZERO_MV_CDF: [[u16; 3]; 2] = [[2175, 32768, 0], [1054, 32768, 0]]; - -pub static INTRA_MODE_CONTEXT: [u8; 13] = [0, 1, 2, 3, 4, 4, 4, 4, 3, 0, 1, 2, 0]; - -pub static MAG_REF_OFFSET_WITH_TX_CLASS: [[[u8; 2]; 3]; 3] = [[[0, 1], [1, 0], [1, 1]], [[0, 1], [1, 0], [0, 2]], [[0, 1], [1, 0], [2, 0]]]; - -pub static MAX_TX_SIZE_RECT: [u8; 22] = [0, 5, 6, 1, 7, 8, 2, 9, 10, 3, 11, 12, 4, 4, 4, 4, 13, 14, 15, 16, 17, 18]; - -pub static MI_HEIGHT_LOG2: [u8; 22] = [0, 1, 0, 1, 2, 1, 2, 3, 2, 3, 4, 3, 4, 5, 4, 5, 2, 0, 3, 1, 4, 2]; - -pub static MI_WIDTH_LOG2: [u8; 22] = [0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 0, 2, 1, 3, 2, 4]; - -pub static MODE_TO_TXFM: [u8; 14] = [0, 1, 2, 0, 3, 1, 2, 2, 1, 3, 1, 2, 3, 0]; - -pub static NUM_4X4_BLOCKS_HIGH: [u8; 22] = [1, 2, 1, 2, 4, 2, 4, 8, 4, 8, 16, 8, 16, 32, 16, 32, 4, 1, 8, 2, 16, 4]; - -pub static NUM_4X4_BLOCKS_WIDE: [u8; 22] = [1, 1, 2, 2, 2, 4, 4, 4, 8, 8, 8, 16, 16, 16, 32, 32, 1, 4, 2, 8, 4, 16]; - -pub static PARTITION_SUBSIZE: [[u8; 22]; 10] = [ - [0, 22, 22, 3, 22, 22, 6, 22, 22, 9, 22, 22, 12, 22, 22, 15, 22, 22, 22, 22, 22, 22], - [22, 22, 22, 2, 22, 22, 5, 22, 22, 8, 22, 22, 11, 22, 22, 14, 22, 22, 22, 22, 22, 22], - [22, 22, 22, 1, 22, 22, 4, 22, 22, 7, 22, 22, 10, 22, 22, 13, 22, 22, 22, 22, 22, 22], - [22, 22, 22, 0, 22, 22, 3, 22, 22, 6, 22, 22, 9, 22, 22, 12, 22, 22, 22, 22, 22, 22], - [22, 22, 22, 2, 22, 22, 5, 22, 22, 8, 22, 22, 11, 22, 22, 14, 22, 22, 22, 22, 22, 22], - [22, 22, 22, 2, 22, 22, 5, 22, 22, 8, 22, 22, 11, 22, 22, 14, 22, 22, 22, 22, 22, 22], - [22, 22, 22, 1, 22, 22, 4, 22, 22, 7, 22, 22, 10, 22, 22, 13, 22, 22, 22, 22, 22, 22], - [22, 22, 22, 1, 22, 22, 4, 22, 22, 7, 22, 22, 10, 22, 22, 13, 22, 22, 22, 22, 22, 22], - [22, 22, 22, 22, 22, 22, 17, 22, 22, 19, 22, 22, 21, 22, 22, 22, 22, 22, 22, 22, 22, 22], - [22, 22, 22, 22, 22, 22, 16, 22, 22, 18, 22, 22, 20, 22, 22, 22, 22, 22, 22, 22, 22, 22], -]; - -pub static SIG_REF_DIFF_OFFSET: [[[u8; 2]; 5]; 3] = - [[[0, 1], [1, 0], [1, 1], [0, 2], [2, 0]], [[0, 1], [1, 0], [0, 2], [0, 3], [0, 4]], [[0, 1], [1, 0], [2, 0], [3, 0], [4, 0]]]; - -pub static SIZE_GROUP: [u8; 22] = [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 0, 0, 1, 1, 2, 2]; - -pub static SM_WEIGHTS_TX_16X16: [u8; 16] = [255, 225, 196, 170, 145, 123, 102, 84, 68, 54, 43, 33, 26, 20, 17, 16]; - -pub static SM_WEIGHTS_TX_32X32: [u8; 32] = - [255, 240, 225, 210, 196, 182, 169, 157, 145, 133, 122, 111, 101, 92, 83, 74, 66, 59, 52, 45, 39, 34, 29, 25, 21, 17, 14, 12, 10, 9, 8, 8]; - -pub static SM_WEIGHTS_TX_4X4: [u8; 4] = [255, 149, 85, 64]; - -pub static SM_WEIGHTS_TX_8X8: [u8; 8] = [255, 197, 146, 105, 73, 50, 37, 32]; - -pub static SUBPEL_FILTERS: [[[i16; 8]; 16]; 6] = [ - [ - [0, 0, 0, 128, 0, 0, 0, 0], - [0, 2, -6, 126, 8, -2, 0, 0], - [0, 2, -10, 122, 18, -4, 0, 0], - [0, 2, -12, 116, 28, -8, 2, 0], - [0, 2, -14, 110, 38, -10, 2, 0], - [0, 2, -14, 102, 48, -12, 2, 0], - [0, 2, -16, 94, 58, -12, 2, 0], - [0, 2, -14, 84, 66, -12, 2, 0], - [0, 2, -14, 76, 76, -14, 2, 0], - [0, 2, -12, 66, 84, -14, 2, 0], - [0, 2, -12, 58, 94, -16, 2, 0], - [0, 2, -12, 48, 102, -14, 2, 0], - [0, 2, -10, 38, 110, -14, 2, 0], - [0, 2, -8, 28, 116, -12, 2, 0], - [0, 0, -4, 18, 122, -10, 2, 0], - [0, 0, -2, 8, 126, -6, 2, 0], - ], - [ - [0, 0, 0, 128, 0, 0, 0, 0], - [0, 2, 28, 62, 34, 2, 0, 0], - [0, 0, 26, 62, 36, 4, 0, 0], - [0, 0, 22, 62, 40, 4, 0, 0], - [0, 0, 20, 60, 42, 6, 0, 0], - [0, 0, 18, 58, 44, 8, 0, 0], - [0, 0, 16, 56, 46, 10, 0, 0], - [0, -2, 16, 54, 48, 12, 0, 0], - [0, -2, 14, 52, 52, 14, -2, 0], - [0, 0, 12, 48, 54, 16, -2, 0], - [0, 0, 10, 46, 56, 16, 0, 0], - [0, 0, 8, 44, 58, 18, 0, 0], - [0, 0, 6, 42, 60, 20, 0, 0], - [0, 0, 4, 40, 62, 22, 0, 0], - [0, 0, 4, 36, 62, 26, 0, 0], - [0, 0, 2, 34, 62, 28, 2, 0], - ], - [ - [0, 0, 0, 128, 0, 0, 0, 0], - [-2, 2, -6, 126, 8, -2, 2, 0], - [-2, 6, -12, 124, 16, -6, 4, -2], - [-2, 8, -18, 120, 26, -10, 6, -2], - [-4, 10, -22, 116, 38, -14, 6, -2], - [-4, 10, -22, 108, 48, -18, 8, -2], - [-4, 10, -24, 100, 60, -20, 8, -2], - [-4, 10, -24, 90, 70, -22, 10, -2], - [-4, 12, -24, 80, 80, -24, 12, -4], - [-2, 10, -22, 70, 90, -24, 10, -4], - [-2, 8, -20, 60, 100, -24, 10, -4], - [-2, 8, -18, 48, 108, -22, 10, -4], - [-2, 6, -14, 38, 116, -22, 10, -4], - [-2, 6, -10, 26, 120, -18, 8, -2], - [-2, 4, -6, 16, 124, -12, 6, -2], - [0, 2, -2, 8, 126, -6, 2, -2], - ], - [ - [0, 0, 0, 128, 0, 0, 0, 0], - [0, 0, 0, 120, 8, 0, 0, 0], - [0, 0, 0, 112, 16, 0, 0, 0], - [0, 0, 0, 104, 24, 0, 0, 0], - [0, 0, 0, 96, 32, 0, 0, 0], - [0, 0, 0, 88, 40, 0, 0, 0], - [0, 0, 0, 80, 48, 0, 0, 0], - [0, 0, 0, 72, 56, 0, 0, 0], - [0, 0, 0, 64, 64, 0, 0, 0], - [0, 0, 0, 56, 72, 0, 0, 0], - [0, 0, 0, 48, 80, 0, 0, 0], - [0, 0, 0, 40, 88, 0, 0, 0], - [0, 0, 0, 32, 96, 0, 0, 0], - [0, 0, 0, 24, 104, 0, 0, 0], - [0, 0, 0, 16, 112, 0, 0, 0], - [0, 0, 0, 8, 120, 0, 0, 0], - ], - [ - [0, 0, 0, 128, 0, 0, 0, 0], - [0, 0, -4, 126, 8, -2, 0, 0], - [0, 0, -8, 122, 18, -4, 0, 0], - [0, 0, -10, 116, 28, -6, 0, 0], - [0, 0, -12, 110, 38, -8, 0, 0], - [0, 0, -12, 102, 48, -10, 0, 0], - [0, 0, -14, 94, 58, -10, 0, 0], - [0, 0, -12, 84, 66, -10, 0, 0], - [0, 0, -12, 76, 76, -12, 0, 0], - [0, 0, -10, 66, 84, -12, 0, 0], - [0, 0, -10, 58, 94, -14, 0, 0], - [0, 0, -10, 48, 102, -12, 0, 0], - [0, 0, -8, 38, 110, -12, 0, 0], - [0, 0, -6, 28, 116, -10, 0, 0], - [0, 0, -4, 18, 122, -8, 0, 0], - [0, 0, -2, 8, 126, -4, 0, 0], - ], - [ - [0, 0, 0, 128, 0, 0, 0, 0], - [0, 0, 30, 62, 34, 2, 0, 0], - [0, 0, 26, 62, 36, 4, 0, 0], - [0, 0, 22, 62, 40, 4, 0, 0], - [0, 0, 20, 60, 42, 6, 0, 0], - [0, 0, 18, 58, 44, 8, 0, 0], - [0, 0, 16, 56, 46, 10, 0, 0], - [0, 0, 14, 54, 48, 12, 0, 0], - [0, 0, 12, 52, 52, 12, 0, 0], - [0, 0, 12, 48, 54, 14, 0, 0], - [0, 0, 10, 46, 56, 16, 0, 0], - [0, 0, 8, 44, 58, 18, 0, 0], - [0, 0, 6, 42, 60, 20, 0, 0], - [0, 0, 4, 40, 62, 22, 0, 0], - [0, 0, 4, 36, 62, 26, 0, 0], - [0, 0, 2, 34, 62, 30, 0, 0], - ], -]; - -pub static SUBSAMPLED_SIZE: [[[u8; 2]; 2]; 22] = [ - [[0, 0], [0, 0]], - [[1, 0], [22, 0]], - [[2, 22], [0, 0]], - [[3, 2], [1, 0]], - [[4, 3], [22, 1]], - [[5, 22], [3, 2]], - [[6, 5], [4, 3]], - [[7, 6], [22, 4]], - [[8, 22], [6, 5]], - [[9, 8], [7, 6]], - [[10, 9], [22, 7]], - [[11, 22], [9, 8]], - [[12, 11], [10, 9]], - [[13, 12], [22, 10]], - [[14, 22], [12, 11]], - [[15, 14], [13, 12]], - [[16, 1], [22, 1]], - [[17, 22], [2, 2]], - [[18, 4], [22, 16]], - [[19, 22], [5, 17]], - [[20, 7], [22, 18]], - [[21, 22], [8, 19]], -]; - -pub static TRANSFORM_ROW_SHIFT: [u8; 19] = [0, 1, 2, 2, 2, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2]; - -pub static TX_HEIGHT: [u8; 19] = [4, 8, 16, 32, 64, 8, 4, 16, 8, 32, 16, 64, 32, 16, 4, 32, 8, 64, 16]; - -pub static TX_HEIGHT_LOG2: [u8; 19] = [2, 3, 4, 5, 6, 3, 2, 4, 3, 5, 4, 6, 5, 4, 2, 5, 3, 6, 4]; - -pub static TX_SIZE_SQR: [u8; 19] = [0, 1, 2, 3, 4, 0, 0, 1, 1, 2, 2, 3, 3, 0, 0, 1, 1, 2, 2]; - -pub static TX_SIZE_SQR_UP: [u8; 19] = [0, 1, 2, 3, 4, 1, 1, 2, 2, 3, 3, 4, 4, 2, 2, 3, 3, 4, 4]; - -pub static TX_TYPE_IN_SET_INTER: [[u8; 16]; 4] = [ - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], - [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], -]; - -pub static TX_TYPE_IN_SET_INTRA: [[u8; 16]; 3] = - [[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0], [1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]]; - -pub static TX_TYPE_INTER_INV_SET3: [u8; 2] = [9, 0]; - -pub static TX_TYPE_INTRA_INV_SET2: [u8; 5] = [9, 0, 3, 1, 2]; - -pub static TX_WIDTH: [u8; 19] = [4, 8, 16, 32, 64, 4, 8, 8, 16, 16, 32, 32, 64, 4, 16, 8, 32, 16, 64]; - -pub static TX_WIDTH_LOG2: [u8; 19] = [2, 3, 4, 5, 6, 2, 3, 3, 4, 4, 5, 5, 6, 2, 4, 3, 5, 4, 6]; diff --git a/crates/av1enc/src/transform.rs b/crates/av1enc/src/transform.rs deleted file mode 100644 index 9d81ead0b..000000000 --- a/crates/av1enc/src/transform.rs +++ /dev/null @@ -1,575 +0,0 @@ -//! Transforms. The inverse transforms are the normative ones of spec 7.13 (mirrored from -//! filmcraft-av1, so that the encoder reconstructs exactly like a decoder); the forward -//! transforms are floating-point matrices scaled to match them (not normative). - -use std::sync::OnceLock; - -use crate::tables::{ADST_ADST, ADST_DCT, COS128_LOOKUP, DCT_ADST, DCT_DCT, TRANSFORM_ROW_SHIFT, TX_WIDTH_LOG2}; - -#[inline(always)] -fn round2_64(x: i64, n: u32) -> i32 { - if n == 0 { x as i32 } else { ((x + (1i64 << (n - 1))) >> n) as i32 } -} - -#[inline(always)] -pub(crate) fn round2(x: i32, n: u32) -> i32 { - if n == 0 { x } else { (x + (1 << (n - 1))) >> n } -} - -#[inline(always)] -fn cos128(angle: i32) -> i64 { - let a = (angle & 255) as usize; - (match a { - 0..=64 => COS128_LOOKUP[a] as i32, - 65..=128 => -(COS128_LOOKUP[128 - a] as i32), - 129..=192 => -(COS128_LOOKUP[a - 128] as i32), - _ => COS128_LOOKUP[256 - a] as i32, - }) as i64 -} - -#[inline(always)] -fn sin128(angle: i32) -> i64 { - cos128(angle - 64) -} - -#[inline(always)] -fn brev(num_bits: u32, x: usize) -> usize { - let mut t = 0; - for i in 0..num_bits { - let bit = (x >> i) & 1; - t += bit << (num_bits - 1 - i); - } - t -} - -#[inline(always)] -fn bf(t: &mut [i32], a: usize, b: usize, angle: i32, flip: bool) { - let x = t[a] as i64 * cos128(angle) - t[b] as i64 * sin128(angle); - let y = t[a] as i64 * sin128(angle) + t[b] as i64 * cos128(angle); - t[a] = round2_64(x, 12); - t[b] = round2_64(y, 12); - if flip { - t.swap(a, b); - } -} - -#[inline(always)] -fn hd(t: &mut [i32], a: usize, b: usize, flip: bool, r: u32) { - let (a, b) = if flip { (b, a) } else { (a, b) }; - let x = t[a]; - let y = t[b]; - let lo = -(1i32 << (r - 1)); - let hi = (1i32 << (r - 1)) - 1; - t[a] = (x + y).clamp(lo, hi); - t[b] = (x - y).clamp(lo, hi); -} - -/// Inverse DCT of length 2^N, N = 2..=5 (7.13.2.3), in place. `N` is a const generic so that the -/// bit-reversal permutations and loop bounds fold at compile time. -#[inline(always)] -fn inverse_dct_n(t: &mut [i32], r: u32) { - let n = N; - let len = 1usize << n; - let mut copy = [0i32; 32]; - copy[..len].copy_from_slice(&t[..len]); - for i in 0..len { - t[i] = copy[brev(n, i)]; - } - inverse_dct_initial_stages(t, n, r); - inverse_dct_final_stages(t, n, r); -} - -/// Rotate the high-frequency branches before the four-point core. -#[inline(always)] -fn inverse_dct_initial_stages(t: &mut [i32], n: u32, r: u32) { - if n >= 5 { - for i in 0..8 { - bf(t, 16 + i, 31 - i, 6 + ((brev(3, 7 - i) as i32) << 3), false); - } - } - if n >= 4 { - for i in 0..4 { - bf(t, 8 + i, 15 - i, 12 + ((brev(2, 3 - i) as i32) << 4), false); - } - } - if n >= 5 { - for i in 0..8 { - hd(t, 16 + 2 * i, 17 + 2 * i, i & 1 == 1, r); - } - } - if n >= 3 { - for i in 0..2 { - bf(t, 4 + i, 7 - i, 56 - 32 * i as i32, false); - } - } - if n >= 4 { - for i in 0..4 { - hd(t, 8 + 2 * i, 9 + 2 * i, i & 1 == 1, r); - } - } - if n >= 5 { - for i in 0..2 { - for j in 0..2 { - bf(t, 30 - 4 * i - j, 17 + 4 * i + j, 24 + ((j as i32) << 6) + (((1 - i) as i32) << 5), true); - } - } - } -} - -/// Evaluate the four-point core and merge progressively larger branches. -#[inline(always)] -fn inverse_dct_final_stages(t: &mut [i32], n: u32, r: u32) { - for i in 0..2 { - bf(t, 2 * i, 2 * i + 1, 32 + 16 * i as i32, i == 0); - } - if n >= 3 { - for i in 0..2 { - hd(t, 4 + 2 * i, 5 + 2 * i, i == 1, r); - } - } - if n >= 4 { - for i in 0..2 { - bf(t, 14 - i, 9 + i, 48 + 64 * i as i32, true); - } - } - if n >= 5 { - for i in 0..4 { - for j in 0..2 { - hd(t, 16 + 4 * i + j, 19 + 4 * i - j, i & 1 == 1, r); - } - } - } - for i in 0..2 { - hd(t, i, 3 - i, false, r); - } - if n >= 3 { - bf(t, 6, 5, 32, true); - } - if n >= 4 { - for i in 0..2 { - for j in 0..2 { - hd(t, 8 + 4 * i + j, 11 + 4 * i - j, i == 1, r); - } - } - } - if n >= 5 { - for i in 0..4 { - bf(t, 29 - i, 18 + i, 48 + (i as i32 >> 1) * 64, true); - } - } - if n >= 3 { - for i in 0..4 { - hd(t, i, 7 - i, false, r); - } - } - if n >= 4 { - for i in 0..2 { - bf(t, 13 - i, 10 + i, 32, true); - } - } - if n >= 5 { - for i in 0..2 { - for j in 0..4 { - hd(t, 16 + i * 8 + j, 23 + i * 8 - j, i == 1, r); - } - } - } - if n >= 4 { - for i in 0..8 { - hd(t, i, 15 - i, false, r); - } - } - if n >= 5 { - for i in 0..4 { - bf(t, 27 - i, 20 + i, 32, true); - } - } - if n >= 5 { - for i in 0..16 { - hd(t, i, 31 - i, false, r); - } - } -} - -const SINPI_1_9: i64 = 1321; -const SINPI_2_9: i64 = 2482; -const SINPI_3_9: i64 = 3344; -const SINPI_4_9: i64 = 3803; - -#[inline(always)] -fn inverse_adst4(t: &mut [i32]) { - let (t0, t1, t2, t3) = (t[0] as i64, t[1] as i64, t[2] as i64, t[3] as i64); - let mut s = [SINPI_1_9 * t0, SINPI_2_9 * t0, SINPI_3_9 * t1, SINPI_4_9 * t2, SINPI_1_9 * t2, SINPI_2_9 * t3, SINPI_4_9 * t3]; - let a7 = t0 - t2; - let b7 = a7 + t3; - s[0] += s[3]; - s[1] -= s[4]; - s[3] = s[2]; - s[2] = SINPI_3_9 * b7; - s[0] += s[5]; - s[1] -= s[6]; - let x = [s[0] + s[3], s[1] + s[3], s[2], s[0] + s[1] - s[3]]; - for i in 0..4 { - t[i] = round2_64(x[i], 12); - } -} - -#[inline(always)] -fn adst_in_permute(t: &mut [i32]) { - let n = N; - let n0 = 1usize << n; - let mut copy = [0i32; 16]; - copy[..n0].copy_from_slice(&t[..n0]); - for i in 0..n0 { - let idx = if i & 1 == 1 { i - 1 } else { n0 - i - 1 }; - t[i] = copy[idx]; - } -} - -#[inline(always)] -fn adst_out_permute(t: &mut [i32]) { - let n = N; - let n0 = 1usize << n; - let mut copy = [0i32; 16]; - copy[..n0].copy_from_slice(&t[..n0]); - for i in 0..n0 { - let a = (i >> 3) & 1; - let b = ((i >> 2) & 1) ^ ((i >> 3) & 1); - let c = ((i >> 1) & 1) ^ ((i >> 2) & 1); - let d = (i & 1) ^ ((i >> 1) & 1); - let idx = ((d << 3) | (c << 2) | (b << 1) | a) >> (4 - n); - t[i] = if i & 1 == 1 { -copy[idx] } else { copy[idx] }; - } -} - -#[inline(always)] -fn inverse_adst8(t: &mut [i32], r: u32) { - adst_in_permute::<3>(t); - for i in 0..4 { - bf(t, 2 * i, 2 * i + 1, 60 - 16 * i as i32, true); - } - for i in 0..4 { - hd(t, i, 4 + i, false, r); - } - for i in 0..2 { - bf(t, 4 + 3 * i, 5 + i, 48 - 32 * i as i32, true); - } - for i in 0..2 { - for j in 0..2 { - hd(t, 4 * j + i, 2 + 4 * j + i, false, r); - } - } - for i in 0..2 { - bf(t, 2 + 4 * i, 3 + 4 * i, 32, true); - } - adst_out_permute::<3>(t); -} - -#[inline(always)] -fn inverse_adst16(t: &mut [i32], r: u32) { - adst_in_permute::<4>(t); - for i in 0..8 { - bf(t, 2 * i, 2 * i + 1, 62 - 8 * i as i32, true); - } - for i in 0..8 { - hd(t, i, 8 + i, false, r); - } - for i in 0..2 { - bf(t, 8 + 2 * i, 9 + 2 * i, 56 - 32 * i as i32, true); - bf(t, 13 + 2 * i, 12 + 2 * i, 8 + 32 * i as i32, true); - } - for i in 0..4 { - for j in 0..2 { - hd(t, 8 * j + i, 4 + 8 * j + i, false, r); - } - } - for i in 0..2 { - for j in 0..2 { - bf(t, 4 + 8 * j + 3 * i, 5 + 8 * j + i, 48 - 32 * i as i32, true); - } - } - for i in 0..2 { - for j in 0..4 { - hd(t, 4 * j + i, 2 + 4 * j + i, false, r); - } - } - for i in 0..4 { - bf(t, 2 + 4 * i, 3 + 4 * i, 32, true); - } - adst_out_permute::<4>(t); -} - -/// 1-D transform kind. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub(crate) enum Kind { - Dct, - Adst, -} - -/// (vertical / column kind, horizontal / row kind) of a transform type. -pub(crate) fn kinds(tx_type: usize) -> (Kind, Kind) { - match tx_type { - DCT_DCT => (Kind::Dct, Kind::Dct), - ADST_DCT => (Kind::Adst, Kind::Dct), - DCT_ADST => (Kind::Dct, Kind::Adst), - ADST_ADST => (Kind::Adst, Kind::Adst), - // The encoder only chooses the four types above. - _ => { - debug_assert!(false, "unsupported transform type {tx_type}"); - (Kind::Dct, Kind::Dct) - } - } -} - -#[inline(always)] -fn inverse_dct(t: &mut [i32], n: u32, r: u32) { - match n { - 2 => inverse_dct_n::<2>(t, r), - 3 => inverse_dct_n::<3>(t, r), - 4 => inverse_dct_n::<4>(t, r), - _ => inverse_dct_n::<5>(t, r), - } -} - -#[inline(always)] -fn inverse_1d(t: &mut [i32], kind: Kind, n: u32, r: u32) { - match kind { - Kind::Dct => inverse_dct(t, n, r), - Kind::Adst => match n { - 2 => inverse_adst4(t), - 3 => inverse_adst8(t, r), - _ => inverse_adst16(t, r), - }, - } -} - -/// 2-D inverse transform (7.13.3) of a square `n x n` block (n = 4..=32) of dequantised -/// coefficients (row stride `n`) into `residual` (row stride `n`). -pub(crate) fn inverse_2d(dequant: &[i32], residual: &mut [i32], tx_sz: usize, tx_type: usize, bit_depth: u32) { - match TX_WIDTH_LOG2[tx_sz] { - 2 => inverse_2d_n::<2>(dequant, residual, tx_sz, tx_type, bit_depth), - 3 => inverse_2d_n::<3>(dequant, residual, tx_sz, tx_type, bit_depth), - 4 => inverse_2d_n::<4>(dequant, residual, tx_sz, tx_type, bit_depth), - _ => inverse_2d_n::<5>(dequant, residual, tx_sz, tx_type, bit_depth), - } -} - -/// `inverse_2d` for a block of width `1 << LOG2`. -fn inverse_2d_n(dequant: &[i32], residual: &mut [i32], tx_sz: usize, tx_type: usize, bit_depth: u32) { - let log2 = LOG2; - let n = 1usize << log2; - let row_shift = TRANSFORM_ROW_SHIFT[tx_sz] as u32; - let row_clamp = bit_depth + 8; - let col_clamp = (bit_depth + 6).max(16); - let (ck, rk) = kinds(tx_type); - let lo = -(1i32 << (col_clamp - 1)); - let hi = (1i32 << (col_clamp - 1)) - 1; - let mut t = [0i32; 32]; - for i in 0..n { - let row = &dequant[i * n..i * n + n]; - if row.iter().all(|&v| v == 0) { - residual[i * n..i * n + n].iter_mut().for_each(|v| *v = 0); - continue; - } - t[..n].copy_from_slice(row); - inverse_1d(&mut t, rk, log2, row_clamp); - for j in 0..n { - residual[i * n + j] = round2(t[j], row_shift).clamp(lo, hi); - } - } - for j in 0..n { - for i in 0..n { - t[i] = residual[i * n + j]; - } - inverse_1d(&mut t, ck, log2, col_clamp); - for i in 0..n { - residual[i * n + j] = round2(t[i], 4); - } - } -} - -/// Orthonormal forward basis matrices `[k][i]` (row k = basis function k), sizes 4..=32. -struct Bases { - dct: [Vec; 4], - adst: [Vec; 3], - /// The same matrices transposed (`[i][k]`). - dct_t: [Vec; 4], - adst_t: [Vec; 3], -} - -fn transpose(m: &[f32]) -> Vec { - let n = (m.len() as f64).sqrt() as usize; - let mut t = vec![0f32; n * n]; - for k in 0..n { - for i in 0..n { - t[i * n + k] = m[k * n + i]; - } - } - t -} - -fn bases() -> &'static Bases { - static B: OnceLock = OnceLock::new(); - B.get_or_init(|| { - let pi = std::f64::consts::PI; - let dct = std::array::from_fn(|l| { - let n = 4usize << l; - let mut m = vec![0f32; n * n]; - for k in 0..n { - let c = if k == 0 { std::f64::consts::FRAC_1_SQRT_2 } else { 1.0 }; - for i in 0..n { - m[k * n + i] = (c * (2.0 / n as f64).sqrt() * (pi * (2 * i + 1) as f64 * k as f64 / (2 * n) as f64).cos()) as f32; - } - } - m - }); - let adst = std::array::from_fn(|l| { - let n = 4usize << l; - let mut m = vec![0f32; n * n]; - for k in 0..n { - for i in 0..n { - m[k * n + i] = if n == 4 { - // the AV1 4-point ADST (sin(pi (i + 1)(2k + 1) / 9) basis) - (2.0 / 3.0 * (pi * ((i + 1) * (2 * k + 1)) as f64 / 9.0).sin()) as f32 - } else { - ((2.0 / n as f64).sqrt() * (pi * ((2 * i + 1) * (2 * k + 1)) as f64 / (4 * n) as f64).sin()) as f32 - }; - } - } - m - }); - let dct_t = std::array::from_fn(|l| transpose(&dct[l])); - let adst_t = std::array::from_fn(|l| transpose(&adst[l])); - Bases { dct, adst, dct_t, adst_t } - }) -} - -fn basis_t(kind: Kind, n: usize) -> &'static [f32] { - let l = n.trailing_zeros() as usize - 2; - match kind { - Kind::Dct => &bases().dct_t[l], - Kind::Adst => &bases().adst_t[l], - } -} - -fn basis(kind: Kind, n: usize) -> &'static [f32] { - let l = n.trailing_zeros() as usize - 2; - match kind { - Kind::Dct => &bases().dct[l], - Kind::Adst => &bases().adst[l], - } -} - -/// Forward 2-D transform of the `n x n` residual (row stride `n`) into orthonormal-domain -/// coefficients scaled by 8 (so that `level = out / q` with the spec's dequantiser). -pub(crate) fn forward_2d(residual: &[i32], out: &mut [f32], n: usize, tx_type: usize) { - match n { - 4 => forward_2d_n::<4, 16>(residual, out, tx_type), - 8 => forward_2d_n::<8, 64>(residual, out, tx_type), - 16 => forward_2d_n::<16, 256>(residual, out, tx_type), - _ => forward_2d_dyn(residual, out, n, tx_type), - } -} - -/// `forward_2d` for a fixed size, so `tmp` is exactly `NN = N * N` floats (no 4 KiB zeroing). -#[inline(always)] -fn forward_2d_n(residual: &[i32], out: &mut [f32], tx_type: usize) { - let n = N; - let (ck, rk) = kinds(tx_type); - let bc = &basis(ck, n)[..NN]; - let brt = &basis_t(rk, n)[..NN]; - let residual = &residual[..NN]; - let out = &mut out[..NN]; - let mut tmp = [0f32; NN]; - for i in 0..n { - let t = &mut tmp[i * n..i * n + n]; - for j in 0..n { - let r = residual[i * n + j]; - if r == 0 { - continue; - } - let r = r as f32; - for (o, &b) in t.iter_mut().zip(&brt[j * n..j * n + n]) { - *o += r * b; - } - } - } - out.iter_mut().for_each(|v| *v = 0.0); - for k in 0..n { - let o = &mut out[k * n..k * n + n]; - for i in 0..n { - let b = 8.0 * bc[k * n + i]; - for (v, &t) in o.iter_mut().zip(&tmp[i * n..i * n + n]) { - *v += b * t; - } - } - } -} - -/// 32x32 (and any other size): a const-generic 32 version measured slower. -fn forward_2d_dyn(residual: &[i32], out: &mut [f32], n: usize, tx_type: usize) { - let (ck, rk) = kinds(tx_type); - let bc = basis(ck, n); - let brt = basis_t(rk, n); - // tmp[i][k] = sum_j residual[i][j] * br[k][j] (horizontal; written as row updates so that - // the inner loops vectorise) - let mut tmp = [0f32; 32 * 32]; - for i in 0..n { - let t = &mut tmp[i * n..i * n + n]; - for j in 0..n { - let r = residual[i * n + j]; - if r == 0 { - continue; - } - let r = r as f32; - for (o, &b) in t.iter_mut().zip(&brt[j * n..j * n + n]) { - *o += r * b; - } - } - } - // out[k][j] = 8 * sum_i bc[k][i] * tmp[i][j] (vertical) - out[..n * n].iter_mut().for_each(|v| *v = 0.0); - for k in 0..n { - let o = &mut out[k * n..k * n + n]; - for i in 0..n { - let b = 8.0 * bc[k * n + i]; - for (v, &t) in o.iter_mut().zip(&tmp[i * n..i * n + n]) { - *v += b * t; - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::tables::{TX_4X4, TX_8X8, TX_16X16, TX_32X32}; - - /// forward (float) then the normative inverse reproduces the residual: the forward basis - /// and scale match the decoder's inverse transforms for every supported size and type. - #[test] - fn forward_matches_inverse() { - let mut seed = 99u32; - let mut rnd = || { - seed = seed.wrapping_mul(1_103_515_245).wrapping_add(12345); - ((seed >> 16) % 201) as i32 - 100 - }; - for (tx, n) in [(TX_4X4, 4), (TX_8X8, 8), (TX_16X16, 16), (TX_32X32, 32)] { - let denom = if tx == TX_32X32 { 2.0 } else { 1.0 }; - for tx_type in [DCT_DCT, ADST_DCT, DCT_ADST, ADST_ADST] { - if n == 32 && tx_type != DCT_DCT { - continue; - } - let res: Vec = (0..n * n).map(|_| rnd()).collect(); - let mut coef = vec![0f32; n * n]; - forward_2d(&res, &mut coef, n, tx_type); - // q = 8 / denom keeps full precision: dequant = level * q / denom = coef - let deq: Vec = coef.iter().map(|&c| (c / denom).round() as i32).collect(); - let mut out = vec![0i32; n * n]; - inverse_2d(&deq, &mut out, tx, tx_type, 8); - let err: i32 = res.iter().zip(&out).map(|(a, b)| (a - b).abs()).max().unwrap(); - assert!(err <= 1, "tx {tx} type {tx_type}: max error {err}"); - } - } - } -} diff --git a/crates/av1enc/src/writer.rs b/crates/av1enc/src/writer.rs deleted file mode 100644 index a10bd2e9a..000000000 --- a/crates/av1enc/src/writer.rs +++ /dev/null @@ -1,1040 +0,0 @@ -//! Tile syntax writer (spec 5.11 with the 8.3 CDF selection). It keeps the same per-4x4 mode -//! info, coefficient contexts and adaptive CDFs as a decoder and writes the decisions taken by -//! `decide.rs` as symbols. The context derivations and the motion vector prediction process -//! (7.10.2) mirror the decoder exactly, since symbols and coded motion vector differences -//! depend on them. - -use crate::cdf::Cdfs; -use crate::decide::{FrameGeom, Leaf, Node, intra_uv_tx_type, scan_for, tx_in_set, tx_set, tx_size_for}; -use crate::loopfilter::LfInfo; -use crate::symbol::SymbolWriter; -use crate::tables::{ - ADST_ADST, ADST_DCT, ALTREF_FRAME, ALTREF2_FRAME, BLOCK_8X8, BLOCK_64X64, BR_CDF_SIZE, BWDREF_FRAME, CLASS0_SIZE, COEFF_BASE_CTX_OFFSET, COEFF_BASE_RANGE, - D67_PRED, DC_PRED, DCT_ADST, DCT_DCT, DEFAULT_INTRA_FRAME_Y_MODE_CDF, GLOBALMV, GOLDEN_FRAME, INTRA_FRAME, INTRA_MODE_CONTEXT, LAST_FRAME, LAST2_FRAME, - LAST3_FRAME, MAG_REF_OFFSET_WITH_TX_CLASS, MAX_ANGLE_DELTA, MAX_REF_MV_STACK_SIZE, MI_HEIGHT_LOG2, MI_WIDTH_LOG2, MV_BORDER, MV_CLASS_0, NEARESTMV, NEARMV, - NEWMV, NUM_4X4_BLOCKS_HIGH, NUM_4X4_BLOCKS_WIDE, NUM_BASE_LEVELS, PARTITION_HORZ, PARTITION_HORZ_4, PARTITION_HORZ_A, PARTITION_HORZ_B, PARTITION_NONE, - PARTITION_SPLIT, PARTITION_SUBSIZE, PARTITION_VERT, PARTITION_VERT_4, PARTITION_VERT_A, PARTITION_VERT_B, REF_CAT_LEVEL, SIG_REF_DIFF_OFFSET, SIZE_GROUP, - SUBSAMPLED_SIZE, TX_32X32, TX_CLASS_2D, TX_HEIGHT, TX_HEIGHT_LOG2, TX_SIZE_SQR, TX_SIZE_SQR_UP, TX_TYPE_INTER_INV_SET3, TX_TYPE_INTRA_INV_SET2, TX_WIDTH, - TX_WIDTH_LOG2, V_PRED, -}; - -type MvP = [i32; 2]; - -/// Mode info per 4x4 (MiSizes, YModes, RefFrames, Mvs, ...). -struct Mi { - cols: usize, - mi_size: Vec, - y_mode: Vec, - ref_frame: Vec<[i8; 2]>, - mv: Vec, - skip: Vec, - is_inter: Vec, - tx_type: Vec, - written: Vec, -} - -impl Mi { - #[inline(always)] - fn idx(&self, r: usize, c: usize) -> usize { - r * self.cols + c - } -} - -/// Output of find_mv_stack. -#[derive(Default)] -struct MvStack { - ref_stack_mv: [MvP; MAX_REF_MV_STACK_SIZE + 1], - weight_stack: [u32; MAX_REF_MV_STACK_SIZE + 1], - num_mv_found: usize, - new_mv_count: usize, - found_match: bool, - close_matches: usize, - total_matches: usize, - new_mv_context: usize, - ref_mv_context: usize, - zero_mv_context: usize, - drl_ctx_stack: [usize; MAX_REF_MV_STACK_SIZE + 1], -} - -pub(crate) struct TileWriter<'a> { - g: &'a FrameGeom, - intra_only: bool, - qidx: u8, - sw: SymbolWriter, - cdf: Box, - kf_y_mode: [[[u16; 14]; 5]; 5], - mi: Mi, - above_level: [Vec; 3], - above_dc: [Vec; 3], - left_level: [Vec; 3], - left_dc: [Vec; 3], - /// LoopfilterTxSizes per plane (row stride mi_cols + 32). - lf_tx: [Vec; 3], - /// Scratch for the levels coded so far in `coeffs`. - quant: Vec, - // current block - r: usize, - c: usize, - bsize: usize, - avail_u: bool, - avail_l: bool, - mvs: MvStack, -} - -fn block_size_px(bs: usize) -> usize { - 4 * NUM_4X4_BLOCKS_WIDE[bs] as usize -} - -fn is_directional(mode: usize) -> bool { - (V_PRED..=D67_PRED).contains(&mode) -} - -impl<'a> TileWriter<'a> { - /// `init`: the CDFs saved by the primary reference frame (load_cdfs), or None for the - /// defaults (init_non_coeff_cdfs / init_coeff_cdfs). - pub fn new(g: &'a FrameGeom, intra_only: bool, qidx: u8, init: Option<&Cdfs>) -> Self { - let n = g.mi_cols * g.mi_rows; - TileWriter { - g, - intra_only, - qidx, - sw: SymbolWriter::new(false), - cdf: match init { - Some(c) => { - let mut c = Box::new(c.clone()); - c.clear_counts(); - c - } - None => Cdfs::new(qidx as u32), - }, - kf_y_mode: DEFAULT_INTRA_FRAME_Y_MODE_CDF, - mi: Mi { - cols: g.mi_cols, - mi_size: vec![0; n], - y_mode: vec![0; n], - ref_frame: vec![[0, -1]; n], - mv: vec![[0, 0]; n], - skip: vec![false; n], - is_inter: vec![false; n], - tx_type: vec![0; n], - written: vec![false; n], - }, - above_level: std::array::from_fn(|_| vec![0; g.mi_cols + 64]), - above_dc: std::array::from_fn(|_| vec![0; g.mi_cols + 64]), - left_level: std::array::from_fn(|_| vec![0; g.mi_rows + 64]), - left_dc: std::array::from_fn(|_| vec![0; g.mi_rows + 64]), - lf_tx: std::array::from_fn(|_| vec![0; (g.mi_cols + 32) * (g.mi_rows + 32)]), - quant: Vec::new(), - r: 0, - c: 0, - bsize: 0, - avail_u: false, - avail_l: false, - mvs: MvStack::default(), - } - } - - /// The tile data and the final CDFs (saved for the frame-end CDF update). - pub fn finish(self) -> (Vec, Box) { - (self.sw.finish(), self.cdf) - } - - /// The mode info the loop filter needs. - pub fn lf_info(&self) -> LfInfo<'_> { - LfInfo { - mi_cols: self.g.mi_cols, - mi_rows: self.g.mi_rows, - width: self.g.width, - height: self.g.height, - mi_size: &self.mi.mi_size, - skip: &self.mi.skip, - is_inter: &self.mi.is_inter, - tx: [&self.lf_tx[0], &self.lf_tx[1], &self.lf_tx[2]], - } - } - - /// Start of a superblock row (clear_left_context). - pub fn start_sb_row(&mut self) { - for p in 0..3 { - self.left_level[p].iter_mut().for_each(|v| *v = 0); - self.left_dc[p].iter_mut().for_each(|v| *v = 0); - } - } - - pub fn superblock(&mut self, r: usize, c: usize, node: &Node) { - self.partition(r, c, BLOCK_64X64, node); - } - - fn partition(&mut self, r: usize, c: usize, bsize: usize, node: &Node) { - let (mi_rows, mi_cols) = (self.g.mi_rows, self.g.mi_cols); - if r >= mi_rows || c >= mi_cols { - return; - } - let avail_u = r > 0; - let avail_l = c > 0; - let n4 = NUM_4X4_BLOCKS_WIDE[bsize] as usize; - let half = n4 >> 1; - let has_rows = r + half < mi_rows; - let has_cols = c + half < mi_cols; - let split = matches!(node, Node::Split(_)); - let bsl = MI_WIDTH_LOG2[bsize] as usize; - let above = avail_u && (MI_WIDTH_LOG2[self.mi.mi_size[self.mi.idx(r - 1, c)] as usize] as usize) < bsl; - let left = avail_l && (MI_HEIGHT_LOG2[self.mi.mi_size[self.mi.idx(r, c - 1)] as usize] as usize) < bsl; - let ctx = left as usize * 2 + above as usize; - let partition = if split { PARTITION_SPLIT } else { PARTITION_NONE }; - if has_rows && has_cols { - let cdf: &mut [u16] = match bsl { - 1 => &mut self.cdf.partition_w8[ctx], - 2 => &mut self.cdf.partition_w16[ctx], - 3 => &mut self.cdf.partition_w32[ctx], - _ => &mut self.cdf.partition_w64[ctx], - }; - self.sw.symbol(partition, cdf); - } else if has_cols || has_rows { - assert!(split, "blocks crossing the frame edge are split"); - let pcdf: &[u16] = match bsl { - 2 => &self.cdf.partition_w16[ctx], - 3 => &self.cdf.partition_w32[ctx], - _ => &self.cdf.partition_w64[ctx], - }; - let pr = |k: usize| pcdf[k] as i32 - if k == 0 { 0 } else { pcdf[k - 1] as i32 }; - let psum = if has_cols { - // split_or_horz - pr(PARTITION_VERT) + pr(PARTITION_SPLIT) + pr(PARTITION_HORZ_A) + pr(PARTITION_VERT_A) + pr(PARTITION_VERT_B) + pr(PARTITION_VERT_4) - } else { - // split_or_vert - pr(PARTITION_HORZ) + pr(PARTITION_SPLIT) + pr(PARTITION_HORZ_A) + pr(PARTITION_HORZ_B) + pr(PARTITION_VERT_A) + pr(PARTITION_HORZ_4) - }; - let cdf = [((1i32 << 15) - psum) as u16, 1 << 15, 0]; - self.sw.symbol_fixed(1, &cdf); - } else { - assert!(split); - } - match node { - Node::Split(ch) => { - let sub = PARTITION_SUBSIZE[PARTITION_SPLIT][bsize] as usize; - self.partition(r, c, sub, &ch[0]); - self.partition(r, c + half, sub, &ch[1]); - self.partition(r + half, c, sub, &ch[2]); - self.partition(r + half, c + half, sub, &ch[3]); - } - Node::Leaf(leaf) => { - debug_assert_eq!(leaf.bsize, bsize); - self.block(r, c, leaf); - } - // Blocks inside the frame are always decided; nothing is coded outside it. - Node::Out => debug_assert!(false, "undecided block inside the frame"), - } - } - - fn block(&mut self, r: usize, c: usize, b: &Leaf) { - self.r = r; - self.c = c; - self.bsize = b.bsize; - self.avail_u = r > 0; - self.avail_l = c > 0; - let bsize = b.bsize; - let n4 = NUM_4X4_BLOCKS_WIDE[bsize] as usize; - // skip - let mut ctx = 0; - if self.avail_u { - ctx += self.mi.skip[self.mi.idx(r - 1, c)] as usize; - } - if self.avail_l { - ctx += self.mi.skip[self.mi.idx(r, c - 1)] as usize; - } - let mut y_mode = b.y_mode; - if self.intra_only { - self.sw.symbol(b.skip as usize, &mut self.cdf.skip[ctx]); - let above = if self.avail_u { self.mi.y_mode[self.mi.idx(r - 1, c)] as usize } else { DC_PRED }; - let left = if self.avail_l { self.mi.y_mode[self.mi.idx(r, c - 1)] as usize } else { DC_PRED }; - let actx = INTRA_MODE_CONTEXT[above] as usize; - let lctx = INTRA_MODE_CONTEXT[left] as usize; - self.sw.symbol(b.y_mode, &mut self.kf_y_mode[actx][lctx]); - self.intra_angle_and_uv(b); - } else { - self.sw.symbol(b.skip as usize, &mut self.cdf.skip[ctx]); - // is_inter - let left = if self.avail_l { self.mi.ref_frame[self.mi.idx(r, c - 1)] } else { [INTRA_FRAME as i8, -1] }; - let above = if self.avail_u { self.mi.ref_frame[self.mi.idx(r - 1, c)] } else { [INTRA_FRAME as i8, -1] }; - let left_intra = left[0] <= INTRA_FRAME as i8; - let above_intra = above[0] <= INTRA_FRAME as i8; - let ctx = if self.avail_u && self.avail_l { - if left_intra && above_intra { 3 } else { (left_intra || above_intra) as usize } - } else if self.avail_u || self.avail_l { - 2 * (if self.avail_u { above_intra } else { left_intra }) as usize - } else { - 0 - }; - self.sw.symbol(b.is_inter as usize, &mut self.cdf.is_inter[ctx]); - if b.is_inter { - y_mode = self.inter_block_mode_info(b, left, above); - } else { - let ctx = SIZE_GROUP[bsize] as usize; - self.sw.symbol(b.y_mode, &mut self.cdf.y_mode[ctx]); - self.intra_angle_and_uv(b); - } - } - // mode info stored before the residual - let (rows, cols) = (n4.min(self.g.mi_rows - r), n4.min(self.g.mi_cols - c)); - for y in 0..rows { - for x in 0..cols { - let i = self.mi.idx(r + y, c + x); - self.mi.y_mode[i] = y_mode as u8; - self.mi.ref_frame[i] = if b.is_inter { [LAST_FRAME as i8, -1] } else { [INTRA_FRAME as i8, -1] }; - self.mi.written[i] = true; - if b.is_inter { - self.mi.mv[i] = b.mv; - } - } - } - if b.skip { - // reset_block_context( ) - for plane in 0..3 { - let s = (plane > 0) as usize; - for i in (c >> s)..((c + n4) >> s) { - self.above_level[plane][i] = 0; - self.above_dc[plane][i] = 0; - } - for i in (r >> s)..((r + n4) >> s) { - self.left_level[plane][i] = 0; - self.left_dc[plane][i] = 0; - } - } - } else { - let log2 = (n4 * 4).trailing_zeros(); - assert!(log2 <= 5, "64x64 blocks are coded as skip blocks"); - for plane in 0..3 { - let s = (plane > 0) as u32; - self.coeffs(plane, (c * 4) >> s, (r * 4) >> s, tx_size_for(log2 - s), b, &b.coefs[plane]); - } - } - for y in 0..rows { - for x in 0..cols { - let i = self.mi.idx(r + y, c + x); - self.mi.is_inter[i] = b.is_inter; - self.mi.skip[i] = b.skip; - self.mi.mi_size[i] = bsize as u8; - } - } - // LoopfilterTxSizes: one transform block per plane - let log2 = (n4 * 4).trailing_zeros(); - let stride = self.g.mi_cols + 32; - for plane in 0..3 { - let s = (plane > 0) as usize; - let tx = tx_size_for(log2 - s as u32) as u8; - let (r0, c0, m) = (r >> s, c >> s, n4 >> s); - for y in r0..r0 + m { - self.lf_tx[plane][y * stride + c0..y * stride + c0 + m].iter_mut().for_each(|v| *v = tx); - } - } - } - - fn intra_angle_and_uv(&mut self, b: &Leaf) { - if self.bsize >= BLOCK_8X8 && is_directional(b.y_mode) { - self.sw.symbol(MAX_ANGLE_DELTA, &mut self.cdf.angle_delta[b.y_mode - V_PRED]); - } - let bs = block_size_px(self.bsize); - if bs <= 32 { - self.sw.symbol(b.uv_mode, &mut self.cdf.uv_mode_cfl_allowed[b.y_mode]); - } else { - self.sw.symbol(b.uv_mode, &mut self.cdf.uv_mode_cfl_not_allowed[b.y_mode]); - } - if self.bsize >= BLOCK_8X8 && is_directional(b.uv_mode) { - self.sw.symbol(MAX_ANGLE_DELTA, &mut self.cdf.angle_delta[b.uv_mode - V_PRED]); - } - } - - /// read_ref_frames (LAST_FRAME), find_mv_stack, the inter mode, DRL and motion vector. - /// Returns the inter mode (YMode). - fn inter_block_mode_info(&mut self, b: &Leaf, left: [i8; 2], above: [i8; 2]) -> usize { - let count = |ft: i8| -> usize { - let mut n = 0; - if self.avail_u { - n += (above[0] == ft) as usize + (above[1] == ft) as usize; - } - if self.avail_l { - n += (left[0] == ft) as usize + (left[1] == ft) as usize; - } - n - }; - let cmp = |a: usize, b: usize| match a.cmp(&b) { - std::cmp::Ordering::Less => 0, - std::cmp::Ordering::Equal => 1, - std::cmp::Ordering::Greater => 2, - }; - let (last, last2, last3, gold) = (count(LAST_FRAME as i8), count(LAST2_FRAME as i8), count(LAST3_FRAME as i8), count(GOLDEN_FRAME as i8)); - let (bwd, alt2, alt) = (count(BWDREF_FRAME as i8), count(ALTREF2_FRAME as i8), count(ALTREF_FRAME as i8)); - let ctx_p1 = cmp(last + last2 + last3 + gold, bwd + alt2 + alt); - let ctx_comp_ref = cmp(last + last2, last3 + gold); - let ctx_comp_ref_p1 = cmp(last, last2); - self.sw.symbol(0, &mut self.cdf.single_ref[ctx_p1][0]); - self.sw.symbol(0, &mut self.cdf.single_ref[ctx_comp_ref][2]); - self.sw.symbol(0, &mut self.cdf.single_ref[ctx_comp_ref_p1][3]); - self.find_mv_stack(); - let mv = b.mv; - let s = &self.mvs; - let mode = if mv == [0, 0] { - GLOBALMV - } else if mv == s.ref_stack_mv[0] { - NEARESTMV - } else if mv == s.ref_stack_mv[1] { - NEARMV - } else { - NEWMV - }; - let (nctx, zctx, rctx) = (s.new_mv_context, s.zero_mv_context, s.ref_mv_context); - self.sw.symbol((mode != NEWMV) as usize, &mut self.cdf.new_mv[nctx]); - if mode != NEWMV { - self.sw.symbol((mode != GLOBALMV) as usize, &mut self.cdf.zero_mv[zctx]); - if mode != GLOBALMV { - self.sw.symbol((mode == NEARMV) as usize, &mut self.cdf.ref_mv[rctx]); - } - } - // DRL: ref_mv_idx 0 for NEWMV, 1 for NEARMV - if mode == NEWMV { - if self.mvs.num_mv_found > 1 { - let ctx = self.mvs.drl_ctx_stack[0]; - self.sw.symbol(0, &mut self.cdf.drl_mode[ctx]); - } - } else if mode == NEARMV && self.mvs.num_mv_found > 2 { - let ctx = self.mvs.drl_ctx_stack[1]; - self.sw.symbol(0, &mut self.cdf.drl_mode[ctx]); - } - if mode == NEWMV { - let pred = self.mvs.ref_stack_mv[0]; - self.write_mv([mv[0] - pred[0], mv[1] - pred[1]]); - } - mode - } - - fn write_mv(&mut self, diff: MvP) { - let joint = ((diff[0] != 0) as usize) << 1 | (diff[1] != 0) as usize; - // MV_JOINT_ZERO, MV_JOINT_HNZVZ (col only), MV_JOINT_HZVNZ (row only), MV_JOINT_HNZVNZ - self.sw.symbol(joint, &mut self.cdf.mv_joint); - for (comp, &d) in diff.iter().enumerate() { - if d != 0 { - self.write_mv_component(comp, d); - } - } - } - - fn write_mv_component(&mut self, comp: usize, v: i32) { - debug_assert!(v % 2 == 0, "quarter-sample precision"); - self.sw.symbol((v < 0) as usize, &mut self.cdf.mv_sign[comp]); - let m = v.unsigned_abs() - 1; // odd: the implied hp bit is 1 - let fr = ((m >> 1) & 3) as usize; - let int = m >> 3; - if int < CLASS0_SIZE as u32 { - self.sw.symbol(MV_CLASS_0, &mut self.cdf.mv_class[comp]); - self.sw.symbol(int as usize, &mut self.cdf.mv_class0_bit[comp]); - self.sw.symbol(fr, &mut self.cdf.mv_class0_fr[comp][int as usize]); - } else { - let class = 31 - int.leading_zeros(); - let d = int - (1 << class); - self.sw.symbol(class as usize, &mut self.cdf.mv_class[comp]); - for i in 0..class as usize { - self.sw.symbol(((d >> i) & 1) as usize, &mut self.cdf.mv_bit[comp][i]); - } - self.sw.symbol(fr, &mut self.cdf.mv_fr[comp]); - } - } - - // ---- coefficients ---------------------------------------------------------------------- - - fn coeffs(&mut self, plane: usize, start_x: usize, start_y: usize, tx_sz: usize, b: &Leaf, levels: &[i32]) { - let x4 = start_x >> 2; - let y4 = start_y >> 2; - let w4 = TX_WIDTH[tx_sz] as usize >> 2; - let h4 = TX_HEIGHT[tx_sz] as usize >> 2; - let tx_sz_ctx = (TX_SIZE_SQR[tx_sz] as usize + TX_SIZE_SQR_UP[tx_sz] as usize + 1) >> 1; - let ptype = (plane > 0) as usize; - let scan = scan_for(tx_sz); - let mut eob = 0; - for (i, &pos) in scan.iter().enumerate() { - if levels[pos as usize] != 0 { - eob = i + 1; - } - } - let ctx = self.all_zero_ctx(plane, tx_sz, x4, y4, w4, h4); - self.sw.symbol((eob == 0) as usize, &mut self.cdf.txb_skip[tx_sz_ctx][ctx]); - let mut cul_level = 0u32; - let mut dc_category = 0u8; - if eob == 0 { - if plane == 0 { - self.set_tx_types(x4, y4, tx_sz, DCT_DCT); - } - } else { - if plane == 0 { - self.write_transform_type(x4, y4, tx_sz, b); - } - let plane_tx_type = self.compute_tx_type(plane, tx_sz, x4, y4, b); - debug_assert!(plane_tx_type == DCT_DCT || matches!(plane_tx_type, ADST_DCT | DCT_ADST | ADST_ADST)); - let eob_multisize = (TX_WIDTH_LOG2[tx_sz] as usize).min(5) + (TX_HEIGHT_LOG2[tx_sz] as usize).min(5) - 4; - let eob_pt = if eob < 2 { eob } else { (32 - ((eob - 1) as u32).leading_zeros()) as usize + 1 }; - let ectx = 0; // TX_CLASS_2D - let s = eob_pt - 1; - match eob_multisize { - 0 => self.sw.symbol(s, &mut self.cdf.eob_pt_16[ptype][ectx]), - 1 => self.sw.symbol(s, &mut self.cdf.eob_pt_32[ptype][ectx]), - 2 => self.sw.symbol(s, &mut self.cdf.eob_pt_64[ptype][ectx]), - 3 => self.sw.symbol(s, &mut self.cdf.eob_pt_128[ptype][ectx]), - 4 => self.sw.symbol(s, &mut self.cdf.eob_pt_256[ptype][ectx]), - 5 => self.sw.symbol(s, &mut self.cdf.eob_pt_512[ptype]), - _ => self.sw.symbol(s, &mut self.cdf.eob_pt_1024[ptype]), - } - if eob_pt >= 3 { - let eob_shift = eob_pt - 3; - let offset = eob - ((1 << (eob_pt - 2)) + 1); - self.sw.symbol((offset >> eob_shift) & 1, &mut self.cdf.eob_extra[tx_sz_ctx][ptype][eob_pt - 3]); - for i in 1..eob_pt - 2 { - let sh = eob_pt - 2 - 1 - i; - self.sw.bool((offset >> sh) & 1 == 1); - } - } - let bwl = TX_WIDTH_LOG2[tx_sz] as usize; - let height = TX_HEIGHT[tx_sz] as usize; - let width = 1usize << bwl; - // Quant[] as the decoder sees it while parsing: the levels coded so far (capped) - let mut quant = std::mem::take(&mut self.quant); - quant.clear(); - quant.resize(width * height, 0); - for c in (0..eob).rev() { - let pos = scan[c] as usize; - let a = levels[pos].unsigned_abs().min(15) as i32; - if c == eob - 1 { - let ctx = coeff_base_eob_ctx(c, bwl, height); - self.sw.symbol((a.min(3) - 1) as usize, &mut self.cdf.coeff_base_eob[tx_sz_ctx][ptype][ctx]); - } else { - let ctx = coeff_base_ctx(&quant, tx_sz, bwl, width, height, pos); - self.sw.symbol(a.min(3) as usize, &mut self.cdf.coeff_base[tx_sz_ctx][ptype][ctx]); - } - if a > NUM_BASE_LEVELS as i32 { - let ctx = coeff_br_ctx(&quant, bwl, height, pos); - let mut rem = a - 3; - for _ in 0..(COEFF_BASE_RANGE / (BR_CDF_SIZE - 1)) { - let br = rem.min(3); - self.sw.symbol(br as usize, &mut self.cdf.coeff_br[tx_sz_ctx.min(TX_32X32)][ptype][ctx]); - rem -= br; - if br < 3 { - break; - } - } - } - quant[pos] = a; - } - self.quant = quant; - for c in 0..eob { - let pos = scan[c] as usize; - let v = levels[pos]; - if v != 0 { - if c == 0 { - let ctx = self.dc_sign_ctx(plane, x4, y4, w4, h4); - self.sw.symbol((v < 0) as usize, &mut self.cdf.dc_sign[ptype][ctx]); - } else { - self.sw.bool(v < 0); - } - } - let a = v.unsigned_abs(); - if a > (NUM_BASE_LEVELS + COEFF_BASE_RANGE) as u32 { - // Golomb code of a - 15 - let x = a - 15 + 1; - let length = 32 - x.leading_zeros(); - for _ in 0..length - 1 { - self.sw.bool(false); - } - self.sw.bool(true); - for i in (0..length - 1).rev() { - self.sw.bool((x >> i) & 1 == 1); - } - } - if pos == 0 && a > 0 { - dc_category = if v < 0 { 1 } else { 2 }; - } - cul_level += a & 0xFFFFF; - } - cul_level = cul_level.min(63); - } - for i in 0..w4 { - if x4 + i < self.above_level[plane].len() { - self.above_level[plane][x4 + i] = cul_level as u8; - self.above_dc[plane][x4 + i] = dc_category; - } - } - for i in 0..h4 { - if y4 + i < self.left_level[plane].len() { - self.left_level[plane][y4 + i] = cul_level as u8; - self.left_dc[plane][y4 + i] = dc_category; - } - } - } - - fn write_transform_type(&mut self, x4: usize, y4: usize, tx_sz: usize, b: &Leaf) { - // transform_type( ) - let set = tx_set(tx_sz, b.is_inter); - let t = b.tx_type_y; - if set > 0 && self.qidx > 0 { - let sqr = TX_SIZE_SQR[tx_sz] as usize; - if b.is_inter { - // The decision only picks types of this set (DCT_DCT otherwise). - let s = TX_TYPE_INTER_INV_SET3.iter().position(|&v| v as usize == t).unwrap_or_default(); - self.sw.symbol(s, &mut self.cdf.inter_tx_type_set3[sqr]); - } else { - let s = TX_TYPE_INTRA_INV_SET2.iter().position(|&v| v as usize == t).unwrap_or_default(); - self.sw.symbol(s, &mut self.cdf.intra_tx_type_set2[sqr][b.y_mode]); - } - } else { - assert_eq!(t, DCT_DCT); - } - self.set_tx_types(x4, y4, tx_sz, t); - } - - fn set_tx_types(&mut self, x4: usize, y4: usize, tx_sz: usize, t: usize) { - for i in 0..(TX_WIDTH[tx_sz] as usize >> 2) { - for j in 0..(TX_HEIGHT[tx_sz] as usize >> 2) { - if y4 + j < self.g.mi_rows && x4 + i < self.g.mi_cols { - let k = self.mi.idx(y4 + j, x4 + i); - self.mi.tx_type[k] = t as u8; - } - } - } - } - - fn compute_tx_type(&self, plane: usize, tx_sz: usize, x4: usize, y4: usize, b: &Leaf) -> usize { - if TX_SIZE_SQR_UP[tx_sz] as usize > TX_32X32 { - return DCT_DCT; - } - if plane == 0 { - return self.mi.tx_type[self.mi.idx(y4, x4)] as usize; - } - if b.is_inter { - let set = tx_set(tx_sz, true); - let xx = self.c.max(x4 << 1); - let yy = self.r.max(y4 << 1); - let t = self.mi.tx_type[self.mi.idx(yy, xx)] as usize; - return if tx_in_set(set, true, t) { t } else { DCT_DCT }; - } - intra_uv_tx_type(b.uv_mode, tx_sz) - } - - fn all_zero_ctx(&self, plane: usize, tx_sz: usize, x4: usize, y4: usize, w4: usize, h4: usize) -> usize { - let s = (plane > 0) as usize; - let max_x4 = self.g.mi_cols >> s; - let max_y4 = self.g.mi_rows >> s; - let w = TX_WIDTH[tx_sz] as usize; - let h = TX_HEIGHT[tx_sz] as usize; - let bsize = SUBSAMPLED_SIZE[self.bsize][s][s] as usize; - let bw = block_size_px(bsize); - let bh = 4 * NUM_4X4_BLOCKS_HIGH[bsize] as usize; - let al = &self.above_level[plane]; - let ll = &self.left_level[plane]; - if plane == 0 { - let mut top = 0u32; - let mut left = 0u32; - for k in 0..w4 { - if x4 + k < max_x4 { - top = top.max(al[x4 + k] as u32); - } - } - for k in 0..h4 { - if y4 + k < max_y4 { - left = left.max(ll[y4 + k] as u32); - } - } - top = top.min(255); - left = left.min(255); - if bw == w && bh == h { - 0 - } else if top == 0 && left == 0 { - 1 - } else if top == 0 || left == 0 { - 2 + (top.max(left) > 3) as usize - } else if top.max(left) <= 3 { - 4 - } else if top.min(left) <= 3 { - 5 - } else { - 6 - } - } else { - let ad = &self.above_dc[plane]; - let ld = &self.left_dc[plane]; - let mut above = 0u8; - let mut left = 0u8; - for i in 0..w4 { - if x4 + i < max_x4 { - above |= al[x4 + i] | ad[x4 + i]; - } - } - for i in 0..h4 { - if y4 + i < max_y4 { - left |= ll[y4 + i] | ld[y4 + i]; - } - } - let mut ctx = (above != 0) as usize + (left != 0) as usize + 7; - if bw * bh > w * h { - ctx += 3; - } - ctx - } - } - - fn dc_sign_ctx(&self, plane: usize, x4: usize, y4: usize, w4: usize, h4: usize) -> usize { - let s = (plane > 0) as usize; - let max_x4 = self.g.mi_cols >> s; - let max_y4 = self.g.mi_rows >> s; - let mut dc_sign = 0i32; - for k in 0..w4 { - if x4 + k < max_x4 { - match self.above_dc[plane][x4 + k] { - 1 => dc_sign -= 1, - 2 => dc_sign += 1, - _ => {} - } - } - } - for k in 0..h4 { - if y4 + k < max_y4 { - match self.left_dc[plane][y4 + k] { - 1 => dc_sign -= 1, - 2 => dc_sign += 1, - _ => {} - } - } - } - match dc_sign.signum() { - -1 => 1, - 1 => 2, - _ => 0, - } - } - - // ---- motion vector prediction (7.10.2), single reference LAST_FRAME -------------------- - - fn inside(&self, r: isize, c: isize) -> bool { - c >= 0 && c < self.g.mi_cols as isize && r >= 0 && r < self.g.mi_rows as isize - } - - fn find_mv_stack(&mut self) { - let n4 = NUM_4X4_BLOCKS_WIDE[self.bsize] as isize; - let (bw4, bh4) = (n4, n4); - self.mvs = MvStack::default(); - self.scan_row(-1); - let mut found_above = self.mvs.found_match; - self.mvs.found_match = false; - self.scan_col(-1); - let mut found_left = self.mvs.found_match; - self.mvs.found_match = false; - if bw4.max(bh4) <= 16 { - self.scan_point(-1, bw4); - } - if self.mvs.found_match { - found_above = true; - } - self.mvs.close_matches = found_above as usize + found_left as usize; - let num_nearest = self.mvs.num_mv_found; - let num_new = self.mvs.new_mv_count; - for idx in 0..num_nearest { - self.mvs.weight_stack[idx] += REF_CAT_LEVEL as u32; - } - self.mvs.zero_mv_context = 0; - // use_ref_frame_mvs = 0: no temporal candidates - self.scan_point(-1, -1); - if self.mvs.found_match { - found_above = true; - } - self.mvs.found_match = false; - self.scan_row(-3); - if self.mvs.found_match { - found_above = true; - } - self.mvs.found_match = false; - self.scan_col(-3); - if self.mvs.found_match { - found_left = true; - } - self.mvs.found_match = false; - if bh4 > 1 { - self.scan_row(-5); - } - if self.mvs.found_match { - found_above = true; - } - self.mvs.found_match = false; - if bw4 > 1 { - self.scan_col(-5); - } - if self.mvs.found_match { - found_left = true; - } - self.mvs.total_matches = found_above as usize + found_left as usize; - let n = self.mvs.num_mv_found; - self.sort_stack(0, num_nearest); - self.sort_stack(num_nearest, n); - if self.mvs.num_mv_found < 2 { - self.extra_search(); - } - self.context_and_clamping(num_new); - } - - fn scan_row(&mut self, delta_row: isize) { - let bw4 = NUM_4X4_BLOCKS_WIDE[self.bsize] as isize; - let (mi_row, mi_col) = (self.r as isize, self.c as isize); - let end4 = bw4.min(self.g.mi_cols as isize - mi_col).min(16); - let mut delta_col = 0; - let use_step16 = bw4 >= 16; - let mut delta_row = delta_row; - if delta_row.abs() > 1 { - delta_row += mi_row & 1; - delta_col = 1 - (mi_col & 1); - } - let mut i = 0; - while i < end4 { - let mv_row = mi_row + delta_row; - let mv_col = mi_col + delta_col + i; - if !self.inside(mv_row, mv_col) { - break; - } - let k = self.mi.idx(mv_row as usize, mv_col as usize); - let mut len = bw4.min(NUM_4X4_BLOCKS_WIDE[self.mi.mi_size[k] as usize] as isize); - if delta_row.abs() > 1 { - len = len.max(2); - } - if use_step16 { - len = len.max(4); - } - self.add_ref_mv_candidate(mv_row as usize, mv_col as usize, len as u32 * 2); - i += len; - } - } - - fn scan_col(&mut self, delta_col: isize) { - let bh4 = NUM_4X4_BLOCKS_HIGH[self.bsize] as isize; - let (mi_row, mi_col) = (self.r as isize, self.c as isize); - let end4 = bh4.min(self.g.mi_rows as isize - mi_row).min(16); - let mut delta_row = 0; - let use_step16 = bh4 >= 16; - let mut delta_col = delta_col; - if delta_col.abs() > 1 { - delta_row = 1 - (mi_row & 1); - delta_col += mi_col & 1; - } - let mut i = 0; - while i < end4 { - let mv_row = mi_row + delta_row + i; - let mv_col = mi_col + delta_col; - if !self.inside(mv_row, mv_col) { - break; - } - let k = self.mi.idx(mv_row as usize, mv_col as usize); - let mut len = bh4.min(NUM_4X4_BLOCKS_HIGH[self.mi.mi_size[k] as usize] as isize); - if delta_col.abs() > 1 { - len = len.max(2); - } - if use_step16 { - len = len.max(4); - } - self.add_ref_mv_candidate(mv_row as usize, mv_col as usize, len as u32 * 2); - i += len; - } - } - - fn scan_point(&mut self, delta_row: isize, delta_col: isize) { - let mv_row = self.r as isize + delta_row; - let mv_col = self.c as isize + delta_col; - if self.inside(mv_row, mv_col) && self.mi.written[self.mi.idx(mv_row as usize, mv_col as usize)] { - self.add_ref_mv_candidate(mv_row as usize, mv_col as usize, 4); - } - } - - fn add_ref_mv_candidate(&mut self, mv_row: usize, mv_col: usize, weight: u32) { - let i = self.mi.idx(mv_row, mv_col); - if !self.mi.is_inter[i] { - return; - } - let rf = self.mi.ref_frame[i]; - for cand_list in 0..2 { - if rf[cand_list] == LAST_FRAME as i8 { - self.search_stack(i, cand_list, weight); - } - } - } - - fn search_stack(&mut self, i: usize, cand_list: usize, weight: u32) { - debug_assert_eq!(cand_list, 0); - let cand_mode = self.mi.y_mode[i] as usize; - // gm_type is IDENTITY: the stored motion vector is used - let mut cand = self.mi.mv[i]; - lower_mv_precision(&mut cand); - if cand_mode == NEWMV { - self.mvs.new_mv_count += 1; - } - self.mvs.found_match = true; - let n = self.mvs.num_mv_found; - if let Some(idx) = (0..n).find(|&k| self.mvs.ref_stack_mv[k] == cand) { - self.mvs.weight_stack[idx] += weight; - } else if n < MAX_REF_MV_STACK_SIZE { - self.mvs.ref_stack_mv[n] = cand; - self.mvs.weight_stack[n] = weight; - self.mvs.num_mv_found += 1; - } - } - - fn sort_stack(&mut self, start: usize, end: usize) { - let mut end = end; - let s = &mut self.mvs; - while end > start { - let mut new_end = start; - for idx in start + 1..end { - if s.weight_stack[idx - 1] < s.weight_stack[idx] { - s.weight_stack.swap(idx - 1, idx); - s.ref_stack_mv.swap(idx - 1, idx); - new_end = idx; - } - } - end = new_end; - } - } - - fn extra_search(&mut self) { - let (mi_row, mi_col) = (self.r as isize, self.c as isize); - let n4 = NUM_4X4_BLOCKS_WIDE[self.bsize] as isize; - let w4 = n4.min(16).min(self.g.mi_cols as isize - mi_col); - let h4 = n4.min(16).min(self.g.mi_rows as isize - mi_row); - let num4x4 = w4.min(h4); - for pass in 0..2 { - let mut idx = 0; - while idx < num4x4 && self.mvs.num_mv_found < 2 { - let (mv_row, mv_col) = if pass == 0 { (mi_row - 1, mi_col + idx) } else { (mi_row + idx, mi_col - 1) }; - if !self.inside(mv_row, mv_col) { - break; - } - let k = self.mi.idx(mv_row as usize, mv_col as usize); - // add_extra_mv_candidate( ) (no sign bias: a single forward reference) - let rfs = self.mi.ref_frame[k]; - for &cand_ref in rfs.iter().take(2) { - if cand_ref > INTRA_FRAME as i8 { - let cand = self.mi.mv[k]; - let n = self.mvs.num_mv_found; - if !(0..n).any(|j| self.mvs.ref_stack_mv[j] == cand) { - self.mvs.ref_stack_mv[n] = cand; - self.mvs.weight_stack[n] = 2; - self.mvs.num_mv_found += 1; - } - } - } - let sz = self.mi.mi_size[k] as usize; - idx += if pass == 0 { NUM_4X4_BLOCKS_WIDE[sz] } else { NUM_4X4_BLOCKS_HIGH[sz] } as isize; - } - } - for idx in self.mvs.num_mv_found..2 { - self.mvs.ref_stack_mv[idx] = [0, 0]; // GlobalMvs[ 0 ] - } - } - - fn context_and_clamping(&mut self, num_new: usize) { - let bs = block_size_px(self.bsize) as i32; - let n = self.mvs.num_mv_found; - for idx in 0..n { - let mut z = 0; - if idx + 1 < n { - let w0 = self.mvs.weight_stack[idx]; - let w1 = self.mvs.weight_stack[idx + 1]; - if w0 >= REF_CAT_LEVEL as u32 { - if w1 < REF_CAT_LEVEL as u32 { - z = 1; - } - } else { - z = 2; - } - } - self.mvs.drl_ctx_stack[idx] = z; - } - let n4 = NUM_4X4_BLOCKS_WIDE[self.bsize] as i32; - for idx in 0..n { - let mut mv = self.mvs.ref_stack_mv[idx]; - let border_r = MV_BORDER as i32 + bs * 8; - let to_top = -((self.r as i32 * 4) * 8); - let to_bottom = ((self.g.mi_rows as i32 - n4 - self.r as i32) * 4) * 8; - mv[0] = mv[0].clamp(to_top - border_r, to_bottom + border_r); - let to_left = -((self.c as i32 * 4) * 8); - let to_right = ((self.g.mi_cols as i32 - n4 - self.c as i32) * 4) * 8; - mv[1] = mv[1].clamp(to_left - border_r, to_right + border_r); - self.mvs.ref_stack_mv[idx] = mv; - } - let (close, total) = (self.mvs.close_matches, self.mvs.total_matches); - if close == 0 { - self.mvs.new_mv_context = total.min(1); - self.mvs.ref_mv_context = total; - } else if close == 1 { - self.mvs.new_mv_context = 3 - num_new.min(1); - self.mvs.ref_mv_context = 2 + total; - } else { - self.mvs.new_mv_context = 5 - num_new.min(1); - self.mvs.ref_mv_context = 5; - } - } -} - -/// lower_mv_precision( ) with allow_high_precision_mv = 0 and force_integer_mv = 0. -fn lower_mv_precision(mv: &mut MvP) { - for c in mv.iter_mut() { - if *c & 1 != 0 { - *c += if *c > 0 { -1 } else { 1 }; - } - } -} - -fn coeff_base_eob_ctx(c: usize, bwl: usize, height: usize) -> usize { - if c == 0 { - 0 - } else if c <= (height << bwl) / 8 { - 1 - } else if c <= (height << bwl) / 4 { - 2 - } else { - 3 - } -} - -/// Context of coeff_base for TX_CLASS_2D (8.3.2). -fn coeff_base_ctx(quant: &[i32], tx_sz: usize, bwl: usize, width: usize, height: usize, pos: usize) -> usize { - let row = pos >> bwl; - let col = pos - (row << bwl); - let mut mag = 0i32; - for off in SIG_REF_DIFF_OFFSET[TX_CLASS_2D].iter() { - let ref_row = row + off[0] as usize; - let ref_col = col + off[1] as usize; - if ref_row < height && ref_col < width { - mag += quant[(ref_row << bwl) + ref_col].abs().min(3); - } - } - let ctx = ((mag + 1) >> 1).min(4) as usize; - if row == 0 && col == 0 { - return 0; - } - ctx + COEFF_BASE_CTX_OFFSET[tx_sz][row.min(4)][col.min(4)] as usize -} - -/// Context of coeff_br for TX_CLASS_2D. -fn coeff_br_ctx(quant: &[i32], bwl: usize, txh: usize, pos: usize) -> usize { - let txw = 1usize << bwl; - let row = pos >> bwl; - let col = pos - (row << bwl); - let mut mag = 0i32; - for off in MAG_REF_OFFSET_WITH_TX_CLASS[TX_CLASS_2D].iter() { - let ref_row = row + off[0] as usize; - let ref_col = col + off[1] as usize; - if ref_row < txh && ref_col < txw { - mag += quant[ref_row * txw + ref_col].min((COEFF_BASE_RANGE + NUM_BASE_LEVELS + 1) as i32); - } - } - let mag = ((mag + 1) >> 1).min(6) as usize; - if pos == 0 { - mag - } else if row < 2 && col < 2 { - mag + 7 - } else { - mag + 14 - } -} diff --git a/crates/av1enc/tests/it/main.rs b/crates/av1enc/tests/it/main.rs deleted file mode 100644 index c66bacde5..000000000 --- a/crates/av1enc/tests/it/main.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! The crate's integration tests, one binary (each module is a former `tests/.rs`). - -mod bench; -mod common; -mod ffmpeg_oracle; -mod roundtrip; diff --git a/crates/effects/src/blur2.rs b/crates/effects/src/blur2.rs index 880ad16c5..31ef8be42 100644 --- a/crates/effects/src/blur2.rs +++ b/crates/effects/src/blur2.rs @@ -228,8 +228,21 @@ fn compound_blur(ctx: &EffectCtx, mut b: Buf) -> Buf { // --------------------------------------------------------------------------------------------- // Camera Lens Blur -/// Half-open horizontal spans (dy, x_left, x_right) of a polygonal / round iris of radius `r`. +/// Largest Camera Lens Blur radius in buffer pixels. Blur Radius goes to 500 in the UI; a project +/// or expression value far beyond it would otherwise build an iris table of one row per pixel of +/// radius (and sum that many rows per output pixel) before any image size check. +pub const CAMERA_LENS_MAX_RADIUS: f64 = 2000.0; + +/// Camera Lens Blur's iris radius in buffer pixels: Blur Radius at the buffer's `scale`, capped +/// at [`CAMERA_LENS_MAX_RADIUS`]. Shared by the CPU and GPU paths. +pub fn camera_lens_radius(ctx: &EffectCtx, scale: f64) -> f64 { + (ctx.params.f("blurRadius").max(0.0) * scale).clamp(0.0, CAMERA_LENS_MAX_RADIUS) +} + +/// Half-open horizontal spans (dy, x_left, x_right) of a polygonal / round iris of radius `r` +/// (capped at [`CAMERA_LENS_MAX_RADIUS`]). fn iris_spans(r: f64, sides: u32, rotation_deg: f64, roundness: f64) -> Vec<(i64, i64, i64)> { + let r = if r.is_nan() { 0.0 } else { r.clamp(0.0, CAMERA_LENS_MAX_RADIUS) }; let ri = r.ceil() as i64; let rot = rotation_deg.to_radians(); let verts: Vec<(f64, f64)> = (0..sides) @@ -292,7 +305,8 @@ pub fn camera_lens_spans(ctx: &EffectCtx, r: f64) -> (Vec<(i64, i64, i64)>, f64) s.2 = (s.2 as f64 * aspect).round() as i64; } } - (spans, r * aspect.max(1.0)) + // Same cap as iris_spans, so the reach matches the spans for unbounded radii. + (spans, r.clamp(0.0, CAMERA_LENS_MAX_RADIUS) * aspect.max(1.0)) } /// Whether Camera Lens Blur needs more than plain iris spans (Diffraction Fringe or a Blur @@ -302,7 +316,7 @@ pub fn camera_lens_plain(ctx: &EffectCtx) -> bool { } fn camera_lens_blur(ctx: &EffectCtx, mut b: Buf) -> Buf { - let r = ctx.params.f("blurRadius").max(0.0) * b.scale; + let r = camera_lens_radius(ctx, b.scale); if r < 0.5 { return b; } @@ -910,6 +924,19 @@ mod tests { assert!(out.img.get(15 + o, 10 + d)[0] < 0.9, "bright map blurs"); } + #[test] + fn iris_spans_cap_a_huge_radius() { + // A project's Blur Radius far beyond the UI's 500 builds a bounded table. + let max_rows = 2 * CAMERA_LENS_MAX_RADIUS as usize + 1; + assert!(iris_spans(1e12, 6, 0.0, 0.0).len() <= max_rows); + assert!(iris_spans(f64::INFINITY, 0, 0.0, 0.0).len() <= max_rows); + assert_eq!(iris_spans(f64::NAN, 0, 0.0, 0.0), vec![(0, 0, 0)]); + let params = Params { values: [("blurRadius".into(), num(1e15))].into_iter().collect() }; + let ctx = EffectCtx { params: ¶ms, time: 0.0, layer_size: [4.0, 4.0], seed: 1, adjustment: false, env: Default::default() }; + assert_eq!(camera_lens_radius(&ctx, 1.0), CAMERA_LENS_MAX_RADIUS); + assert!(camera_lens_spans(&ctx, camera_lens_radius(&ctx, 1.0)).0.len() <= max_rows); + } + #[test] fn iris_spans_hexagon_is_symmetric() { let s = iris_spans(10.0, 6, 0.0, 0.0); diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index 019596f80..532bb28c9 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -98,14 +98,14 @@ pub use sim::particle_state; pub use sim3::{PgBlit, PgPlan, playground_plan, playground_state}; pub use stylize2::strobe_on; // CPU helpers the GPU kernels share (effectcraft-gpu). -pub use blur2::{camera_lens_plain, camera_lens_spans, radial_fast_pad}; +pub use blur2::{CAMERA_LENS_MAX_RADIUS, camera_lens_plain, camera_lens_radius, camera_lens_spans, radial_fast_pad}; pub use generate::{four_color_softening, gen_mode}; pub use generate2::pattern_kind as cell_pattern_kind; pub use generate2::{BoltSeg, Contour, Ghost, RadioPlan, Wave, flare_ghosts, lightning_segments, radio_plan}; pub use generate3::{ Coverage, MarksPlan, PaintPlan, Seg, eyedropper_color, marks_plan, paint_bucket_region, paint_plan, plan_reads_pixels, poly_segs, scribble_style, }; -pub use noise::GrainLook; +pub use noise::{GrainLook, MEDIAN_MAX_RADIUS}; pub use textfx::{BoltPlan, TextLook, TextPass, bolt_plan, text_passes}; pub use transition::{GridWipe, place_layer}; // effectcraft-gpu fx_noise. diff --git a/crates/effects/src/noise.rs b/crates/effects/src/noise.rs index b6f76a268..09217f831 100644 --- a/crates/effects/src/noise.rs +++ b/crates/effects/src/noise.rs @@ -370,9 +370,16 @@ fn quant(v: f32) -> u16 { (v.clamp(0.0, 1.0) * (NB - 1) as f32).round() as u16 } +/// Largest median window radius (buffer pixels) for Median, Median (Legacy), Dust & Scratches +/// and the other median users. Radius goes to 255 in the UI; filling one (2r + 1)² window per +/// row costs the square of the radius, so a project value far beyond it is held here (also +/// keeping the window's pixel count well inside `u32` / `i32` on the GPU). +pub const MEDIAN_MAX_RADIUS: usize = 512; + /// Per-channel median over a (2r+1)² window of the premultiplied channels (values quantised to /// 512 levels in 0..1, edges repeated). pub(crate) fn median_image(img: &Image, r: usize) -> Image { + let r = r.min(MEDIAN_MAX_RADIUS); let (w, h) = (img.width as usize, img.height as usize); if r == 0 || w == 0 || h == 0 { return img.clone(); @@ -871,6 +878,17 @@ mod tests { } } + #[test] + fn median_radius_is_capped() { + // A project's Radius far beyond the UI's 255 takes the capped window, not a + // (2r + 1)² fill per row (or an overflow) on a tiny image. + let mut img = Image::filled(4, 3, [0.2, 0.3, 0.4, 1.0]); + img.set(1, 1, [1.0, 1.0, 1.0, 1.0]); + let capped = median_image(&img, MEDIAN_MAX_RADIUS); + assert_eq!(median_image(&img, usize::MAX).data, capped.data); + assert_eq!(median_image(&img, MEDIAN_MAX_RADIUS + 1000).data, capped.data); + } + #[test] fn median_matches_brute_force() { let (w, h) = (9u32, 7u32); diff --git a/crates/effects/src/object_matte.rs b/crates/effects/src/object_matte.rs index 8a9750282..86534a178 100644 --- a/crates/effects/src/object_matte.rs +++ b/crates/effects/src/object_matte.rs @@ -301,7 +301,8 @@ pub fn frozen_alpha(params: &Params, layer_size: [f64; 2], fps: f64, f: i64, w: let fr = fc.frames.get(&f)?; let [fw, fh] = fc.size; let expected = fw.checked_mul(fh)?; - if fw == 0 || fh == 0 || expected > rle::MAX_PLANE_BYTES { + // Only the layer's own full-resolution grid (see Roto Brush's frozen mattes). + if fc.size != roto::frozen_grid(layer_size) || fw == 0 || fh == 0 || expected > rle::MAX_PLANE_BYTES { return None; } let a = rle::decode_bounded(&fr.a, expected)?; @@ -719,6 +720,12 @@ mod tests { p.values.insert("objectPoint".into(), Value::Vec2([40.0, 40.0])); p.values.insert(FROZEN.into(), Value::Str(fc2.to_json())); assert_eq!(frozen_alpha(&p, [100.0, 80.0], 25.0, 2, 100, 80, None), None); + // A compact plane claiming a size other than the layer's grid is not decoded. + let mut fc3 = fc.clone(); + fc3.size = [4000, 4000]; + fc3.frames.insert(2, roto::freeze_frame(&vec![1.0; 4000 * 4000], &[])); + p.values.insert(FROZEN.into(), Value::Str(fc3.to_json())); + assert_eq!(frozen_alpha(&p, [100.0, 80.0], 25.0, 2, 100, 80, None), None); } /// Mattes frozen with a model stay usable in a session where that model is not loaded (a diff --git a/crates/effects/src/perspective.rs b/crates/effects/src/perspective.rs index 69c63caf9..8f55bc408 100644 --- a/crates/effects/src/perspective.rs +++ b/crates/effects/src/perspective.rs @@ -394,7 +394,7 @@ pub fn specs() -> Vec { }; let render = || popup(&["Full", "Outside", "Inside"]); let mut cyl = vec![ - p("radius", "Radius", num(100.0), slider(0.0, 400.0, 0.0, 200.0, 1)), + p("radius", "Radius", num(100.0), slider(0.0, 30000.0, 0.0, 200.0, 1)), p("position", "Position", pt(0.5, 0.5), ParamUi::Point), p("rotation", "Rotation", num(0.0), ParamUi::Angle), p("rotationX", "Rotation X", num(0.0), ParamUi::Angle), @@ -565,6 +565,23 @@ mod tests { assert_eq!(out.get(50 + 18, 10)[3], 0.0); } + /// #3368: Radius takes the range After Effects' CC Cylinder has, up to 30000 %, and a cylinder + /// that large still renders (nearly flat, fully opaque) instead of clamping to 400 %. + #[test] + fn cylinder_radius_goes_up_to_thirty_thousand_percent() { + let spec = find("ec.perspective.cccylinder").unwrap(); + let radius = spec.params.iter().find(|p| p.id == "radius").unwrap(); + assert!(matches!(radius.ui, ParamUi::Slider { min, max, .. } if min == 0.0 && max == 30000.0), "{:?}", radius.ui); + // The cylinder's axis on the left edge: at 400 % its radius is 400 / 100 * 100 / 2π ≈ 63.7 px, + // so a pixel 70 px along is past it there and inside the 30000 % one. + let img = Image::filled(100, 20, [0.5, 0.5, 0.5, 1.0]); + let at_edge = |radius: f64| run("ec.perspective.cccylinder", &img, &[("radius", num(radius)), ("position", Value::Vec2([0.0, 10.0]))]).img; + let clamped = at_edge(400.0).get(70, 10); + assert_eq!(clamped[3], 0.0, "beyond a 400 % cylinder: {clamped:?}"); + let px = at_edge(30000.0).get(70, 10); + assert!(px.iter().all(|v| v.is_finite()) && px[3] > 0.99, "{px:?}"); + } + #[test] fn cylinder_axis_rotations_affect_geometry_and_texture() { let img = Image::filled(100, 100, [0.5, 0.5, 0.5, 1.0]); diff --git a/crates/effects/src/puppet/mesh.rs b/crates/effects/src/puppet/mesh.rs index f01ee8f78..d5fd66dae 100644 --- a/crates/effects/src/puppet/mesh.rs +++ b/crates/effects/src/puppet/mesh.rs @@ -614,11 +614,72 @@ pub fn triangulate(outline: &[[f64; 2]], opts: &MeshOpts) -> Mesh { Mesh { verts, tris, outline: poly } } +/// The Expansion range the outline honours, in layer pixels (the property's slider range). A +/// project file can store any number; outside this range it is clamped, so the padded grid +/// below stays a few hundred pixels larger than the buffer. +pub const EXPANSION_RANGE: (f64, f64) = (-100.0, 200.0); +/// Most buffer pixels of padding the outline grid takes on each side (Expansion × buffer scale). +const MAX_PAD: f64 = 4096.0; +/// Most cells the padded outline grid may have (it is expanded with two `f64` distance buffers). +const MAX_GRID_CELLS: usize = 1 << 25; +/// Most the alpha is shrunk by, per axis, to make the padded grid fit; a layer needing more has no +/// outline. +const MAX_DOWNSAMPLE: usize = 8; + /// The outline of the alpha region under `seed` (buffer pixels → layer space via `scale` and -/// `offset`), expanded by `expansion` layer pixels. +/// `offset`), expanded by `expansion` layer pixels (clamped to [`EXPANSION_RANGE`]). When the +/// padded grid would pass [`MAX_PAD`] or [`MAX_GRID_CELLS`] (a high-scale layer with a large +/// Expansion), the alpha is traced at a coarser resolution instead of being skipped. pub fn outline(alpha: &[f32], w: usize, h: usize, scale: f64, offset: [f64; 2], seed: [f64; 2], expansion: f64) -> Vec<[f64; 2]> { - // Pad so expansion can grow past the buffer edge. - let pad = (expansion.max(0.0) * scale).ceil() as usize + 2; + let expansion = if expansion.is_finite() { expansion.clamp(EXPANSION_RANGE.0, EXPANSION_RANGE.1) } else { 0.0 }; + if alpha.len() < w.saturating_mul(h) { + return vec![]; + } + let want = (expansion.max(0.0) * scale).ceil(); + if !want.is_finite() { + return vec![]; + } + for k in 1..=MAX_DOWNSAMPLE { + let pad = (want / k as f64).ceil(); + let (kw, kh) = (w.div_ceil(k), h.div_ceil(k)); + let fits = pad <= MAX_PAD && { + let pad = pad as usize + 2; + kw.checked_add(2 * pad).zip(kh.checked_add(2 * pad)).and_then(|(pw, ph)| pw.checked_mul(ph)).is_some_and(|c| c <= MAX_GRID_CELLS) + }; + if !fits { + continue; + } + if k == 1 { + return outline_padded(alpha, [w, h], scale, offset, seed, expansion, pad as usize + 2); + } + let small = shrink(alpha, w, h, k); + let kf = k as f64; + return outline_padded(&small, [kw, kh], scale / kf, [offset[0] / kf, offset[1] / kf], seed, expansion, pad as usize + 2); + } + vec![] +} + +/// `alpha` (`w × h`) with every `k × k` block averaged into one pixel. +fn shrink(alpha: &[f32], w: usize, h: usize, k: usize) -> Vec { + let (kw, kh) = (w.div_ceil(k), h.div_ceil(k)); + let mut out = vec![0.0f32; kw * kh]; + for y in 0..kh { + for x in 0..kw { + let (x1, y1) = (((x + 1) * k).min(w), ((y + 1) * k).min(h)); + let (mut sum, mut n) = (0.0f32, 0u32); + for yy in y * k..y1 { + for xx in x * k..x1 { + sum += alpha[yy * w + xx]; + n += 1; + } + } + out[y * kw + x] = sum / n.max(1) as f32; + } + } + out +} + +fn outline_padded(alpha: &[f32], [w, h]: [usize; 2], scale: f64, offset: [f64; 2], seed: [f64; 2], expansion: f64, pad: usize) -> Vec<[f64; 2]> { let (pw, ph) = (w + 2 * pad, h + 2 * pad); let mut on = vec![false; pw * ph]; for y in 0..h { diff --git a/crates/effects/src/puppet/tests.rs b/crates/effects/src/puppet/tests.rs index 10426d46c..c2db999e1 100644 --- a/crates/effects/src/puppet/tests.rs +++ b/crates/effects/src/puppet/tests.rs @@ -106,6 +106,42 @@ fn expansion_grows_the_outline() { assert!((am - pi * 225.0).abs() < 60.0, "{am}"); } +/// An imported Expansion far past the property's range is clamped instead of padding the grid +/// by that many pixels (a 1e12 expansion would ask for terabytes). +#[test] +fn hostile_expansion_is_clamped() { + let (w, h) = (40, 40); + let alpha = disc_alpha(w, h, [20.0, 20.0], 10.0); + let max = signed_area(&outline(&alpha, w, h, 1.0, [0.0; 2], [20.0, 20.0], EXPANSION_RANGE.1)).abs(); + // A non-finite Expansion is taken as zero (the unexpanded outline), not clamped to the maximum. + let zero = signed_area(&outline(&alpha, w, h, 1.0, [0.0; 2], [20.0, 20.0], 0.0)).abs(); + for e in [1e12, f64::MAX, f64::INFINITY, f64::NAN] { + let a = signed_area(&outline(&alpha, w, h, 1.0, [0.0; 2], [20.0, 20.0], e)).abs(); + let want = if e.is_finite() { max } else { zero }; + assert!((a - want).abs() < 1.0, "{e}: {a} vs {want}"); + } + assert!(outline(&alpha, w, h, 1e9, [0.0; 2], [20.0, 20.0], 100.0).is_empty()); + let _ = outline(&alpha, w, h, 1.0, [0.0; 2], [20.0, 20.0], -1e12); +} + +/// A high-scale layer with a large Expansion is traced at a coarser resolution, not skipped, and a +/// tiny buffer with a huge padded grid stays within the grid budget. +#[test] +fn high_scale_expansion_is_traced_coarser_instead_of_skipped() { + let (w, h) = (40, 40); + let alpha = disc_alpha(w, h, [20.0, 20.0], 10.0); + // Scale 32, Expansion 130: 4160 px of padding, past MAX_PAD, so it is shrunk by 2. + let scale = 32.0; + let o = outline(&alpha, w, h, scale, [0.0; 2], [20.0, 20.0], 130.0); + let area = signed_area(&o).abs(); + // The disc has radius 10 / 32 layer px; expanded by 130. + let r = 10.0 / scale + 130.0; + assert!((area - std::f64::consts::PI * r * r).abs() < 0.03 * std::f64::consts::PI * r * r, "{area}"); + // One pixel at scale 20.48 and Expansion 1e12 (clamped): its 8197 px square grid is over budget. + let o = outline(&[1.0], 1, 1, 20.48, [0.0; 2], [0.5, 0.5], 1e12); + assert!(o.len() >= 3); +} + fn rect_mesh(w: f64, h: f64, tris: f64) -> Mesh { triangulate(&[[0.0, 0.0], [w, 0.0], [w, h], [0.0, h]], &MeshOpts { triangles: tris, density: 50.0, expansion: 0.0 }) } diff --git a/crates/effects/src/roto.rs b/crates/effects/src/roto.rs index f9c26f417..733535b21 100644 --- a/crates/effects/src/roto.rs +++ b/crates/effects/src/roto.rs @@ -378,10 +378,22 @@ pub fn purge() { c.bytes = 0; } +/// The pixel size of the layer-space grid at `scale` (`round(size · scale)`, at least 1). +fn grid_size(layer_size: [f64; 2], scale: f64) -> (u32, u32) { + (((layer_size[0] * scale).round() as u32).max(1), ((layer_size[1] * scale).round() as u32).max(1)) +} + +/// The full-resolution layer grid frozen mattes are stored at. A frozen cache claiming any +/// other size did not come from freezing this layer and is not decoded. +pub fn frozen_grid(layer_size: [f64; 2]) -> [usize; 2] { + let (w, h) = grid_size(layer_size, 1.0); + [w as usize, h as usize] +} + /// The layer-space grid (`round(size · scale)` pixels) of a buffer. pub fn layer_grid(buf: &Buf, layer_size: [f64; 2]) -> Image { let s = if buf.scale > 0.0 { buf.scale } else { 1.0 }; - let (w, h) = (((layer_size[0] * s).round() as u32).max(1), ((layer_size[1] * s).round() as u32).max(1)); + let (w, h) = grid_size(layer_size, s); let (ox, oy) = (buf.offset[0].round() as i64, buf.offset[1].round() as i64); let mut img = Image::new(w, h); img.rows_mut().for_each(|(y, row)| { @@ -1212,8 +1224,11 @@ pub fn freeze_frame(alpha: &[f32], band: &[u8]) -> FrozenFrame { fn matte_for(ctx: &EffectCtx, f: i64, img: &Arc, scale: f64) -> Option<(Vec, Vec, f64)> { let (w, h) = (img.width as usize, img.height as usize); // Frozen mattes. + // Only the layer's own full-resolution grid: a project cannot make a tiny layer decode (and + // convert to floats) planes as large as the RLE allows. Any other size falls through to a computed matte. if let Some(fc) = frozen(ctx.params) && frozen_applies(&fc, ctx.params, ctx.layer_size, ctx.fps()) + && fc.size == frozen_grid(ctx.layer_size) && let Some(fr) = fc.frames.get(&f) { let [fw, fh] = fc.size; @@ -1409,6 +1424,33 @@ mod tests { params } + #[test] + fn mismatched_frozen_size_falls_back_to_a_computed_matte() { + let mut d = RotoData::default(); + d.add(Stroke { kind: StrokeKind::Fg, frame: 0, radius: 3.0, points: vec![[35.0, 40.0], [45.0, 40.0]], object: 0 }, 20, [0, 24]); + let mut params = params_with(&d); + let fc = + FrozenCache { key: frozen_key(¶ms, [100.0, 80.0], 25.0), size: [10, 8], frames: BTreeMap::from([(5, freeze_frame(&[1.0; 80], &[0; 80]))]) }; + let host = Seq; + let render = |params: &Params| { + let ctx = EffectCtx { + params, + time: 5.0 / 25.0 + 0.001, + layer_size: [100.0, 80.0], + seed: 1, + adjustment: false, + env: EffectEnv { host: Some(&host), frame_rate: 25.0, ..Default::default() }, + }; + crate::apply(&specs()[0], &ctx, Buf { img: frame(5.0 / 25.0 + 0.001), offset: [0.0; 2], scale: 1.0 }).img + }; + let computed = render(¶ms); + params.values.insert(FROZEN.into(), Value::Str(fc.to_json())); + let out = render(¶ms); + assert!(out.data[(40 * 100 + 50) as usize][3] > 0.5, "the disk is missing from the output"); + // The cache's all-opaque matte would leave the background opaque too; the fallback must match a render with no cache. + assert!(out.data == computed.data, "the mismatched frozen cache was used"); + } + #[test] fn renders_the_segmented_disk_on_demand() { let mut d = RotoData::default(); @@ -1980,6 +2022,18 @@ mod tests { } } + /// Frozen mattes are only decoded at the size the freeze pass stores: the layer grid at + /// full resolution. + #[test] + fn frozen_grid_is_the_full_resolution_layer_grid() { + for size in [[100.0, 80.0], [99.6, 0.2], [1.0, 1.0]] { + let buf = Buf { img: Image::new(3, 3), offset: [0.0; 2], scale: 1.0 }; + let g = layer_grid(&buf, size); + assert_eq!(frozen_grid(size), [g.width as usize, g.height as usize]); + } + assert_eq!(frozen_grid([100.0, 80.0]), [100, 80]); + } + #[test] fn saved_tracking_decoders_validate_dimensions_and_plane_lengths() { let valid = rle::encode(&[255, 0, 255, 0]); diff --git a/crates/effects/src/sim/engine.rs b/crates/effects/src/sim/engine.rs index beda8c318..8d9914e31 100644 --- a/crates/effects/src/sim/engine.rs +++ b/crates/effects/src/sim/engine.rs @@ -8,9 +8,11 @@ use super::{Buf, EffectCtx, Image, IntoParallelRefMutIterator, ParallelIterator, /// Steps per second of the stepped particle simulations. pub(crate) const SPS: f64 = 60.0; -/// Number of whole steps reached at layer time `t`. +/// Number of whole steps reached at layer time `t` (at most [`crate::util::MAX_SIM_STEPS`]: the +/// per-step histories built from it, such as [`rate_history`], stay bounded however late a +/// project places the layer's time). pub(crate) fn steps_at(t: f64) -> u64 { - if t <= 0.0 { 0 } else { (t * SPS + 1e-6).floor() as u64 } + if t <= 0.0 { 0 } else { ((t * SPS + 1e-6).floor() as u64).min(crate::util::MAX_SIM_STEPS) } } #[derive(Clone, Debug)] diff --git a/crates/effects/src/util.rs b/crates/effects/src/util.rs index 9c14a6c69..390750c6e 100644 --- a/crates/effects/src/util.rs +++ b/crates/effects/src/util.rs @@ -562,13 +562,22 @@ struct SimEntry { /// Checkpoint every this many steps. pub const SIM_CHECKPOINT: u64 = 15; +/// Most steps a simulation runs: three hours of layer time at 60 steps per second. The layer +/// time comes from the project (start time, stretch), so without a limit a tiny file could ask +/// for years of steps; past this the state stays where it is. +pub const MAX_SIM_STEPS: u64 = 60 * 60 * 60 * 3; + +/// Most checkpoints kept per simulation (and made by one run, which spaces them further apart +/// as it goes). +const MAX_SIM_CHECKPOINTS: usize = 256; + impl SimCache { pub const fn new(cap: usize) -> SimCache { SimCache { inner: std::sync::Mutex::new(Vec::new()), cap } } - /// State after `target` steps. `init` builds step 0; `step(state, i)` advances from step - /// `i` to `i + 1`. + /// State after `target` steps (at most [`MAX_SIM_STEPS`]). `init` builds step 0; + /// `step(state, i)` advances from step `i` to `i + 1`. pub fn run(&self, key: u64, target: u64, init: impl FnOnce() -> S, step: impl FnMut(&mut S, u64)) -> std::sync::Arc { self.run_where(key, target, init, step, |_, _| true) } @@ -586,6 +595,7 @@ impl SimCache { valid: impl Fn(u64, &S) -> bool, ) -> std::sync::Arc { use std::sync::Arc; + let target = target.min(MAX_SIM_STEPS); let start = { let mut g = self.inner.lock().unwrap_or_else(std::sync::PoisonError::into_inner); let tick = g.iter().map(|e| e.used).max().unwrap_or(0) + 1; @@ -604,11 +614,17 @@ impl SimCache { if !had_start { fresh.push((0, Arc::new(st.clone()))); } + let mut every = SIM_CHECKPOINT; while i < target { step(&mut st, i); i += 1; - if i % SIM_CHECKPOINT == 0 && i != target { + if i % every == 0 && i != target { fresh.push((i, Arc::new(st.clone()))); + // A long run keeps a bounded number of states: spread them twice as far apart. + if fresh.len() > MAX_SIM_CHECKPOINTS { + every = every.saturating_mul(2); + fresh.retain(|(s, _)| s % every == 0); + } } } let out = Arc::new(st); @@ -628,19 +644,23 @@ impl SimCache { }; let e = &mut g[idx]; e.used = tick; - for (s, st) in fresh { - e.checkpoints.retain(|(c, _)| *c != s); - e.checkpoints.push((s, st)); - } - // Keep memory bounded: drop unaligned (exact-target) checkpoints first, then thin. - if e.checkpoints.len() > 256 { - e.checkpoints.retain(|(s, _)| s % SIM_CHECKPOINT == 0); + // Fresh checkpoints replace those at the same step (one pass, not one per checkpoint). + let steps: std::collections::HashSet = fresh.iter().map(|(s, _)| *s).collect(); + e.checkpoints.retain(|(c, _)| !steps.contains(c)); + e.checkpoints.extend(fresh); + // Keep memory bounded: drop unaligned (exact-target) checkpoints first, then thin. The + // state just asked for keeps its slot, so asking for it again is answered from the cache. + if e.checkpoints.len() > MAX_SIM_CHECKPOINTS { + e.checkpoints.retain(|(s, _)| s % SIM_CHECKPOINT == 0 && *s != target); e.checkpoints.sort_by_key(|(s, _)| *s); - let mut k = 0; + // Drop only the excess, evenly spaced, so the rest keep their seek distance. + let (len, excess) = (e.checkpoints.len(), (e.checkpoints.len() + 1).saturating_sub(MAX_SIM_CHECKPOINTS)); + let mut i = 0; e.checkpoints.retain(|_| { - k += 1; - k % 2 == 1 + i += 1; + (i * excess) / len == ((i - 1) * excess) / len }); + e.checkpoints.push((target, out.clone())); } out } @@ -818,6 +838,45 @@ mod tests { assert_eq!(d, e); } + /// Layer time from a project cannot run a simulation without end, and a long run keeps a + /// bounded number of checkpoints that still give the same answer when seeking. + #[test] + fn sim_cache_bounds_steps_and_checkpoints() { + let c: SimCache = SimCache::new(2); + let count = |s: &mut u64, _: u64| *s += 1; + assert_eq!(*c.run(1, u64::MAX, || 0, count), MAX_SIM_STEPS); + let n = c.inner.lock().map(|g| g.iter().map(|e| e.checkpoints.len()).max().unwrap_or(0)).unwrap_or(0); + assert!(n <= MAX_SIM_CHECKPOINTS + 1, "{n} checkpoints"); + let fresh: SimCache = SimCache::new(2); + let step = |s: &mut u64, i: u64| *s = s.wrapping_mul(6364136223846793005).wrapping_add(i); + let far = *fresh.run(1, 100_000, || 7, step); + let mid = *fresh.run(1, 54_321, || 7, step); + let again: SimCache = SimCache::new(2); + assert_eq!(*again.run(1, 54_321, || 7, step), mid); + assert_eq!(*again.run(1, 100_000, || 7, step), far); + } + + /// A request that leaves the cache over its checkpoint cap still finds its own state there. + #[test] + fn sim_cache_keeps_the_requested_state_when_trimming() { + let c: SimCache = SimCache::new(2); + let steps = std::cell::Cell::new(0u64); + let count = |s: &mut u64, _: u64| { + steps.set(steps.get() + 1); + *s += 1; + }; + let target = SIM_CHECKPOINT * (MAX_SIM_CHECKPOINTS as u64 - 1) + SIM_CHECKPOINT - 1; + assert_eq!(*c.run(1, target, || 0, count), target); + let n = c.inner.lock().map(|g| g.iter().map(|e| e.checkpoints.len()).max().unwrap_or(0)).unwrap_or(0); + assert!(n <= MAX_SIM_CHECKPOINTS, "{n} checkpoints"); + steps.set(0); + assert_eq!(*c.run(1, target, || 0, count), target); + assert_eq!(steps.get(), 0, "the second request is answered from the cache"); + // Only the excess is dropped: the other aligned checkpoints stay. + let kept = c.inner.lock().map(|g| g[0].checkpoints.len()).unwrap_or(0); + assert_eq!(kept, MAX_SIM_CHECKPOINTS, "trimmed to the cap, not halved"); + } + #[test] fn poly_helpers() { let sq = [[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]]; diff --git a/crates/effects/src/utility.rs b/crates/effects/src/utility.rs index fca9d607e..f0275eab1 100644 --- a/crates/effects/src/utility.rs +++ b/crates/effects/src/utility.rs @@ -344,7 +344,9 @@ pub fn load_lut(src: &str) -> Option> { { return v.clone(); } - let inline = src.contains('\n') || src.contains("LUT_"); + // `.cube` text always spans lines (a size header, then data rows); a path never does, even one + // whose file name contains `LUT_` (#3454). Trimmed, as the path is, so a trailing newline is a path. + let inline = src.trim().contains('\n'); let parsed = if inline { parse_cube(src) } else { std::fs::read_to_string(src.trim()).ok().and_then(|t| parse_cube(&t)) }.map(Arc::new); if let Ok(mut m) = cache.lock() { m.insert(src.to_string(), parsed.clone()); @@ -484,6 +486,20 @@ mod tests { assert_eq!(out.img, img); } + /// #3454: a path containing `LUT_` was taken for `.cube` text and the file never loaded. + #[test] + fn a_path_whose_name_contains_lut_loads_the_file() { + let dir = std::env::temp_dir().join(format!("ec-fx-3454-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("LUT_x.cube"); + std::fs::write(&path, identity_cube(2)).unwrap(); + let path = path.to_string_lossy().into_owned(); + assert!(load_lut(&path).is_some(), "{path}"); + assert!(load_lut(&format!("{path}\r\n")).is_some(), "{path} with a trailing newline"); + assert!(load_lut(&identity_cube(2)).is_some(), "inline text"); + std::fs::remove_dir_all(&dir).unwrap(); + } + #[test] fn one_d_lut_with_domain() { let lut = parse_cube("LUT_1D_SIZE 2\nDOMAIN_MIN 0 0 0\nDOMAIN_MAX 2 2 2\n1 1 1\n0 0 0\n").unwrap(); diff --git a/crates/engine/src/analysis/roto.rs b/crates/engine/src/analysis/roto.rs index 3fe049e20..246c8899c 100644 --- a/crates/engine/src/analysis/roto.rs +++ b/crates/engine/src/analysis/roto.rs @@ -219,7 +219,16 @@ fn fnv(s: &str) -> u64 { /// What the effect's input frames are made of: the layer's source and timing, its masks and the /// effects above the Roto Brush. pub fn input_key(p: &Project, comp: ItemId, layer: &Layer, uid: Uid) -> String { - let mut s = crate::warp::signature(p, comp, layer); + input_key_with_signature(crate::warp::signature(p, comp, layer), layer, uid) +} + +/// Recognize the input key serialized before 0.23 without re-keying its frozen matte. +fn input_key_before_placed_page(p: &Project, comp: ItemId, layer: &Layer, uid: Uid) -> Option { + let signature = crate::warp::signature_before_placed_page(p, comp, layer)?; + Some(input_key_with_signature(signature, layer, uid)) +} + +fn input_key_with_signature(mut s: String, layer: &Layer, uid: Uid) -> String { if let Some(m) = layer.masks() { s.push_str(&format!("{m:?}")); } @@ -247,7 +256,9 @@ pub(crate) fn sync(before: &Project, after: &mut Project) -> Vec<(ItemId, LayerI for g in fxg.groups().filter(|g| is_roto(g) || is_object_matte(g)) { let key = input_key(after, *cid, l, g.uid); let old = before.comp(*cid).and_then(|bc| bc.layer(l.id)).and_then(|ol| ol.props.find_group(g.uid)); - if str_of(g, INPUT_KEY) != key { + let saved_key = str_of(g, INPUT_KEY); + // Keep a matching old key: the frozen cache itself also hashes that key. + if saved_key != key && input_key_before_placed_page(after, *cid, l, g.uid).as_deref() != Some(saved_key.as_str()) { set.push((*cid, l.id, g.uid, key)); todo.push((*cid, l.id, g.uid)); } else if old.is_none_or(|o| guard_of(o) != guard_of(g)) { diff --git a/crates/engine/src/analysis/warp.rs b/crates/engine/src/analysis/warp.rs index 71ee45ee6..0ab576d2c 100644 --- a/crates/engine/src/analysis/warp.rs +++ b/crates/engine/src/analysis/warp.rs @@ -119,6 +119,20 @@ pub fn signature(p: &Project, comp: ItemId, layer: &Layer) -> String { }, _ => String::new(), }; + signature_with_source(p, comp, layer, &src) +} + +/// Before `Footage::placed_page` was added in 0.23, the persisted signature hashed the +/// same Debug representation without that last field. Only ordinary footage can match it. +pub(crate) fn signature_before_placed_page(p: &Project, comp: ItemId, layer: &Layer) -> Option { + let LayerSource::Footage { item } = &layer.source else { return None }; + let ItemKind::Footage(footage) = &p.item(*item)?.kind else { return None }; + let src = format!("{footage:?}"); + let old = src.strip_suffix(", placed_page: false }")?; + Some(signature_with_source(p, comp, layer, &format!("{old} }}"))) +} + +fn signature_with_source(p: &Project, comp: ItemId, layer: &Layer, src: &str) -> String { let rate = p.comp(comp).map(|c| format!("{:?}", c.frame_rate)).unwrap_or_default(); let s = format!( "{:?}|{src}|{:?}|{:?}|{:?}|{}|{:?}|{rate}", diff --git a/crates/engine/src/cmd/file_more.rs b/crates/engine/src/cmd/file_more.rs index 168529c47..3088e8828 100644 --- a/crates/engine/src/cmd/file_more.rs +++ b/crates/engine/src/cmd/file_more.rs @@ -395,6 +395,9 @@ fn collect_files(s: &mut Session, p: &Value) -> Result { // reads (other footage, a proxy, the project file itself), and then nothing is written. let protected = s.protected_files(); let mut used_names = BTreeSet::new(); + // Where each file name's numbering resumes: a name repeated many times is numbered from its + // own file name, not by prefixing the previous candidate again. + let mut next_k: BTreeMap = BTreeMap::new(); let mut plan: Vec<(ItemId, Vec<(String, String)>)> = vec![]; for it in s.project.items.values() { let ItemKind::Footage(f) = &it.kind else { continue }; @@ -404,11 +407,17 @@ fn collect_files(s: &mut Session, p: &Value) -> Result { let files: Vec<&String> = if f.sequence.is_empty() { vec![&f.path] } else { f.sequence.iter().collect() }; let mut pairs = vec![]; for src in files { - let mut name = std::path::Path::new(src).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "footage".into()); - let mut k = 2; - while !used_names.insert(name.clone()) { - name = format!("{k}_{name}"); - k += 1; + let file = std::path::Path::new(src).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "footage".into()); + let mut name = file.clone(); + if !used_names.insert(name.clone()) { + let k = next_k.entry(file.clone()).or_insert(2); + loop { + name = format!("{k}_{file}"); + *k += 1; + if used_names.insert(name.clone()) { + break; + } + } } let dst = format!("{folder}/(Footage)/{name}"); // A file that is already where its copy goes stays (it is not copied onto itself). diff --git a/crates/engine/src/cmd/frame_export.rs b/crates/engine/src/cmd/frame_export.rs index d6f491fb0..61e1ac82d 100644 --- a/crates/engine/src/cmd/frame_export.rs +++ b/crates/engine/src/cmd/frame_export.rs @@ -9,6 +9,8 @@ //! premultiplied): the composite as `R`, `G`, `B`, `A` plus one `.R/G/B/A` channel //! set per layer (the layer-prefixed channel convention multi-layer EXR readers use). +use std::collections::{HashMap, HashSet}; + use effectcraft_color::BlendMode; use effectcraft_project::{ItemId, LayerId, Project}; use effectcraft_raster::Image; @@ -141,17 +143,22 @@ pub fn layered_psd(s: &Session, cid: ItemId, t: Tick, scale: f64) -> Result<(Vec /// A unique EXR layer name (no dots: they separate the layer from the channel). Printable ASCII only: /// the `exr` crate panics on characters it cannot store in a channel name (#1702). -fn exr_name(name: &str, used: &mut Vec) -> String { +/// `used` holds the names given so far, `next` the suffix each base's search resumes at: many +/// layers of one name take a name each without retrying every suffix before theirs. +fn exr_name(name: &str, used: &mut HashSet, next: &mut HashMap) -> String { let base: String = name.chars().map(|c| if c == '.' || !(c == ' ' || c.is_ascii_graphic()) { '_' } else { c }).collect(); let base = if base.trim().is_empty() { "Layer".to_string() } else { base }; - let mut n = base.clone(); - let mut k = 2; - while used.contains(&n) { - n = format!("{base} {k}"); - k += 1; + if used.insert(base.clone()) { + return base; + } + let k = next.entry(base.clone()).or_insert(2); + loop { + let n = format!("{base} {k}"); + *k += 1; + if used.insert(n.clone()) { + return n; + } } - used.push(n.clone()); - n } /// The multi-layer EXR bytes of a frame and the channel names. @@ -186,9 +193,9 @@ pub fn layered_exr(s: &Session, cid: ItemId, t: Tick, scale: f64) -> Result<(Vec } }; push("", &full); - let mut used = vec![]; + let (mut used, mut next) = (HashSet::new(), HashMap::new()); for l in &layers { - let n = exr_name(&l.name, &mut used); + let n = exr_name(&l.name, &mut used, &mut next); push(&n, &l.image); } let layer = Layer::new((w, h), LayerAttributes::default(), Encoding::SMALL_LOSSLESS, AnyChannels::sort(channels.into())); @@ -248,3 +255,23 @@ pub fn specs() -> Vec { ), ] } + +#[cfg(test)] +mod tests { + use std::collections::{HashMap, HashSet}; + + use super::exr_name; + + #[test] + fn many_layers_of_one_name_take_a_name_each_in_order() { + let (mut used, mut next) = (HashSet::new(), HashMap::new()); + // `A 3` is taken by a layer of its own before the repeats reach it. + let names: Vec = ["A", "A", "A 3", "A", "A.b"].iter().map(|n| exr_name(n, &mut used, &mut next)).collect(); + assert_eq!(names, ["A", "A 2", "A 3", "A 4", "A_b"]); + // 20 000 layers of one name: each found without retrying the suffixes before it. + for _ in 0..20_000 { + exr_name("Same", &mut used, &mut next); + } + assert_eq!(next["Same"], 20_001); + } +} diff --git a/crates/engine/src/cmd/relink.rs b/crates/engine/src/cmd/relink.rs index d899decbe..e74e4bf97 100644 --- a/crates/engine/src/cmd/relink.rs +++ b/crates/engine/src/cmd/relink.rs @@ -1,6 +1,6 @@ //! Recover moved media without replacing interpretation, sequence ranges or layer timing. -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; use std::path::Path; use effectcraft_project::{Footage, FootageKind, ItemId, ItemKind}; @@ -11,6 +11,8 @@ use crate::{Result, Session, cmd}; const COMMAND: &str = "file.relinkFootage"; const ENTRY_LIMIT: usize = 100_000; +/// Candidates listed for one missing item (more than one already makes it ambiguous). +const MAX_CANDIDATES: usize = 32; fn enabled(_: &Session) -> std::result::Result<(), String> { if cfg!(target_arch = "wasm32") { Err("folder relinking requires the desktop filesystem".into()) } else { Ok(()) } @@ -95,6 +97,8 @@ fn run(s: &mut Session, p: &Value) -> Result { let wait = p.get("wait").and_then(Value::as_bool).unwrap_or(false); s.spawn_task("relink", "Relinking missing footage", wait, move |ctl| { let index = files(&folder, ctl)?; + // Each candidate file is probed once, however many missing items share its name. + let mut probes: HashMap> = HashMap::new(); let mut rows = vec![]; let mut updates = vec![]; for (n, (id, old)) in missing.iter().enumerate() { @@ -104,19 +108,21 @@ fn run(s: &mut Session, p: &Value) -> Result { let supported = matches!(old.kind, FootageKind::Still | FootageKind::Sequence | FootageKind::Video | FootageKind::Audio) && old.layer.is_none() && old.page == 0; let name = Path::new(&old.path).file_name().and_then(std::ffi::OsStr::to_str).unwrap_or(""); - let paths = index.get(name).cloned().unwrap_or_default(); + let paths = index.get(name).map_or(&[][..], Vec::as_slice); let mut candidates = vec![]; let mut accepted = vec![]; - for path in paths { + // Past a few candidates the item is ambiguous anyway: list no more of them. + for path in paths.iter().take(MAX_CANDIDATES) { if ctl.cancelled() { return Err(crate::render_queue::CANCELLED.into()); } let mut reason = "source type requires manual replacement".to_string(); let mut confidence = "low"; if supported { - match importer.probe(&path) { - Ok(f) if compatible(old, &f) => { - if old.kind != FootageKind::Sequence || (!old.sequence.is_empty() && sequence_at(old, &path).is_some()) { + let probed = probes.entry(path.clone()).or_insert_with(|| importer.probe(path)); + match probed { + Ok(f) if compatible(old, f) => { + if old.kind != FootageKind::Sequence || (!old.sequence.is_empty() && sequence_at(old, path).is_some()) { confidence = "high"; reason = "exact filename and compatible source metadata".into(); accepted.push(path.clone()); @@ -284,6 +290,19 @@ mod tests { assert_eq!(get(&s, id), expected); } + #[test] + fn many_same_named_candidates_list_at_most_the_cap() { + let dir = Fixture::new(); + let (mut s, id, old) = setup(&dir, "plate.png"); + for i in 0..MAX_CANDIDATES + 8 { + dir.file(&format!("new/{i}/plate.png"), b"valid"); + } + let r = search(&mut s, &dir, false); + assert_eq!(r["items"][0]["status"], "ambiguous"); + assert_eq!(r["items"][0]["candidates"].as_array().unwrap().len(), MAX_CANDIDATES); + assert_eq!(get(&s, id), old); + } + #[test] fn duplicates_and_invalid_media_are_reported_without_edits() { let dir = Fixture::new(); diff --git a/crates/engine/src/cmd/time.rs b/crates/engine/src/cmd/time.rs index 556c02bcb..49a3caede 100644 --- a/crates/engine/src/cmd/time.rs +++ b/crates/engine/src/cmd/time.rs @@ -43,7 +43,9 @@ pub fn display_duration(s: &Session, comp: &Comp, len: Tick) -> String { } /// Parse a time typed in the project's Time Display Style (relative `+n`/`-n` from frame -/// `current`) into a comp frame. +/// `current`) into a comp frame. In Timecode style, in a comp whose Start Timecode is negative, a +/// `-` before a timecode with separators (`-0:00:00:12`, `-1.00`) is the sign of a displayed time, +/// not a step back; a bare `-n` still steps back. pub fn parse_display_time(s: &Session, comp: &Comp, text: &str, current: i64) -> std::result::Result { let fr = comp.frame_rate; let st = &s.project.settings; @@ -53,6 +55,20 @@ pub fn parse_display_time(s: &Session, comp: &Comp, text: &str, current: i64) -> effectcraft_time::parse_feet_frames(text, pf, current + start).map(|f| f - start).map_err(|e| e.0) } None => { + // A comp whose Start Timecode is negative shows the times before 0 with a `-` + // (`-0:00:00:12`), so there a `-` before a timecode with separators is the sign of a + // displayed time and typing what the display shows lands on the frame it shows. A bare + // `-n` still moves n frames back. + if st.time_display == TimeDisplayStyle::Timecode + && comp.display_start < Tick::ZERO + && let Some(body) = text.trim().strip_prefix('-').map(str::trim_start) + && body.contains([':', ';', '.', ',']) + && !body.starts_with(['+', '-']) + { + let drop_frame = fr.supports_drop_frame() && body.contains(';'); + let shown = effectcraft_time::parse_timecode(body, fr, drop_frame, current).map_err(|e| e.0)?; + return Ok(shown.saturating_neg().saturating_sub(fr.frame_at(comp.display_start))); + } // The Timeline shows non-drop timecode (`:`), so typed `:`/`.` text is read as non-drop // too; only explicit `;` fields mean drop-frame (#890). let drop_frame = fr.supports_drop_frame() && text.contains(';'); diff --git a/crates/engine/src/footage/sequence.rs b/crates/engine/src/footage/sequence.rs index fc2dab2b0..891ebdbe1 100644 --- a/crates/engine/src/footage/sequence.rs +++ b/crates/engine/src/footage/sequence.rs @@ -11,6 +11,8 @@ //! The browser has no folders: its file table lists the files picked or dropped so far, so //! picking every frame of a sequence imports it there too. +use std::collections::HashMap; + use effectcraft_project::sequence::{frame_number, missing_frame_ranges, sequence_name, split_frame_number}; use effectcraft_project::{Footage, FootageKind}; use effectcraft_time::FrameRate; @@ -126,10 +128,22 @@ fn sort_run(files: &mut Vec, alphabetical: bool) { files.dedup(); } -/// The files of the run `path` belongs to, from its folder. -fn run_files(services: &dyn Services, path: &str, alphabetical: bool) -> Vec { +type RunKey = (String, String, String); + +/// The files of the run `path` belongs to, from its folder. `folders` keeps each folder's files +/// by run, listed once for all the files picked from it. +fn run_files(services: &dyn Services, path: &str, alphabetical: bool, folders: &mut HashMap>>) -> Vec { let Some(key) = run_key(path, alphabetical) else { return vec![] }; - let mut files: Vec = services.list_dir(&key.0).into_iter().filter(|p| run_key(p, alphabetical).as_ref() == Some(&key)).collect(); + let runs = folders.entry(key.0.clone()).or_insert_with(|| { + let mut runs: HashMap> = HashMap::new(); + for p in services.list_dir(&key.0) { + if let Some(k) = run_key(&p, alphabetical) { + runs.entry(k).or_default().push(p); + } + } + runs + }); + let mut files = runs.get(&key).cloned().unwrap_or_default(); sort_run(&mut files, alphabetical); files } @@ -138,27 +152,41 @@ fn run_files(services: &dyn Services, path: &str, alphabetical: bool) -> Vec Vec { + // Each folder's runs, listed once for everything that needs them (a picked folder's listing + // is kept for the run lookup of its lone files). + let mut folders: HashMap>> = HashMap::new(); // Each file, and whether it may join a sequence. let mut files: Vec<(String, bool)> = vec![]; for p in paths { if services.is_dir(p) { - let mut inside: Vec = - services.list_dir(p).into_iter().filter(|f| matches!(crate::media_browser::kind_of(f), Some(k) if k != "project")).collect(); + let listing = services.list_dir(p); + let mut runs: HashMap> = HashMap::new(); + for f in &listing { + if let Some(k) = run_key(f, opts.alphabetical) { + runs.entry(k).or_default().push(f.clone()); + } + } + if let Some(dir) = runs.keys().next().map(|k| k.0.clone()) { + folders.entry(dir).or_insert(runs); + } + let mut inside: Vec = listing.into_iter().filter(|f| matches!(crate::media_browser::kind_of(f), Some(k) if k != "project")).collect(); inside.sort_by_key(|f| file_name(f).to_lowercase()); files.extend(inside.into_iter().map(|f| (f, opts.folders))); } else { files.push((p.clone(), opts.sequence)); } } - // Runs in the order their first file was picked. - let mut runs: Vec<((String, String, String), Vec)> = vec![]; + // Runs in the order their first file was picked (found by key, not by a scan of the runs). + let mut runs: Vec<(RunKey, Vec)> = vec![]; + let mut run_of: HashMap = HashMap::new(); let mut out: Vec> = vec![]; let mut slot: Vec = vec![]; for (f, joins) in files { match run_key(&f, opts.alphabetical).filter(|_| joins) { - Some(key) => match runs.iter_mut().find(|(k, _)| *k == key) { - Some((_, run)) => run.push(f), + Some(key) => match run_of.get(&key) { + Some(&i) => runs[i].1.push(f), None => { + run_of.insert(key.clone(), runs.len()); runs.push((key, vec![f])); slot.push(out.len()); out.push(None); @@ -173,7 +201,7 @@ pub fn group(services: &dyn Services, paths: &[String], opts: SequenceOptions) - if let [one] = picked.as_slice() && opts.sequence { - let all = run_files(services, one, opts.alphabetical); + let all = run_files(services, one, opts.alphabetical, &mut folders); if all.len() >= 2 { picked = all; } @@ -315,6 +343,59 @@ mod tests { assert_eq!(sequences(&t, &strs(&["/s/a_0001.png", "/s/c.png"]), false).len(), 1); } + /// Thousands of files picked one by one from one folder: the folder is listed once, not + /// once per file, and each finds its run by key. + #[test] + fn picked_files_of_one_folder_list_it_once() { + struct Counting(Table, std::sync::atomic::AtomicUsize); + impl Services for Counting { + fn read_file(&self, path: &str) -> std::io::Result> { + self.0.read_file(path) + } + fn write_file(&self, p: &str, b: &[u8]) -> std::io::Result<()> { + self.0.write_file(p, b) + } + fn list_dir(&self, dir: &str) -> Vec { + self.1.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + self.0.list_dir(dir) + } + fn is_dir(&self, path: &str) -> bool { + self.0.is_dir(path) + } + } + let names: Vec<&'static str> = (0..2000).map(|i| &*Box::leak(format!("/s/x{i}_1.png").into_boxed_str())).collect(); + let t = Counting(Table(names.clone()), Default::default()); + let picked: Vec = names.iter().map(|s| s.to_string()).collect(); + let g = group(&t, &picked, SequenceOptions::default()); + assert_eq!(g.len(), 2000); + assert_eq!(t.1.load(std::sync::atomic::Ordering::Relaxed), 1); + } + + /// A picked folder's listing serves the run lookup of its lone files: one listing in all. + #[test] + fn a_picked_folder_is_listed_once() { + struct Counting(Table, std::sync::atomic::AtomicUsize); + impl Services for Counting { + fn read_file(&self, path: &str) -> std::io::Result> { + self.0.read_file(path) + } + fn write_file(&self, p: &str, b: &[u8]) -> std::io::Result<()> { + self.0.write_file(p, b) + } + fn list_dir(&self, dir: &str) -> Vec { + self.1.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + self.0.list_dir(dir) + } + fn is_dir(&self, path: &str) -> bool { + self.0.is_dir(path) + } + } + let t = Counting(Table(vec!["/s/a_0001.png", "/s/b_0001.png", "/s/b_0002.png"]), Default::default()); + let g = group(&t, &strs(&["/s"]), SequenceOptions { sequence: true, folders: true, alphabetical: false }); + assert_eq!(g.len(), 2, "{g:?}"); + assert_eq!(t.1.load(std::sync::atomic::Ordering::Relaxed), 1); + } + /// #431: `file.import` brings picked or dropped numbered stills in as stills; only a folder or /// an explicit `sequence: true` makes a sequence, as After Effects' Sequence checkbox does. #[test] diff --git a/crates/engine/src/home/templates.rs b/crates/engine/src/home/templates.rs index 0b15baaf5..f411fa5bd 100644 --- a/crates/engine/src/home/templates.rs +++ b/crates/engine/src/home/templates.rs @@ -751,6 +751,9 @@ fn footage_dir(s: &Session, name: &str, footage_dir: Option<&str>, project_path: fn extract_footage(s: &Session, proj: &mut Project, entries: &[(String, Vec)], dir: &str) -> Result> { let mut written = std::collections::HashMap::new(); let mut used = std::collections::HashSet::new(); + // Where each (item, file) pair's numbering resumes, so repeated names don't retry every + // number before theirs. + let mut next = std::collections::HashMap::new(); for (k, v) in entries.iter().filter(|(k, _)| k.starts_with("media/")) { // `media//` → one folder; a name another item took gets the item's id. let file = safe_file_name(&base_name(k)); @@ -763,10 +766,10 @@ fn extract_footage(s: &Session, proj: &mut Project, entries: &[(String, Vec) // case-insensitive volume (Windows and macOS as they come), and Win32 drops a trailing // dot or space, so `a.png.` is `a.png` there. The name written stays as it is. let mut name = file.clone(); - let mut n = 0usize; + let n = next.entry((item, same_file_key(&file))).or_insert(0usize); while !used.insert(same_file_key(&name)) { - n += 1; - name = if n == 1 { format!("{item}-{file}") } else { format!("{item}-{n}-{file}") }; + *n += 1; + name = if *n == 1 { format!("{item}-{file}") } else { format!("{item}-{n}-{file}") }; } let out = format!("{dir}/{name}"); s.services.store_file(&out, v).map_err(|e| EngineError::Other(format!("cannot write {out}: {e}")))?; diff --git a/crates/engine/src/interchange/vector.rs b/crates/engine/src/interchange/vector.rs index b52517cb3..569e8e5bc 100644 --- a/crates/engine/src/interchange/vector.rs +++ b/crates/engine/src/interchange/vector.rs @@ -477,35 +477,51 @@ pub fn split_at_images(doc: &Doc) -> Vec { }) .sum() } - /// Keep the shapes between images `seg − 1` and `seg`; `seen` counts images passed. - fn prune(g: &effectcraft_svg::Group, seg: usize, seen: &mut usize) -> effectcraft_svg::Group { - let mut out = g.clone(); - out.children = vec![]; + /// `g` without its children, holding `children` instead (nothing below `g` is copied). + fn shell(g: &effectcraft_svg::Group, children: Vec) -> effectcraft_svg::Group { + effectcraft_svg::Group { + name: g.name.clone(), + transform: g.transform, + opacity: g.opacity, + children, + clip: g.clip.clone(), + blend: g.blend, + mask: g.mask.clone(), + isolated: g.isolated, + knockout: g.knockout, + } + } + /// One traversal for every segment: the pieces of `g` as (segment, group), by ascending + /// segment; `seen` counts images passed. Each shape is copied once, into its own segment. + fn split(g: &effectcraft_svg::Group, seen: &mut usize) -> Vec<(usize, effectcraft_svg::Group)> { + let mut by: Vec<(usize, Vec)> = vec![]; + let mut put = |seg: usize, node: Node| match by.last_mut() { + Some(last) if last.0 == seg => last.1.push(node), + _ => by.push((seg, vec![node])), + }; for c in &g.children { match c { Node::Image(_) => *seen += 1, - Node::Shape(_) => { - if *seen == seg { - out.children.push(c.clone()); - } - } + Node::Shape(_) => put(*seen, c.clone()), Node::Group(s) => { - let p = prune(s, seg, seen); - if !p.children.is_empty() { - out.children.push(Node::Group(p)); + for (seg, part) in split(s, seen) { + put(seg, Node::Group(part)); } } } } - out + by.into_iter().map(|(seg, children)| (seg, shell(g, children))).collect() } let n = count(&doc.root); - (0..=n) - .map(|seg| { - let mut d = doc.clone(); - d.root = prune(&doc.root, seg, &mut 0); - d - }) + let mut roots: Vec> = vec![None; n + 1]; + for (seg, part) in split(&doc.root, &mut 0) { + if let Some(slot) = roots.get_mut(seg) { + *slot = Some(part); + } + } + roots + .into_iter() + .map(|root| Doc { width: doc.width, height: doc.height, root: root.unwrap_or_else(|| shell(&doc.root, vec![])), skipped: doc.skipped.clone() }) .collect() } @@ -655,3 +671,73 @@ pub fn masks_layer(proj: &mut Project, comp: &Comp, name: &str, outlines: Vec Node { + let geom = Geom::Rect { x: 0.0, y: 0.0, w: 1.0, h: 1.0, rx: 0.0, ry: 0.0 }; + Node::Shape(Shape { + name: name.into(), + geom, + transform: Affine::IDENTITY, + fill: None, + fill_opacity: 1.0, + fill_rule: effectcraft_svg::FillRule::NonZero, + stroke: None, + opacity: 1.0, + }) + } + fn image() -> Node { + Node::Image(Image { name: "i".into(), transform: Affine::IDENTITY, width: 1, height: 1, rgba: std::sync::Arc::new(vec![0; 4]), opacity: 1.0 }) + } + fn group(name: &str, children: Vec) -> Node { + let mut g = Group::new(name); + g.children = children; + Node::Group(g) + } + fn names(g: &Group, out: &mut Vec) { + for c in &g.children { + match c { + Node::Shape(s) => out.push(s.name.clone()), + Node::Group(s) => { + out.push(format!("<{}", s.name)); + names(s, out); + out.push(format!("{}>", s.name)); + } + Node::Image(_) => out.push("IMG".into()), + } + } + } + + /// Segment `k` keeps the shapes between image `k - 1` and `k`, with the groups that hold them. + #[test] + fn split_keeps_each_segments_shapes_and_their_groups() { + let mut root = Group::new("root"); + root.children = vec![ + shape("a"), + image(), + group("g1", vec![shape("b"), image(), shape("c"), group("g2", vec![shape("d"), image(), shape("e")])]), + image(), + group("empty", vec![image()]), + shape("f"), + ]; + let doc = Doc { width: 10.0, height: 10.0, root, skipped: vec![] }; + let segs = split_at_images(&doc); + assert_eq!(segs.len(), 6); + let got: Vec> = segs + .iter() + .map(|d| { + let mut v = vec![]; + names(&d.root, &mut v); + v + }) + .collect(); + let want: Vec> = + vec![vec!["a"], vec![""], vec!["", "g1>"], vec!["", "g1>"], vec![], vec!["f"]]; + assert_eq!(got, want); + assert!(segs.iter().all(|d| d.root.name == "root" && d.width == 10.0)); + } +} diff --git a/crates/engine/src/output_guard.rs b/crates/engine/src/output_guard.rs index 46c4356ed..73d938a32 100644 --- a/crates/engine/src/output_guard.rs +++ b/crates/engine/src/output_guard.rs @@ -509,12 +509,27 @@ impl Session { /// is not for an export to replace. For callers that write the file themselves; call it /// before the work, not after it. pub fn refuse_export(&self, path: &str) -> crate::Result<()> { + self.refuse_script_write(path)?; match self.blocking(path) { Some(source) => Err(crate::EngineError::Other(source.refusal(path, Use::Export))), None => Ok(()), } } + /// `Err` when a script issued the running command ([`Session::running_for_script`]) and + /// Settings ▸ Scripting & Expressions ▸ Allow Scripts to Write Files is off. The scripting + /// wrappers check this too, but a script can run any command directly (`app.run`, `__exec`), + /// so the commands that write a path they are given check here, through + /// [`Session::refuse_export`] and [`Session::write_output`]. + pub fn refuse_script_write(&self, path: &str) -> crate::Result<()> { + if self.running_for_script() && !self.prefs.scripting.allow_scripts_write_files { + return Err(crate::EngineError::Other(format!( + "Unable to write {path}: scripts can't write files unless Settings ▸ Scripting & Expressions ▸ Allow Scripts to Write Files and Access Network is on" + ))); + } + Ok(()) + } + /// Write `bytes` to `path` through the host's services, unless the project reads the file /// there ([`Session::refuse_export`]). Every command that writes to a path it is given /// writes through this; Save and Save As (the project's own file) do not. @@ -525,6 +540,7 @@ impl Session { /// [`Session::write_output`] against files already found ([`Session::protected_files`]), /// for a command that writes many. pub(crate) fn write_output_with(&self, protected: &Protected, path: &str, bytes: &[u8]) -> crate::Result<()> { + self.refuse_script_write(path)?; if let Some(source) = protected.find(Path::new(path)) { return Err(crate::EngineError::Other(source.refusal(path, Use::Export))); } diff --git a/crates/engine/src/scriptui.rs b/crates/engine/src/scriptui.rs index 6d36a2e7e..69e572ec2 100644 --- a/crates/engine/src/scriptui.rs +++ b/crates/engine/src/scriptui.rs @@ -195,6 +195,15 @@ impl ImageRef { } } +/// Most edges a ScriptUI `fillPath` is filled with (a path past it fills its first edges). +pub const MAX_FILL_EDGES: usize = 65_536; +/// Most horizontal bands a ScriptUI fill is cut into (vertices and edge crossings); the topmost are filled. +pub const MAX_FILL_BANDS: usize = 262_144; +/// Most triangles a ScriptUI fill produces. +pub const MAX_FILL_TRIANGLES: usize = 1 << 22; +/// Most band-by-edge steps a fill takes while cutting bands. +pub const MAX_FILL_WORK: usize = 1 << 31; + impl PathSeg { /// The subpaths of `path` as polylines (ellipses flattened to `n` segments), each with /// whether it is closed. @@ -248,8 +257,16 @@ impl PathSeg { /// self-intersecting and holed paths fill correctly (frontends' polygon fills are often /// convex-only). Exact trapezoid decomposition: the plane is cut into horizontal bands at /// every vertex and edge crossing; inside a band no edges cross, so each inside span is a - /// trapezoid (two triangles). + /// trapezoid (two triangles). Edges are compared pairwise and every band looks at every edge, + /// on the UI thread, for geometry a script chooses, so a path past [`MAX_FILL_EDGES`] edges, + /// [`MAX_FILL_BANDS`] bands, [`MAX_FILL_TRIANGLES`] triangles or [`MAX_FILL_WORK`] steps (edge + /// pairs compared, then edges looked at per band) is filled only as far as the limit allows + /// (topmost part first) and a warning is logged. pub fn fill_triangles(path: &[PathSeg], n: usize, even_odd: bool) -> Vec<[[f64; 2]; 3]> { + Self::fill_triangles_within(path, n, even_odd, [MAX_FILL_EDGES, MAX_FILL_BANDS, MAX_FILL_TRIANGLES, MAX_FILL_WORK]) + } + + fn fill_triangles_within(path: &[PathSeg], n: usize, even_odd: bool, [max_edges, max_bands, max_triangles, max_work]: [usize; 4]) -> Vec<[[f64; 2]; 3]> { // Non-horizontal edges, top to bottom, with their winding direction. let mut edges: Vec<([f64; 2], [f64; 2], i32)> = vec![]; for (pts, _) in PathSeg::polylines(path, n) { @@ -261,15 +278,37 @@ impl PathSeg { edges.push(if a[1] < b[1] { (a, b, 1) } else { (b, a, -1) }); } } + if edges.len() > max_edges { + log::warn!("fillPath: {} edges, filling only the first {max_edges}", edges.len()); + edges.truncate(max_edges); + } if edges.len() < 2 { return vec![]; } let x_at = |e: &([f64; 2], [f64; 2], i32), y: f64| e.0[0] + (e.1[0] - e.0[0]) * (y - e.0[1]) / (e.1[1] - e.0[1]); let mut ys: Vec = edges.iter().flat_map(|e| [e.0[1], e.1[1]]).collect(); - // Where two edges cross, a band boundary. - for i in 0..edges.len() { - for j in i + 1..edges.len() { + // Where two edges cross, a band boundary. Edges by top, so each is compared only with the + // ones that start above its bottom (the pairs that share some height); the pairs and the + // boundaries found are counted against the budgets, and past one the rest are not looked + // for. A band that holds a crossing that was not looked for is not filled (its edges + // change order inside it, which the band loop below tells), so what is drawn is never + // outside the path. + let mut truncated = false; + let mut order: Vec = (0..edges.len()).collect(); + order.sort_by(|&a, &b| edges[a].0[1].total_cmp(&edges[b].0[1])); + let mut pairs = 0usize; + 'cross: for (k, &i) in order.iter().enumerate() { + for &j in &order[k + 1..] { let (a, b) = (&edges[i], &edges[j]); + if b.0[1] >= a.1[1] { + break; + } + pairs += 1; + if pairs > max_work || ys.len() > max_bands { + log::warn!("fillPath: over its crossing budget; bands with crossings that were not cut are left unfilled"); + truncated = true; + break 'cross; + } let (lo, hi) = (a.0[1].max(b.0[1]), a.1[1].min(b.1[1])); if hi <= lo { continue; @@ -282,6 +321,11 @@ impl PathSeg { } ys.sort_by(f64::total_cmp); ys.dedup_by(|a, b| (*a - *b).abs() < 1e-9); + if ys.len() > max_bands { + log::warn!("fillPath: {} bands, filling only the topmost {max_bands}", ys.len()); + ys.truncate(max_bands); + } + let mut work = 0usize; let mut out = vec![]; let mut row: Vec<(f64, f64, f64, i32)> = vec![]; for w in ys.windows(2) { @@ -290,8 +334,14 @@ impl PathSeg { row.clear(); row.extend(edges.iter().filter(|e| e.0[1] < ym && ym < e.1[1]).map(|e| (x_at(e, ym), x_at(e, ya), x_at(e, yb), e.2))); row.sort_by(|a, b| a.0.total_cmp(&b.0)); + // With crossings missing, edges that swap places inside the band make its trapezoids + // wrong: it is left out. + let swapped = truncated && !row.windows(2).all(|w| w[0].1 <= w[1].1 + 1e-9 && w[0].2 <= w[1].2 + 1e-9); let mut wind = 0; for k in 0..row.len().saturating_sub(1) { + if swapped { + break; + } wind += row[k].3; let inside = if even_odd { wind % 2 != 0 } else { wind != 0 }; if inside { @@ -300,6 +350,13 @@ impl PathSeg { out.push([[l.1, ya], [r.2, yb], [l.2, yb]]); } } + // (Every band's edge scan counts, a band left out included.) + work = work.saturating_add(edges.len()); + if out.len() > max_triangles || work > max_work { + log::warn!("fillPath: over its triangle or work budget, filled down to y = {yb}"); + out.truncate(max_triangles); + break; + } } out } @@ -951,4 +1008,50 @@ mod tests { let open = vec![PathSeg::M { x: 0.0, y: 0.0 }, PathSeg::L { x: 4.0, y: 0.0 }, PathSeg::L { x: 0.0, y: 4.0 }]; assert!((area(&PathSeg::fill_triangles(&open, 48, false)) - 8.0).abs() < 1e-9); } + + /// A script's path past the fill budgets is filled as far as they allow, never emptied. + #[test] + fn a_fill_past_its_budgets_draws_what_fits() { + let area = |t: &[[[f64; 2]; 3]]| { + t.iter().map(|t| ((t[1][0] - t[0][0]) * (t[2][1] - t[0][1]) - (t[2][0] - t[0][0]) * (t[1][1] - t[0][1])).abs() / 2.0).sum::() + }; + let rect = |x: f64, y: f64, w: f64, h: f64| PathSeg::R { x, y, w, h }; + let rects: Vec = (0..8).map(|i| rect(2.0 * i as f64, 0.0, 1.0, 10.0)).collect(); + let full = PathSeg::fill_triangles(&rects, 48, false); + assert!((area(&full) - 80.0).abs() < 1e-9); + // Edges over the limit: the first edges still fill (4 edges = 2 rectangles). + let t = PathSeg::fill_triangles_within(&rects, 48, false, [4, usize::MAX, usize::MAX, usize::MAX]); + assert!((area(&t) - 20.0).abs() < 1e-9, "{}", area(&t)); + // Triangles over the limit: truncated, not empty. + let t = PathSeg::fill_triangles_within(&rects, 48, false, [usize::MAX, usize::MAX, 6, usize::MAX]); + assert_eq!(t.len(), 6); + // Bands over the limit: the topmost band fills. + let stair: Vec = (0..4).map(|i| rect(0.0, i as f64, 1.0, 1.0)).collect(); + let t = PathSeg::fill_triangles_within(&stair, 48, false, [usize::MAX, 2, usize::MAX, usize::MAX]); + assert!(!t.is_empty() && area(&t) < 4.0); + // Work over the limit stops the cut. + let t = PathSeg::fill_triangles_within(&stair, 48, false, [usize::MAX, usize::MAX, usize::MAX, 1]); + assert!(!t.is_empty() && area(&t) < 4.0); + // Many crossing edges (every pair of 120 diagonals meets): the crossing scan is held to the + // band and work budgets too. + let fan: Vec = (0..60) + .flat_map(|i| { + let x = i as f64; + [PathSeg::M { x, y: 0.0 }, PathSeg::L { x: 60.0 - x, y: 100.0 }, PathSeg::L { x: x + 0.5, y: 100.0 }] + }) + .collect(); + for limits in [[usize::MAX, 50, usize::MAX, usize::MAX], [usize::MAX, usize::MAX, usize::MAX, 500]] { + let t = PathSeg::fill_triangles_within(&fan, 48, false, limits); + assert!(area(&t) <= area(&PathSeg::fill_triangles(&fan, 48, false)) + 1e-6, "{limits:?}"); + } + // Bands whose crossings were not looked for are not filled: a bow tie (area 50, crossing at + // y = 5) under a crossing budget of one boundary gives no overlapping triangles. + let bow = vec![PathSeg::M { x: 0.0, y: 0.0 }, PathSeg::L { x: 10.0, y: 10.0 }, PathSeg::L { x: 0.0, y: 10.0 }, PathSeg::L { x: 10.0, y: 0.0 }]; + assert!((area(&PathSeg::fill_triangles(&bow, 48, false)) - 50.0).abs() < 1e-9); + let t = PathSeg::fill_triangles_within(&bow, 48, false, [usize::MAX, 2, usize::MAX, usize::MAX]); + assert!(area(&t) <= 50.0 + 1e-9, "{}", area(&t)); + // Bands left out still count against the work budget (it ends the scan after the first). + let t = PathSeg::fill_triangles_within(&bow, 48, false, [usize::MAX, 2, usize::MAX, 1]); + assert!(area(&t) <= 50.0 + 1e-9, "{}", area(&t)); + } } diff --git a/crates/engine/src/session/render.rs b/crates/engine/src/session/render.rs index 2dd8a3e7f..c3740196c 100644 --- a/crates/engine/src/session/render.rs +++ b/crates/engine/src/session/render.rs @@ -103,15 +103,22 @@ impl Session { effectcraft_project::ItemKind::Footage(f) if f.has_video => { let f = f.clone(); Some(Box::new(move || { - let img = effectcraft_render::cineon_frame(footage.frame(item, &f, Tick(0))?, &f); - // Point-sample down to at most `max_side` on the long side. - let step = (img.width.max(img.height) as f32 / max_side.max(1) as f32).ceil().max(1.0) as u32; - let (w, h) = (img.width.div_ceil(step), img.height.div_ceil(step)); + // Vector footage is drawn at the thumbnail's size: its declared size can be a + // 16384² canvas, gigabytes of pixels to point-sample down to a few dozen. + let scale = (max_side.max(1) as f64 / f.width.max(f.height).max(1) as f64).min(1.0); + let src = footage.vector_frame(item, &f, scale).or_else(|| footage.frame(item, &f, Tick(0)))?; + let img = effectcraft_render::cineon_frame(src, &f); + // Point-sample down to at most `max_side` on the long side (a drawn size one + // pixel over it, from rounding, is shrunk by that pixel, not halved). + let (long, max) = (u64::from(img.width.max(img.height)), u64::from(max_side.max(1))); + let fit = |side: u32| if long <= max { side } else { ((u64::from(side) * max + long / 2) / long).max(1) as u32 }; + let (w, h) = (fit(img.width), fit(img.height)); let full = img.to_rgba8(); let mut px = Vec::with_capacity((w * h * 4) as usize); for y in 0..h { for x in 0..w { - let i = (((y * step) * img.width + x * step) * 4) as usize; + let (sx, sy) = (u64::from(x) * u64::from(img.width) / u64::from(w), u64::from(y) * u64::from(img.height) / u64::from(h)); + let i = ((sy * u64::from(img.width) + sx) * 4) as usize; px.extend_from_slice(&full[i..i + 4]); } } diff --git a/crates/engine/src/settings/shortcuts.rs b/crates/engine/src/settings/shortcuts.rs index 44e2261a2..2d3996d57 100644 --- a/crates/engine/src/settings/shortcuts.rs +++ b/crates/engine/src/settings/shortcuts.rs @@ -550,6 +550,10 @@ impl Keymaps { } } +/// Most conflicts [`ShortcutTable::all_conflicts`] lists: a preset binding every command to one +/// key would otherwise list every pair of them. +pub const MAX_CONFLICTS: usize = 10_000; + /// The active preset resolved against every bindable command. #[derive(Clone, Debug, Default)] pub struct ShortcutTable { @@ -584,8 +588,15 @@ impl ShortcutTable { } /// Every active binding: (keys, bindable). + /// Each bindable's keys once: an imported preset can repeat a binding any number of times. pub fn bindings(&self) -> Vec<(&str, &Bindable)> { - self.bindables.iter().flat_map(|b| self.keys.get(&b.key).into_iter().flatten().map(move |k| (k.as_str(), b))).collect() + self.bindables + .iter() + .flat_map(|b| { + let mut seen = std::collections::HashSet::new(); + self.keys.get(&b.key).into_iter().flatten().filter(move |k| seen.insert(k.as_str())).map(move |k| (k.as_str(), b)) + }) + .collect() } /// Other bindables using `keys` in an overlapping scope. @@ -607,6 +618,7 @@ impl ShortcutTable { /// Every pair of commands sharing keys in the same scope, or in two contexts (which can be /// active at once): only one of the two can run, or neither ([`pick`]). (Other pairs in /// different scopes are intended, see [`Conflict::overrides`].) + /// At most [`MAX_CONFLICTS`] are listed. pub fn all_conflicts(&self) -> Vec<(String, Conflict)> { let mut by_keys: BTreeMap<&str, Vec<&Bindable>> = BTreeMap::new(); for (k, b) in self.bindings() { @@ -618,6 +630,9 @@ impl ShortcutTable { for (k, bs) in by_keys { for (i, a) in bs.iter().enumerate() { for b in &bs[i + 1..] { + if out.len() >= MAX_CONFLICTS { + return out; + } if a.scope == b.scope || (is_context(&a.scope) && is_context(&b.scope)) { out.push(( a.key.clone(), diff --git a/crates/engine/src/system/logging.rs b/crates/engine/src/system/logging.rs index 2bb3518a1..da9d0f4f6 100644 --- a/crates/engine/src/system/logging.rs +++ b/crates/engine/src/system/logging.rs @@ -125,7 +125,13 @@ fn utc_parts(secs: u64, nanos: u32) -> (i64, chrono::NaiveDateTime) { /// One log line (without the newline): ` [] : `. fn format_line(ts: &str, level: log::Level, thread: &str, target: &str, message: &str) -> String { - let message = message.strip_suffix('\n').unwrap_or(message); + // CRLF becomes LF first, so a message ending in "\r\n" loses its final break like one ending + // in "\n" (and its carriage return is not taken for a lone one). + let message = message.replace("\r\n", "\n"); + let message = message.strip_suffix('\n').unwrap_or(&message); + // A message's own line breaks (a path or name from a project or a script) continue it on + // indented lines, so they can't pass for records of their own. + let message = message.replace('\r', "\\r").replace('\n', "\n\t"); format!("{ts} {level:<5} [{thread}] {target}: {message}") } @@ -523,4 +529,22 @@ mod tests { "2026-10-08T07:59:17.728Z WARN [main] effectcraft_engine::autosave: disk full" ); } + + #[test] + fn line_breaks_in_a_message_continue_it_on_indented_lines() { + let forged = "cache at /tmp/x\n2026-10-08T07:59:18.000Z ERROR [main] effectcraft_engine: forged\rend"; + let line = format_line("2026-10-08T07:59:17.728Z", log::Level::Info, "main", "t", forged); + assert_eq!(line.lines().count(), 2); + assert!(line.lines().nth(1).unwrap().starts_with('\t'), "{line}"); + assert!(!line.contains('\r'), "{line}"); + } + + #[test] + fn a_trailing_crlf_is_one_line_break_not_a_lone_carriage_return() { + let line = |m| format_line("t", log::Level::Info, "main", "t", m); + assert_eq!(line("cache\r\n"), line("cache")); + assert_eq!(line("a\r\nb\r\n"), line("a\nb")); + // A lone one, even at the end, is still escaped. + assert!(line("cache\r").ends_with("cache\\r")); + } } diff --git a/crates/engine/src/tests/export_overwrite.rs b/crates/engine/src/tests/export_overwrite.rs index 5cc65b1d5..507a9be4b 100644 --- a/crates/engine/src/tests/export_overwrite.rs +++ b/crates/engine/src/tests/export_overwrite.rs @@ -331,3 +331,17 @@ fn background_pre_render_movies_are_checked_again_before_publication() { assert!(listing(&f.dir).iter().all(|name| !name.starts_with(".effectcraft-prerender-"))); } } + +/// Footage files sharing a name are numbered from that name (`3_a.mov`, not `3_2_a.mov`). +#[test] +fn collect_files_numbers_repeated_names_from_the_name() { + let mut f = collecting("collect-repeated", "a.mov"); + for d in ["in2", "in3"] { + std::fs::create_dir_all(f.path(d)).unwrap(); + f.footage(&format!("{d}/a.mov")); + } + collect(&mut f).unwrap(); + let mut names = listing(&f.path("out/(Footage)")); + names.sort(); + assert_eq!(names, ["2_a.mov", "3_a.mov", "a.mov"]); +} diff --git a/crates/engine/src/tests/fixtures/object-matte-0.22.ecproj b/crates/engine/src/tests/fixtures/object-matte-0.22.ecproj new file mode 100644 index 000000000..9a85aaa65 --- /dev/null +++ b/crates/engine/src/tests/fixtures/object-matte-0.22.ecproj @@ -0,0 +1,947 @@ +{ + "savedBy": "0.22.0", + "activeComp": 1, + "openComps": [ + 1 + ], + "schema": 2, + "settings": { + "bit_depth": "Bpc8", + "linearize": false, + "working_space": null, + "blend_linear": false, + "time_display": "Timecode", + "frame_start": 0, + "audio_sample_rate": 48000, + "gpu_acceleration": true, + "color_engine": "Adobe", + "hdr": "Clip", + "output_space": null, + "working_gamma": 2.2 + }, + "items": { + "1": { + "id": 1, + "name": "Main", + "label": "Sandstone", + "comment": "", + "parent": null, + "kind": { + "type": "Comp", + "width": 64, + "height": 48, + "pixel_aspect": 1.0, + "frame_rate": { + "num": 30, + "den": 1 + }, + "duration": 25401600000, + "display_start": 0, + "background": [ + 0.0, + 0.0, + 0.0 + ], + "work_area": [ + 0, + 25401600000 + ], + "layers": [ + { + "id": 3, + "name": "effectcraft-3430-subject.mp4", + "source": { + "type": "Footage", + "item": 2 + }, + "label": "Aqua", + "comment": "", + "start_time": 0, + "in_point": 0, + "out_point": 25401600000, + "stretch": 100.0, + "switches": { + "video": true, + "audio": true, + "solo": false, + "locked": false, + "shy": false, + "collapse": false, + "quality": "Best", + "sampling": "Bilinear", + "effects": true, + "frame_blend": "Off", + "motion_blur": false, + "adjustment": false, + "three_d": false, + "guide": false + }, + "blend_mode": "Normal", + "preserve_transparency": false, + "track_matte": null, + "parent": null, + "markers": [], + "auto_orient": "Off", + "props": { + "uid": 4, + "match": "layer", + "name": "effectcraft-3430-subject.mp4", + "kind": { + "kind": "Plain" + }, + "enabled": true, + "children": [ + { + "node": "Group", + "uid": 5, + "match": "masks", + "name": "Masks", + "kind": { + "kind": "Plain" + }, + "enabled": true, + "children": [] + }, + { + "node": "Group", + "uid": 6, + "match": "effects", + "name": "Effects", + "kind": { + "kind": "Plain" + }, + "enabled": true, + "children": [ + { + "node": "Group", + "uid": 26, + "match": "ec.matte.objectmatte", + "name": "Object Matte", + "kind": { + "kind": "Effect", + "effect": "ec.matte.objectmatte" + }, + "enabled": true, + "children": [ + { + "node": "Prop", + "uid": 27, + "match": "objectPoint", + "name": "Object Point", + "value": { + "t": "Vec2", + "v": [ + 32.0, + 24.0 + ] + }, + "ui": { + "kind": "Point" + }, + "spatial": true, + "hold_only": false, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + { + "node": "Prop", + "uid": 28, + "match": "useBox", + "name": "Use Box", + "value": { + "t": "Bool", + "v": true + }, + "ui": { + "kind": "Checkbox" + }, + "spatial": false, + "hold_only": true, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + { + "node": "Prop", + "uid": 29, + "match": "boxTopLeft", + "name": "Box Top Left", + "value": { + "t": "Vec2", + "v": [ + 14.0, + 8.0 + ] + }, + "ui": { + "kind": "Point" + }, + "spatial": true, + "hold_only": false, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + { + "node": "Prop", + "uid": 30, + "match": "boxBottomRight", + "name": "Box Bottom Right", + "value": { + "t": "Vec2", + "v": [ + 50.0, + 40.0 + ] + }, + "ui": { + "kind": "Point" + }, + "spatial": true, + "hold_only": false, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + { + "node": "Group", + "uid": 32, + "match": "objectTracking", + "name": "Tracking", + "kind": { + "kind": "Plain" + }, + "enabled": true, + "children": [ + { + "node": "Prop", + "uid": 31, + "match": "trackObject", + "name": "Track Object", + "value": { + "t": "Bool", + "v": true + }, + "ui": { + "kind": "Checkbox" + }, + "spatial": false, + "hold_only": true, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + { + "node": "Prop", + "uid": 33, + "match": "referenceFrame", + "name": "Reference Frame", + "value": { + "t": "Scalar", + "v": 0.0 + }, + "ui": { + "kind": "Number" + }, + "spatial": false, + "hold_only": false, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + { + "node": "Prop", + "uid": 34, + "match": "framesBefore", + "name": "Frames Before", + "value": { + "t": "Scalar", + "v": 0.0 + }, + "ui": { + "kind": "Slider", + "min": 0.0, + "max": 600.0, + "slider_min": 0.0, + "slider_max": 120.0, + "decimals": 0 + }, + "spatial": false, + "hold_only": false, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + { + "node": "Prop", + "uid": 35, + "match": "framesAfter", + "name": "Frames After", + "value": { + "t": "Scalar", + "v": 0.0 + }, + "ui": { + "kind": "Slider", + "min": 0.0, + "max": 600.0, + "slider_min": 0.0, + "slider_max": 120.0, + "decimals": 0 + }, + "spatial": false, + "hold_only": false, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + } + ] + }, + { + "node": "Group", + "uid": 37, + "match": "objectMatte", + "name": "Matte", + "kind": { + "kind": "Plain" + }, + "enabled": true, + "children": [ + { + "node": "Prop", + "uid": 36, + "match": "feather", + "name": "Feather", + "value": { + "t": "Scalar", + "v": 0.0 + }, + "ui": { + "kind": "Slider", + "min": 0.0, + "max": 100.0, + "slider_min": 0.0, + "slider_max": 20.0, + "decimals": 1 + }, + "spatial": false, + "hold_only": false, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + { + "node": "Prop", + "uid": 38, + "match": "contrast", + "name": "Contrast", + "value": { + "t": "Scalar", + "v": 0.0 + }, + "ui": { + "kind": "Slider", + "min": 0.0, + "max": 100.0, + "slider_min": 0.0, + "slider_max": 100.0, + "decimals": 0 + }, + "spatial": false, + "hold_only": false, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + { + "node": "Prop", + "uid": 39, + "match": "shiftEdge", + "name": "Shift Edge", + "value": { + "t": "Scalar", + "v": 0.0 + }, + "ui": { + "kind": "Slider", + "min": -100.0, + "max": 100.0, + "slider_min": -100.0, + "slider_max": 100.0, + "decimals": 0 + }, + "spatial": false, + "hold_only": false, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + } + ] + }, + { + "node": "Prop", + "uid": 40, + "match": "invert", + "name": "Invert", + "value": { + "t": "Bool", + "v": false + }, + "ui": { + "kind": "Checkbox" + }, + "spatial": false, + "hold_only": true, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + { + "node": "Prop", + "uid": 41, + "match": "view", + "name": "View", + "value": { + "t": "Enum", + "v": 0 + }, + "ui": { + "kind": "Popup", + "options": [ + "Final Output", + "Matte" + ] + }, + "spatial": false, + "hold_only": true, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + { + "node": "Prop", + "uid": 42, + "match": "inputKey", + "name": "Input Key", + "value": { + "t": "Str", + "v": "cbefe0c85977c4aa" + }, + "ui": { + "kind": "Hidden" + }, + "spatial": false, + "hold_only": true, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + { + "node": "Prop", + "uid": 43, + "match": "frozen", + "name": "Frozen", + "value": { + "t": "Str", + "v": "{\"key\":\"1a097b69366afd7f\",\"size\":[64,48],\"frames\":{\"0\":{\"a\":\"gBgAkAX/IAAg/yAAIP8gACD/IAAg/yAAIP8gACD/IAAg/yAAIP8gACD/IAAg/yAAIP8gACD/IAAg/yAAIP8gACD/IAAg/yAAIP8gACD/IAAg/yAAIP8gACD/IAAg/yAAIP8gACD/IAAg/yAAIP8gACD/IACQBQ==\",\"b\":\"AA==\"}}}" + }, + "ui": { + "kind": "Hidden" + }, + "spatial": false, + "hold_only": true, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + } + ] + } + ] + }, + { + "node": "Group", + "uid": 15, + "match": "transform", + "name": "Transform", + "kind": { + "kind": "Plain" + }, + "enabled": true, + "children": [ + { + "node": "Prop", + "uid": 7, + "match": "anchor", + "name": "Anchor Point", + "value": { + "t": "Vec3", + "v": [ + 32.0, + 24.0, + 0.0 + ] + }, + "ui": { + "kind": "Point" + }, + "spatial": true, + "hold_only": false, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 2, + "static_only": false + }, + { + "node": "Prop", + "uid": 8, + "match": "position", + "name": "Position", + "value": { + "t": "Vec3", + "v": [ + 32.0, + 24.0, + 0.0 + ] + }, + "ui": { + "kind": "Point" + }, + "spatial": true, + "hold_only": false, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 2, + "static_only": false + }, + { + "node": "Prop", + "uid": 9, + "match": "scale", + "name": "Scale", + "value": { + "t": "Vec3", + "v": [ + 100.0, + 100.0, + 100.0 + ] + }, + "ui": { + "kind": "Percent" + }, + "spatial": false, + "hold_only": false, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 2, + "static_only": false + }, + { + "node": "Prop", + "uid": 10, + "match": "orientation", + "name": "Orientation", + "value": { + "t": "Vec3", + "v": [ + 0.0, + 0.0, + 0.0 + ] + }, + "ui": { + "kind": "Angle" + }, + "spatial": false, + "hold_only": false, + "three_d_only": true, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + { + "node": "Prop", + "uid": 11, + "match": "rotationX", + "name": "X Rotation", + "value": { + "t": "Scalar", + "v": 0.0 + }, + "ui": { + "kind": "Angle" + }, + "spatial": false, + "hold_only": false, + "three_d_only": true, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + { + "node": "Prop", + "uid": 12, + "match": "rotationY", + "name": "Y Rotation", + "value": { + "t": "Scalar", + "v": 0.0 + }, + "ui": { + "kind": "Angle" + }, + "spatial": false, + "hold_only": false, + "three_d_only": true, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + { + "node": "Prop", + "uid": 13, + "match": "rotation", + "name": "Rotation", + "value": { + "t": "Scalar", + "v": 0.0 + }, + "ui": { + "kind": "Angle" + }, + "spatial": false, + "hold_only": false, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + { + "node": "Prop", + "uid": 14, + "match": "opacity", + "name": "Opacity", + "value": { + "t": "Scalar", + "v": 100.0 + }, + "ui": { + "kind": "Slider", + "min": 0.0, + "max": 100.0, + "slider_min": 0.0, + "slider_max": 100.0, + "decimals": 0 + }, + "spatial": false, + "hold_only": false, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + } + ] + }, + { + "node": "Group", + "uid": 16, + "match": "materialOptions", + "name": "Material Options", + "kind": { + "kind": "Plain" + }, + "enabled": true, + "children": [ + { + "node": "Prop", + "uid": 17, + "match": "castsShadows", + "name": "Casts Shadows", + "value": { + "t": "Enum", + "v": 0 + }, + "ui": { + "kind": "Popup", + "options": [ + "Off", + "On", + "Only" + ] + }, + "spatial": false, + "hold_only": true, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + { + "node": "Prop", + "uid": 18, + "match": "lightTransmission", + "name": "Light Transmission", + "value": { + "t": "Scalar", + "v": 0.0 + }, + "ui": { + "kind": "Percent" + }, + "spatial": false, + "hold_only": false, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + { + "node": "Prop", + "uid": 19, + "match": "acceptsShadows", + "name": "Accepts Shadows", + "value": { + "t": "Enum", + "v": 1 + }, + "ui": { + "kind": "Popup", + "options": [ + "Off", + "On", + "Only" + ] + }, + "spatial": false, + "hold_only": true, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + { + "node": "Prop", + "uid": 20, + "match": "acceptsLights", + "name": "Accepts Lights", + "value": { + "t": "Bool", + "v": true + }, + "ui": { + "kind": "Checkbox" + }, + "spatial": false, + "hold_only": true, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + { + "node": "Prop", + "uid": 21, + "match": "ambient", + "name": "Ambient", + "value": { + "t": "Scalar", + "v": 100.0 + }, + "ui": { + "kind": "Percent" + }, + "spatial": false, + "hold_only": false, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + { + "node": "Prop", + "uid": 22, + "match": "diffuse", + "name": "Diffuse", + "value": { + "t": "Scalar", + "v": 50.0 + }, + "ui": { + "kind": "Percent" + }, + "spatial": false, + "hold_only": false, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + { + "node": "Prop", + "uid": 23, + "match": "specularIntensity", + "name": "Specular Intensity", + "value": { + "t": "Scalar", + "v": 50.0 + }, + "ui": { + "kind": "Percent" + }, + "spatial": false, + "hold_only": false, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + { + "node": "Prop", + "uid": 24, + "match": "specularShininess", + "name": "Specular Shininess", + "value": { + "t": "Scalar", + "v": 5.0 + }, + "ui": { + "kind": "Percent" + }, + "spatial": false, + "hold_only": false, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + { + "node": "Prop", + "uid": 25, + "match": "metal", + "name": "Metal", + "value": { + "t": "Scalar", + "v": 100.0 + }, + "ui": { + "kind": "Percent" + }, + "spatial": false, + "hold_only": false, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + } + ] + } + ] + } + } + ], + "markers": [], + "shutter_angle": 180.0, + "shutter_phase": -90.0, + "motion_blur_samples": 16, + "motion_blur_adaptive_limit": 128, + "renderer": "Classic3D", + "hide_shy": false, + "enable_motion_blur": true, + "enable_frame_blending": true, + "draft_3d": false, + "preserve_frame_rate": false, + "preserve_resolution": false, + "poster_time": 0, + "global_light": { + "angle": { + "uid": 0, + "match": "globalLightAngle", + "name": "Global Light Angle", + "value": { + "t": "Scalar", + "v": 120.0 + }, + "ui": { + "kind": "Angle" + }, + "spatial": false, + "hold_only": false, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + }, + "altitude": { + "uid": 0, + "match": "globalLightAltitude", + "name": "Global Light Altitude", + "value": { + "t": "Scalar", + "v": 30.0 + }, + "ui": { + "kind": "Angle" + }, + "spatial": false, + "hold_only": false, + "three_d_only": false, + "two_d_only": false, + "shown_dims": 0, + "static_only": false + } + } + } + }, + "2": { + "id": 2, + "name": "effectcraft-3430-subject.mp4", + "label": "Aqua", + "comment": "", + "parent": null, + "kind": { + "type": "Footage", + "path": "/tmp/effectcraft-3430-subject.mp4", + "kind": "Video", + "width": 64, + "height": 48, + "pixel_aspect": 1.0, + "frame_rate": { + "num": 30, + "den": 1 + }, + "native_rate": null, + "duration": 25401600000, + "has_video": true, + "has_audio": false, + "alpha": "Ignore", + "premul_color": [ + 0.0, + 0.0, + 0.0 + ], + "loop_count": 1, + "codec": "H.264 (High)", + "missing": false, + "color_profile": "Rec709" + } + } + }, + "next_id": 44 +} \ No newline at end of file diff --git a/crates/engine/src/tests/fixtures/object-matte-0.22.ecproj.attribution b/crates/engine/src/tests/fixtures/object-matte-0.22.ecproj.attribution new file mode 100644 index 000000000..c20c8b02c --- /dev/null +++ b/crates/engine/src/tests/fixtures/object-matte-0.22.ecproj.attribution @@ -0,0 +1,8 @@ +asset: object-matte-0.22.ecproj +title: One frozen Object Matte from EffectCraft 0.22.0 +author: lisn0 +source: original work +license: MIT OR Apache-2.0 +license-file: ../../../../../LICENSE-MIT +added: 2026-11-11 by lisn0 +notes: Saved by the released 0.22.0 CLI using a generated 64x48 orange rectangle on a blue background. Contains one frozen alpha plane. The source path is a generic test path, and the source video is unnecessary for the state-preservation regression test. diff --git a/crates/engine/src/tests/roto.rs b/crates/engine/src/tests/roto.rs index 99c155265..e751681c6 100644 --- a/crates/engine/src/tests/roto.rs +++ b/crates/engine/src/tests/roto.rs @@ -603,6 +603,35 @@ fn object_matte_background_job_cancel_and_freeze() { assert_eq!(status(&mut s)["frozen"], true); } +/// A queue edit preserves a frozen matte made before the footage signature gained placed_page. +#[test] +fn render_queue_keeps_frozen_object_matte_with_legacy_footage_key() { + use effectcraft_effects::roto::{FROZEN, INPUT_KEY}; + use effectcraft_keyframe::Value::Str; + + hold_roto_cache_test_lock(); + // Saved by the released 0.22.0 CLI, independently of the compatibility helper. + let project = effectcraft_project::Project::from_json(include_str!("fixtures/object-matte-0.22.ecproj")).unwrap(); + let mut s = Session::default(); + s.replace_project(project, None); + let cid = effectcraft_project::ItemId(1); + let clip = LayerId(3); + let uid = 26; + s.open_comp(cid); + let params = |s: &Session| crate::roto::params_static(s.project.comp(cid).unwrap().layer(clip).unwrap().props.find_group(uid).unwrap()); + let legacy = params(&s).get(INPUT_KEY).unwrap().clone(); + let saved = params(&s).get(FROZEN).unwrap().clone(); + let layer = s.project.comp(cid).unwrap().layer(clip).unwrap(); + assert_ne!(legacy, Str(crate::roto::input_key(&s.project, cid, layer, uid))); + + s.execute_checked("renderQueue.add", json!({"comp": cid.0, "format": "png", "output": "matte.png"})).unwrap(); + assert_eq!(params(&s).get(INPUT_KEY), Some(&legacy)); + assert_eq!(params(&s).get(FROZEN), Some(&saved)); + + s.execute_checked("layer.timing", json!({"layer": clip.0, "out": 0.05})).unwrap(); + assert_eq!(params(&s).get(FROZEN), Some(&Str(String::new()))); +} + /// Computed Object Matte segmentations go to the persistent disk cache: a second session on the /// same folder and project gets them back without segmenting (its frames cannot even be read /// here), and another prompt (other keys) finds nothing. diff --git a/crates/engine/src/tests/settings.rs b/crates/engine/src/tests/settings.rs index 0f8d6ffa2..42bcefc97 100644 --- a/crates/engine/src/tests/settings.rs +++ b/crates/engine/src/tests/settings.rs @@ -788,6 +788,26 @@ fn context_shortcuts_on_the_same_keys_conflict() { assert!(t.conflicts(b, "F9").iter().any(|c| c.key == "test.paint" && !c.overrides)); } +/// A preset repeating one binding many times lists it once, and conflicts stay bounded. +#[test] +fn repeated_bindings_are_one_binding() { + let mut s = Session::default(); + s.set_ui_commands(vec![ + crate::shortcuts::UiCommand { id: "test.a".into(), label: "A".into(), shortcut: Some("F9".into()), scope: Some("Timeline".into()) }, + crate::shortcuts::UiCommand { id: "test.b".into(), label: "B".into(), shortcut: Some("F9".into()), scope: Some("Timeline".into()) }, + ]); + let mut t = s.shortcuts().clone(); + t.keys.insert("test.a".into(), vec!["F9".into(); 10_000]); + assert_eq!(t.bindings().iter().filter(|(_, b)| b.key == "test.a").count(), 1); + assert_eq!(t.all_conflicts().iter().filter(|(a, c)| a == "test.a" && c.key == "test.b").count(), 1); + // Many commands on one key: the listing stops at its cap. + for i in 0..200 { + t.bindables.push(crate::shortcuts::Bindable { key: format!("test.many{i}"), ..t.find("test.b").unwrap().clone() }); + t.keys.insert(format!("test.many{i}"), vec!["F9".into()]); + } + assert_eq!(t.all_conflicts().len(), crate::shortcuts::MAX_CONFLICTS); +} + // ---------------------------------------------------------------- auto-save / recovery /// The names in `d`, sorted. diff --git a/crates/engine/src/tests/timeline.rs b/crates/engine/src/tests/timeline.rs index 55475eef3..b5a44239b 100644 --- a/crates/engine/src/tests/timeline.rs +++ b/crates/engine/src/tests/timeline.rs @@ -649,6 +649,31 @@ fn time_set_keeps_the_frame_shown_in_the_current_time_field() { assert_eq!(r["frame"], 13); } +/// A comp that starts before 0 shows its first frames as negative timecode (`-0:00:00:12`). +/// Typing back what the current-time display shows (Go to Time opens with it) stays on that frame; +/// the leading `-` used to move back from the current frame instead. A bare `-n` still steps back, +/// and in a comp that starts at 0 a `-` stays relative. +#[test] +fn time_set_round_trips_negative_displayed_timecode() { + let mut s = Session::default(); + s.execute("comp.new", json!({"name": "Pre-roll", "width": 64, "height": 64, "frameRate": 24, "duration": 4, "startTime": -1})).unwrap(); + for frame in [0_i64, 1, 12, 23, 24, 25, 60] { + let r = s.execute("time.set", json!({"frame": frame})).unwrap(); + let display = r["display"].as_str().unwrap().to_string(); + let r = s.execute("time.set", json!({"timecode": display})).unwrap(); + assert_eq!(r["frame"], frame, "displayed {display}"); + } + s.execute("time.set", json!({"frame": 12})).unwrap(); + assert_eq!(s.execute("time.set", json!({"timecode": "-0:00:00:12"})).unwrap()["display"], "-0:00:00:12"); + assert_eq!(s.execute("time.set", json!({"timecode": "-5"})).unwrap()["frame"], 7, "a bare -n steps back"); + assert_eq!(s.execute("time.set", json!({"timecode": "-0:00:00:20"})).unwrap()["frame"], 4); + assert!(s.execute("time.set", json!({"timecode": "- -1:00"})).is_err(), "a second sign is still an error"); + + s.execute("comp.settings", json!({"startTime": 0})).unwrap(); + s.execute("time.set", json!({"frame": 12})).unwrap(); + assert_eq!(s.execute("time.set", json!({"timecode": "-0:00:00:02"})).unwrap()["frame"], 10, "relative when the comp starts at 0"); +} + /// Frames display style: typed digits are a frame count, so 00100 is frame 100, not 1 s (#530). #[test] fn time_set_reads_frames_style_digits_as_a_frame_count() { diff --git a/crates/engine/src/tests/vector_import.rs b/crates/engine/src/tests/vector_import.rs index 10c07c2ad..2f41b16b2 100644 --- a/crates/engine/src/tests/vector_import.rs +++ b/crates/engine/src/tests/vector_import.rs @@ -383,3 +383,58 @@ fn psd_with_damaged_layer_section_imports_the_flattened_image() { let ItemKind::Footage(f) = &s.project.item(item).unwrap().kind else { panic!() }; assert!(f.layer.is_none(), "the merged image, not a Photoshop layer"); } + +/// A vector footage thumbnail is drawn at the thumbnail's size, never at its declared size. +#[test] +fn vector_footage_thumbnails_are_drawn_at_thumbnail_size() { + use std::sync::{Arc, Mutex}; + + use effectcraft_project::{Footage, ItemId}; + use effectcraft_raster::Image; + use effectcraft_time::Tick; + + struct Vector(Mutex>); + impl crate::render::FootageSource for Vector { + fn frame(&self, _: ItemId, _: &Footage, _: Tick) -> Option> { + panic!("the full-size frame was decoded"); + } + fn vector_frame(&self, _: ItemId, f: &Footage, scale: f64) -> Option> { + self.0.lock().unwrap().push(scale); + Some(Arc::new(Image::new((f.width as f64 * scale).ceil() as u32, (f.height as f64 * scale).ceil() as u32))) + } + } + let source = Arc::new(Vector(Mutex::new(vec![]))); + let mut s = Session { footage: source.clone(), ..Default::default() }; + let f = Footage { path: "/m/huge.svg".into(), codec: "SVG".into(), width: 16_000, height: 8_000, has_video: true, ..Default::default() }; + let item = std::sync::Arc::make_mut(&mut s.project).add_item("huge.svg", effectcraft_color::Label::Sandstone, None, ItemKind::Footage(f)); + let (w, h, _) = (s.thumbnail_job(item, Tick::ZERO, 64).unwrap())().unwrap(); + assert_eq!((w, h), (64, 32)); + assert_eq!(*source.0.lock().unwrap(), [64.0 / 16_000.0]); +} + +/// A vector footage thumbnail whose drawn size is a pixel over the requested one is shrunk by +/// that pixel, not halved by an integer step. +#[test] +fn a_vector_thumbnail_one_pixel_over_is_not_halved() { + use std::sync::Arc; + + use effectcraft_project::{Footage, ItemId}; + use effectcraft_raster::Image; + use effectcraft_time::Tick; + + struct Over; + impl crate::render::FootageSource for Over { + fn frame(&self, _: ItemId, _: &Footage, _: Tick) -> Option> { + None + } + fn vector_frame(&self, _: ItemId, _: &Footage, _: f64) -> Option> { + Some(Arc::new(Image::new(64, 64))) + } + } + let mut s = Session { footage: Arc::new(Over), ..Default::default() }; + let f = Footage { path: "/m/sq.svg".into(), codec: "SVG".into(), width: 121, height: 121, has_video: true, ..Default::default() }; + let item = std::sync::Arc::make_mut(&mut s.project).add_item("sq.svg", effectcraft_color::Label::Sandstone, None, ItemKind::Footage(f)); + let (w, h, px) = (s.thumbnail_job(item, Tick::ZERO, 63).unwrap())().unwrap(); + assert_eq!((w, h), (63, 63)); + assert_eq!(px.len(), 63 * 63 * 4); +} diff --git a/crates/export/Cargo.toml b/crates/export/Cargo.toml index c477d2787..164b10a7f 100644 --- a/crates/export/Cargo.toml +++ b/crates/export/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "effectcraft-export" -description = "Render Queue jobs and output modules: H.264 MP4 (+AAC), ProRes and uncompressed MOV (+PCM) via FilmCraft, HEVC / AV1 MP4 (+AAC) and WebM (VP9 or AV1 + Opus) via EffectCraft encoders, PNG/JPEG/TIFF/EXR/Targa sequences, animated GIF, WAV/AIFF" +description = "Render Queue jobs and output modules: H.264 MP4 (+AAC), ProRes and uncompressed MOV (+PCM) , HEVC / AV1 MP4 (+AAC) and WebM (VP9 or AV1) via craft-libs codecs, Opus via effectcraft-opusenc, PNG/JPEG/TIFF/EXR/Targa sequences, animated GIF, WAV/AIFF" version.workspace = true edition.workspace = true license.workspace = true @@ -14,14 +14,10 @@ effectcraft-time = { workspace = true } effectcraft-project = { workspace = true } effectcraft-raster = { workspace = true } effectcraft-render = { workspace = true } -filmcraft-h264enc = { workspace = true } -filmcraft-prores = { workspace = true } -filmcraft-aac = { workspace = true } -filmcraft-isobmff = { workspace = true } +craft-video = { workspace = true } +craft-audio = { workspace = true } +craft-media = { workspace = true } effectcraft-opusenc = { workspace = true } -effectcraft-vp9enc = { workspace = true } -effectcraft-hevcenc = { workspace = true } -effectcraft-av1enc = { workspace = true } image = { workspace = true, features = ["png", "jpeg", "tiff", "exr", "tga"] } exr = { workspace = true } gif = { workspace = true } @@ -31,9 +27,11 @@ thiserror = { workspace = true } log = { workspace = true } [dev-dependencies] +tempfile = { workspace = true } effectcraft-media = { workspace = true } effectcraft-effects = { workspace = true } effectcraft-color = { workspace = true } -filmcraft-opus = { workspace = true } +filmcraft-hevc = { workspace = true } +filmcraft-av1 = { workspace = true } filmcraft-vp9 = { workspace = true } effectcraft-keyframe = { workspace = true } diff --git a/crates/export/src/encode.rs b/crates/export/src/encode.rs index dd972d326..8caad8dea 100644 --- a/crates/export/src/encode.rs +++ b/crates/export/src/encode.rs @@ -1,13 +1,13 @@ -//! Movie export: FilmCraft H.264, HEVC (`effectcraft-hevcenc`) and AV1 (`effectcraft-av1enc`) +//! Movie export: H.264 (`craft_video::h264`), HEVC (`craft_video::hevc`) and AV1 (`craft_video::av1`) //! → MP4 (+ AAC), ProRes and uncompressed `raw ` → MOV (+ PCM), muxed by -//! FilmCraft's ISO BMFF / QuickTime writer. +//! craft-media's ISO BMFF / QuickTime writer. use std::io::Write; +use craft_media::isobmff::{Brand, ColorInfo, FourCc, Mp4Writer, PcmConfig, SampleEntry, TrackConfig, WriteSample, WriterOptions}; use effectcraft_project::render_queue::{AudioFormat, Channels, OutputFormat, ProResProfile}; use effectcraft_project::{Comp, ProjectSettings}; use effectcraft_time::{FrameRate, Tick}; -use filmcraft_isobmff::{Brand, ColorInfo, FourCc, Mp4Writer, PcmConfig, SampleEntry, TrackConfig, WriteSample, WriterOptions}; use rayon::prelude::*; use crate::{Cx, ExportError, Report, Result, State, batch_size, wants_audio}; @@ -117,7 +117,7 @@ pub(crate) trait VideoEncoder: Send { // ---------------------------------------------------------------- H.264 struct H264 { - enc: filmcraft_h264enc::Encoder, + enc: craft_video::h264::Encoder, w: u32, h: u32, rate: FrameRate, @@ -129,26 +129,26 @@ struct H264 { impl H264 { fn new(w: u32, h: u32, rate: FrameRate, kbps: u32, codes: Codes) -> Result { - let mut cfg = filmcraft_h264enc::EncoderConfig::new(w, h, rate.num as u32, rate.den as u32); - cfg.format = filmcraft_h264enc::PacketFormat::LengthPrefixed; + let mut cfg = craft_video::h264::EncoderConfig::new(w, h, rate.num as u32, rate.den as u32); + cfg.format = craft_video::h264::PacketFormat::LengthPrefixed; cfg.aud = false; cfg.keyint = (rate.as_f64() * 2.0).round().max(1.0) as u32; let kbps = kbps.max(100); - cfg.rate = filmcraft_h264enc::RateControl::Vbr { target_kbps: kbps, max_kbps: kbps * 3 / 2 }; - cfg.color = filmcraft_h264enc::ColorConfig { primaries: codes.primaries, transfer: codes.transfer, matrix: codes.matrix, full_range: false }; - let enc = filmcraft_h264enc::Encoder::new(cfg).map_err(enc)?; + cfg.rate = craft_video::h264::RateControl::Vbr { target_kbps: kbps, max_kbps: kbps * 3 / 2 }; + cfg.color = craft_video::h264::ColorConfig { primaries: codes.primaries, transfer: codes.transfer, matrix: codes.matrix, full_range: false }; + let enc = craft_video::h264::Encoder::new(cfg).map_err(enc)?; Ok(H264 { enc, w, h, rate, codes, y: vec![], u: vec![], v: vec![] }) } - fn packets(&self, ps: Vec) -> Vec { + fn packets(&self, ps: Vec) -> Vec { ps.into_iter().map(|p| Packet { data: p.data, key: p.keyframe, cto: (p.pts - p.dts) as i32 }).collect() } } impl VideoEncoder for H264 { fn sample_entry(&self) -> SampleEntry { - let cfg = filmcraft_isobmff::AvcConfig::parse(&self.enc.avcc()).unwrap_or_else(|_| { + let cfg = craft_media::isobmff::AvcConfig::parse(&self.enc.avcc()).unwrap_or_else(|_| { let (sps, pps) = self.enc.sps_pps(); - filmcraft_isobmff::AvcConfig::new(vec![sps], vec![pps], 4) + craft_media::isobmff::AvcConfig::new(vec![sps], vec![pps], 4) }); let mut entry = SampleEntry::avc(cfg, self.w as u16, self.h as u16); tag_nclx_limited(&mut entry, self.codes); @@ -170,7 +170,7 @@ impl VideoEncoder for H264 { fn encode_yuv420(&mut self, p: &Yuv420, index: u64) -> Result> { let (w, cw) = (self.w as usize, (self.w as usize).div_ceil(2)); let (y, u, v) = p.slices(w, self.h as usize); - let frame = filmcraft_h264enc::YuvFrame { y, u, v, y_stride: w, uv_stride: cw }; + let frame = craft_video::h264::YuvFrame { y, u, v, y_stride: w, uv_stride: cw }; let ps = self.enc.try_encode(&frame, index as i64 * self.rate.den).map_err(enc)?; Ok(self.packets(ps)) } @@ -188,8 +188,8 @@ pub(crate) use effectcraft_raster::yuv::rgba_to_yuv420; // ---------------------------------------------------------------- ProRes struct ProRes { - enc: filmcraft_prores::Encoder, - profile: filmcraft_prores::Profile, + enc: craft_video::prores::Encoder, + profile: craft_video::prores::Profile, w: u32, h: u32, alpha: bool, @@ -202,8 +202,8 @@ fn prores_colr(c: Codes) -> ColorInfo { ColorInfo::Nclc { primaries: c.primaries.into(), transfer: c.transfer.into(), matrix: c.matrix.into() } } -fn prores_profile(p: ProResProfile) -> filmcraft_prores::Profile { - use filmcraft_prores::Profile; +fn prores_profile(p: ProResProfile) -> craft_video::prores::Profile { + use craft_video::prores::Profile; match p { ProResProfile::Proxy => Profile::Proxy, ProResProfile::Lt => Profile::Lt, @@ -220,18 +220,18 @@ impl ProRes { let profile = if channels == Channels::Rgba && !profile.is_4444() { ProResProfile::P4444 } else { profile }; let profile = prores_profile(profile); let alpha = channels == Channels::Rgba; - let mut cfg = filmcraft_prores::EncoderConfig::new(profile, w, h); + let mut cfg = craft_video::prores::EncoderConfig::new(profile, w, h); cfg.encode_alpha = alpha; // The frame header carries the same H.273 codes as the `colr` atom, so readers that trust // either one agree. if let ColorInfo::Nclc { primaries, transfer, matrix } = colr { let code = |c| u8::try_from(c).unwrap_or(2); - cfg.color = filmcraft_prores::ColorInfo { primaries: code(primaries), transfer: code(transfer), matrix: code(matrix) }; + cfg.color = craft_video::prores::ColorInfo { primaries: code(primaries), transfer: code(transfer), matrix: code(matrix) }; } - ProRes { enc: filmcraft_prores::Encoder::with_config(cfg), profile, w, h, alpha, colr } + ProRes { enc: craft_video::prores::Encoder::with_config(cfg), profile, w, h, alpha, colr } } fn is_444(&self) -> bool { - matches!(self.profile, filmcraft_prores::Profile::P4444 | filmcraft_prores::Profile::P4444Xq) + matches!(self.profile, craft_video::prores::Profile::P4444 | craft_video::prores::Profile::P4444Xq) } } @@ -244,8 +244,8 @@ impl VideoEncoder for ProRes { entry } fn encode(&mut self, rgba: &[u8], _index: u64) -> Result> { - let chroma = if self.is_444() { filmcraft_prores::ChromaFormat::Yuv444 } else { filmcraft_prores::ChromaFormat::Yuv422 }; - let mut fr = filmcraft_prores::Frame::new(self.w, self.h, chroma, 10, self.alpha); + let chroma = if self.is_444() { craft_video::prores::ChromaFormat::Yuv444 } else { craft_video::prores::ChromaFormat::Yuv422 }; + let mut fr = craft_video::prores::Frame::new(self.w, self.h, chroma, 10, self.alpha).map_err(enc)?; rgba_to_yuv_10(rgba, self.w as usize, self.h as usize, self.is_444(), &mut fr.y, &mut fr.cb, &mut fr.cr); if let Some(a) = fr.alpha.as_mut() { for (d, s) in a.iter_mut().zip(rgba.as_chunks::<4>().0.iter()) { @@ -291,7 +291,7 @@ fn rgba_to_yuv_10(rgba: &[u8], w: usize, h: usize, full_chroma: bool, y: &mut [u // ---------------------------------------------------------------- audio enum Audio { - Aac(Box), + Aac(Box), Pcm, } @@ -401,7 +401,7 @@ pub(crate) async fn movie(job: &Cx<'_>, comp: &Comp, w: u32, h: u32, st: &mut St // from the media duration and drop the last frame. let frames_ts = st.total as i128 * rate.den as i128; let seg = (frames_ts * movie_ts + rate.num as i128 / 2) / rate.num as i128; - vcfg.edits = vec![filmcraft_isobmff::Edit { segment_duration: seg as u64, media_time: start, media_rate: 0x10000 }]; + vcfg.edits = vec![craft_media::isobmff::Edit { segment_duration: seg as u64, media_time: start, media_rate: 0x10000 }]; } let vt = mux.add_track(vcfg).map_err(mux_err)?; let sr = job.output.audio_sample_rate.clamp(8_000, 192_000); @@ -411,7 +411,7 @@ pub(crate) async fn movie(job: &Cx<'_>, comp: &Comp, w: u32, h: u32, st: &mut St let mut audio = None; if with_audio { let (a, entry, start) = if brand == Brand::Mp4 { - let e = filmcraft_aac::Encoder::new(filmcraft_aac::EncoderConfig::cbr(sr, chans as usize, 160_000 * chans)).map_err(enc)?; + let e = craft_audio::aac::Encoder::new(craft_audio::aac::EncoderConfig::cbr(sr, chans as usize, 160_000 * chans)).map_err(enc)?; let entry = SampleEntry::aac(e.audio_specific_config(), chans, sr); let priming = e.priming_samples() as i64; (Audio::Aac(Box::new(e)), entry, Some(priming)) diff --git a/crates/export/src/hevc_av1.rs b/crates/export/src/hevc_av1.rs index d43b1c5ed..539b6e383 100644 --- a/crates/export/src/hevc_av1.rs +++ b/crates/export/src/hevc_av1.rs @@ -1,10 +1,10 @@ -//! HEVC (`effectcraft-hevcenc`) and AV1 (`effectcraft-av1enc`) video encoders for the MP4 +//! HEVC (`craft_video::hevc`) and AV1 (`craft_video::av1`) video encoders for the MP4 //! movie writer (`hvc1` / `av01` sample entries) and AV1 in WebM, configured from the Output //! Module's [`VideoCodecOptions`]. +use craft_media::isobmff::{Av1Config, CodecConfig, FourCc, HevcConfig, SampleEntry}; use effectcraft_project::render_queue::{CodecProfile, OutputModule, RateControlMode, VideoCodecOptions}; use effectcraft_time::FrameRate; -use filmcraft_isobmff::{Av1Config, CodecConfig, FourCc, HevcConfig, SampleEntry}; use rayon::prelude::*; use crate::encode::{Codes, Packet, VideoEncoder}; @@ -55,7 +55,7 @@ struct Planes { // ---------------------------------------------------------------- HEVC pub(crate) struct Hevc { - enc: effectcraft_hevcenc::Encoder, + enc: craft_video::hevc::Encoder, w: u32, h: u32, bits: u8, @@ -64,8 +64,8 @@ pub(crate) struct Hevc { } /// The HEVC encoder settings for an output module. -pub(crate) fn hevc_config(w: u32, h: u32, rate: FrameRate, om: &OutputModule) -> effectcraft_hevcenc::EncoderConfig { - use effectcraft_hevcenc as he; +pub(crate) fn hevc_config(w: u32, h: u32, rate: FrameRate, om: &OutputModule) -> craft_video::hevc::EncoderConfig { + use craft_video::hevc as he; let o: &VideoCodecOptions = &om.codec; let mut cfg = he::EncoderConfig::new(w, h, rate.num as u32, rate.den as u32); cfg.profile = match o.profile { @@ -86,7 +86,7 @@ impl Hevc { let mut cfg = hevc_config(w, h, rate, om); (cfg.colour_primaries, cfg.transfer_characteristics, cfg.matrix_coeffs) = (codes.primaries, codes.transfer, codes.matrix); let bits = cfg.profile.bit_depth(); - let enc = effectcraft_hevcenc::Encoder::new(cfg).map_err(enc_err)?; + let enc = craft_video::hevc::Encoder::new(cfg).map_err(enc_err)?; Ok(Hevc { enc, w, h, bits, codes, planes: Planes::default() }) } } @@ -102,8 +102,8 @@ impl VideoEncoder for Hevc { let (w, h) = (self.w as usize, self.h as usize); let p = &mut self.planes; rgba_to_yuv420_bits(rgba, w, h, self.bits, &mut p.y, &mut p.u, &mut p.v); - let frame = effectcraft_hevcenc::Frame { y: &p.y, u: &p.u, v: &p.v, y_stride: w, uv_stride: w.div_ceil(2) }; - let pk = self.enc.encode(&frame); + let frame = craft_video::hevc::Frame { y: &p.y, u: &p.u, v: &p.v, y_stride: w, uv_stride: w.div_ceil(2) }; + let pk = self.enc.encode(&frame).map_err(enc_err)?; Ok(vec![Packet { data: pk.data, key: pk.keyframe, cto: 0 }]) } fn flush(&mut self) -> Result> { @@ -114,7 +114,7 @@ impl VideoEncoder for Hevc { // ---------------------------------------------------------------- AV1 pub(crate) struct Av1 { - enc: effectcraft_av1enc::Encoder, + enc: craft_video::av1::Encoder, w: u32, h: u32, bits: u8, @@ -123,8 +123,8 @@ pub(crate) struct Av1 { } /// The AV1 encoder settings for an output module. -pub(crate) fn av1_config(w: u32, h: u32, rate: FrameRate, om: &OutputModule) -> effectcraft_av1enc::EncoderConfig { - use effectcraft_av1enc as ae; +pub(crate) fn av1_config(w: u32, h: u32, rate: FrameRate, om: &OutputModule) -> craft_video::av1::EncoderConfig { + use craft_video::av1 as ae; let o: &VideoCodecOptions = &om.codec; let mut cfg = ae::EncoderConfig::new(w, h, rate.num as u32, rate.den as u32); cfg.bit_depth = o.profile.bit_depth(); @@ -142,7 +142,7 @@ impl Av1 { let mut cfg = av1_config(w, h, rate, om); (cfg.color_primaries, cfg.transfer_characteristics, cfg.matrix_coefficients) = (codes.primaries, codes.transfer, codes.matrix); let bits = cfg.bit_depth; - let enc = effectcraft_av1enc::Encoder::new(cfg).map_err(enc_err)?; + let enc = craft_video::av1::Encoder::new(cfg).map_err(enc_err)?; Ok(Av1 { enc, w, h, bits, codes, planes: Planes::default() }) } /// The `av1C` record (also the WebM CodecPrivate). @@ -162,8 +162,8 @@ impl VideoEncoder for Av1 { let (w, h) = (self.w as usize, self.h as usize); let p = &mut self.planes; rgba_to_yuv420_bits(rgba, w, h, self.bits, &mut p.y, &mut p.u, &mut p.v); - let frame = effectcraft_av1enc::Frame { y: &p.y, u: &p.u, v: &p.v, y_stride: w, uv_stride: w.div_ceil(2) }; - let pk = self.enc.encode(&frame); + let frame = craft_video::av1::Frame { y: &p.y, u: &p.u, v: &p.v, y_stride: w, uv_stride: w.div_ceil(2) }; + let pk = self.enc.encode(&frame).map_err(enc_err)?; Ok(vec![Packet { data: pk.data, key: pk.keyframe, cto: 0 }]) } fn flush(&mut self) -> Result> { @@ -196,9 +196,9 @@ mod tests { om.keyframe_interval = 12; let rate = FrameRate::new(30, 1); let h = hevc_config(64, 32, rate, &om); - assert_eq!(h.profile, effectcraft_hevcenc::Profile::Main10); + assert_eq!(h.profile, craft_video::hevc::Profile::Main10); assert_eq!(h.level_idc, Some(123)); - assert_eq!(h.rate, effectcraft_hevcenc::RateControl::ConstantQp(4)); + assert_eq!(h.rate, craft_video::hevc::RateControl::ConstantQp(4)); assert_eq!(h.keyint, 12); let a = av1_config(64, 32, rate, &om); assert_eq!((a.bit_depth, a.level_idx, a.keyint), (10, Some(9), 12)); @@ -206,6 +206,6 @@ mod tests { om.keyframe_interval = 0; om.bitrate_kbps = 3000; let a = av1_config(64, 32, rate, &om); - assert_eq!((a.rate, a.keyint), (effectcraft_av1enc::RateControl::Bitrate { kbps: 3000 }, 60)); + assert_eq!((a.rate, a.keyint), (craft_video::av1::RateControl::Bitrate { kbps: 3000 }, 60)); } } diff --git a/crates/export/src/lib.rs b/crates/export/src/lib.rs index 21b7501fa..2687eadad 100644 --- a/crates/export/src/lib.rs +++ b/crates/export/src/lib.rs @@ -2,16 +2,16 @@ //! //! | Format | Container / files | Video | Alpha | Audio | //! |---|---|---|---|---| -//! | H.264 | MP4 | FilmCraft H.264 High, 8-bit 4:2:0 BT.709 limited, VBR | no | AAC-LC (FilmCraft), mono/stereo | +//! | H.264 | MP4 | `craft_video::h264` High, 8-bit 4:2:0 BT.709 limited, VBR | no | AAC-LC (`craft_audio::aac`), mono/stereo | //! | ProRes | QuickTime MOV | FilmCraft ProRes 422 Proxy/LT/422/HQ, 10-bit; 4444/4444 XQ 4:4:4 | 4444 (16-bit alpha coding) | PCM 16/24-bit or 32-bit float, mono/stereo | //! | Uncompressed | QuickTime MOV (`raw `) | 8-bit RGB (depth 24, rows padded to even bytes) or ARGB (depth 32) | RGB + Alpha (straight or premultiplied) | PCM 16/24-bit or 32-bit float, mono/stereo | //! | PNG / JPEG / TIFF sequence | one file per frame | 8-bit | PNG, TIFF | — | //! | OpenEXR sequence | one file per frame, scan lines | 32-bit float (default) or half float, linear light, premultiplied; compression None, RLE, ZIPS, ZIP (default), PIZ, PXR24, B44, B44A, DWAA, DWAB | yes | — | //! | Targa sequence | one file per frame | uncompressed 8-bit: 24-bit RGB, 32-bit RGB + Alpha (8 alpha bits), 8-bit grey for Alpha; top-left origin | yes (straight or premultiplied) | — | //! | Animated GIF | GIF89a | 256-colour palette per frame (NeuQuant) | 1-bit | — | -//! | WebM | WebM (Matroska) | VP9 profile 0 (`effectcraft-vp9enc`: key + inter frames, a key frame every 2 s, loop filter), 8-bit 4:2:0; or AV1 (`effectcraft-av1enc`) | VP9 alpha (BlockAdditional) | Opus (`effectcraft-opusenc`: SILK / hybrid / CELT by bitrate and application), 48 kHz | -//! | HEVC | MP4 (`hvc1`) | `effectcraft-hevcenc` Main / Main 10 4:2:0, I + P slices, bitrate or constant quality, level, key-frame interval | no | AAC-LC (FilmCraft) | -//! | AV1 | MP4 (`av01`) | `effectcraft-av1enc` Main 8/10-bit 4:2:0, key + inter frames | no | AAC-LC (FilmCraft) | +//! | WebM | WebM (Matroska) | VP9 profile 0 (`craft_video::vp9`: key + inter frames, a key frame every 2 s, loop filter), 8-bit 4:2:0; or AV1 (`craft_video::av1`) | VP9 alpha (BlockAdditional) | Opus (`effectcraft-opusenc`: SILK / hybrid / CELT by bitrate and application), 48 kHz | +//! | HEVC | MP4 (`hvc1`) | `craft_video::hevc` Main / Main 10 4:2:0, I + P slices, bitrate or constant quality, level, key-frame interval | no | AAC-LC (`craft_audio::aac`) | +//! | AV1 | MP4 (`av01`) | `craft_video::av1` Main 8/10-bit 4:2:0, key + inter frames | no | AAC-LC (`craft_audio::aac`) | //! | WAV / AIFF | RIFF WAVE / AIFF (AIFF-C for float) | — | — | PCM 16/24-bit or 32-bit float, mono/stereo (audio only) | //! //! Every frame goes through the internal `pipeline` module: the Render Settings overrides (Effects, Solo Switches, @@ -122,6 +122,11 @@ pub struct JobOptions<'a> { /// Settings ▸ Memory & CPU ▸ GPU Frames Rendered at Once: 1 to 3, or `None` for Auto (from /// the device memory a frame measured, #3335). `EFFECTCRAFT_GPU_INFLIGHT` overrides it. pub gpu_frames: Option, + /// Reads the bytes of memory the system can hand out now (`None`: unknown), for Auto's CPU + /// frames in flight (#1615): read at most once per job, when a job renders on the CPU after + /// a GPU frame measured a frame's working memory. `None`: CPU frames are capped only by + /// `EFFECTCRAFT_CPU_INFLIGHT`. + pub free_memory: Option<&'a (dyn Fn() -> Option + Sync)>, } /// The layer caches of a Render Queue run (see [`JobOptions::caches`]): one per compositor, diff --git a/crates/export/src/pipeline.rs b/crates/export/src/pipeline.rs index 922b1b4ea..d447e0980 100644 --- a/crates/export/src/pipeline.rs +++ b/crates/export/src/pipeline.rs @@ -137,6 +137,17 @@ pub(crate) fn cpu_inflight(var: Option<&str>) -> Option { } } +/// Auto's CPU frames in flight (#1615): as many frames as half of the `free` bytes the system +/// can hand out hold at `frame` bytes each, 1 to `threads`. `frame` is a GPU frame's measured +/// working memory: both compositors keep 16-byte RGBA pixels (`[f32; 4]`, `Rgba32Float`), and +/// the other half is headroom for the layer caches, decoded footage, the encoder and the +/// estimate being low. `None` (not capped) when either is unknown or `frame` is 0. +pub(crate) fn auto_cpu_frames(frame: Option, free: Option, threads: usize) -> Option { + let (frame, free) = (frame.filter(|&f| f > 0)?, free?); + let n = free / 2 / frame; + Some(usize::try_from(n).unwrap_or(usize::MAX).clamp(1, threads.max(1))) +} + /// The frames-in-flight caps of a batch: `gpu` (#879) for the GPU's memory, `cpu` (#1615) for /// the process's. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -253,6 +264,9 @@ pub(crate) struct Cx<'a> { gpu_frames: Mutex>, /// A frame has rendered on the GPU compositor: its memory can be measured. gpu_rendered: std::sync::atomic::AtomicBool, + /// Auto's CPU frames in flight once settled ([`Cx::auto_cpu_frames`]; `Some(None)`: not + /// capped). + cpu_frames: Mutex>>, /// (output frame, seconds) of every rendered frame (Plus Per Frame Info logs). pub frame_times: Option>>, /// Files written to an overflow folder. @@ -337,6 +351,7 @@ impl<'a> Cx<'a> { choice: Mutex::new(None), gpu_frames: Mutex::new(None), gpu_rendered: Default::default(), + cpu_frames: Mutex::new(None), frame_times, overflowed: Mutex::new(vec![]), written: Mutex::new(vec![]), @@ -555,9 +570,11 @@ impl<'a> Cx<'a> { let timed = |on: crate::JobRenderer, frames: &[u64]| { let warm = render(first, on); // The GPU's frames in flight settle with its warm-up frame (Auto measures it). + // The CPU's frames are held to what the job would render at once on it, from the + // GPU frames just measured (#1615): uncapped, they could exhaust RAM here already. let caps = match on { crate::JobRenderer::Gpu => Inflight { gpu: Some(self.gpu_inflight()), cpu: cpu_cap }, - crate::JobRenderer::Cpu => Inflight { gpu: None, cpu: cpu_cap }, + crate::JobRenderer::Cpu => Inflight { gpu: None, cpu: cpu_cap.or_else(|| self.auto_cpu_frames()) }, }; let t0 = Instant::now(); let gpu_live = || on == crate::JobRenderer::Cpu || self.job.accel.is_some_and(|a| a.available()); @@ -660,7 +677,48 @@ impl<'a> Cx<'a> { /// The GPU and CPU frames-in-flight caps every frame loop shares (#1615). fn inflight(&self) -> Inflight { - Inflight { gpu: self.gpu_cap(), cpu: cpu_inflight(std::env::var(CPU_INFLIGHT_ENV).ok().as_deref()) } + Inflight { gpu: self.gpu_cap(), cpu: self.cpu_cap() } + } + + /// CPU frames in flight (#1615): `EFFECTCRAFT_CPU_INFLIGHT`, else Auto's + /// ([`Cx::auto_cpu_frames`]) while the job renders on the CPU (it chose the CPU's + /// compositor, or the GPU stopped for good). `None`: not capped. + fn cpu_cap(&self) -> Option { + if let Some(n) = cpu_inflight(std::env::var(CPU_INFLIGHT_ENV).ok().as_deref()) { + return Some(n); + } + let on_cpu = self.backend() == crate::JobRenderer::Cpu || self.job.accel.is_some_and(|a| !a.available()); + if on_cpu { self.auto_cpu_frames() } else { None } + } + + /// Auto's CPU frames in flight ([`auto_cpu_frames`]), settled once per job from the working + /// memory a GPU frame measured and the memory the system has free + /// ([`crate::JobOptions::free_memory`]). `None` (not capped, as before #1615) until a GPU + /// frame has rendered, and in a job that never renders one (a Software Rendering project): + /// there is no per-frame estimate then. + fn auto_cpu_frames(&self) -> Option { + if let Some(n) = *self.cpu_frames.lock().unwrap_or_else(std::sync::PoisonError::into_inner) { + return n; + } + if !self.gpu_rendered.load(std::sync::atomic::Ordering::Relaxed) { + return None; + } + // Read outside the lock: the allocator report walks every allocation, and reading the + // system's memory can take a second (PowerShell on Windows). + let frame = self.job.accel.and_then(|a| a.frame_memory()).map(|m| m.frame); + let free = self.job.options.free_memory.and_then(|read| read()); + let n = auto_cpu_frames(frame, free, rayon::current_num_threads()); + let mut slot = self.cpu_frames.lock().unwrap_or_else(std::sync::PoisonError::into_inner); + *slot.get_or_insert_with(|| { + let mib = |b: Option| b.map_or_else(|| "unknown".to_string(), |b| format!("{} MiB", b >> 20)); + let (frame, free) = (mib(frame), mib(free)); + let label = &self.job.options.label; + match n { + Some(n) => log::info!(target: "effectcraft_export", "{label}: {n} CPU frame(s) at once (Auto: {frame} per frame on the GPU, {free} free)"), + None => log::info!(target: "effectcraft_export", "{label}: CPU frames not capped (Auto: {frame} per frame on the GPU, {free} free)"), + } + n + }) } /// Output frame `i`: sampled (fields, pulldown), at the colour depth, cropped and resized. @@ -1285,6 +1343,75 @@ mod tests { assert_eq!(describe_choice(&cx.choice().unwrap()), "Software Rendering (measured on the first frames: GPU 117.0 ms/frame, CPU 68.0 ms/frame)"); } + #[test] + fn auto_cpu_frames_fit_half_the_free_memory() { + // #1615: 16 threads at 1378 MiB a frame (a GTX 1050 Ti user's comp) with 10 GiB free + // render 3 frames at once, not 16. + assert_eq!(auto_cpu_frames(Some(1378 << 20), Some(10 << 30), 16), Some(3)); + assert_eq!(auto_cpu_frames(Some(1 << 28), Some(64 << 30), 16), Some(16), "at most one per thread"); + assert_eq!(auto_cpu_frames(Some(4 << 30), Some(2 << 30), 16), Some(1), "at least one"); + assert_eq!(auto_cpu_frames(Some(1 << 30), Some(0), 8), Some(1)); + assert_eq!(auto_cpu_frames(Some(1 << 20), Some(u64::MAX), 0), Some(1)); + // Unknown: not capped, as before. + assert_eq!(auto_cpu_frames(None, Some(8 << 30), 16), None); + assert_eq!(auto_cpu_frames(Some(1 << 30), None, 16), None); + assert_eq!(auto_cpu_frames(Some(0), Some(8 << 30), 16), None); + } + + #[test] + fn a_job_that_chose_the_cpu_after_measuring_the_gpu_caps_its_frames() { + // #1615: a GPU project that measured the CPU faster rendered one frame per thread at + // once, so a heavy comp ran out of RAM (an exit without an error, 0.23.1 on Windows). + // Its frames now fit half the free memory at the GPU's measured size per frame. + use effectcraft_project::render_queue::{OutputFormat, OutputModule, RenderSettings}; + if std::env::var_os(CPU_INFLIGHT_ENV).is_some() || std::env::var_os(RENDERER_ENV).is_some() { + return; + } + let (settings, output) = (RenderSettings::default(), OutputModule::for_format(OutputFormat::PngSequence)); + let mut p = Project::default(); + p.settings.gpu_acceleration = true; + let comp = Comp::new(16, 8, effectcraft_time::FrameRate::FPS_25, Tick::from_seconds_f64(1.0)); + let cid = p.add_item("Comp", effectcraft_color::Label::Sandstone, None, effectcraft_project::ItemKind::Comp(comp.into())); + let accel = MemAccel(effectcraft_render::FrameMemory { budget: 3 << 30, peak: 2 << 30, frame: 1 << 30, reserve: 0 }); + let reads = AtomicUsize::new(0); + let free = || { + reads.fetch_add(1, Ordering::SeqCst); + Some(6u64 << 30) + }; + let job = Job { + project: &p, + footage: &effectcraft_render::NoFootage, + expr: None, + accel: Some(&accel), + comp: cid, + settings: &settings, + output: &output, + path: "/c/f_[#####].png", + sink: None, + nested_switches: true, + options: crate::JobOptions { free_memory: Some(&free), ..Default::default() }, + }; + let pool = rayon::ThreadPoolBuilder::new().num_threads(16).build().unwrap(); + pool.install(|| { + let cx = Cx::new(&job); + assert_eq!(cx.inflight().cpu, None, "nothing measured yet"); + let c = cx.comp().unwrap(); + cx.frame_on(c, 0, crate::JobRenderer::Gpu); + assert_eq!(cx.inflight().cpu, None, "the GPU renders: its own cap holds"); + assert_eq!(reads.load(Ordering::SeqCst), 0, "the free memory is read only for CPU frames"); + cx.decide(Choice { renderer: crate::JobRenderer::Cpu, gpu_ms: Some(1417.3), cpu_ms: Some(655.8), why: "measured on the first frames" }); + assert_eq!(cx.inflight(), Inflight { gpu: None, cpu: Some(3) }, "3 GiB of 6 GiB free at 1 GiB a frame"); + assert_eq!(cx.inflight().cpu, Some(3)); + assert_eq!(reads.load(Ordering::SeqCst), 1, "settled once per job"); + }); + // Without a free-memory reading the job renders uncapped, as before. + let job = Job { options: Default::default(), ..job }; + let cx = Cx::new(&job); + cx.frame_on(cx.comp().unwrap(), 0, crate::JobRenderer::Gpu); + cx.decide(Choice { renderer: crate::JobRenderer::Cpu, gpu_ms: Some(2.0), cpu_ms: Some(1.0), why: "measured on the first frames" }); + assert_eq!(cx.inflight().cpu, None); + } + #[test] fn cpu_inflight_is_off_unless_a_positive_number() { assert_eq!(cpu_inflight(None), None); diff --git a/crates/export/src/uncompressed.rs b/crates/export/src/uncompressed.rs index 1bd1d9bad..f5cfc33d5 100644 --- a/crates/export/src/uncompressed.rs +++ b/crates/export/src/uncompressed.rs @@ -7,7 +7,7 @@ //! A R G B / R G B order, and each row is padded to an even number of bytes (only depth 24 at odd //! widths needs it). -use filmcraft_isobmff::{CodecConfig, FourCc, SampleEntry, VideoParams}; +use craft_media::isobmff::{CodecConfig, FourCc, SampleEntry, VideoParams}; use crate::encode::{Packet, VideoEncoder}; use crate::{ExportError, Result}; diff --git a/crates/export/src/webm.rs b/crates/export/src/webm.rs index 976eaf6e0..2802ac928 100644 --- a/crates/export/src/webm.rs +++ b/crates/export/src/webm.rs @@ -5,7 +5,7 @@ //! (size patched at the end), Info, Tracks and one Cluster per render batch. Video frames are //! SimpleBlocks; with alpha (Channels: RGB + Alpha) each frame is a BlockGroup whose //! BlockAdditional (BlockAddID 1) carries a second VP9 frame coding the alpha channel as luma, -//! with `AlphaMode` 1 on the track, as WebM defines. VP9 comes from `effectcraft-vp9enc` (key +//! with `AlphaMode` 1 on the track, as WebM defines. VP9 comes from `craft_video::vp9` (key //! frames every two seconds, inter frames between them: SimpleBlocks carry the key flag, inter //! BlockGroups a ReferenceBlock; the alpha stream's key frames line up with the colour stream's), //! Opus from `effectcraft-opusenc` (CELT, 48 kHz stereo, 20 ms packets, @@ -142,7 +142,7 @@ pub(crate) async fn webm(job: &Cx<'_>, comp: &Comp, w: u32, h: u32, st: &mut Sta let colour_channels = if job.output.channels == Channels::Alpha { Channels::Alpha } else { Channels::Rgb }; let quality = job.output.quality.clamp(1, 100); let fps = rate.as_f64().max(1.0); - let cfg = effectcraft_vp9enc::EncoderConfig { + let cfg = craft_video::vp9::EncoderConfig { width: w, height: h, quality, @@ -152,8 +152,8 @@ pub(crate) async fn webm(job: &Cx<'_>, comp: &Comp, w: u32, h: u32, st: &mut Sta frame_rate: fps, loop_filter: true, }; - let mut color_enc = effectcraft_vp9enc::Vp9Encoder::new(cfg.clone()); - let mut alpha_enc = alpha.then(|| effectcraft_vp9enc::Vp9Encoder::new(cfg.clone())); + let mut color_enc = craft_video::vp9::Vp9Encoder::new(cfg.clone()); + let mut alpha_enc = alpha.then(|| craft_video::vp9::Vp9Encoder::new(cfg.clone())); let with_audio = wants_audio(job); let opus_channels = if job.output.audio_channels == 1 { 1usize } else { 2 }; let mut opus = with_audio.then(|| opus_encoder(job)); @@ -266,12 +266,12 @@ pub(crate) async fn webm(job: &Cx<'_>, comp: &Comp, w: u32, h: u32, st: &mut Sta for (j, px) in pixels.iter().enumerate() { let (color, a) = rayon::join( || { - let (y, u, vv) = effectcraft_vp9enc::rgba_to_yuv420(px, w, h, false); + let (y, u, vv) = craft_video::vp9::rgba_to_yuv420(px, w, h, false); color_enc.encode_frame(&y, &u, &vv, false) }, || { alpha_enc.as_mut().map(|ea| { - let (y, u, vv) = effectcraft_vp9enc::alpha_to_yuv420(px, w, h); + let (y, u, vv) = craft_video::vp9::alpha_to_yuv420(px, w, h); ea.encode_frame(&y, &u, &vv, false).data }) }, diff --git a/crates/av1enc/tests/it/bench.rs b/crates/export/tests/it/av1_encoder/bench.rs similarity index 94% rename from crates/av1enc/tests/it/bench.rs rename to crates/export/tests/it/av1_encoder/bench.rs index b29817e1a..302308c50 100644 --- a/crates/av1enc/tests/it/bench.rs +++ b/crates/export/tests/it/av1_encoder/bench.rs @@ -1,13 +1,13 @@ //! Speed / efficiency measurements on ffmpeg-generated test sources (ignored by default): -//! `cargo test -p effectcraft-av1enc --test it bench:: -- --ignored --nocapture`. +//! `cargo test -p effectcraft-export --test it av1_encoder::bench:: -- --ignored --nocapture`. -use crate::common; +use super::common; use std::process::Command; use std::time::Instant; use common::*; -use effectcraft_av1enc::{Encoder, EncoderConfig, RateControl}; +use craft_video::av1::{Encoder, EncoderConfig, RateControl}; /// Raw 8-bit 4:2:0 frames of an ffmpeg lavfi source, or None without ffmpeg. fn lavfi(src: &str, w: usize, h: usize, frames: usize) -> Option> { @@ -52,7 +52,7 @@ fn run(name: &str, pics: &[Pic], rate: RateControl, keyint: u32) { let mut sizes = Vec::new(); for p in pics { let t = Instant::now(); - let pkt = enc.encode(&p.frame()); + let pkt = enc.encode(&p.frame()).expect("encode"); let dt = t.elapsed().as_secs_f64(); if pkt.keyframe { t_key += dt; diff --git a/crates/av1enc/tests/it/common/mod.rs b/crates/export/tests/it/av1_encoder/common.rs similarity index 97% rename from crates/av1enc/tests/it/common/mod.rs rename to crates/export/tests/it/av1_encoder/common.rs index 048e3de17..3cd42a584 100644 --- a/crates/av1enc/tests/it/common/mod.rs +++ b/crates/export/tests/it/av1_encoder/common.rs @@ -1,6 +1,6 @@ #![allow(dead_code)] -use effectcraft_av1enc::{Encoder, EncoderConfig, Frame, Packet}; +use craft_video::av1::{Encoder, EncoderConfig, Frame, Packet}; /// A synthetic 4:2:0 picture. pub struct Pic { @@ -95,7 +95,7 @@ pub fn encode_and_check(cfg: EncoderConfig, pics: &[Pic]) -> Run { let mut dec = filmcraft_av1::Decoder::with_threads(1); let mut run = Run { packets: Vec::new(), recons: Vec::new(), psnr_y: Vec::new(), lf_levels: Vec::new() }; for (i, p) in pics.iter().enumerate() { - let pkt = enc.encode(&p.frame()); + let pkt = enc.encode(&p.frame()).expect("encode"); let rec = enc.last_reconstruction().expect("reconstruction"); let out = dec.decode(&pkt.data).unwrap_or_else(|e| panic!("frame {i}: decode error {e:?}")); assert_eq!(out.len(), 1, "frame {i}: one picture per packet"); diff --git a/crates/av1enc/tests/it/ffmpeg_oracle.rs b/crates/export/tests/it/av1_encoder/ffmpeg_oracle.rs similarity index 95% rename from crates/av1enc/tests/it/ffmpeg_oracle.rs rename to crates/export/tests/it/av1_encoder/ffmpeg_oracle.rs index b35cbf496..6e235ed3b 100644 --- a/crates/av1enc/tests/it/ffmpeg_oracle.rs +++ b/crates/export/tests/it/av1_encoder/ffmpeg_oracle.rs @@ -2,12 +2,12 @@ //! bit for bit. Skipped when `ffmpeg` (with libdav1d) is not installed. ffmpeg's native `av1` //! decoder only drives hardware decoders, so it is not used. -use crate::common; +use super::common; use std::process::Command; use common::*; -use effectcraft_av1enc::{Encoder, EncoderConfig, Packet, RateControl}; +use craft_video::av1::{Encoder, EncoderConfig, Packet, RateControl}; fn have_libdav1d() -> bool { let Ok(out) = Command::new("ffmpeg").args(["-hide_banner", "-decoders"]).output() else { @@ -53,12 +53,12 @@ fn check(name: &str, cfg: EncoderConfig, pics: &[Pic]) -> Vec { let mut recons = Vec::new(); let mut levels = Vec::new(); for p in pics { - packets.push(enc.encode(&p.frame())); + packets.push(enc.encode(&p.frame()).expect("encode")); recons.push(enc.last_reconstruction().expect("reconstruction")); levels.push(enc.last_loop_filter_level().expect("level")); } - let dir = std::env::temp_dir().join(format!("effectcraft-av1enc-{}-{name}", std::process::id())); - std::fs::create_dir_all(&dir).expect("temp dir"); + let guard = tempfile::tempdir().expect("temp dir"); + let dir = guard.path().to_path_buf(); let input = dir.join("in.ivf"); let output = dir.join("out.yuv"); std::fs::write(&input, ivf(w, h, &packets)).expect("write ivf"); diff --git a/crates/export/tests/it/av1_encoder/mod.rs b/crates/export/tests/it/av1_encoder/mod.rs new file mode 100644 index 000000000..d33ea0c55 --- /dev/null +++ b/crates/export/tests/it/av1_encoder/mod.rs @@ -0,0 +1,6 @@ +//! craft-video's AV1 encoder against the filmcraft-av1 decoder and ffmpeg's libdav1d. + +mod bench; +mod common; +mod ffmpeg_oracle; +mod roundtrip; diff --git a/crates/av1enc/tests/it/roundtrip.rs b/crates/export/tests/it/av1_encoder/roundtrip.rs similarity index 93% rename from crates/av1enc/tests/it/roundtrip.rs rename to crates/export/tests/it/av1_encoder/roundtrip.rs index edc98c370..8e7dcc418 100644 --- a/crates/av1enc/tests/it/roundtrip.rs +++ b/crates/export/tests/it/av1_encoder/roundtrip.rs @@ -1,10 +1,10 @@ //! Round trips through FilmCraft's spec-derived AV1 decoder: decoded frames must equal the //! encoder's own reconstruction bit for bit. -use crate::common; +use super::common; use common::*; -use effectcraft_av1enc::{EncoderConfig, RateControl}; +use craft_video::av1::{EncoderConfig, RateControl}; fn cfg(w: u32, h: u32, bd: u8, q: u8, keyint: u32) -> EncoderConfig { let mut c = EncoderConfig::new(w, h, 30, 1); @@ -89,8 +89,8 @@ fn bitrate_mode_is_accurate_over_many_groups() { let pics: Vec = (0..n).map(|t| synth(192, 128, bd, t)).collect(); let mut c = cfg(192, 128, bd as u8, 0, keyint); c.rate = RateControl::Bitrate { kbps }; - let mut enc = effectcraft_av1enc::Encoder::new(c).unwrap(); - let bits: usize = pics.iter().map(|p| enc.encode(&p.frame()).data.len() * 8).sum(); + let mut enc = craft_video::av1::Encoder::new(c).unwrap(); + let bits: usize = pics.iter().map(|p| enc.encode(&p.frame()).expect("encode").data.len() * 8).sum(); let got = bits as f64 / (n as f64 / 30.0) / 1000.0; eprintln!("{bd} bit, keyint {keyint}: {got:.1} kbit/s for {kbps}"); assert!((got / kbps as f64 - 1.0).abs() < 0.05, "{got:.1} kbit/s for a {kbps} kbit/s target ({bd} bit, keyint {keyint})"); @@ -99,7 +99,7 @@ fn bitrate_mode_is_accurate_over_many_groups() { #[test] fn headers() { - let enc = effectcraft_av1enc::Encoder::new(cfg(1920, 1080, 10, 100, 60)).unwrap(); + let enc = craft_video::av1::Encoder::new(cfg(1920, 1080, 10, 100, 60)).unwrap(); let c = enc.av1c(); assert_eq!(c[0], 0x81); assert_eq!(c[1] & 0x1f, 8, "1080p30 is level 4.0"); diff --git a/crates/hevcenc/tests/roundtrip.rs b/crates/export/tests/it/hevc_encoder.rs similarity index 98% rename from crates/hevcenc/tests/roundtrip.rs rename to crates/export/tests/it/hevc_encoder.rs index 7591517e9..c19ff9cbd 100644 --- a/crates/hevcenc/tests/roundtrip.rs +++ b/crates/export/tests/it/hevc_encoder.rs @@ -1,7 +1,7 @@ //! Round trips through the filmcraft-hevc decoder and (when installed) ffmpeg: decoded pictures must //! equal the encoder's own reconstruction bit-exactly. -use effectcraft_hevcenc::{Encoder, EncoderConfig, Frame, Profile, RateControl}; +use craft_video::hevc::{Encoder, EncoderConfig, Frame, Profile, RateControl}; use std::process::Command; struct Pic { @@ -74,7 +74,7 @@ fn encode(cfg: EncoderConfig, pics: &[Pic]) -> Run { let mut enc = Encoder::new(cfg).unwrap(); let mut run = Run { packets: vec![], keys: vec![], recons: vec![], hvcc: enc.hvcc(), sets: enc.parameter_sets(), psnr_y: vec![] }; for p in pics { - let pkt = enc.encode(&p.frame()); + let pkt = enc.encode(&p.frame()).expect("encode"); let rec = enc.last_reconstruction().unwrap().clone(); run.psnr_y.push(psnr(&p.y, &rec[0], bd)); run.packets.push(pkt.data); @@ -393,7 +393,7 @@ fn hvcc_layout() { } /// Speed / size / quality report on ffmpeg-generated 1080p test content: -/// `cargo test -p effectcraft-hevcenc --test roundtrip bench_1080p -- --ignored --nocapture` +/// `cargo test -p effectcraft-export --test it hevc_encoder::bench_1080p -- --ignored --nocapture` /// (`HEVCENC_BENCH_SRC` selects another lavfi source). #[test] #[ignore] @@ -424,7 +424,7 @@ fn bench_1080p() { let mut enc = Encoder::new(cfg(w as u32, h as u32, qp, 30, Profile::Main)).unwrap(); for (i, p) in pics.iter().enumerate() { let t = std::time::Instant::now(); - let pkt = enc.encode(&p.frame()); + let pkt = enc.encode(&p.frame()).expect("encode"); let dt = t.elapsed(); let rec = enc.last_reconstruction().unwrap(); eprintln!( diff --git a/crates/export/tests/it/main.rs b/crates/export/tests/it/main.rs index 07ee96c05..77dc22b49 100644 --- a/crates/export/tests/it/main.rs +++ b/crates/export/tests/it/main.rs @@ -1,10 +1,12 @@ //! The crate's integration tests, one binary (each module is a former `tests/.rs`). mod audio_sample_positions; +mod av1_encoder; mod banding; mod exr_scene_linear; mod h264_gpu_yuv; mod hevc_av1_roundtrip; +mod hevc_encoder; #[cfg(not(target_arch = "wasm32"))] mod mov_audio_sample_positions; mod movie_bytes; diff --git a/crates/export/tests/it/movie_bytes.rs b/crates/export/tests/it/movie_bytes.rs index 76b89d1c7..00e19b396 100644 --- a/crates/export/tests/it/movie_bytes.rs +++ b/crates/export/tests/it/movie_bytes.rs @@ -97,7 +97,11 @@ fn h264_movie_bytes_are_pinned() { // FilmCraft 09aaf9b's encoder lists its settings in the first frame's user-data SEI (211 // bytes where 0.1.1's name took 50): 161 bytes more than the FilmCraft 2c5ba7a pin. The encoder's // version string "0.8.0" -> "0.12.0" in that SEI adds one more byte (and one to the size fields). - assert_eq!((data.len(), fnv1a(&data)), (104_342, 17_327_612_315_319_115_693), "H.264 MP4"); + // craft-video's encoder (craft-libs) is that encoder at a later FilmCraft revision than the + // d5ead4b pin: it names itself "craft-h264enc 0.1.0" in the SEI (5 bytes fewer), and with these + // settings its packets match the pin's for the first ten frames and differ after (24 bytes more + // in all, with the size fields). + assert_eq!((data.len(), fnv1a(&data)), (104_366, 4_413_091_804_733_884_658), "H.264 MP4"); } #[test] diff --git a/crates/export/tests/it/movie_color.rs b/crates/export/tests/it/movie_color.rs index 1a5371d91..e93384093 100644 --- a/crates/export/tests/it/movie_color.rs +++ b/crates/export/tests/it/movie_color.rs @@ -6,13 +6,13 @@ use std::path::Path; use std::process::Command; +use craft_media::isobmff::{ColorInfo, TrackKind}; use effectcraft_color::Label; use effectcraft_export::render_queue::{OutputFormat, OutputModule, RenderSettings, TimeSpan}; use effectcraft_export::{Job, export}; use effectcraft_project::{ColorSpace, Comp, ItemKind, LayerSource, Project, Solid, build}; use effectcraft_render::NoFootage; use effectcraft_time::{FrameRate, Tick}; -use filmcraft_isobmff::{ColorInfo, TrackKind}; /// (name, Output color space, H.273 primaries / transfer / matrix, the same as ffprobe names them) const ROWS: &[(&str, Option, [u16; 3], [&str; 3])] = &[ @@ -81,7 +81,7 @@ fn mp4_tags_describe_the_frames() { render(output, format, &path); let bytes = std::fs::read(&path).expect("read movie"); - let mp4 = filmcraft_isobmff::open(bytes.as_slice()).expect("demux"); + let mp4 = craft_media::isobmff::open(bytes.as_slice()).expect("demux"); let track = mp4.tracks.iter().find(|t| t.kind == TrackKind::Video).expect("video track"); let colr = track.entries[0].video.as_ref().and_then(|v| v.color.clone()); assert_eq!(colr, Some(ColorInfo::Nclx { primaries, transfer, matrix, full_range: false }), "{codec} {name}: colr"); diff --git a/crates/export/tests/it/ms_rounded_footage.rs b/crates/export/tests/it/ms_rounded_footage.rs index 3d6880232..2be6c3cd8 100644 --- a/crates/export/tests/it/ms_rounded_footage.rs +++ b/crates/export/tests/it/ms_rounded_footage.rs @@ -9,6 +9,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; +use craft_media::isobmff::{AvcConfig, Brand, Mp4Writer, SampleEntry, TrackConfig, WriteSample, WriterOptions}; use effectcraft_color::Label; use effectcraft_export::render_queue::{OutputFormat, OutputModule, RenderSettings, TimeSpan}; use effectcraft_export::{Job, export}; @@ -16,7 +17,6 @@ use effectcraft_media::MediaPool; use effectcraft_project::{Comp, Footage, ItemKind, LayerSource, Project, build}; use effectcraft_raster::Image; use effectcraft_time::{FrameRate, Tick}; -use filmcraft_isobmff::{AvcConfig, Brand, Mp4Writer, SampleEntry, TrackConfig, WriteSample, WriterOptions}; const W: u32 = 128; const H: u32 = 32; @@ -38,18 +38,18 @@ fn ms_rounded(rate: FrameRate, n: i64) -> Vec { /// An H.264 MP4 (video track timescale 1000) with one frame per entry of `durations` (ms); /// frame k shows k in binary. fn write_movie(path: &Path, durations: &[u32]) { - let mut cfg = filmcraft_h264enc::EncoderConfig::new(W, H, 30, 1); - cfg.format = filmcraft_h264enc::PacketFormat::LengthPrefixed; + let mut cfg = craft_video::h264::EncoderConfig::new(W, H, 30, 1); + cfg.format = craft_video::h264::PacketFormat::LengthPrefixed; cfg.aud = false; cfg.bframes = 0; cfg.keyint = 30; cfg.scenecut = false; - let mut enc = filmcraft_h264enc::Encoder::new(cfg).expect("encoder"); + let mut enc = craft_video::h264::Encoder::new(cfg).expect("encoder"); let (u, v) = (vec![128u8; (W * H / 4) as usize], vec![128u8; (W * H / 4) as usize]); let mut packets = Vec::new(); for k in 0..durations.len() { let y: Vec = (0..W * H).map(|i| if (k >> (i % W * BARS / W)) & 1 == 1 { 235 } else { 16 }).collect(); - let frame = filmcraft_h264enc::YuvFrame { y: &y, u: &u, v: &v, y_stride: W as usize, uv_stride: W as usize / 2 }; + let frame = craft_video::h264::YuvFrame { y: &y, u: &u, v: &v, y_stride: W as usize, uv_stride: W as usize / 2 }; packets.extend(enc.try_encode(&frame, k as i64).expect("encode")); } packets.extend(enc.flush()); diff --git a/crates/export/tests/it/opus_roundtrip.rs b/crates/export/tests/it/opus_roundtrip.rs index 3cd1edeee..c333946d8 100644 --- a/crates/export/tests/it/opus_roundtrip.rs +++ b/crates/export/tests/it/opus_roundtrip.rs @@ -20,9 +20,9 @@ fn encode(pcm: &[f32], channels: u8, bitrate: u32) -> (OpusEncoder, Vec> (enc, packets) } -/// Decodes with filmcraft-opus, drops the pre-skip and returns planar channels of `len` samples. +/// Decodes with craft-audio, drops the pre-skip and returns planar channels of `len` samples. fn decode(head: &[u8], packets: &[Vec], len: usize) -> Vec> { - let mut dec = filmcraft_opus::Decoder::new(head).expect("OpusHead"); + let mut dec = craft_audio::opus::Decoder::new(head).expect("OpusHead"); dec.set_trim_pre_skip(true); let mut out: Vec> = vec![Vec::new(); dec.channels()]; for p in packets { @@ -342,7 +342,8 @@ fn ffmpeg_oracle_decodes_ogg_opus() { } } let (enc, packets) = encode(&pcm, channels, rate); - let path = std::env::temp_dir().join(format!("effectcraft-opusenc-{}-{channels}.opus", std::process::id())); + let guard = tempfile::tempdir().unwrap(); + let path = guard.path().join("out.opus"); std::fs::write(&path, ogg_opus(&enc, &packets, len)).unwrap(); let out = Command::new("ffmpeg") .args(["-v", "error", "-i"]) diff --git a/crates/export/tests/it/prores_color.rs b/crates/export/tests/it/prores_color.rs index a54a17293..b2033c015 100644 --- a/crates/export/tests/it/prores_color.rs +++ b/crates/export/tests/it/prores_color.rs @@ -5,6 +5,7 @@ use std::path::Path; use std::process::Command; +use craft_media::isobmff::{ColorInfo, TrackKind}; use effectcraft_color::Label; use effectcraft_export::render_queue::{Channels, OutputFormat, OutputModule, RenderSettings, TimeSpan, sequence_path}; use effectcraft_export::{Job, export}; @@ -12,7 +13,6 @@ use effectcraft_media::MediaPool; use effectcraft_project::{ColorSpace, Comp, ItemKind, LayerSource, Project, Solid, build}; use effectcraft_render::{FootageSource, NoFootage}; use effectcraft_time::{FrameRate, Tick}; -use filmcraft_isobmff::{ColorInfo, TrackKind}; struct Row { name: &'static str, @@ -114,7 +114,7 @@ fn prores_colr_describes_the_frames() { render(row, &path); let bytes = std::fs::read(&path).expect("read movie"); - let mov = filmcraft_isobmff::open(bytes.as_slice()).expect("demux"); + let mov = craft_media::isobmff::open(bytes.as_slice()).expect("demux"); let track = mov.tracks.iter().find(|t| t.kind == TrackKind::Video).expect("video track"); let [primaries, transfer, matrix] = row.codes; let colr = track.entries[0].video.as_ref().and_then(|v| v.color.clone()); diff --git a/crates/export/tests/it/render_settings.rs b/crates/export/tests/it/render_settings.rs index 39a7e88ff..99e3d9c19 100644 --- a/crates/export/tests/it/render_settings.rs +++ b/crates/export/tests/it/render_settings.rs @@ -389,8 +389,8 @@ fn wav_and_aiff_channels_and_formats() { #[test] fn cancelled_export_removes_only_files_it_opened() { use effectcraft_export::ExportError; - let dir = std::env::temp_dir().join(format!("ec-cancel-owned-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); + let guard = tempfile::tempdir().unwrap(); + let dir = guard.path().to_path_buf(); let (p, cid) = project(FrameRate::new(25, 1), |_, _| {}); struct Overflow; impl StorageQuota for Overflow { diff --git a/crates/export/tests/it/roundtrip.rs b/crates/export/tests/it/roundtrip.rs index c8634fa21..bcf7b9d90 100644 --- a/crates/export/tests/it/roundtrip.rs +++ b/crates/export/tests/it/roundtrip.rs @@ -288,15 +288,15 @@ fn prores_alpha_final_run_is_present_in_exported_movies() { run_frames(&project, comp, &MediaPool::new(), &output, &path, 1); let bytes = std::fs::read(&path).expect("exported MOV"); - let movie = filmcraft_isobmff::open(bytes.as_slice()).expect("demux exported MOV"); - let video = movie.tracks.iter().find(|track| track.kind == filmcraft_isobmff::TrackKind::Video).expect("video track"); + let movie = craft_media::isobmff::open(bytes.as_slice()).expect("demux exported MOV"); + let video = movie.tracks.iter().find(|track| track.kind == craft_media::isobmff::TrackKind::Video).expect("video track"); assert_eq!(video.samples.len(), 1); let sample = &video.samples[0]; let frame = &bytes[sample.offset as usize..][..sample.size as usize]; - let (header, start, size) = filmcraft_prores::FrameHeader::parse(frame).expect("ProRes header"); + let (header, start, size) = craft_video::prores::FrameHeader::parse(frame).expect("ProRes header"); assert_eq!((header.width, header.height), (16, 16)); let picture = &frame[start..size]; - let header = filmcraft_prores::PictureHeader::parse(picture).expect("picture header"); + let header = craft_video::prores::PictureHeader::parse(picture).expect("picture header"); assert_eq!(header.num_slices, 1); let slice = &picture[header.header_size + 2..]; let color_bytes: usize = slice[2..8].as_chunks::<2>().0.iter().map(|b| u16::from_be_bytes(*b) as usize).sum(); diff --git a/crates/export/tests/it/uncompressed_mov.rs b/crates/export/tests/it/uncompressed_mov.rs index 1f7a66576..200d3116a 100644 --- a/crates/export/tests/it/uncompressed_mov.rs +++ b/crates/export/tests/it/uncompressed_mov.rs @@ -8,6 +8,7 @@ use std::path::{Path, PathBuf}; use std::process::Command; +use craft_media::isobmff::TrackKind; use effectcraft_color::Label; use effectcraft_export::render_queue::{AudioFormat, AudioOutput, Channels, OutputFormat, OutputModule, RenderSettings, TimeSpan, sequence_path}; use effectcraft_export::{Job, export}; @@ -15,7 +16,6 @@ use effectcraft_media::MediaPool; use effectcraft_project::{Comp, ItemId, ItemKind, LayerSource, Project, Solid, build}; use effectcraft_render::FootageSource; use effectcraft_time::{FrameRate, Tick}; -use filmcraft_isobmff::TrackKind; const W: u32 = 33; const H: u32 = 20; @@ -103,7 +103,7 @@ fn reference_frames(p: &Project, cid: ItemId, footage: &dyn FootageSource, om: & /// The frames as stored in the movie, unpacked to RGBA (rows padded to even bytes). fn stored_frames(path: &Path, alpha: bool) -> Vec> { let bytes = std::fs::read(path).expect("read movie"); - let mov = filmcraft_isobmff::open(bytes.as_slice()).expect("demux"); + let mov = craft_media::isobmff::open(bytes.as_slice()).expect("demux"); assert!(mov.is_quicktime); let t = mov.tracks.iter().find(|t| t.kind == TrackKind::Video).expect("video track"); let e = &t.entries[0]; @@ -204,7 +204,7 @@ fn uncompressed_with_pcm_audio() { let path = d.join("comp.mov"); assert!(run(&p, cid, &pool, &om, &path).audio); let bytes = std::fs::read(&path).expect("read"); - let mov = filmcraft_isobmff::open(bytes.as_slice()).expect("demux"); + let mov = craft_media::isobmff::open(bytes.as_slice()).expect("demux"); let a = mov.tracks.iter().find(|t| t.kind == TrackKind::Audio).expect("audio track"); assert_eq!(a.timescale, 48_000); // 0.6 s of audio at 48 kHz. diff --git a/crates/export/tests/it/vp9_roundtrip.rs b/crates/export/tests/it/vp9_roundtrip.rs index 1432e3892..f9d36770d 100644 --- a/crates/export/tests/it/vp9_roundtrip.rs +++ b/crates/export/tests/it/vp9_roundtrip.rs @@ -1,7 +1,7 @@ -//! The VP9 intra encoder (`effectcraft-vp9enc`) round-trips through FilmCraft's VP9 decoder (and +//! The VP9 intra encoder (`craft_video::vp9`) round-trips through FilmCraft's VP9 decoder (and //! ffmpeg as an external oracle when installed): sizes, lossless exactness, PSNR. -use effectcraft_vp9enc::{EncoderConfig, Vp9Encoder}; +use craft_video::vp9::{EncoderConfig, Vp9Encoder}; use filmcraft_vp9::{Decoder, Plane}; fn plane_u8(p: &Plane) -> Vec { @@ -130,8 +130,8 @@ fn ffmpeg_decodes_our_stream() { ivf.extend_from_slice(&(frame.len() as u32).to_le_bytes()); ivf.extend_from_slice(&0u64.to_le_bytes()); ivf.extend_from_slice(&frame); - let dir = std::env::temp_dir().join(format!("effectcraft-vp9-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); + let guard = tempfile::tempdir().unwrap(); + let dir = guard.path().to_path_buf(); let path = dir.join("a.ivf"); std::fs::write(&path, &ivf).unwrap(); let out = std::process::Command::new("ffmpeg") @@ -291,8 +291,8 @@ fn ffmpeg_matches(w: usize, h: usize, frames: &[Vec], recon: &[[Vec; 3]] ivf.extend_from_slice(&(t as u64).to_le_bytes()); ivf.extend_from_slice(f); } - let dir = std::env::temp_dir().join(format!("effectcraft-vp9-{name}-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); + let guard = tempfile::Builder::new().prefix(&format!("effectcraft-vp9-{name}-")).tempdir().unwrap(); + let dir = guard.path().to_path_buf(); let path = dir.join("a.ivf"); std::fs::write(&path, &ivf).unwrap(); let out = std::process::Command::new("ffmpeg") diff --git a/crates/gpu/src/fx_generate.rs b/crates/gpu/src/fx_generate.rs index c6f2d1def..893208a15 100644 --- a/crates/gpu/src/fx_generate.rs +++ b/crates/gpu/src/fx_generate.rs @@ -133,7 +133,7 @@ fn radial_fast(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { } fn camera_lens(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { - let r = ctx.params.f("blurRadius").max(0.0) * b.scale; + let r = effectcraft_effects::camera_lens_radius(ctx, b.scale); if r < 0.5 { return Some(b); } diff --git a/crates/gpu/src/fx_noise.rs b/crates/gpu/src/fx_noise.rs index 9ae503994..a7dd0a3f5 100644 --- a/crates/gpu/src/fx_noise.rs +++ b/crates/gpu/src/fx_noise.rs @@ -202,6 +202,8 @@ pub(crate) fn fractal(e: &mut Enc, ctx: &EffectCtx, b: GBuf, turbulent: bool) -> /// noise::median_image (of the straight colour with alpha 1 when `straight`). pub(crate) fn median_image(e: &mut Enc, img: &GpuImage, r: usize, straight: bool) -> GpuImage { + // As on the CPU: the window's radius is capped, so its first fill stays bounded. + let r = r.min(effectcraft_effects::MEDIAN_MAX_RADIUS); let q = run(e, "fxn_quant", &with_u0([straight as u32, 0, 0, 0]), img, None, None); // Preserve the CPU's rounded level / 511 values before the strict Dust & Scratches // threshold. GPU division (even residual-corrected) may differ by one ulp. diff --git a/crates/hevcenc/Cargo.toml b/crates/hevcenc/Cargo.toml deleted file mode 100644 index 136611ff2..000000000 --- a/crates/hevcenc/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "effectcraft-hevcenc" -description = "Clean-room pure-Rust HEVC (H.265) encoder (Main / Main 10, I and P slices, CABAC) for MP4 export, from ITU-T H.265" -version.workspace = true -edition.workspace = true -license.workspace = true -rust-version.workspace = true - -[lints] -workspace = true - -[dependencies] - -[dev-dependencies] -filmcraft-hevc = { workspace = true } diff --git a/crates/hevcenc/README.md b/crates/hevcenc/README.md deleted file mode 100644 index 5c4e5debe..000000000 --- a/crates/hevcenc/README.md +++ /dev/null @@ -1,88 +0,0 @@ -# effectcraft-hevcenc - -Clean-room, pure-Rust HEVC / H.265 encoder for EffectCraft's MP4 export (layer L0, `std` only, -no `unsafe`, builds for `wasm32-unknown-unknown`). - -## What it produces - -- **Profiles:** Main (8-bit) and Main 10 (10-bit), 4:2:0, Main tier. `general_level_idc` is taken - from the config or chosen as the lowest level whose MaxLumaPs, MaxLumaSr (and, in bitrate mode, - MaxBR) fit (Table A.8). -- **Parameter sets:** VPS, SPS (with VUI: BT.709 colour description, `video_full_range_flag`, - timing info from the frame rate) and PPS, as raw NAL units (`Encoder::parameter_sets`) and as an - `hvcC` record (ISO/IEC 14496-15 §8.3.3.1, `lengthSizeMinusOne` 3, complete VPS / SPS / PPS - arrays, `Encoder::hvcc`). -- **Pictures:** one slice per picture; `IDR_N_LP` every `keyint` pictures (`1` = all intra, `0` = - only the first), `TRAIL_R` P pictures otherwise, each referencing the previous picture (one - short-term RPS in the SPS, `num_ref_idx_l0_active` 1, no reordering, pts = dts). Packets are - length-prefixed (4-byte big-endian) NAL units without parameter sets. -- **Picture size:** any even width / height; the coded size is padded to a multiple of 8 by edge - replication and cropped with the conformance window. Odd sizes are rejected with - `Error::InvalidConfig` (4:2:0 conformance-window offsets are in units of two luma samples). - -## Coding tools - -- 64x64 CTUs, coding units 64x64 .. 8x8 chosen by rate-distortion optimisation: every candidate is - actually predicted, transformed, quantised and reconstructed, and its rate is measured with a - CABAC bit estimator that evolves the same context states as the real coder. Split decisions - compare the best unsplit candidate with the recursively coded children (early exit after a skip). -- **Intra:** all 35 modes (SATD search on a coarse grid plus refinement and the MPMs), 2Nx2N at - 8..32 and NxN (four 4x4 PUs) at 8x8, chroma in DM mode, MPM / remaining-mode coding, reference - substitution, [1 2 1] and strong (32x32) smoothing, DC / angular boundary filters. -- **Inter (P):** skip and merge (spatial candidates + zero candidates, `MaxNumMergeCand` 5) and AMVP - with mvd coding; integer motion search (predictor starts + square pattern, steps 16..1) and - half / quarter-sample refinement on pre-interpolated half-sample planes; the coded prediction uses - the normative 8-tap / 4-tap interpolation filters and reference-picture edge clamping. -- **Transforms:** DST-VII 4x4 (intra luma) and DCT 4..32 (inverse by even/odd decomposition, - bit-identical to the matrix form), dead-zone quantisation (1/3 intra, 1/6 inter), flat scaling, - sign data hiding, inferred transform splits only (`max_transform_hierarchy_depth_*` 0, so 64x64 - CUs use four 32x32 TUs). -- **Entropy coding:** CABAC (context initialisation by slice QP and init type, decision / bypass / - terminate bins, flush), `end_of_slice_segment_flag`, byte alignment, emulation prevention. -- **In-loop filters:** the deblocking filter is on (the encoder deblocks its own reconstruction, so - references match the decoder); SAO is disabled. -- **Rate control:** `ConstantQp(qp)` or `Bitrate { kbps }`, a per-picture QP from a bits-x-Qstep - model per picture type (I pictures get roughly 4x the bits of a P picture) with a buffer that - spreads over- and undershoot over the following pictures. - -Not implemented: B pictures / reordering, multiple references, TMVP, AMP and other inter partition -shapes, residual quadtree splits, transform skip, scaling lists, PCM, SAO, RDOQ, cu_qp_delta / -adaptive quantisation, tiles / WPP / multiple slices, range extensions (4:2:2, 4:4:4, > 10 bits). - -## Measurements - -On a shared, heavily loaded machine, opt-level 3: about 0.45 s of CPU per 1920x1080 picture -(ffmpeg `testsrc2`, QP 22 / 32). On ffmpeg's `mandelbrot` 1080p source (8 pictures, IPPP, QP 32) -the stream is about 12 % larger at about 0.2 dB lower mean PSNR-Y than x265 `--preset medium` at -the same QP without psy tuning (110.5 kB / 42.3 dB vs 98.3 kB / 42.5 dB). The synthetic test image -reaches 45.8 dB PSNR-Y at QP 22. - -## Tests - -`cargo test -p effectcraft-hevcenc`: - -- every stream is decoded with the first-party `filmcraft-hevc` decoder and compared bit-exactly - with the encoder's reconstruction (`Encoder::last_reconstruction`, hidden API); -- when `ffmpeg` is on `PATH`, the Annex-B elementary stream is decoded with - `ffmpeg -v error -xerror` (no stderr output allowed) and compared bit-exactly as well (skipped - otherwise). ffmpeg is used only as an external test oracle; -- covered: 8-bit and 10-bit, all-intra and IP, QP 0 noise (escape codes), QP 0..51, strong - deblocking at high QP, sizes 8x8 .. 200x136 with cropping, POC wrap (300 pictures without IDR), - bitrate mode within 2x of the target, `hvcC` layout; -- `bench_1080p` (ignored) reports speed, size and PSNR on 1080p content generated by ffmpeg. - -## Specifications - -- ITU-T H.265 (V8, 08/2021) | ISO/IEC 23008-2, *High efficiency video coding*: 7 (syntax and - semantics), 8.4 - 8.7 (intra / inter prediction, scaling and transforms, deblocking), 9.3 (CABAC, - including the informative encoder description of 9.3.4), Annex A (profiles, tiers and levels), - Annex E (VUI). -- ISO/IEC 14496-15 (2019), §8.3.3.1 HEVCDecoderConfigurationRecord. - -Implemented from the specification text. Constant tables (CABAC context layout and initValues, -rangeTabLPS / transIdx, scan orders, the DCT / DST matrices, intra angles, interpolation filters, -the chroma QP table, the deblocking beta / tC tables) and the decoder-side reconstruction -arithmetic (intra reference substitution / filtering / prediction, fractional interpolation, -deblocking) are mirrored from FilmCraft's first-party, spec-derived `filmcraft-hevc` decoder (same -organisation, MIT OR Apache-2.0), so that encoder and decoder reconstruct identically. No -third-party encoder or decoder source (x265, libde265, ffmpeg, HM, ...) was consulted. diff --git a/crates/hevcenc/src/bitstream.rs b/crates/hevcenc/src/bitstream.rs deleted file mode 100644 index 27ed3063e..000000000 --- a/crates/hevcenc/src/bitstream.rs +++ /dev/null @@ -1,135 +0,0 @@ -//! RBSP bit writer (7.2, 9.2 Exp-Golomb) and NAL unit encapsulation with emulation prevention (7.3.1). - -/// MSB-first bit writer. -#[derive(Default)] -pub struct BitWriter { - buf: Vec, - acc: u64, - nbits: u32, -} - -impl BitWriter { - pub fn new() -> Self { - Self::default() - } - - /// Write the `n` (<= 32) low bits of `v`. - #[inline] - pub fn put(&mut self, v: u32, n: u32) { - debug_assert!(n <= 32); - if n == 0 { - return; - } - let v = (v as u64) & ((1u64 << n) - 1); - self.acc = (self.acc << n) | v; - self.nbits += n; - while self.nbits >= 8 { - self.nbits -= 8; - self.buf.push((self.acc >> self.nbits) as u8); - } - self.acc &= (1u64 << self.nbits) - 1; - } - - #[inline] - pub fn bit(&mut self, b: u32) { - self.put(b & 1, 1); - } - - pub fn flag(&mut self, b: bool) { - self.put(b as u32, 1); - } - - /// ue(v). - pub fn ue(&mut self, v: u32) { - let x = v as u64 + 1; - let len = 64 - x.leading_zeros(); // bits of x - // len - 1 leading zeros, then x in len bits - self.put(0, len - 1); - if len > 32 { - self.put((x >> 32) as u32, len - 32); - self.put(x as u32, 32); - } else { - self.put(x as u32, len); - } - } - - /// se(v). - pub fn se(&mut self, v: i32) { - let k = if v > 0 { 2 * v as u32 - 1 } else { (-(v as i64) as u32) * 2 }; - self.ue(k); - } - - pub fn is_aligned(&self) -> bool { - self.nbits == 0 - } - - /// rbsp_trailing_bits(): stop bit and zero alignment. - pub fn trailing(&mut self) { - self.bit(1); - self.align_zero(); - } - - pub fn align_zero(&mut self) { - if self.nbits > 0 { - self.put(0, 8 - self.nbits); - } - } - - pub fn into_bytes(mut self) -> Vec { - self.align_zero(); - self.buf - } -} - -/// Build a NAL unit (2-byte header, `nuh_layer_id` 0, `TemporalId` 0) from an RBSP, inserting -/// emulation prevention bytes. -pub fn nal_unit(nal_type: u8, rbsp: &[u8]) -> Vec { - let mut out = Vec::with_capacity(rbsp.len() + rbsp.len() / 64 + 4); - out.push(nal_type << 1); - out.push(1); - let mut zeros = 0; - for &b in rbsp { - if zeros >= 2 && b <= 3 { - out.push(3); - zeros = 0; - } - out.push(b); - if b == 0 { - zeros += 1; - } else { - zeros = 0; - } - } - // cabac_zero_words / trailing zero bytes never occur: every RBSP ends with the stop bit. - out -} - -pub const NAL_TRAIL_R: u8 = 1; -pub const NAL_IDR_N_LP: u8 = 20; -pub const NAL_VPS: u8 = 32; -pub const NAL_SPS: u8 = 33; -pub const NAL_PPS: u8 = 34; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn exp_golomb() { - let mut w = BitWriter::new(); - w.ue(0); // 1 - w.ue(1); // 010 - w.ue(2); // 011 - w.ue(3); // 00100 - w.se(-1); // ue(2) = 011 - w.trailing(); - // 1 010 011 00100 011 + stop bit: 1010 0110 0100 0111 - assert_eq!(w.into_bytes(), vec![0xa6, 0x47]); - } - - #[test] - fn emulation_prevention() { - let n = nal_unit(1, &[0, 0, 1, 0, 0, 0, 0, 0, 4]); - assert_eq!(&n[2..], &[0, 0, 3, 1, 0, 0, 3, 0, 0, 3, 0, 4]); - } -} diff --git a/crates/hevcenc/src/cabac.rs b/crates/hevcenc/src/cabac.rs deleted file mode 100644 index 220b999cd..000000000 --- a/crates/hevcenc/src/cabac.rs +++ /dev/null @@ -1,220 +0,0 @@ -//! CABAC: context initialisation (9.3.2.2), the arithmetic encoder (9.3.4.x, informative encoder -//! description: EncodeDecision, EncodeBypass, EncodeTerminate, EncodeFlush) and a fractional-bit -//! estimator with the same context-state evolution, used for rate-distortion decisions. - -use crate::bitstream::BitWriter; -use crate::tables::{CABAC_INIT, NEXT_STATE, NUM_CTX, RANGE_TAB_LPS}; -use std::sync::OnceLock; - -/// Context states, each `pStateIdx << 1 | valMps`. -pub type Contexts = [u8; NUM_CTX]; - -/// Initialise all context variables for `slice_qp` and `init_type` (9.3.2.2). -pub fn init_contexts(slice_qp: i32, init_type: usize) -> Contexts { - let mut ctx = [0u8; NUM_CTX]; - let qp = slice_qp.clamp(0, 51); - for (c, &v) in ctx.iter_mut().zip(CABAC_INIT[init_type].iter()) { - let slope = (v >> 4) as i32; - let offset = (v & 15) as i32; - let m = slope * 5 - 45; - let n = (offset << 3) - 16; - let pre = (((m * qp) >> 4) + n).clamp(1, 126); - *c = if pre <= 63 { ((63 - pre) << 1) as u8 } else { (((pre - 64) << 1) | 1) as u8 }; - } - ctx -} - -/// Destination of binarised syntax elements: the real arithmetic coder or the bit estimator. -pub trait Sink { - fn decision(&mut self, ctx: usize, bin: u32); - fn bypass(&mut self, bin: u32); - /// `n` bypass bins carrying the low `n` bits of `v`, MSB first. - fn bypass_bits(&mut self, v: u32, n: u32) { - for i in (0..n).rev() { - self.bypass((v >> i) & 1); - } - } - fn terminate(&mut self, bin: u32); -} - -/// Arithmetic encoder writing into a bit writer. -pub struct CabacEncoder { - pub ctx: Contexts, - low: u32, - range: u32, - outstanding: u32, - first: bool, - pub w: BitWriter, -} - -impl CabacEncoder { - /// Start encoding slice data into `w` (which must be byte aligned). - pub fn new(w: BitWriter, ctx: Contexts) -> Self { - debug_assert!(w.is_aligned()); - CabacEncoder { ctx, low: 0, range: 510, outstanding: 0, first: true, w } - } - - #[inline] - fn put_bit(&mut self, b: u32) { - if self.first { - self.first = false; - } else { - self.w.bit(b); - } - while self.outstanding > 0 { - self.w.bit(1 - b); - self.outstanding -= 1; - } - } - - #[inline] - fn renorm(&mut self) { - while self.range < 256 { - if self.low < 256 { - self.put_bit(0); - } else if self.low >= 512 { - self.low -= 512; - self.put_bit(1); - } else { - self.low -= 256; - self.outstanding += 1; - } - self.range <<= 1; - self.low <<= 1; - } - } - - /// EncodeFlush after a terminating bin equal to 1; the final bit written is the - /// rbsp_stop_one_bit. Returns the writer (byte aligned with zero bits). - pub fn finish(mut self) -> BitWriter { - self.range = 2; - self.renorm(); - let b = (self.low >> 9) & 1; - self.put_bit(b); - self.w.put(((self.low >> 7) & 3) | 1, 2); - self.w.align_zero(); - self.w - } -} - -impl Sink for CabacEncoder { - #[inline] - fn decision(&mut self, ctx_idx: usize, bin: u32) { - let s = self.ctx[ctx_idx] as usize; - let mps = (s & 1) as u32; - let q = ((self.range >> 6) & 3) as usize; - let lps = RANGE_TAB_LPS[s >> 1][q] as u32; - self.range -= lps; - if bin != mps { - self.low += self.range; - self.range = lps; - self.ctx[ctx_idx] = NEXT_STATE[s][1]; - } else { - self.ctx[ctx_idx] = NEXT_STATE[s][0]; - } - self.renorm(); - } - - #[inline] - fn bypass(&mut self, bin: u32) { - self.low <<= 1; - if bin != 0 { - self.low += self.range; - } - if self.low >= 1024 { - self.put_bit(1); - self.low -= 1024; - } else if self.low < 512 { - self.put_bit(0); - } else { - self.low -= 512; - self.outstanding += 1; - } - } - - fn terminate(&mut self, bin: u32) { - self.range -= 2; - if bin != 0 { - self.low += self.range; - // the caller flushes with finish() - } else { - self.renorm(); - } - } -} - -/// Fractional bit cost (1/32768 bit units) of coding the MPS / LPS in each state. -fn cost_table() -> &'static [[u32; 2]; 64] { - static T: OnceLock<[[u32; 2]; 64]> = OnceLock::new(); - T.get_or_init(|| { - let mut t = [[0u32; 2]; 64]; - // pLPS(s) = 0.5 * alpha^s with alpha = (0.01875 / 0.5)^(1/63) (9.3.4.3 design) - let alpha = (0.01875f64 / 0.5).powf(1.0 / 63.0); - for (s, e) in t.iter_mut().enumerate() { - let p = 0.5 * alpha.powi(s as i32); - e[0] = (-(1.0 - p).log2() * 32768.0).round() as u32; - e[1] = (-p.log2() * 32768.0).round() as u32; - } - t - }) -} - -pub const BIT: u64 = 32768; - -/// Bit estimator: tracks context states like the encoder and accumulates the ideal code length. -#[derive(Clone)] -pub struct Estimator { - pub ctx: Contexts, - /// Accumulated cost in 1/32768 bits. - pub bits: u64, - table: &'static [[u32; 2]; 64], -} - -impl Estimator { - pub fn new(ctx: Contexts) -> Self { - Estimator { ctx, bits: 0, table: cost_table() } - } -} - -impl Sink for Estimator { - #[inline] - fn decision(&mut self, ctx_idx: usize, bin: u32) { - let s = self.ctx[ctx_idx] as usize; - let lps = (bin != (s & 1) as u32) as usize; - self.bits += self.table[s >> 1][lps] as u64; - self.ctx[ctx_idx] = NEXT_STATE[s][lps]; - } - #[inline] - fn bypass(&mut self, _bin: u32) { - self.bits += BIT; - } - #[inline] - fn bypass_bits(&mut self, _v: u32, n: u32) { - self.bits += BIT * n as u64; - } - fn terminate(&mut self, _bin: u32) { - self.bits += 7; // ~0 bits for a 0 bin - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn context_init_formula() { - let ctx = init_contexts(30, 0); - // initValue 154: m = 0, n = 64 -> pStateIdx 0, valMps 1 - assert_eq!(ctx[crate::tables::CU_TRANSQUANT_BYPASS], 1); - // split_cu_flag ctx 0 (initValue 139) at QP 30: preCtxState 62 - assert_eq!(ctx[crate::tables::SPLIT_CU], (63 - 62) << 1); - } - - #[test] - fn costs_are_sane() { - let t = cost_table(); - assert_eq!(t[0][0], 32768); - assert_eq!(t[0][1], 32768); - assert!(t[62][0] < 1000 && t[62][1] > 5 * 32768); - } -} diff --git a/crates/hevcenc/src/deblock.rs b/crates/hevcenc/src/deblock.rs deleted file mode 100644 index 6d326e5d4..000000000 --- a/crates/hevcenc/src/deblock.rs +++ /dev/null @@ -1,148 +0,0 @@ -//! Deblocking filter (8.7.2) over a whole reconstructed picture, mirrored from the filmcraft-hevc -//! decoder: all vertical edges on the 8x8 grid first, then all horizontal edges. One slice, one QP, -//! uni-directional prediction from a single reference picture, offsets 0. - -use crate::frame::{Blk, E_EDGE_H, E_EDGE_V, F_CBF, F_INTRA}; -use crate::tables::{BETA_TABLE, TC_TABLE, qpc_420}; - -#[inline] -fn tc_beta(q: i32, bs: i32, bd: u32) -> (i32, i32) { - let qb = q.clamp(0, 51); - let qt = (q + 2 * (bs - 1)).clamp(0, 53); - ((TC_TABLE[qt as usize] as i32) << (bd - 8), (BETA_TABLE[qb as usize] as i32) << (bd - 8)) -} - -/// Boundary strength (8.7.2.4) between 4x4 blocks p and q on a transform / prediction edge. -fn boundary_strength(p: &Blk, q: &Blk) -> i32 { - if (p.flags | q.flags) & F_INTRA != 0 { - return 2; - } - if (p.flags | q.flags) & F_CBF != 0 { - return 1; - } - let far = (p.mv[0] as i32 - q.mv[0] as i32).abs() >= 4 || (p.mv[1] as i32 - q.mv[1] as i32).abs() >= 4; - far as i32 -} - -/// Deblock the planes in place. `qp` is the slice QpY (without QpBdOffset). -pub fn deblock(planes: &mut [Vec; 3], blk: &[Blk], w: usize, h: usize, qp: i32, bd: u32) { - let (w4, h4) = (w / 4, h / 4); - let cw = w / 2; - // chroma tc (bS 2 only; QpC from the averaged luma QP, no offsets) - let qt = (qpc_420(qp) + 2).clamp(0, 53); - let tc_c = (TC_TABLE[qt as usize] as i32) << (bd - 8); - // vertical edges - for by in 0..h4 { - for bx in (2..w4).step_by(2) { - let qi = by * w4 + bx; - if blk[qi].flags & E_EDGE_V == 0 { - continue; - } - let bs = boundary_strength(&blk[qi - 1], &blk[qi]); - if bs == 0 { - continue; - } - let (x, y) = (bx * 4, by * 4); - let (tc, beta) = tc_beta(qp, bs, bd); - filter_luma(&mut planes[0], y * w + x, 1, w, tc, beta, bd); - // chroma: 8x8 chroma grid (16 luma samples), one 4-sample chroma segment per 8 luma rows - if bs == 2 && bx % 4 == 0 && by % 2 == 0 { - for c in 1..3 { - filter_chroma(&mut planes[c], (y / 2) * cw + x / 2, 1, cw, tc_c, bd); - } - } - } - } - // horizontal edges - for by in (2..h4).step_by(2) { - for bx in 0..w4 { - let qi = by * w4 + bx; - if blk[qi].flags & E_EDGE_H == 0 { - continue; - } - let bs = boundary_strength(&blk[qi - w4], &blk[qi]); - if bs == 0 { - continue; - } - let (x, y) = (bx * 4, by * 4); - let (tc, beta) = tc_beta(qp, bs, bd); - filter_luma(&mut planes[0], y * w + x, w, 1, tc, beta, bd); - if bs == 2 && by % 4 == 0 && bx % 2 == 0 { - for c in 1..3 { - filter_chroma(&mut planes[c], (y / 2) * cw + x / 2, cw, 1, tc_c, bd); - } - } - } - } -} - -/// Filter one 4-sample luma edge segment. `o` = offset of q0 of the first line, `step` = distance -/// between samples across the edge, `along` = distance between lines. -fn filter_luma(s: &mut [u16], o: usize, step: usize, along: usize, tc: i32, beta: i32, bd: u32) { - let px = |s: &[u16], k: usize, i: usize| s[o + k * along - (i + 1) * step] as i32; - let qx = |s: &[u16], k: usize, i: usize| s[o + k * along + i * step] as i32; - let dp0 = (px(s, 0, 2) - 2 * px(s, 0, 1) + px(s, 0, 0)).abs(); - let dp3 = (px(s, 3, 2) - 2 * px(s, 3, 1) + px(s, 3, 0)).abs(); - let dq0 = (qx(s, 0, 2) - 2 * qx(s, 0, 1) + qx(s, 0, 0)).abs(); - let dq3 = (qx(s, 3, 2) - 2 * qx(s, 3, 1) + qx(s, 3, 0)).abs(); - let (dpq0, dpq3) = (dp0 + dq0, dp3 + dq3); - let (dp, dq) = (dp0 + dp3, dq0 + dq3); - let d = dpq0 + dpq3; - if d >= beta { - return; - } - let dsam = |s: &[u16], k: usize, dpq: i32| -> bool { - dpq < (beta >> 2) - && (px(s, k, 3) - px(s, k, 0)).abs() + (qx(s, k, 0) - qx(s, k, 3)).abs() < (beta >> 3) - && (px(s, k, 0) - qx(s, k, 0)).abs() < ((5 * tc + 1) >> 1) - }; - let strong = dsam(s, 0, 2 * dpq0) && dsam(s, 3, 2 * dpq3); - let dep = dp < ((beta + (beta >> 1)) >> 3); - let deq = dq < ((beta + (beta >> 1)) >> 3); - let max = (1i32 << bd) - 1; - for k in 0..4 { - let base = o + k * along; - let p = |i: usize| s[base - (i + 1) * step] as i32; - let q = |i: usize| s[base + i * step] as i32; - let (p0, p1, p2, p3) = (p(0), p(1), p(2), p(3)); - let (q0, q1, q2, q3) = (q(0), q(1), q(2), q(3)); - if strong { - let t2 = 2 * tc; - s[base - step] = ((p2 + 2 * p1 + 2 * p0 + 2 * q0 + q1 + 4) >> 3).clamp(p0 - t2, p0 + t2) as u16; - s[base - 2 * step] = ((p2 + p1 + p0 + q0 + 2) >> 2).clamp(p1 - t2, p1 + t2) as u16; - s[base - 3 * step] = ((2 * p3 + 3 * p2 + p1 + p0 + q0 + 4) >> 3).clamp(p2 - t2, p2 + t2) as u16; - s[base] = ((p1 + 2 * p0 + 2 * q0 + 2 * q1 + q2 + 4) >> 3).clamp(q0 - t2, q0 + t2) as u16; - s[base + step] = ((p0 + q0 + q1 + q2 + 2) >> 2).clamp(q1 - t2, q1 + t2) as u16; - s[base + 2 * step] = ((p0 + q0 + q1 + 3 * q2 + 2 * q3 + 4) >> 3).clamp(q2 - t2, q2 + t2) as u16; - } else { - let mut delta = (9 * (q0 - p0) - 3 * (q1 - p1) + 8) >> 4; - if delta.abs() >= tc * 10 { - continue; - } - delta = delta.clamp(-tc, tc); - s[base - step] = (p0 + delta).clamp(0, max) as u16; - s[base] = (q0 - delta).clamp(0, max) as u16; - if dep { - let dp = ((((p2 + p0 + 1) >> 1) - p1 + delta) >> 1).clamp(-(tc >> 1), tc >> 1); - s[base - 2 * step] = (p1 + dp).clamp(0, max) as u16; - } - if deq { - let dq = ((((q2 + q0 + 1) >> 1) - q1 - delta) >> 1).clamp(-(tc >> 1), tc >> 1); - s[base + step] = (q1 + dq).clamp(0, max) as u16; - } - } - } -} - -/// Filter one 4-sample chroma edge segment. -fn filter_chroma(s: &mut [u16], o: usize, step: usize, along: usize, tc: i32, bd: u32) { - let max = (1i32 << bd) - 1; - for k in 0..4 { - let base = o + k * along; - let (p0, p1) = (s[base - step] as i32, s[base - 2 * step] as i32); - let (q0, q1) = (s[base] as i32, s[base + step] as i32); - let delta = ((((q0 - p0) << 2) + p1 - q1 + 4) >> 3).clamp(-tc, tc); - s[base - step] = (p0 + delta).clamp(0, max) as u16; - s[base] = (q0 - delta).clamp(0, max) as u16; - } -} diff --git a/crates/hevcenc/src/frame.rs b/crates/hevcenc/src/frame.rs deleted file mode 100644 index ee141e3e7..000000000 --- a/crates/hevcenc/src/frame.rs +++ /dev/null @@ -1,1481 +0,0 @@ -//! Picture coding: CTU loop, rate-distortion mode decisions (with a CABAC bit estimator) and the -//! slice_segment_data() syntax (7.3.8): coding quadtree, coding unit, prediction unit, transform tree -//! and residual coding. Reconstruction mirrors the decoding process exactly (no in-loop filters). - -use crate::cabac::{BIT, CabacEncoder, Estimator, Sink}; -use crate::inter::{self, PlaneRef}; -use crate::intra::{self, Refs}; -use crate::me::MeRef; -use crate::params::{LOG2_CTB, LOG2_MAX_TB, LOG2_MIN_CB, MAX_MERGE_CAND}; -use crate::tables::{ - ABS_MVD_GT0, ABS_MVD_GT1, CBF_CHROMA, CBF_LUMA, CODED_SUB_BLOCK, CTX_IDX_MAP, CU_SKIP, GT1, GT2, INTRA_CHROMA, LAST_X_PREFIX, LAST_Y_PREFIX, MERGE_FLAG, - MERGE_IDX, MVP_FLAG, PART_MODE, PRED_MODE, PREV_INTRA_LUMA, RQT_ROOT_CBF, SCAN_4, SIG_COEFF, SPLIT_CU, qpc_420, scan_order, -}; -use crate::transform; - -pub const F_INTRA: u8 = 1; -pub const F_SKIP: u8 = 2; -/// The luma transform block covering this 4x4 block has non-zero coefficients. -pub const F_CBF: u8 = 4; -/// The left / top edge of this 4x4 block is a transform (and prediction) block edge. -pub const E_EDGE_V: u8 = 8; -pub const E_EDGE_H: u8 = 16; - -/// Per 4x4 luma block state of the current picture. -#[derive(Clone, Copy, Default)] -pub struct Blk { - pub depth: u8, - pub flags: u8, - /// IntraPredModeY (DC for inter blocks). - pub ipm: u8, - /// L0 motion vector (quarter samples) of inter blocks. - pub mv: [i16; 2], -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum CuMode { - Intra { nxn: bool, modes: [u8; 4] }, - Skip { idx: u8 }, - Merge { idx: u8 }, - Amvp { mv: [i16; 2], mvp: u8 }, -} - -#[derive(Clone, Copy, Debug)] -struct CuDec { - x: u32, - y: u32, - log2: u32, - mode: CuMode, -} - -/// One coded transform block of the current CU. -#[derive(Clone, Copy)] -struct Tb { - c: u8, - /// Luma-coordinate region the block belongs to (for cbf derivation). - lx: u32, - ly: u32, - log2: u32, - nz: bool, - off: usize, - scan: u8, -} - -/// Saved samples and block state of a square region. -struct Snap { - x: usize, - y: usize, - n: usize, - planes: [Vec; 3], - blk: Vec, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum SliceKind { - I, - P, -} - -/// Picture geometry and slice coding parameters shared by the CTU encoder. -pub struct FrameConfig { - pub bit_depth: u32, - pub width: usize, - pub height: usize, - pub kind: SliceKind, - pub slice_qp: i32, -} - -/// A square node in the coding or transform quadtree. -#[derive(Clone, Copy)] -struct TreeNode { - x: usize, - y: usize, - log2: u32, - depth: u32, -} - -impl TreeNode { - fn children(self) -> impl Iterator { - let h = 1usize << (self.log2 - 1); - [(0, 0), (h, 0), (0, h), (h, h)].into_iter().map(move |(dx, dy)| Self { x: self.x + dx, y: self.y + dy, log2: self.log2 - 1, depth: self.depth + 1 }) - } -} - -/// Transform syntax inherited from the parent node and the enclosing CU. -#[derive(Clone, Copy)] -struct TransformState { - blk_idx: u32, - parent: [bool; 2], - intra: bool, - nxn: bool, -} - -pub struct FrameCoder<'a> { - pub bd: u32, - pub w: usize, - pub h: usize, - pub cw: usize, - pub ch: usize, - w4: usize, - wctb: usize, - hctb: usize, - pub kind: SliceKind, - /// QP' (with QpBdOffset) for luma and chroma. - slice_qp: i32, - qp_y: i32, - qp_c: i32, - lambda: f64, - lambda_sad: f64, - src: &'a [Vec; 3], - pub rec: [Vec; 3], - refp: Option<&'a [Vec; 3]>, - me: Option<&'a MeRef>, - blk: Vec, - tbs: Vec, - coefs: Vec, - pred: Vec, - scratch: [Vec; 2], -} - -#[inline] -fn morton(x: u32, y: u32) -> u32 { - let mut r = 0; - for i in 0..5 { - r |= ((x >> i) & 1) << (2 * i); - r |= ((y >> i) & 1) << (2 * i + 1); - } - r -} - -/// Intra modes tried in the coarse pass of the SATD search. -const COARSE_MODES: [u32; 11] = [0, 1, 2, 6, 10, 14, 18, 22, 26, 30, 34]; - -impl<'a> FrameCoder<'a> { - pub fn new(config: FrameConfig, src: &'a [Vec; 3], refp: Option<&'a [Vec; 3]>, me: Option<&'a MeRef>) -> Self { - let FrameConfig { bit_depth: bd, width: w, height: h, kind, slice_qp } = config; - let off = 6 * (bd as i32 - 8); - let qpi = slice_qp.clamp(-off, 57); - let qp_c = qpc_420(qpi) + off; - let base = if kind == SliceKind::I { 0.57 } else { 0.62 }; - let lambda = base * 2f64.powf((slice_qp as f64 - 12.0) / 3.0) * (1u64 << (2 * (bd - 8))) as f64; - let ctb = 1usize << LOG2_CTB; - FrameCoder { - bd, - w, - h, - cw: w / 2, - ch: h / 2, - w4: w / 4, - wctb: w.div_ceil(ctb), - hctb: h.div_ceil(ctb), - kind, - slice_qp, - qp_y: slice_qp + off, - qp_c, - lambda, - lambda_sad: lambda.sqrt(), - src, - rec: [vec![0; w * h], vec![0; w * h / 4], vec![0; w * h / 4]], - refp, - me, - blk: vec![Blk::default(); (w / 4) * (h / 4)], - tbs: Vec::with_capacity(16), - coefs: Vec::with_capacity(64 * 64 * 2), - pred: vec![0; 64 * 64], - scratch: [vec![0; 32 * 32], vec![0; 32 * 32]], - } - } - - // --------------------------------------------------------------------------------------- - // neighbourhood - - /// z-scan order availability (6.4.1) of (xn, yn) for the block at (xc, yc): single slice, no tiles. - #[inline] - fn zavail(&self, xc: i32, yc: i32, xn: i32, yn: i32) -> bool { - if xn < 0 || yn < 0 || xn >= self.w as i32 || yn >= self.h as i32 { - return false; - } - let s = LOG2_CTB; - let ctb_n = (yn >> s) as usize * self.wctb + (xn >> s) as usize; - let ctb_c = (yc >> s) as usize * self.wctb + (xc >> s) as usize; - if ctb_n != ctb_c { - ctb_n < ctb_c - } else { - let m = (1 << s) - 1; - morton(((xn & m) >> 2) as u32, ((yn & m) >> 2) as u32) <= morton(((xc & m) >> 2) as u32, ((yc & m) >> 2) as u32) - } - } - - #[inline] - fn blk_at(&self, x: i32, y: i32) -> &Blk { - &self.blk[(y as usize >> 2) * self.w4 + (x as usize >> 2)] - } - - fn fill(&mut self, x: usize, y: usize, n: usize, f: impl Fn(&mut Blk)) { - for by in (y >> 2)..((y + n) >> 2) { - for b in &mut self.blk[by * self.w4 + (x >> 2)..by * self.w4 + ((x + n) >> 2)] { - f(b); - } - } - } - - /// Mark the CU's left / top edges and, for CUs larger than the maximum transform size, the - /// inner transform edges (deblocking). - fn mark_edges(&mut self, x: usize, y: usize, log2: u32) { - let n = 1usize << log2; - let t = 1usize << log2.min(LOG2_MAX_TB); - for ty in (y..y + n).step_by(t) { - for tx in (x..x + n).step_by(t) { - for by in ty / 4..(ty + t) / 4 { - self.blk[by * self.w4 + tx / 4].flags |= E_EDGE_V; - } - for b in &mut self.blk[(ty / 4) * self.w4 + tx / 4..(ty / 4) * self.w4 + (tx + t) / 4] { - b.flags |= E_EDGE_H; - } - } - } - } - - /// In-loop deblocking of the finished picture. - pub fn deblock(&mut self) { - crate::deblock::deblock(&mut self.rec, &self.blk, self.w, self.h, self.slice_qp, self.bd); - } - - fn snap(&self, x: usize, y: usize, n: usize) -> Snap { - let mut planes: [Vec; 3] = Default::default(); - for (c, p) in planes.iter_mut().enumerate() { - let (px, py, pn, stride) = if c == 0 { (x, y, n, self.w) } else { (x / 2, y / 2, n / 2, self.cw) }; - p.reserve(pn * pn); - for r in 0..pn { - p.extend_from_slice(&self.rec[c][(py + r) * stride + px..][..pn]); - } - } - let mut blk = Vec::with_capacity((n / 4) * (n / 4)); - for by in (y >> 2)..((y + n) >> 2) { - blk.extend_from_slice(&self.blk[by * self.w4 + (x >> 2)..by * self.w4 + ((x + n) >> 2)]); - } - Snap { x, y, n, planes, blk } - } - - fn restore(&mut self, s: &Snap) { - let (x, y, n) = (s.x, s.y, s.n); - for c in 0..3 { - let (px, py, pn, stride) = if c == 0 { (x, y, n, self.w) } else { (x / 2, y / 2, n / 2, self.cw) }; - for r in 0..pn { - self.rec[c][(py + r) * stride + px..][..pn].copy_from_slice(&s.planes[c][r * pn..r * pn + pn]); - } - } - let bw = n / 4; - for (i, by) in ((y >> 2)..((y + n) >> 2)).enumerate() { - self.blk[by * self.w4 + (x >> 2)..by * self.w4 + (x >> 2) + bw].copy_from_slice(&s.blk[i * bw..i * bw + bw]); - } - } - - // --------------------------------------------------------------------------------------- - // picture loop - - /// Code all CTUs of the picture into `enc` (one slice); the caller flushes the encoder. - pub fn encode(&mut self, enc: &mut CabacEncoder) { - let ctb = 1usize << LOG2_CTB; - let total = self.wctb * self.hctb; - for i in 0..total { - let (x, y) = ((i % self.wctb) * ctb, (i / self.wctb) * ctb); - let mut est = Estimator::new(enc.ctx); - let mut decs = Vec::with_capacity(64); - self.decide(x, y, LOG2_CTB, 0, &mut est, &mut decs); - let mut idx = 0; - self.write_cq(enc, TreeNode { x, y, log2: LOG2_CTB, depth: 0 }, &decs, &mut idx); - debug_assert_eq!(idx, decs.len()); - debug_assert_eq!(est.ctx, enc.ctx); - enc.terminate((i + 1 == total) as u32); // end_of_slice_segment_flag - } - } - - fn write_split(&self, s: &mut S, x: usize, y: usize, depth: u32, split: bool) { - let (x, y) = (x as i32, y as i32); - let mut inc = 0; - if self.zavail(x, y, x - 1, y) && self.blk_at(x - 1, y).depth as u32 > depth { - inc += 1; - } - if self.zavail(x, y, x, y - 1) && self.blk_at(x, y - 1).depth as u32 > depth { - inc += 1; - } - s.decision(SPLIT_CU + inc, split as u32); - } - - /// Final pass: re-code the decided quadtree with the real arithmetic coder. - fn write_cq(&mut self, enc: &mut CabacEncoder, node: TreeNode, decs: &[CuDec], idx: &mut usize) { - let TreeNode { x, y, log2, depth } = node; - if x >= self.w || y >= self.h { - return; - } - let n = 1usize << log2; - let inside = x + n <= self.w && y + n <= self.h; - let d = decs[*idx]; - let leaf = d.x as usize == x && d.y as usize == y && d.log2 == log2; - if inside && log2 > LOG2_MIN_CB { - self.write_split(enc, x, y, depth, !leaf); - } else { - debug_assert_eq!(leaf, log2 == LOG2_MIN_CB); - } - if leaf { - *idx += 1; - let (eff, _) = self.encode_cu(enc, x, y, log2, depth, d.mode); - debug_assert_eq!(eff, d.mode); - } else { - for child in node.children() { - self.write_cq(enc, child, decs, idx); - } - } - } - - #[inline] - fn cost(&self, dist: u64, bits: u64) -> f64 { - dist as f64 + self.lambda * bits as f64 / BIT as f64 - } - - /// Rate-distortion decision for the quadtree node at (x, y); leaves the chosen reconstruction - /// and block state in place, advances `est` and appends the decided CUs. Returns the RD cost. - fn decide(&mut self, x: usize, y: usize, log2: u32, depth: u32, est: &mut Estimator, decs: &mut Vec) -> f64 { - if x >= self.w || y >= self.h { - return 0.0; - } - let n = 1usize << log2; - let inside = x + n <= self.w && y + n <= self.h; - let can_split = log2 > LOG2_MIN_CB; - let base = est.bits; - let leaf_ok = inside && (self.kind == SliceKind::P || log2 <= LOG2_MAX_TB); - let mut best: Option<(f64, Estimator, Snap, CuMode)> = None; - if leaf_ok { - let mut e0 = est.clone(); - if can_split { - self.write_split(&mut e0, x, y, depth, false); - } - let try_mode = |me: &mut Self, mode: CuMode, best: &mut Option<(f64, Estimator, Snap, CuMode)>| { - let mut e = e0.clone(); - let (eff, dist) = me.encode_cu(&mut e, x, y, log2, depth, mode); - let cost = me.cost(dist, e.bits - base); - if best.as_ref().is_none_or(|b| cost < b.0) { - *best = Some((cost, e, me.snap(x, y, n), eff)); - } - }; - let mut inter_satd = u64::MAX; - if self.kind == SliceKind::P { - let merge = self.merge_list(x, y, n); - let (midx, msatd) = self.best_merge(x, y, n, &merge); - try_mode(self, CuMode::Skip { idx: midx as u8 }, &mut best); - try_mode(self, CuMode::Merge { idx: midx as u8 }, &mut best); - let amvp = self.amvp(x, y, n); - let (mv, mvp, asatd) = self.motion_search(x, y, n, &amvp, &merge); - if mv != merge[midx] { - try_mode(self, CuMode::Amvp { mv, mvp }, &mut best); - } - inter_satd = msatd.min(asatd); - } - if log2 <= LOG2_MAX_TB && !matches!(best, Some((_, _, _, CuMode::Skip { .. }))) { - let (mode, satd) = self.search_intra_mode(x, y, log2); - if satd < inter_satd { - try_mode(self, CuMode::Intra { nxn: false, modes: [mode as u8; 4] }, &mut best); - if log2 == LOG2_MIN_CB { - let modes = self.search_nxn_modes(x, y); - try_mode(self, CuMode::Intra { nxn: true, modes }, &mut best); - } - } - } - if let Some(b) = &best { - self.restore(&b.2); - } - } - let early = matches!(best, Some((_, _, _, CuMode::Skip { .. }))); - if can_split && (!leaf_ok || !early) { - let mut e = est.clone(); - if inside { - self.write_split(&mut e, x, y, depth, true); - } - let limit = best.as_ref().map_or(f64::INFINITY, |b| b.0); - let mut cost = self.cost(0, e.bits - base); - let mut d2 = Vec::new(); - let h = n / 2; - for (dx, dy) in [(0, 0), (h, 0), (0, h), (h, h)] { - cost += self.decide(x + dx, y + dy, log2 - 1, depth + 1, &mut e, &mut d2); - if cost >= limit { - break; - } - } - if cost < limit { - *est = e; - decs.extend(d2); - return cost; - } - } - // A coding unit always has at least one candidate mode (intra, or skip/merge in P - // slices); without one, report an infinite cost. - let Some((cost, e, snap, mode)) = best else { return f64::INFINITY }; - self.restore(&snap); - *est = e; - decs.push(CuDec { x: x as u32, y: y as u32, log2, mode }); - cost - } - - // --------------------------------------------------------------------------------------- - // coding unit - - fn skip_ctx(&self, x: usize, y: usize) -> usize { - let (x, y) = (x as i32, y as i32); - let mut inc = 0; - if self.zavail(x, y, x - 1, y) && self.blk_at(x - 1, y).flags & F_SKIP != 0 { - inc += 1; - } - if self.zavail(x, y, x, y - 1) && self.blk_at(x, y - 1).flags & F_SKIP != 0 { - inc += 1; - } - inc - } - - /// Predict, transform, quantise and reconstruct one CU and write its syntax into `s`. - /// Returns the effective mode (a residual-free merge becomes a skip) and the SSE. - fn encode_cu(&mut self, s: &mut S, x: usize, y: usize, log2: u32, depth: u32, mode: CuMode) -> (CuMode, u64) { - let n = 1usize << log2; - self.tbs.clear(); - self.coefs.clear(); - let p_slice = self.kind == SliceKind::P; - let skip_ctx = if p_slice { self.skip_ctx(x, y) } else { 0 }; - let d8 = depth as u8; - match mode { - CuMode::Intra { nxn, modes } => { - self.fill(x, y, n, |b| { - b.depth = d8; - b.flags = F_INTRA; - b.ipm = modes[0]; - }); - self.mark_edges(x, y, log2); - if nxn { - let h = n / 2; - for k in 1..4 { - let m = modes[k]; - self.fill(x + (k & 1) * h, y + (k >> 1) * h, h, |b| b.ipm = m); - } - for k in 0..4 { - let (px, py) = (x + (k & 1) * h, y + (k >> 1) * h); - self.code_tb(0, px, py, 2, Some(modes[k] as u32), (x, y)); - } - for c in 1..3 { - self.code_tb(c, x / 2, y / 2, 2, Some(modes[0] as u32), (x, y)); - } - } else { - self.code_tus(x, y, log2, Some(modes[0] as u32)); - } - if p_slice { - s.decision(CU_SKIP + skip_ctx, 0); - s.decision(PRED_MODE, 1); - } - if log2 == LOG2_MIN_CB { - s.decision(PART_MODE, (!nxn) as u32); - } - self.write_intra_modes(s, x, y, n, nxn, &modes); - s.decision(INTRA_CHROMA, 0); // intra_chroma_pred_mode 4 (DM) - let mut ti = 0; - self.write_tt(s, TreeNode { x, y, log2, depth: 0 }, TransformState { blk_idx: 0, parent: [true, true], intra: true, nxn }, &mut ti); - (mode, self.cu_dist(x, y, n)) - } - _ => { - let mv = match mode { - CuMode::Skip { idx } | CuMode::Merge { idx } => self.merge_list(x, y, n)[idx as usize], - CuMode::Amvp { mv, .. } => mv, - // Handled by the arm above. - CuMode::Intra { .. } => [0, 0], - }; - let amvp = if let CuMode::Amvp { .. } = mode { self.amvp(x, y, n) } else { [[0; 2]; 2] }; - let skip = matches!(mode, CuMode::Skip { .. }); - self.fill(x, y, n, |b| { - b.depth = d8; - b.flags = if skip { F_SKIP } else { 0 }; - b.ipm = 1; - b.mv = mv; - }); - self.mark_edges(x, y, log2); - self.predict_inter(x, y, n, mv); - let mut eff = mode; - let mut root = false; - if !skip { - self.code_tus(x, y, log2, None); - root = self.tbs.iter().any(|t| t.nz); - if !root && let CuMode::Merge { idx } = mode { - // rqt_root_cbf would be inferred: code a skipped CU instead - eff = CuMode::Skip { idx }; - self.fill(x, y, n, |b| b.flags |= F_SKIP); - } - } - match eff { - CuMode::Skip { idx } => { - s.decision(CU_SKIP + skip_ctx, 1); - write_merge_idx(s, idx as usize); - } - CuMode::Merge { idx } => { - s.decision(CU_SKIP + skip_ctx, 0); - s.decision(PRED_MODE, 0); - s.decision(PART_MODE, 1); // PART_2Nx2N - s.decision(MERGE_FLAG, 1); - write_merge_idx(s, idx as usize); - let mut ti = 0; - self.write_tt( - s, - TreeNode { x, y, log2, depth: 0 }, - TransformState { blk_idx: 0, parent: [true, true], intra: false, nxn: false }, - &mut ti, - ); - } - CuMode::Amvp { mv, mvp } => { - s.decision(CU_SKIP + skip_ctx, 0); - s.decision(PRED_MODE, 0); - s.decision(PART_MODE, 1); - s.decision(MERGE_FLAG, 0); - let p = amvp[mvp as usize]; - write_mvd(s, [mv[0] as i32 - p[0] as i32, mv[1] as i32 - p[1] as i32]); - s.decision(MVP_FLAG, mvp as u32); - s.decision(RQT_ROOT_CBF, root as u32); - if root { - let mut ti = 0; - self.write_tt( - s, - TreeNode { x, y, log2, depth: 0 }, - TransformState { blk_idx: 0, parent: [true, true], intra: false, nxn: false }, - &mut ti, - ); - } - } - // Handled by the arm above. - CuMode::Intra { .. } => {} - } - (eff, self.cu_dist(x, y, n)) - } - } - } - - /// Transform units of a 2Nx2N CU (split to the maximum TB size), luma then Cb, Cr per TU. - fn code_tus(&mut self, x: usize, y: usize, log2: u32, intra_mode: Option) { - let tl = log2.min(LOG2_MAX_TB); - let t = 1usize << tl; - let k = 1usize << (log2 - tl); - for i in 0..k * k { - // z-order of at most 2x2 TUs - let (tx, ty) = (x + (i & 1) * t, y + (i >> 1) * t); - self.code_tb(0, tx, ty, tl, intra_mode, (tx, ty)); - self.code_tb(1, tx / 2, ty / 2, tl - 1, intra_mode, (tx, ty)); - self.code_tb(2, tx / 2, ty / 2, tl - 1, intra_mode, (tx, ty)); - } - } - - fn cu_dist(&self, x: usize, y: usize, n: usize) -> u64 { - let mut d = 0u64; - for c in 0..3 { - let (px, py, pn, stride) = if c == 0 { (x, y, n, self.w) } else { (x / 2, y / 2, n / 2, self.cw) }; - for r in 0..pn { - let o = (py + r) * stride + px; - let a = &self.src[c][o..o + pn]; - let b = &self.rec[c][o..o + pn]; - d += a.iter().zip(b).map(|(&p, &q)| (p as i64 - q as i64).pow(2) as u64).sum::(); - } - } - d - } - - /// Code one transform block of component `c` at component position (x, y): optional intra - /// prediction, residual, transform, quantisation and reconstruction. - fn code_tb(&mut self, c: usize, x: usize, y: usize, log2: u32, intra_mode: Option, lpos: (usize, usize)) { - let n = 1usize << log2; - let stride = if c == 0 { self.w } else { self.cw }; - if let Some(m) = intra_mode { - let refs = self.build_refs(c, x, y, log2); - let bd = self.bd; - predict_from_refs(&refs, c, m, n, bd, &mut self.rec[c][y * stride + x..], stride); - } - let mut scr = std::mem::take(&mut self.scratch); - let [r, coef] = &mut scr; - for yy in 0..n { - let o = (y + yy) * stride + x; - for xx in 0..n { - r[yy * n + xx] = self.src[c][o + xx] as i32 - self.rec[c][o + xx] as i32; - } - } - let intra = intra_mode.is_some(); - let dst = intra && c == 0 && n == 4; - transform::forward(&r[..n * n], &mut coef[..n * n], n, dst, self.bd); - let qp = if c == 0 { self.qp_y } else { self.qp_c }; - let off = self.coefs.len(); - self.coefs.resize(off + n * n, 0); - let lv = &mut self.coefs[off..off + n * n]; - let scan = match intra_mode { - Some(m) if log2 == 2 || (log2 == 3 && c == 0) => scan_idx_for_mode(m), - _ => 0, - }; - let nz = transform::quantize(&coef[..n * n], lv, n, qp, intra, self.bd) > 0; - if nz { - transform::sign_hiding(&coef[..n * n], lv, log2, scan as usize, qp, self.bd); - } - if nz { - let d = &mut r[..n * n]; - transform::dequantize(lv, d, n, qp, self.bd); - transform::inverse(d, n, dst, self.bd); - let max = (1i32 << self.bd) - 1; - for yy in 0..n { - let o = (y + yy) * stride + x; - for xx in 0..n { - let v = &mut self.rec[c][o + xx]; - *v = (*v as i32 + d[yy * n + xx]).clamp(0, max) as u16; - } - } - } - self.scratch = scr; - if nz && c == 0 { - self.fill(x, y, n, |b| b.flags |= F_CBF); - } - self.tbs.push(Tb { c: c as u8, lx: lpos.0 as u32, ly: lpos.1 as u32, log2, nz, off, scan }); - } - - /// Reference samples (substituted, unfiltered) of an intra block, as in 8.4.4.2.1 / 8.4.4.2.2. - fn build_refs(&self, c: usize, x0: usize, y0: usize, log2: u32) -> Refs { - let n = 1usize << log2; - let sc = if c == 0 { 1 } else { 2 }; - let (x0, y0) = (x0 as i32, y0 as i32); - let (xl, yl) = (x0 * sc, y0 * sc); - let unit = 4 / sc as usize; - let stride = if c == 0 { self.w } else { self.cw }; - let mut al = [false; 129]; - let mut at = [false; 129]; - let plane = &self.rec[c]; - let mut r = Refs::default(); - if self.zavail(xl, yl, xl - 1, yl - 1) { - al[0] = true; - at[0] = true; - let v = plane[(y0 - 1) as usize * stride + (x0 - 1) as usize]; - r.left[0] = v; - r.top[0] = v; - } - let mut k = 0; - while k < 2 * n { - let yn = y0 + k as i32; - if self.zavail(xl, yl, xl - 1, yn * sc) { - for i in 0..unit { - al[1 + k + i] = true; - r.left[1 + k + i] = plane[(yn as usize + i) * stride + (x0 - 1) as usize]; - } - } - let xn = x0 + k as i32; - if self.zavail(xl, yl, xn * sc, yl - 1) { - let o = (y0 - 1) as usize * stride + xn as usize; - for i in 0..unit { - at[1 + k + i] = true; - r.top[1 + k + i] = plane[o + i]; - } - } - k += unit; - } - intra::substitute(&mut r, &al, &at, n, self.bd); - r - } - - // --------------------------------------------------------------------------------------- - // syntax writers - - /// Candidate modes candModeList (8.4.2) of the prediction block at (xp, yp). - fn mpm(&self, xp: usize, yp: usize) -> [u32; 3] { - let (xp, yp) = (xp as i32, yp as i32); - let cand_a = if self.zavail(xp, yp, xp - 1, yp) { - let b = self.blk_at(xp - 1, yp); - if b.flags & F_INTRA != 0 { b.ipm as u32 } else { 1 } - } else { - 1 - }; - let cand_b = if self.zavail(xp, yp, xp, yp - 1) && yp > ((yp >> LOG2_CTB) << LOG2_CTB) { - let b = self.blk_at(xp, yp - 1); - if b.flags & F_INTRA != 0 { b.ipm as u32 } else { 1 } - } else { - 1 - }; - if cand_a == cand_b { - if cand_a < 2 { [0, 1, 26] } else { [cand_a, 2 + ((cand_a + 29) % 32), 2 + ((cand_a - 2 + 1) % 32)] } - } else { - let c = if cand_a != 0 && cand_b != 0 { - 0 - } else if cand_a != 1 && cand_b != 1 { - 1 - } else { - 26 - }; - [cand_a, cand_b, c] - } - } - - fn write_intra_modes(&self, s: &mut S, x: usize, y: usize, n: usize, nxn: bool, modes: &[u8; 4]) { - let nb = if nxn { 2 } else { 1 }; - let pb = n / nb; - let mut codes = [(false, 0u32); 4]; - for j in 0..nb { - for i in 0..nb { - let k = j * nb + i; - let list = self.mpm(x + i * pb, y + j * pb); - let m = modes[k] as u32; - codes[k] = match list.iter().position(|&c| c == m) { - Some(idx) => (true, idx as u32), - None => { - let mut sorted = list; - sorted.sort_unstable(); - let mut r = m; - for &c in sorted.iter().rev() { - if r > c { - r -= 1; - } - } - (false, r) - } - }; - } - } - for code in codes.iter().take(nb * nb) { - s.decision(PREV_INTRA_LUMA, code.0 as u32); - } - for &(prev, v) in codes.iter().take(nb * nb) { - if prev { - s.bypass((v > 0) as u32); - if v > 0 { - s.bypass((v > 1) as u32); - } - } else { - s.bypass_bits(v, 5); - } - } - } - - fn chroma_cbf(&self, c: u8, x0: usize, y0: usize, n: usize) -> bool { - self.tbs.iter().any(|t| t.c == c && t.nz && (t.lx as usize) >= x0 && (t.lx as usize) < x0 + n && (t.ly as usize) >= y0 && (t.ly as usize) < y0 + n) - } - - /// transform_tree() (7.3.8.8) over the coded transform blocks of the CU; no split flag is ever - /// coded (max_transform_hierarchy_depth_* = 0), splits are inferred. - fn write_tt(&self, s: &mut S, node: TreeNode, state: TransformState, ti: &mut usize) { - let TreeNode { x: x0, y: y0, log2, depth } = node; - let TransformState { blk_idx, parent, intra, nxn } = state; - let n = 1usize << log2; - let split = log2 > LOG2_MAX_TB || (nxn && depth == 0); - let mut cbf_c = [false; 2]; - if log2 > 2 { - for c in 0..2 { - if depth == 0 || parent[c] { - cbf_c[c] = self.chroma_cbf(c as u8 + 1, x0, y0, n); - s.decision(CBF_CHROMA + depth as usize, cbf_c[c] as u32); - } - } - } else if depth > 0 { - cbf_c = parent; - } - if split { - for (i, child) in node.children().enumerate() { - self.write_tt(s, child, TransformState { blk_idx: i as u32, parent: cbf_c, ..state }, ti); - } - return; - } - let tb = self.tbs[*ti]; - *ti += 1; - debug_assert_eq!(tb.c, 0); - if intra || depth != 0 || cbf_c[0] || cbf_c[1] { - s.decision(CBF_LUMA + (depth == 0) as usize, tb.nz as u32); - } else { - debug_assert!(tb.nz, "inferred cbf_luma needs coefficients"); - } - if tb.nz { - let n2 = 1usize << (2 * tb.log2); - residual_coding(s, &self.coefs[tb.off..tb.off + n2], tb.log2, 0, tb.scan as usize); - } - if log2 > 2 || blk_idx == 3 { - for c in 0..2 { - let tb = self.tbs[*ti]; - *ti += 1; - debug_assert_eq!(tb.c as usize, c + 1); - debug_assert_eq!(tb.nz, cbf_c[c]); - if cbf_c[c] { - let n2 = 1usize << (2 * tb.log2); - residual_coding(s, &self.coefs[tb.off..tb.off + n2], tb.log2, c + 1, tb.scan as usize); - } - } - } - } - - // --------------------------------------------------------------------------------------- - // inter prediction - - /// Prediction block availability (6.4.2) for a 2Nx2N CU: available and not intra. - #[inline] - fn pb_avail(&self, xp: i32, yp: i32, xn: i32, yn: i32) -> bool { - self.zavail(xp, yp, xn, yn) && self.blk_at(xn, yn).flags & F_INTRA == 0 - } - - /// Merge candidate list (8.5.3.2.2 - 8.5.3.2.5) of a 2Nx2N P-slice CU: spatial candidates and - /// zero candidates (no temporal candidate, one reference picture). - pub fn merge_list(&self, x: usize, y: usize, n: usize) -> [[i16; 2]; MAX_MERGE_CAND] { - let (xp, yp, w, h) = (x as i32, y as i32, n as i32, n as i32); - let mv = |p: (i32, i32)| self.blk_at(p.0, p.1).mv; - let mut list = [[0i16; 2]; MAX_MERGE_CAND]; - let mut k = 0; - let pa1 = (xp - 1, yp + h - 1); - let av_a1 = self.pb_avail(xp, yp, pa1.0, pa1.1); - if av_a1 { - list[k] = mv(pa1); - k += 1; - } - let pb1 = (xp + w - 1, yp - 1); - let av_b1 = self.pb_avail(xp, yp, pb1.0, pb1.1); - let fl_b1 = av_b1 && !(av_a1 && mv(pa1) == mv(pb1)); - if fl_b1 { - list[k] = mv(pb1); - k += 1; - } - let pb0 = (xp + w, yp - 1); - let fl_b0 = self.pb_avail(xp, yp, pb0.0, pb0.1) && !(av_b1 && mv(pb1) == mv(pb0)); - if fl_b0 { - list[k] = mv(pb0); - k += 1; - } - let pa0 = (xp - 1, yp + h); - let fl_a0 = self.pb_avail(xp, yp, pa0.0, pa0.1) && !(av_a1 && mv(pa1) == mv(pa0)); - if fl_a0 { - list[k] = mv(pa0); - k += 1; - } - let pb2 = (xp - 1, yp - 1); - let fl_b2 = self.pb_avail(xp, yp, pb2.0, pb2.1) - && !((av_a1 && mv(pa1) == mv(pb2)) || (av_b1 && mv(pb1) == mv(pb2))) - && (fl_a0 as u32 + av_a1 as u32 + fl_b0 as u32 + fl_b1 as u32) != 4; - if fl_b2 { - list[k] = mv(pb2); - } - // remaining entries are zero candidates with refIdx 0 - list - } - - /// AMVP candidates (8.5.3.2.6, 8.5.3.2.7) of a 2Nx2N CU with one reference picture. - pub fn amvp(&self, x: usize, y: usize, n: usize) -> [[i16; 2]; 2] { - let (xp, yp, w, h) = (x as i32, y as i32, n as i32, n as i32); - let pa = [(xp - 1, yp + h), (xp - 1, yp + h - 1)]; - let av_a = [self.pb_avail(xp, yp, pa[0].0, pa[0].1), self.pb_avail(xp, yp, pa[1].0, pa[1].1)]; - let is_scaled = av_a[0] || av_a[1]; - let mut a = None; - for k in 0..2 { - if av_a[k] && a.is_none() { - a = Some(self.blk_at(pa[k].0, pa[k].1).mv); - } - } - let pb = [(xp + w, yp - 1), (xp + w - 1, yp - 1), (xp - 1, yp - 1)]; - let mut b = None; - for p in pb { - if b.is_none() && self.pb_avail(xp, yp, p.0, p.1) { - b = Some(self.blk_at(p.0, p.1).mv); - } - } - if !is_scaled && b.is_some() { - a = b; - } - let mut list = [[0i16; 2]; 2]; - match (a, b) { - (Some(a), Some(b)) => { - list[0] = a; - if b != a { - list[1] = b; - } - } - (Some(a), None) => list[0] = a, - (None, Some(b)) => list[0] = b, - (None, None) => {} - } - list - } - - fn predict_inter(&mut self, x: usize, y: usize, n: usize, mv: [i16; 2]) { - // Inter modes are only tried in P slices, which have a reference picture. - let Some(refp) = self.refp else { return }; - for c in 0..3 { - let (px, py, pn, pw, ph) = if c == 0 { (x, y, n, self.w, self.h) } else { (x / 2, y / 2, n / 2, self.cw, self.ch) }; - let pr = PlaneRef { data: &refp[c], w: pw, h: ph }; - if c == 0 { - inter::mc_luma(pr, self.bd, px as i32, py as i32, mv, (pn, pn), &mut self.pred); - } else { - inter::mc_chroma(pr, self.bd, px as i32, py as i32, mv, (pn, pn), &mut self.pred); - } - inter::put_uni(&self.pred, pn, pn, self.bd, &mut self.rec[c][py * pw + px..], pw); - } - } - - // --------------------------------------------------------------------------------------- - // estimation helpers - - /// Best merge index by SATD of the (approximate) luma prediction. - fn best_merge(&self, x: usize, y: usize, n: usize, list: &[[i16; 2]; MAX_MERGE_CAND]) -> (usize, u64) { - let Some(me) = self.me else { return (0, u64::MAX / 4) }; - let mut buf = vec![0u16; n * n]; - let mut best = (0, u64::MAX); - for (i, &mv) in list.iter().enumerate() { - if list[..i].contains(&mv) { - continue; - } - me.approx_pred(x as i32, y as i32, n, mv, &mut buf); - let satd = satd(&self.src[0][y * self.w + x..], self.w, &buf, n, n) as u64; - let cost = satd + (self.lambda_sad * (i + 1) as f64) as u64; - if cost < best.1 { - best = (i, cost); - } - } - best - } - - /// Integer then fractional motion search. Returns (mv, mvp index, SATD cost). - fn motion_search(&self, x: usize, y: usize, n: usize, amvp: &[[i16; 2]; 2], merge: &[[i16; 2]; MAX_MERGE_CAND]) -> ([i16; 2], u8, u64) { - let Some(me) = self.me else { return (amvp[0], 0, u64::MAX / 4) }; - let (xi, yi) = (x as i32, y as i32); - let src = &self.src[0][y * self.w + x..]; - let ls = self.lambda_sad; - let mv_bits = |mv: [i32; 2]| -> f64 { amvp.iter().map(|p| mvd_bits(mv[0] - p[0] as i32) + mvd_bits(mv[1] - p[1] as i32)).fold(f64::MAX, f64::min) }; - let (lo_x, hi_x, lo_y, hi_y) = me.int_range(xi, yi, n); - let clamp_i = |v: [i32; 2]| [v[0].clamp(lo_x, hi_x), v[1].clamp(lo_y, hi_y)]; - let sad_cost = |v: [i32; 2]| -> f64 { me.sad(src, self.w, xi + v[0], yi + v[1], n) as f64 + ls * mv_bits([v[0] * 4, v[1] * 4]) }; - // starting points - let mut best = [0i32; 2]; - let mut best_cost = sad_cost(best); - for mv in amvp.iter().chain(merge.iter()) { - let v = clamp_i([(mv[0] as i32 + 2) >> 2, (mv[1] as i32 + 2) >> 2]); - let c = sad_cost(v); - if c < best_cost { - best_cost = c; - best = v; - } - } - // square pattern search with decreasing steps - let mut step = 16; - while step >= 1 { - let mut moved = true; - let mut iters = 0; - while moved && iters < 4 { - moved = false; - iters += 1; - let center = best; - for (dx, dy) in [(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1)] { - let v = clamp_i([center[0] + dx * step, center[1] + dy * step]); - if v == center { - continue; - } - let c = sad_cost(v); - if c < best_cost { - best_cost = c; - best = v; - moved = true; - } - } - } - step /= 2; - } - // fractional refinement with SATD on the approximate interpolated planes - let mut buf = vec![0u16; n * n]; - let mut satd_cost = |mv: [i32; 2]| -> f64 { - me.approx_pred(xi, yi, n, [mv[0] as i16, mv[1] as i16], &mut buf); - satd(src, self.w, &buf, n, n) as f64 + ls * mv_bits(mv) - }; - let mut bq = [best[0] * 4, best[1] * 4]; - let mut bc = satd_cost(bq); - for step in [2, 1] { - let center = bq; - for (dx, dy) in [(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1)] { - let v = [center[0] + dx * step, center[1] + dy * step]; - let c = satd_cost(v); - if c < bc { - bc = c; - bq = v; - } - } - } - let mv = [bq[0] as i16, bq[1] as i16]; - let bits = |p: &[i16; 2]| mvd_bits(bq[0] - p[0] as i32) + mvd_bits(bq[1] - p[1] as i32); - let mvp = if bits(&amvp[1]) < bits(&amvp[0]) { 1 } else { 0 }; - (mv, mvp, bc as u64) - } - - /// Best 2Nx2N intra luma mode by SATD (coarse grid, then refinement around the best angle). - fn search_intra_mode(&self, x: usize, y: usize, log2: u32) -> (u32, u64) { - let n = 1usize << log2; - let refs = self.build_refs(0, x, y, log2); - let mpm = self.mpm(x, y); - let mut buf = vec![0u16; n * n]; - let src = &self.src[0][y * self.w + x..]; - let mut eval = |m: u32| -> u64 { - predict_from_refs(&refs, 0, m, n, self.bd, &mut buf, n); - let bits = match mpm.iter().position(|&c| c == m) { - Some(0) => 2.0, - Some(_) => 3.0, - None => 6.0, - }; - satd(src, self.w, &buf, n, n) as u64 + (self.lambda_sad * bits) as u64 - }; - let mut best = (1u32, u64::MAX); - let mut tried = [false; 35]; - for &m in COARSE_MODES.iter().chain(mpm.iter()) { - if !tried[m as usize] { - tried[m as usize] = true; - let c = eval(m); - if c < best.1 { - best = (m, c); - } - } - } - if best.0 >= 2 { - for d in [2i32, 1] { - let center = best.0 as i32; - for m in [center - d, center + d] { - if (2..=34).contains(&m) && !tried[m as usize] { - tried[m as usize] = true; - let c = eval(m as u32); - if c < best.1 { - best = (m as u32, c); - } - } - } - } - } - best - } - - /// Modes of the four 4x4 PUs of an NxN intra CU, chosen sequentially (each PU reconstructed - /// before the next one is searched, as in the real coding order). - fn search_nxn_modes(&mut self, x: usize, y: usize) -> [u8; 4] { - let mut modes = [1u8; 4]; - self.fill(x, y, 8, |b| { - b.flags = F_INTRA; - b.ipm = 1; - }); - for k in 0..4 { - let (px, py) = (x + (k & 1) * 4, y + (k >> 1) * 4); - let (m, _) = self.search_intra_mode(px, py, 2); - modes[k] = m as u8; - self.fill(px, py, 4, |b| b.ipm = m as u8); - self.tbs.clear(); - self.coefs.clear(); - self.code_tb(0, px, py, 2, Some(m), (px, py)); - } - modes - } -} - -/// Filter (luma) and predict an intra block from substituted references. -fn predict_from_refs(refs: &Refs, c: usize, mode: u32, n: usize, bd: u32, out: &mut [u16], os: usize) { - if c == 0 { - let mut r = refs.clone(); - intra::filter(&mut r, mode, n, true, true, bd); - intra::predict(&r, mode, n, true, bd, out, os); - } else { - intra::predict(refs, mode, n, false, bd, out, os); - } -} - -/// scanIdx (7.4.9.11) for an intra prediction mode. -fn scan_idx_for_mode(m: u32) -> u8 { - if (6..=14).contains(&m) { - 2 - } else if (22..=30).contains(&m) { - 1 - } else { - 0 - } -} - -/// Approximate bits of one motion vector difference component. -fn mvd_bits(v: i32) -> f64 { - if v == 0 { 1.0 } else { (2 * (32 - (v.unsigned_abs()).leading_zeros()) + 1) as f64 } -} - -fn write_merge_idx(s: &mut S, idx: usize) { - if MAX_MERGE_CAND > 1 { - s.decision(MERGE_IDX, (idx > 0) as u32); - if idx > 0 { - let mut i = 1; - while i < MAX_MERGE_CAND - 1 { - let b = idx > i; - s.bypass(b as u32); - if !b { - break; - } - i += 1; - } - } - } -} - -/// mvd_coding() (7.3.8.9). -fn write_mvd(s: &mut S, d: [i32; 2]) { - let g0 = [d[0] != 0, d[1] != 0]; - s.decision(ABS_MVD_GT0, g0[0] as u32); - s.decision(ABS_MVD_GT0, g0[1] as u32); - for k in 0..2 { - if g0[k] { - s.decision(ABS_MVD_GT1, (d[k].abs() > 1) as u32); - } - } - for k in 0..2 { - if g0[k] { - let a = d[k].unsigned_abs(); - if a > 1 { - // abs_mvd_minus2: EG1 - let mut v = a - 2; - let mut kk = 1; - while v >= (1 << kk) { - s.bypass(1); - v -= 1 << kk; - kk += 1; - } - s.bypass(0); - s.bypass_bits(v, kk); - } - s.bypass((d[k] < 0) as u32); - } - } -} - -/// coeff_abs_level_remaining binarisation (9.3.3.11): TR prefix up to 3 with Rice suffix, then -/// an escape with an Exp-Golomb-like suffix. -fn write_remaining(s: &mut S, v: u32, rice: u32) { - if v < (4 << rice) { - let p = v >> rice; - for _ in 0..p { - s.bypass(1); - } - s.bypass(0); - s.bypass_bits(v & ((1 << rice) - 1), rice); - } else { - let u = v - (2 << rice); - let kr = 31 - u.leading_zeros(); // k + rice - let k = kr - rice; - for _ in 0..k + 3 { - s.bypass(1); - } - s.bypass(0); - s.bypass_bits(u - (1 << kr), kr); - } -} - -/// last_sig_coeff_{x,y}_prefix value and suffix for a position. -fn last_prefix(v: u32) -> (u32, u32, u32) { - if v < 4 { - return (v, 0, 0); - } - let mut p = 4; - loop { - let nb = (p >> 1) - 1; - let min = (1 << nb) * (2 + (p & 1)); - let next_nb = ((p + 1) >> 1) - 1; - let next = (1 << next_nb) * (2 + ((p + 1) & 1)); - if v < next { - return (p, v - min, nb); - } - p += 1; - } -} - -/// Context increment for significant_coeff_flag (H.265 9.3.4.2). -fn significance_context(group: (usize, usize), pos: (usize, usize), log2: u32, prev_csbf: u8, c: usize, scan_idx: usize) -> usize { - let (xs, ys) = group; - let (xp, yp) = pos; - let (xc, yc) = (xs * 4 + xp, ys * 4 + yp); - if log2 == 2 { - CTX_IDX_MAP[(yc << 2) + xc] as usize - } else if xc + yc == 0 { - 0 - } else { - let mut sc = match prev_csbf { - 0 => { - if xp + yp == 0 { - 2 - } else if xp + yp < 3 { - 1 - } else { - 0 - } - } - 1 => { - if yp == 0 { - 2 - } else if yp == 1 { - 1 - } else { - 0 - } - } - 2 => { - if xp == 0 { - 2 - } else if xp == 1 { - 1 - } else { - 0 - } - } - _ => 2, - }; - if c == 0 { - if xs + ys > 0 { - sc += 3; - } - sc += if log2 == 3 { if scan_idx == 0 { 9 } else { 15 } } else { 21 }; - } else { - sc += if log2 == 3 { 9 } else { 12 }; - } - sc - } -} - -/// Absolute levels, signs and Rice remainders for one significant coefficient group. -fn write_coefficient_levels(s: &mut S, sig: &[u8], at: impl Fn(usize) -> i32, i: usize, c: usize, prev_g1: &mut Option) { - let nsig = sig.len(); - let abs_at = |k: usize| at(sig[k] as usize).unsigned_abs(); - let mut ctx_set = if i == 0 || c > 0 { 0 } else { 2 }; - if *prev_g1 == Some(0) { - ctx_set += 1; - } - let mut g1ctx = 1u32; - let mut gt1 = [0u8; 16]; - let mut first_g1: Option = None; - let c_off_g1 = if c > 0 { 16 } else { 0 }; - for k in 0..nsig.min(8) { - let f = abs_at(k) > 1; - s.decision(GT1 + ctx_set * 4 + g1ctx.min(3) as usize + c_off_g1, f as u32); - gt1[k] = f as u8; - if f { - g1ctx = 0; - if first_g1.is_none() { - first_g1 = Some(k); - } - } else if g1ctx > 0 { - g1ctx += 1; - } - } - *prev_g1 = Some(g1ctx); - let mut gt2 = 0u32; - if let Some(k) = first_g1 { - gt2 = (abs_at(k) > 2) as u32; - s.decision(GT2 + ctx_set + if c > 0 { 4 } else { 0 }, gt2); - } - // sign data hiding: the sign of the first coefficient in scan order (sig[nsig - 1]) is - // inferred from the parity of the sum of absolute levels - let hidden = sig[0] as i32 - sig[nsig - 1] as i32 > 3; - let nsigns = if hidden { nsig - 1 } else { nsig }; - for k in 0..nsigns { - s.bypass((at(sig[k] as usize) < 0) as u32); - } - if hidden { - let sum: u32 = (0..nsig).map(abs_at).sum(); - debug_assert_eq!(at(sig[nsig - 1] as usize) < 0, sum & 1 == 1, "sign hiding parity"); - } - let mut rice = 0u32; - for k in 0..nsig { - let mut base = 1 + gt1[k] as u32; - if Some(k) == first_g1 { - base += gt2; - } - let thresh = if k < 8 { if Some(k) == first_g1 { 3 } else { 2 } } else { 1 }; - if base == thresh { - let level = abs_at(k); - write_remaining(s, level - base, rice); - if level > 3 * (1 << rice) { - rice = (rice + 1).min(4); - } - } - } -} - -/// residual_coding() (7.3.8.11) of one transform block of levels (row-major, n x n). No transform -/// skip; signs use sign data hiding where the coefficient span permits it. -fn residual_coding(s: &mut S, lv: &[i32], log2: u32, c: usize, scan_idx: usize) { - let n = 1usize << log2; - let log2sb = log2 - 2; - let sb_w = 1usize << log2sb; - let sb_scan = scan_order(log2sb, scan_idx); - let pos_scan = &SCAN_4[scan_idx]; - let at = |xs: usize, ys: usize, p: usize| lv[(ys * 4 + pos_scan[p].1 as usize) * n + xs * 4 + pos_scan[p].0 as usize]; - // last significant coefficient in scan order - let mut last = None; - 'outer: for i in (0..sb_scan.len()).rev() { - let (xs, ys) = (sb_scan[i].0 as usize, sb_scan[i].1 as usize); - for p in (0..16).rev() { - if at(xs, ys, p) != 0 { - last = Some((i, p)); - break 'outer; - } - } - } - // Callers only code blocks with a nonzero level (coded_block_flag = 1). - let Some((last_sb, last_pos)) = last else { return }; - let lx = sb_scan[last_sb].0 as u32 * 4 + pos_scan[last_pos].0 as u32; - let ly = sb_scan[last_sb].1 as u32 * 4 + pos_scan[last_pos].1 as u32; - let (cx, cy) = if scan_idx == 2 { (ly, lx) } else { (lx, ly) }; - let cmax = (log2 << 1) - 1; - let (off, shift) = if c == 0 { (3 * (log2 - 2) + ((log2 - 1) >> 2), (log2 + 1) >> 2) } else { (15, log2 - 2) }; - let (px, sx, nbx) = last_prefix(cx); - let (py, sy, nby) = last_prefix(cy); - for (p, base) in [(px, LAST_X_PREFIX), (py, LAST_Y_PREFIX)] { - for i in 0..p { - s.decision(base + (off + (i >> shift)) as usize, 1); - } - if p < cmax { - s.decision(base + (off + (p >> shift)) as usize, 0); - } - } - if px > 3 { - s.bypass_bits(sx, nbx); - } - if py > 3 { - s.bypass_bits(sy, nby); - } - let mut csbf = [0u8; 64]; - let mut prev_g1: Option = None; - let c_off_sig = if c == 0 { 0 } else { 27 }; - for i in (0..=last_sb).rev() { - let (xs, ys) = (sb_scan[i].0 as usize, sb_scan[i].1 as usize); - let right = if xs + 1 < sb_w { csbf[ys * 8 + xs + 1] } else { 0 }; - let below = if ys + 1 < sb_w { csbf[(ys + 1) * 8 + xs] } else { 0 }; - let mut infer_dc = false; - if i < last_sb && i > 0 { - let has = (0..16).any(|p| at(xs, ys, p) != 0); - let inc = (right | below).min(1) as usize + if c > 0 { 2 } else { 0 }; - s.decision(CODED_SUB_BLOCK + inc, has as u32); - csbf[ys * 8 + xs] = has as u8; - infer_dc = true; - } else { - csbf[ys * 8 + xs] = 1; - } - if csbf[ys * 8 + xs] == 0 { - continue; - } - let mut sig = [0u8; 16]; - let mut nsig = 0; - let start: i32 = if i == last_sb { - sig[0] = last_pos as u8; - nsig = 1; - last_pos as i32 - 1 - } else { - 15 - }; - let prev_csbf = right + (below << 1); - let mut np = start; - while np >= 0 { - let p = np as usize; - let (xp, yp) = (pos_scan[p].0 as usize, pos_scan[p].1 as usize); - let (xc, yc) = (xs * 4 + xp, ys * 4 + yp); - let v = lv[yc * n + xc] != 0; - if np > 0 || !infer_dc { - let sig_ctx = significance_context((xs, ys), (xp, yp), log2, prev_csbf, c, scan_idx); - s.decision(SIG_COEFF + c_off_sig + sig_ctx, v as u32); - if v { - sig[nsig] = p as u8; - nsig += 1; - infer_dc = false; - } - } else { - debug_assert!(v, "inferred DC coefficient must be non-zero"); - sig[nsig] = 0; - nsig += 1; - } - np -= 1; - } - if nsig == 0 { - continue; - } - write_coefficient_levels(s, &sig[..nsig], |p| at(xs, ys, p), i, c, &mut prev_g1); - } -} - -/// Sum of absolute Hadamard-transformed differences (8x8 blocks, 4x4 for 4-sample blocks). -pub fn satd(src: &[u16], ss: usize, pred: &[u16], ps: usize, n: usize) -> u32 { - if n == 4 { - return satd4(src, ss, pred, ps); - } - let mut total = 0; - for by in (0..n).step_by(8) { - for bx in (0..n).step_by(8) { - total += satd8(&src[by * ss + bx..], ss, &pred[by * ps + bx..], ps); - } - } - total -} - -fn satd4(src: &[u16], ss: usize, pred: &[u16], ps: usize) -> u32 { - let mut d = [0i32; 16]; - for y in 0..4 { - for x in 0..4 { - d[y * 4 + x] = src[y * ss + x] as i32 - pred[y * ps + x] as i32; - } - } - let mut m = [0i32; 16]; - for y in 0..4 { - let r = &d[y * 4..y * 4 + 4]; - let (a, b, c, e) = (r[0] + r[3], r[1] + r[2], r[1] - r[2], r[0] - r[3]); - m[y * 4] = a + b; - m[y * 4 + 1] = e + c; - m[y * 4 + 2] = a - b; - m[y * 4 + 3] = e - c; - } - let mut sum = 0; - for x in 0..4 { - let (r0, r1, r2, r3) = (m[x], m[4 + x], m[8 + x], m[12 + x]); - let (a, b, c, e) = (r0 + r3, r1 + r2, r1 - r2, r0 - r3); - sum += (a + b).abs() + (e + c).abs() + (a - b).abs() + (e - c).abs(); - } - ((sum + 1) >> 1) as u32 -} - -fn hadamard8(v: &mut [i32; 8]) { - let mut t = *v; - let mut h = 1; - while h < 8 { - for i in (0..8).step_by(h * 2) { - for j in i..i + h { - let (a, b) = (t[j], t[j + h]); - t[j] = a + b; - t[j + h] = a - b; - } - } - h *= 2; - } - *v = t; -} - -fn satd8(src: &[u16], ss: usize, pred: &[u16], ps: usize) -> u32 { - let mut m = [[0i32; 8]; 8]; - for y in 0..8 { - for x in 0..8 { - m[y][x] = src[y * ss + x] as i32 - pred[y * ps + x] as i32; - } - hadamard8(&mut m[y]); - } - let mut sum = 0; - for x in 0..8 { - let mut col = [0i32; 8]; - for y in 0..8 { - col[y] = m[y][x]; - } - hadamard8(&mut col); - sum += col.iter().map(|v| v.abs()).sum::(); - } - ((sum + 2) >> 2) as u32 -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn last_prefix_groups() { - assert_eq!(last_prefix(3), (3, 0, 0)); - assert_eq!(last_prefix(4), (4, 0, 1)); - assert_eq!(last_prefix(5), (4, 1, 1)); - assert_eq!(last_prefix(7), (5, 1, 1)); - assert_eq!(last_prefix(8), (6, 0, 2)); - assert_eq!(last_prefix(15), (7, 3, 2)); - assert_eq!(last_prefix(16), (8, 0, 3)); - assert_eq!(last_prefix(31), (9, 7, 3)); - } -} diff --git a/crates/hevcenc/src/inter.rs b/crates/hevcenc/src/inter.rs deleted file mode 100644 index 03bf50ffb..000000000 --- a/crates/hevcenc/src/inter.rs +++ /dev/null @@ -1,157 +0,0 @@ -//! Fractional sample interpolation (8.5.3.3.3) and default weighted prediction (8-262), mirrored -//! from the filmcraft-hevc decoder. Reference sample fetches clamp to the picture (8-228, 8-229). - -use crate::tables::{CHROMA_FILTER, LUMA_FILTER}; - -/// One plane of a reference picture. -#[derive(Clone, Copy)] -pub struct PlaneRef<'a> { - pub data: &'a [u16], - pub w: usize, - pub h: usize, -} - -impl PlaneRef<'_> { - /// Copy a w x h window at (x0, y0) with edge replication. - fn window(&self, x0: i32, y0: i32, w: usize, h: usize, out: &mut [i16], os: usize) { - let (pw, ph) = (self.w as i32, self.h as i32); - let inside_x = x0 >= 0 && x0 + w as i32 <= pw; - for r in 0..h { - let yy = (y0 + r as i32).clamp(0, ph - 1) as usize; - let row = &self.data[yy * self.w..yy * self.w + self.w]; - let dst = &mut out[r * os..r * os + w]; - if inside_x { - for (d, &s) in dst.iter_mut().zip(&row[x0 as usize..x0 as usize + w]) { - *d = s as i16; - } - } else { - for (c, d) in dst.iter_mut().enumerate() { - *d = row[(x0 + c as i32).clamp(0, pw - 1) as usize] as i16; - } - } - } - } -} - -/// Luma prediction samples (14-bit intermediate) of a w x h block at (x, y) displaced by the -/// quarter-sample vector `mv`, into `out` (stride w). -pub fn mc_luma(f: PlaneRef, bd: u32, x: i32, y: i32, mv: [i16; 2], size: (usize, usize), out: &mut [i16]) { - let (w, h) = size; - let (fx, fy) = ((mv[0] & 3) as usize, (mv[1] & 3) as usize); - let xi = x + (mv[0] as i32 >> 2); - let yi = y + (mv[1] as i32 >> 2); - let shift1 = bd.min(12) - 8; - let shift3 = 14 - bd; - let ws = w + 7; - let mut win = vec![0i16; ws * (h + 7)]; - if fx == 0 && fy == 0 { - f.window(xi, yi, w, h, &mut win, ws); - for r in 0..h { - for c in 0..w { - out[r * w + c] = win[r * ws + c] << shift3; - } - } - return; - } - f.window(xi - 3, yi - 3, w + 7, h + 7, &mut win, ws); - let hf = &LUMA_FILTER[fx]; - let vf = &LUMA_FILTER[fy]; - if fy == 0 { - for r in 0..h { - let row = &win[(r + 3) * ws..]; - for c in 0..w { - let s: i32 = (0..8).map(|k| hf[k] as i32 * row[c + k] as i32).sum(); - out[r * w + c] = (s >> shift1) as i16; - } - } - } else if fx == 0 { - for r in 0..h { - for c in 0..w { - let s: i32 = (0..8).map(|k| vf[k] as i32 * win[(r + k) * ws + c + 3] as i32).sum(); - out[r * w + c] = (s >> shift1) as i16; - } - } - } else { - let mut tmp = vec![0i16; ws * (h + 7)]; - for r in 0..h + 7 { - let row = &win[r * ws..]; - for c in 0..w { - let s: i32 = (0..8).map(|k| hf[k] as i32 * row[c + k] as i32).sum(); - tmp[r * ws + c] = (s >> shift1) as i16; - } - } - for r in 0..h { - for c in 0..w { - let s: i32 = (0..8).map(|k| vf[k] as i32 * tmp[(r + k) * ws + c] as i32).sum(); - out[r * w + c] = (s >> 6) as i16; - } - } - } -} - -/// Chroma prediction samples of a w x h block at chroma position (x, y) with a 1/8-sample vector. -pub fn mc_chroma(f: PlaneRef, bd: u32, x: i32, y: i32, mv: [i16; 2], size: (usize, usize), out: &mut [i16]) { - let (w, h) = size; - let (fx, fy) = ((mv[0] & 7) as usize, (mv[1] & 7) as usize); - let xi = x + (mv[0] as i32 >> 3); - let yi = y + (mv[1] as i32 >> 3); - let shift1 = bd.min(12) - 8; - let shift3 = 14 - bd; - let ws = w + 7; - let mut win = vec![0i16; ws * (h + 7)]; - if fx == 0 && fy == 0 { - f.window(xi, yi, w, h, &mut win, ws); - for r in 0..h { - for c in 0..w { - out[r * w + c] = win[r * ws + c] << shift3; - } - } - return; - } - f.window(xi - 1, yi - 1, w + 3, h + 3, &mut win, ws); - let hf = &CHROMA_FILTER[fx]; - let vf = &CHROMA_FILTER[fy]; - if fy == 0 { - for r in 0..h { - let row = &win[(r + 1) * ws..]; - for c in 0..w { - let s: i32 = (0..4).map(|k| hf[k] as i32 * row[c + k] as i32).sum(); - out[r * w + c] = (s >> shift1) as i16; - } - } - } else if fx == 0 { - for r in 0..h { - for c in 0..w { - let s: i32 = (0..4).map(|k| vf[k] as i32 * win[(r + k) * ws + c + 1] as i32).sum(); - out[r * w + c] = (s >> shift1) as i16; - } - } - } else { - let mut tmp = vec![0i16; ws * (h + 7)]; - for r in 0..h + 3 { - let row = &win[r * ws..]; - for c in 0..w { - let s: i32 = (0..4).map(|k| hf[k] as i32 * row[c + k] as i32).sum(); - tmp[r * ws + c] = (s >> shift1) as i16; - } - } - for r in 0..h { - for c in 0..w { - let s: i32 = (0..4).map(|k| vf[k] as i32 * tmp[(r + k) * ws + c] as i32).sum(); - out[r * w + c] = (s >> 6) as i16; - } - } - } -} - -/// Default weighted prediction, one list (8-262): 14-bit samples to `bd`-bit samples. -pub fn put_uni(p: &[i16], w: usize, h: usize, bd: u32, dst: &mut [u16], ds: usize) { - let shift = 14 - bd; - let off = if shift > 0 { 1 << (shift - 1) } else { 0 }; - let max = (1i32 << bd) - 1; - for r in 0..h { - for c in 0..w { - dst[r * ds + c] = ((p[r * w + c] as i32 + off) >> shift).clamp(0, max) as u16; - } - } -} diff --git a/crates/hevcenc/src/intra.rs b/crates/hevcenc/src/intra.rs deleted file mode 100644 index f1fe9e4cb..000000000 --- a/crates/hevcenc/src/intra.rs +++ /dev/null @@ -1,254 +0,0 @@ -//! Intra sample prediction (8.4.4.2), mirrored from the filmcraft-hevc decoder so the encoder -//! reconstructs exactly like a decoder: reference substitution, filtering (incl. strong intra -//! smoothing), planar, DC and the 33 angular modes. - -use crate::tables::{INTRA_PRED_ANGLE, INV_ANGLE}; - -/// Reference samples of an nTbS block: `left[0]` = `top[0]` = p[-1][-1], `left[1 + y]` = p[-1][y], -/// `top[1 + x]` = p[x][-1] for x, y = 0..2*nTbS-1. -#[derive(Clone)] -pub struct Refs { - pub left: [u16; 129], - pub top: [u16; 129], -} - -impl Default for Refs { - fn default() -> Self { - Refs { left: [0; 129], top: [0; 129] } - } -} - -/// Substitution process (8.4.4.2.2). `avail_left[1 + y]` / `avail_top[1 + x]` (index 0 = corner) tell -/// which samples are available; unavailable ones are replaced. -pub fn substitute(r: &mut Refs, avail_left: &[bool; 129], avail_top: &[bool; 129], n: usize, bit_depth: u32) { - let n2 = 2 * n; - // Sequence order: p[-1][2n-1] .. p[-1][-1], p[0][-1] .. p[2n-1][-1] - let any = avail_left[..=n2].iter().any(|&a| a) || avail_top[1..=n2].iter().any(|&a| a); - if !any { - let v = 1u16 << (bit_depth - 1); - r.left[..=n2].fill(v); - r.top[..=n2].fill(v); - return; - } - if !avail_left[n2] { - // search upwards then along the top - let mut found = None; - for y in (0..n2).rev() { - if avail_left[y] { - found = Some(r.left[y]); - break; - } - } - if found.is_none() { - for x in 1..=n2 { - if avail_top[x] { - found = Some(r.top[x]); - break; - } - } - } - r.left[n2] = found.unwrap_or(0); - } - for y in (0..n2).rev() { - if !avail_left[y] { - r.left[y] = r.left[y + 1]; - } - } - r.top[0] = r.left[0]; - for x in 1..=n2 { - if !avail_top[x] { - r.top[x] = r.top[x - 1]; - } - } -} - -/// Filtering process of neighbouring samples (8.4.4.2.3) for luma (and 4:4:4 chroma). -pub fn filter(r: &mut Refs, mode: u32, n: usize, strong_enabled: bool, is_luma: bool, bit_depth: u32) { - if mode == 1 || n == 4 { - return; - } - let min_dist = (mode as i32 - 26).abs().min((mode as i32 - 10).abs()); - let thres = match n { - 8 => 7, - 16 => 1, - _ => 0, - }; - if min_dist <= thres { - return; - } - let n2 = 2 * n; - let c = r.left[0] as i32; - if strong_enabled && is_luma && n == 32 { - let (bl, tr) = (r.left[n2] as i32, r.top[n2] as i32); - let thr = 1 << (bit_depth - 5); - if (c + tr - 2 * r.top[n] as i32).abs() < thr && (c + bl - 2 * r.left[n] as i32).abs() < thr { - for i in 0..63 { - r.left[1 + i] = (((63 - i as i32) * c + (i as i32 + 1) * bl + 32) >> 6) as u16; - r.top[1 + i] = (((63 - i as i32) * c + (i as i32 + 1) * tr + 32) >> 6) as u16; - } - return; - } - } - let (l, t) = (r.left, r.top); - let corner = ((l[1] as u32 + 2 * c as u32 + t[1] as u32 + 2) >> 2) as u16; - for i in 1..n2 { - r.left[i] = ((l[i - 1] as u32 + 2 * l[i] as u32 + l[i + 1] as u32 + 2) >> 2) as u16; - r.top[i] = ((t[i - 1] as u32 + 2 * t[i] as u32 + t[i + 1] as u32 + 2) >> 2) as u16; - } - r.left[0] = corner; - r.top[0] = corner; -} - -/// Predict an n x n block with `mode` into `out` (stride `os`). `edge_filters`: DC / horizontal / -/// vertical boundary smoothing (luma blocks smaller than 32x32). -pub fn predict(r: &Refs, mode: u32, n: usize, edge_filters: bool, bit_depth: u32, out: &mut [u16], os: usize) { - let max = (1i32 << bit_depth) - 1; - match mode { - 0 => { - // planar - let log2 = n.trailing_zeros(); - let tr = r.top[1 + n] as i32; - let bl = r.left[1 + n] as i32; - for y in 0..n { - let l = r.left[1 + y] as i32; - for x in 0..n { - let t = r.top[1 + x] as i32; - let v = ((n - 1 - x) as i32 * l + (x as i32 + 1) * tr + (n - 1 - y) as i32 * t + (y as i32 + 1) * bl + n as i32) >> (log2 + 1); - out[y * os + x] = v as u16; - } - } - } - 1 => { - let log2 = n.trailing_zeros(); - let sum: u32 = r.top[1..=n].iter().map(|&v| v as u32).sum::() + r.left[1..=n].iter().map(|&v| v as u32).sum::(); - let dc = ((sum + n as u32) >> (log2 + 1)) as u16; - for y in 0..n { - out[y * os..y * os + n].fill(dc); - } - if edge_filters && n < 32 { - let d = dc as u32; - out[0] = ((r.left[1] as u32 + 2 * d + r.top[1] as u32 + 2) >> 2) as u16; - for x in 1..n { - out[x] = ((r.top[1 + x] as u32 + 3 * d + 2) >> 2) as u16; - } - for y in 1..n { - out[y * os] = ((r.left[1 + y] as u32 + 3 * d + 2) >> 2) as u16; - } - } - } - _ => { - let angle = INTRA_PRED_ANGLE[mode as usize] as i32; - // reference array ref[-n..=2n] stored at offset n - let mut refa = [0i32; 3 * 64 + 1]; - let base = n as i32; - let (main, side) = if mode >= 18 { (&r.top, &r.left) } else { (&r.left, &r.top) }; - for x in 0..=n { - refa[(base + x as i32) as usize] = main[x] as i32; // ref[x] = p[-1+x][-1] - } - if angle < 0 { - let lim = (n as i32 * angle) >> 5; - if lim < -1 { - let inv = INV_ANGLE[(mode - 11) as usize] as i32; - for x in lim..=-1 { - let idx = -1 + ((x * inv + 128) >> 8); - refa[(base + x) as usize] = side[(idx + 1) as usize] as i32; - } - } - } else { - for x in n + 1..=2 * n { - refa[(base + x as i32) as usize] = main[x] as i32; - } - } - let horizontal = mode < 18; - for j in 0..n { - let pos = (j as i32 + 1) * angle; - let idx = pos >> 5; - let fact = pos & 31; - for i in 0..n { - let k = (base + i as i32 + idx + 1) as usize; - let v = if fact != 0 { ((32 - fact) * refa[k] + fact * refa[k + 1] + 16) >> 5 } else { refa[k] }; - if horizontal { - out[i * os + j] = v as u16; - } else { - out[j * os + i] = v as u16; - } - } - } - if edge_filters && n < 32 { - if mode == 26 { - for y in 0..n { - let v = r.top[1] as i32 + ((r.left[1 + y] as i32 - r.left[0] as i32) >> 1); - out[y * os] = v.clamp(0, max) as u16; - } - } else if mode == 10 { - for x in 0..n { - let v = r.left[1] as i32 + ((r.top[1 + x] as i32 - r.top[0] as i32) >> 1); - out[x] = v.clamp(0, max) as u16; - } - } - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn refs(f: impl Fn(i32) -> u16) -> Refs { - let mut r = Refs::default(); - r.left[0] = f(0); - r.top[0] = f(0); - for i in 1..129 { - r.left[i] = f(-(i as i32)); - r.top[i] = f(i as i32); - } - r - } - - #[test] - fn dc_and_planar_flat() { - let r = refs(|_| 100); - let mut out = [0u16; 64]; - predict(&r, 1, 8, true, 8, &mut out, 8); - assert!(out.iter().all(|&v| v == 100)); - predict(&r, 0, 8, true, 8, &mut out, 8); - assert!(out.iter().all(|&v| v == 100)); - } - - #[test] - fn pure_vertical_and_horizontal() { - let r = refs(|i| (128 + i) as u16); - let mut out = [0u16; 16]; - predict(&r, 26, 4, false, 8, &mut out, 4); - for y in 0..4 { - for x in 0..4 { - assert_eq!(out[y * 4 + x], r.top[1 + x]); - } - } - predict(&r, 10, 4, false, 8, &mut out, 4); - for y in 0..4 { - for x in 0..4 { - assert_eq!(out[y * 4 + x], r.left[1 + y]); - } - } - // diagonal mode 34 reads top[x + y + 2] - predict(&r, 34, 4, false, 8, &mut out, 4); - assert_eq!(out[4 + 1], r.top[1 + 1 + 1 + 1]); - } - - #[test] - fn substitution() { - let mut r = Refs::default(); - let mut al = [false; 129]; - let at = [false; 129]; - r.left[3] = 77; - al[3] = true; - substitute(&mut r, &al, &at, 4, 8); - assert!(r.left[..=8].iter().all(|&v| v == 77)); - assert!(r.top[..=8].iter().all(|&v| v == 77)); - let mut r = Refs::default(); - substitute(&mut r, &[false; 129], &[false; 129], 4, 10); - assert_eq!(r.top[5], 512); - } -} diff --git a/crates/hevcenc/src/lib.rs b/crates/hevcenc/src/lib.rs deleted file mode 100644 index 1934143a0..000000000 --- a/crates/hevcenc/src/lib.rs +++ /dev/null @@ -1,365 +0,0 @@ -//! Clean-room pure-Rust HEVC (H.265) encoder, implemented from ITU-T Rec. H.265 (ISO/IEC 23008-2). -//! -//! Main and Main 10 profiles, 4:2:0, one slice per picture: IDR pictures and single-reference P -//! pictures (no reordering), 64x64 CTUs with rate-distortion coding-unit decisions (8x8 .. 64x64), -//! intra (35 modes, NxN at 8x8), merge / skip and AMVP inter prediction with quarter-sample motion, -//! DCT / DST transforms and CABAC. The deblocking filter is on; SAO is not used. -//! -//! ``` -//! use effectcraft_hevcenc::{Encoder, EncoderConfig, Frame}; -//! let mut enc = Encoder::new(EncoderConfig::new(64, 48, 30, 1)).unwrap(); -//! let (y, c) = (vec![128u16; 64 * 48], vec![128u16; 32 * 24]); -//! let packet = enc.encode(&Frame { y: &y, u: &c, v: &c, y_stride: 64, uv_stride: 32 }); -//! assert!(packet.keyframe && !packet.data.is_empty()); -//! let _hvcc = enc.hvcc(); -//! ``` - -#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] - -mod bitstream; -mod cabac; -mod deblock; -mod frame; -mod inter; -mod intra; -mod me; -mod params; -#[rustfmt::skip] -mod tables; -mod transform; - -use bitstream::{BitWriter, NAL_IDR_N_LP, NAL_TRAIL_R, nal_unit}; -use frame::{FrameCoder, SliceKind}; -use params::{LOG2_MAX_POC_LSB, LOG2_MIN_CB, MAX_MERGE_CAND, SeqParams}; - -/// Profile written in the parameter sets. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum Profile { - /// Main: 8-bit 4:2:0. - #[default] - Main, - /// Main 10: 10-bit 4:2:0. - Main10, -} - -impl Profile { - pub fn bit_depth(self) -> u8 { - match self { - Profile::Main => 8, - Profile::Main10 => 10, - } - } - fn idc(self) -> u8 { - match self { - Profile::Main => 1, - Profile::Main10 => 2, - } - } -} - -/// Rate control. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RateControl { - /// Constant QP (0–51; lower is better). - ConstantQp(u8), - /// Average bitrate target in kbit/s (frame-level QP adaptation). - Bitrate { kbps: u32 }, -} - -/// Encoder settings. -#[derive(Clone, Debug, PartialEq)] -pub struct EncoderConfig { - /// Picture size; must be even (4:2:0 cropping is in units of two luma samples). - pub width: u32, - pub height: u32, - /// Frame rate numerator / denominator (VUI timing and rate control). - pub fps_num: u32, - pub fps_den: u32, - pub profile: Profile, - /// `general_level_idc` (30 × level, e.g. 93 = level 3.1); `None` picks the lowest level that fits. - pub level_idc: Option, - pub rate: RateControl, - /// Frames between IDR pictures (1 = all intra, 0 = only the first picture). - pub keyint: u32, - /// BT.709 limited range is signalled in the VUI unless this is set. - pub full_range: bool, - /// The VUI colour description (ITU-T H.273 code points); BT.709 by default. - pub colour_primaries: u8, - pub transfer_characteristics: u8, - pub matrix_coeffs: u8, -} - -impl EncoderConfig { - pub fn new(width: u32, height: u32, fps_num: u32, fps_den: u32) -> Self { - EncoderConfig { - width, - height, - fps_num, - fps_den, - profile: Profile::Main, - level_idc: None, - rate: RateControl::ConstantQp(28), - keyint: 60, - full_range: false, - colour_primaries: 1, - transfer_characteristics: 1, - matrix_coeffs: 1, - } - } -} - -/// One input picture, 4:2:0, samples at the profile's bit depth (8-bit values 0–255 for Main). -/// Luma is `width` x `height`, chroma `width / 2` x `height / 2`. -#[derive(Clone, Copy, Debug)] -pub struct Frame<'a> { - pub y: &'a [u16], - pub u: &'a [u16], - pub v: &'a [u16], - pub y_stride: usize, - pub uv_stride: usize, -} - -/// One coded picture (an access unit): NAL units, each prefixed by a 4-byte big-endian length -/// (ISO/IEC 14496-15 sample format; parameter sets are only in [`Encoder::hvcc`]). -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Packet { - pub data: Vec, - /// IDR picture. - pub keyframe: bool, -} - -#[derive(Debug, PartialEq, Eq)] -pub enum Error { - InvalidConfig(String), -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::InvalidConfig(s) => write!(f, "invalid HEVC encoder config: {s}"), - } - } -} - -impl std::error::Error for Error {} - -/// Frame-level rate control for [`RateControl::Bitrate`]: a bits ≈ X / Qstep model per picture type -/// and a buffer that spreads over- / undershoot over the following pictures. -struct RateCtl { - target: f64, - /// sum(actual) - sum(target), bits. - buffer: f64, - /// Complexity X = bits * Qstep for I and P pictures. - x: [Option; 2], - last_qp: [i32; 2], - init_qp: i32, -} - -fn qstep(qp: i32) -> f64 { - 2f64.powf((qp as f64 - 4.0) / 6.0) -} - -impl RateCtl { - fn new(kbps: u32, cfg: &EncoderConfig) -> Self { - let fps = cfg.fps_num as f64 / cfg.fps_den.max(1) as f64; - let target = kbps as f64 * 1000.0 / fps.max(0.001); - let bpp = target / (cfg.width as f64 * cfg.height as f64); - let init_qp = (28.0 - 6.0 * (bpp / 0.08).log2()).round().clamp(10.0, 46.0) as i32; - RateCtl { target, buffer: 0.0, x: [None, None], last_qp: [init_qp - 3, init_qp], init_qp } - } - - fn qp(&self, idr: bool) -> i32 { - let t = idr as usize ^ 1; // 0 = I, 1 = P - let tp = (self.target - self.buffer / 12.0).clamp(0.3 * self.target, 2.0 * self.target); - let want = if idr { 4.0 * tp } else { tp }; - let x = match (self.x[t], self.x[0]) { - (Some(x), _) => x, - (None, Some(xi)) => xi / 5.0, - (None, None) => return if idr { self.init_qp - 3 } else { self.init_qp }, - }; - let qp = (4.0 + 6.0 * (x / want).log2()).round() as i32; - qp.clamp(self.last_qp[t] - 6, self.last_qp[t] + 6).clamp(1, 51) - } - - fn update(&mut self, idr: bool, qp: i32, bits: f64) { - let t = idr as usize ^ 1; - let x = bits * qstep(qp); - self.x[t] = Some(match self.x[t] { - Some(old) => 0.5 * old + 0.5 * x, - None => x, - }); - self.last_qp[t] = qp; - self.buffer += bits - self.target; - } -} - -/// HEVC encoder. Pictures come out in input order (no reordering: I and P only). -pub struct Encoder { - cfg: EncoderConfig, - seq: SeqParams, - sets: [Vec; 3], - frames: u64, - /// POC of the previous picture (relative to the last IDR). - poc: u32, - reference: Option<[Vec; 3]>, - recon: Option<[Vec; 3]>, - rc: Option, -} - -impl Encoder { - pub fn new(cfg: EncoderConfig) -> Result { - if cfg.width == 0 || cfg.height == 0 { - return Err(Error::InvalidConfig("empty frame".into())); - } - if !cfg.width.is_multiple_of(2) || !cfg.height.is_multiple_of(2) { - return Err(Error::InvalidConfig(format!("{}x{}: 4:2:0 HEVC needs even dimensions", cfg.width, cfg.height))); - } - if cfg.width > 16384 || cfg.height > 16384 { - return Err(Error::InvalidConfig("picture larger than 16384 samples".into())); - } - if cfg.fps_num == 0 || cfg.fps_den == 0 { - return Err(Error::InvalidConfig("zero frame rate".into())); - } - match cfg.rate { - RateControl::ConstantQp(q) if q > 51 => return Err(Error::InvalidConfig(format!("QP {q} > 51"))), - RateControl::Bitrate { kbps: 0 } => return Err(Error::InvalidConfig("zero bitrate".into())), - _ => {} - } - let min_cb = 1u32 << LOG2_MIN_CB; - let (coded_width, coded_height) = (cfg.width.div_ceil(min_cb) * min_cb, cfg.height.div_ceil(min_cb) * min_cb); - let kbps = match cfg.rate { - RateControl::Bitrate { kbps } => Some(kbps), - RateControl::ConstantQp(_) => None, - }; - let level_idc = cfg.level_idc.unwrap_or_else(|| params::choose_level(coded_width, coded_height, cfg.fps_num, cfg.fps_den, kbps)); - let seq = SeqParams { - coded_width, - coded_height, - width: cfg.width, - height: cfg.height, - bit_depth: cfg.profile.bit_depth() as u32, - profile_idc: cfg.profile.idc(), - level_idc, - full_range: cfg.full_range, - colour_primaries: cfg.colour_primaries, - transfer_characteristics: cfg.transfer_characteristics, - matrix_coeffs: cfg.matrix_coeffs, - fps_num: cfg.fps_num, - fps_den: cfg.fps_den, - }; - let sets = [params::vps(&seq), params::sps(&seq), params::pps()]; - let rc = kbps.map(|k| RateCtl::new(k, &cfg)); - Ok(Encoder { cfg, seq, sets, frames: 0, poc: 0, reference: None, recon: None, rc }) - } - - pub fn config(&self) -> &EncoderConfig { - &self.cfg - } - - /// VPS, SPS, PPS NAL units (2-byte NAL header, emulation prevention, no start codes or length - /// prefixes). - pub fn parameter_sets(&self) -> [Vec; 3] { - self.sets.clone() - } - - /// HEVCDecoderConfigurationRecord (`hvcC` payload) with the parameter sets. - pub fn hvcc(&self) -> Vec { - params::hvcc(&self.seq, &self.sets) - } - - /// `general_level_idc` written in the parameter sets. - pub fn level_idc(&self) -> u8 { - self.seq.level_idc - } - - /// Reconstruction of the last encoded picture (what a decoder outputs), cropped planes - /// `[Y, Cb, Cr]` with strides `width` and `width / 2`. For tests. - #[doc(hidden)] - pub fn last_reconstruction(&self) -> Option<&[Vec; 3]> { - self.recon.as_ref() - } - - /// Copy the input into coded-size planes, replicating the right / bottom edges. - fn pad_input(&self, f: &Frame) -> [Vec; 3] { - let max = (1u32 << self.seq.bit_depth) - 1; - let mut out: [Vec; 3] = Default::default(); - for (c, plane) in out.iter_mut().enumerate() { - let (src, stride, w, h, cw, ch) = if c == 0 { - (f.y, f.y_stride, self.seq.width, self.seq.height, self.seq.coded_width, self.seq.coded_height) - } else { - let s = if c == 1 { f.u } else { f.v }; - (s, f.uv_stride, self.seq.width / 2, self.seq.height / 2, self.seq.coded_width / 2, self.seq.coded_height / 2) - }; - let (w, h, cw, ch) = (w as usize, h as usize, cw as usize, ch as usize); - assert!(stride >= w && src.len() >= (h - 1) * stride + w, "HEVC encoder: plane {c} smaller than {w}x{h} (stride {stride})"); - plane.reserve(cw * ch); - for y in 0..ch { - let row = &src[y.min(h - 1) * stride..][..w]; - plane.extend(row.iter().map(|&v| (v as u32).min(max) as u16)); - let last = plane.last().copied().unwrap_or(0); - plane.extend(std::iter::repeat_n(last, cw - w)); - } - } - out - } - - /// Encodes one picture. - pub fn encode(&mut self, frame: &Frame) -> Packet { - let src = self.pad_input(frame); - let keyint = self.cfg.keyint as u64; - let idr = self.reference.is_none() || (keyint > 0 && self.frames.is_multiple_of(keyint)); - self.poc = if idr { 0 } else { self.poc + 1 }; - let qp = match (&self.rc, self.cfg.rate) { - (Some(rc), _) => rc.qp(idr), - (None, RateControl::ConstantQp(q)) => q as i32, - (None, RateControl::Bitrate { .. }) => 28, - }; - let (w, h) = (self.seq.coded_width as usize, self.seq.coded_height as usize); - let bd = self.seq.bit_depth; - let kind = if idr { SliceKind::I } else { SliceKind::P }; - let me = if idr { None } else { self.reference.as_ref().map(|r| me::MeRef::new(&r[0], w, h, bd)) }; - let refp = if idr { None } else { self.reference.as_ref() }; - let mut fc = FrameCoder::new(frame::FrameConfig { bit_depth: bd, width: w, height: h, kind, slice_qp: qp }, &src, refp, me.as_ref()); - - // slice_segment_header() (7.3.6.1) - let mut bw = BitWriter::new(); - bw.put(1, 1); // first_slice_segment_in_pic_flag - if idr { - bw.put(0, 1); // no_output_of_prior_pics_flag - } - bw.ue(0); // slice_pic_parameter_set_id - bw.ue(if idr { 2 } else { 1 }); // slice_type I / P - if !idr { - bw.put(self.poc & ((1 << LOG2_MAX_POC_LSB) - 1), LOG2_MAX_POC_LSB); // slice_pic_order_cnt_lsb - bw.put(1, 1); // short_term_ref_pic_set_sps_flag (the only SPS set, no index coded) - bw.put(0, 1); // num_ref_idx_active_override_flag - bw.ue(5 - MAX_MERGE_CAND as u32); // five_minus_max_num_merge_cand - } - bw.se(qp - 26); // slice_qp_delta (init_qp = 26) - // byte_alignment() - bw.put(1, 1); - bw.align_zero(); - - let ctx = cabac::init_contexts(qp, if idr { 0 } else { 1 }); - let mut enc = cabac::CabacEncoder::new(bw, ctx); - fc.encode(&mut enc); - fc.deblock(); - let rbsp = enc.finish().into_bytes(); - let nal = nal_unit(if idr { NAL_IDR_N_LP } else { NAL_TRAIL_R }, &rbsp); - let mut data = Vec::with_capacity(nal.len() + 4); - data.extend_from_slice(&(nal.len() as u32).to_be_bytes()); - data.extend_from_slice(&nal); - - if let Some(rc) = &mut self.rc { - rc.update(idr, qp, data.len() as f64 * 8.0); - } - // output picture: cropped reconstruction - let rec = std::mem::take(&mut fc.rec); - let (ow, oh) = (self.seq.width as usize, self.seq.height as usize); - let crop = |p: &[u16], stride: usize, w: usize, h: usize| -> Vec { (0..h).flat_map(|y| p[y * stride..y * stride + w].iter().copied()).collect() }; - self.recon = Some([crop(&rec[0], w, ow, oh), crop(&rec[1], w / 2, ow / 2, oh / 2), crop(&rec[2], w / 2, ow / 2, oh / 2)]); - self.reference = Some(rec); - self.frames += 1; - Packet { data, keyframe: idr } - } -} diff --git a/crates/hevcenc/src/me.rs b/crates/hevcenc/src/me.rs deleted file mode 100644 index 5f4cb2ce3..000000000 --- a/crates/hevcenc/src/me.rs +++ /dev/null @@ -1,144 +0,0 @@ -//! Motion search reference: the previous reconstructed luma plane padded by edge replication, plus -//! half-sample planes interpolated with the 8-tap half-sample filter. Quarter-sample positions are -//! approximated by averaging neighbouring half-grid samples. Used for estimation only; the coded -//! prediction always uses the normative interpolation in `inter`. - -use crate::tables::LUMA_FILTER; - -const MARGIN: i32 = 80; - -pub struct MeRef { - /// [integer, horizontal half, vertical half, diagonal half] samples, each `stride` x `rows`. - planes: [Vec; 4], - stride: usize, - rows: usize, - w: i32, - h: i32, -} - -impl MeRef { - pub fn new(y: &[u16], w: usize, h: usize, bd: u32) -> MeRef { - let m = MARGIN as usize; - let (stride, rows) = (w + 2 * m, h + 2 * m); - let mut pad = vec![0u16; stride * rows]; - for r in 0..rows { - let sy = (r as i32 - MARGIN).clamp(0, h as i32 - 1) as usize; - let src = &y[sy * w..sy * w + w]; - let row = &mut pad[r * stride..r * stride + stride]; - row[..m].fill(src[0]); - row[m..m + w].copy_from_slice(src); - row[m + w..].fill(src[w - 1]); - } - let max = (1i32 << bd) - 1; - let f = &LUMA_FILTER[2]; - let filt_h = |p: &[u16]| -> Vec { - let mut o = vec![0u16; stride * rows]; - for r in 0..rows { - let row = &p[r * stride..r * stride + stride]; - for x in 0..stride { - let mut s = 0i32; - for k in 0..8 { - let xx = (x as i32 - 3 + k as i32).clamp(0, stride as i32 - 1) as usize; - s += f[k] as i32 * row[xx] as i32; - } - o[r * stride + x] = ((s + 32) >> 6).clamp(0, max) as u16; - } - } - o - }; - let filt_v = |p: &[u16]| -> Vec { - let mut o = vec![0u16; stride * rows]; - let mut acc = vec![0i32; stride]; - for r in 0..rows { - acc.fill(0); - for k in 0..8 { - let rr = (r as i32 - 3 + k as i32).clamp(0, rows as i32 - 1) as usize; - let src = &p[rr * stride..rr * stride + stride]; - let coef = f[k] as i32; - for (a, &s) in acc.iter_mut().zip(src) { - *a += coef * s as i32; - } - } - for (d, &a) in o[r * stride..r * stride + stride].iter_mut().zip(&acc) { - *d = ((a + 32) >> 6).clamp(0, max) as u16; - } - } - o - }; - let hp = filt_h(&pad); - let vp = filt_v(&pad); - let dp = filt_v(&hp); - MeRef { planes: [pad, hp, vp, dp], stride, rows, w: w as i32, h: h as i32 } - } - - /// Integer motion vector range keeping an n x n block (plus filter margin) inside the padding. - pub fn int_range(&self, x: i32, y: i32, n: usize) -> (i32, i32, i32, i32) { - let n = n as i32; - let lo_x = -MARGIN + 4 - x; - let hi_x = self.w + MARGIN - n - 4 - x; - let lo_y = -MARGIN + 4 - y; - let hi_y = self.h + MARGIN - n - 4 - y; - (lo_x.max(-256), hi_x.min(256), lo_y.max(-256), hi_y.min(256)) - } - - /// SAD of the source block against the integer-position reference block at (rx, ry). - pub fn sad(&self, src: &[u16], ss: usize, rx: i32, ry: i32, n: usize) -> u32 { - let p = &self.planes[0]; - let base = (ry + MARGIN) as usize * self.stride + (rx + MARGIN) as usize; - let mut s = 0u32; - for r in 0..n { - let a = &src[r * ss..r * ss + n]; - let b = &p[base + r * self.stride..base + r * self.stride + n]; - s += a.iter().zip(b).map(|(&x, &y)| (x as i32 - y as i32).unsigned_abs()).sum::(); - } - s - } - - /// Approximate luma prediction of an n x n block at (x, y) with quarter-sample vector `mv`. - pub fn approx_pred(&self, x: i32, y: i32, n: usize, mv: [i16; 2], out: &mut [u16]) { - // per axis: one or two (half-sample fraction, integer offset) taps - let axis = |v: i32| -> [(usize, i32); 2] { - if v & 1 == 0 { - let hv = v >> 1; - [((hv & 1) as usize, hv >> 1); 2] - } else { - let (a, b) = ((v - 1) >> 1, (v + 1) >> 1); - [((a & 1) as usize, a >> 1), ((b & 1) as usize, b >> 1)] - } - }; - let (mx, my) = (mv[0] as i32, mv[1] as i32); - let ax = axis(mx); - let ay = axis(my); - let two = mx & 1 != 0 || my & 1 != 0; - let taps = [(ax[0], ay[0]), (ax[1], ay[1])]; - let ntaps = if two { 2 } else { 1 }; - let mut first = true; - for &((fx, ox), (fy, oy)) in taps.iter().take(ntaps) { - let plane = &self.planes[fx + 2 * fy]; - let (bx, by) = (x + ox + MARGIN, y + oy + MARGIN); - let inside = bx >= 0 && by >= 0 && bx as usize + n <= self.stride && by as usize + n <= self.rows; - for r in 0..n { - let dst = &mut out[r * n..r * n + n]; - if inside { - let o = (by as usize + r) * self.stride + bx as usize; - let src = &plane[o..o + n]; - if first { - dst.copy_from_slice(src); - } else { - for (d, &s) in dst.iter_mut().zip(src) { - *d = ((*d as u32 + s as u32 + 1) >> 1) as u16; - } - } - } else { - let yy = (by + r as i32).clamp(0, self.rows as i32 - 1) as usize; - for (c, d) in dst.iter_mut().enumerate() { - let xx = (bx + c as i32).clamp(0, self.stride as i32 - 1) as usize; - let s = plane[yy * self.stride + xx]; - *d = if first { s } else { ((*d as u32 + s as u32 + 1) >> 1) as u16 }; - } - } - } - first = false; - } - } -} diff --git a/crates/hevcenc/src/params.rs b/crates/hevcenc/src/params.rs deleted file mode 100644 index 89bac938e..000000000 --- a/crates/hevcenc/src/params.rs +++ /dev/null @@ -1,265 +0,0 @@ -//! Parameter sets (7.3.2.1 VPS, 7.3.2.2 SPS with E.2.1 VUI, 7.3.2.3 PPS), profile_tier_level (7.3.3), -//! level selection (Annex A, Table A.8) and the `hvcC` record (ISO/IEC 14496-15 §8.3.3.1). - -use crate::bitstream::{BitWriter, NAL_PPS, NAL_SPS, NAL_VPS, nal_unit}; - -/// Fixed coding structure of the encoder. -pub const LOG2_CTB: u32 = 6; -pub const LOG2_MIN_CB: u32 = 3; -pub const LOG2_MIN_TB: u32 = 2; -pub const LOG2_MAX_TB: u32 = 5; -pub const LOG2_MAX_POC_LSB: u32 = 8; -/// MaxNumMergeCand. -pub const MAX_MERGE_CAND: usize = 5; - -/// Sequence-level settings shared by the parameter-set writers. -#[derive(Clone, Debug)] -pub struct SeqParams { - /// Coded (padded) size, multiples of the minimum CB size. - pub coded_width: u32, - pub coded_height: u32, - /// Output (cropped) size. - pub width: u32, - pub height: u32, - pub bit_depth: u32, - pub profile_idc: u8, - pub level_idc: u8, - pub full_range: bool, - pub colour_primaries: u8, - pub transfer_characteristics: u8, - pub matrix_coeffs: u8, - pub fps_num: u32, - pub fps_den: u32, -} - -/// (level_idc, MaxLumaPs, MaxLumaSr, MaxBR main tier in kbit/s) of Table A.8 (and A.9 for luma rates). -const LEVELS: [(u8, u64, u64, u64); 13] = [ - (30, 36_864, 552_960, 128), - (60, 122_880, 3_686_400, 1_500), - (63, 245_760, 7_372_800, 3_000), - (90, 552_960, 16_588_800, 6_000), - (93, 983_040, 33_177_600, 10_000), - (120, 2_228_224, 66_846_720, 12_000), - (123, 2_228_224, 133_693_440, 20_000), - (150, 8_912_896, 267_386_880, 25_000), - (153, 8_912_896, 534_773_760, 40_000), - (156, 8_912_896, 1_069_547_520, 60_000), - (180, 35_651_584, 1_069_547_520, 60_000), - (183, 35_651_584, 2_139_095_040, 120_000), - (186, 35_651_584, 4_278_190_080, 240_000), -]; - -/// Lowest level whose picture size, sample rate and (when known) bitrate limits fit. -pub fn choose_level(width: u32, height: u32, fps_num: u32, fps_den: u32, kbps: Option) -> u8 { - let ps = width as u64 * height as u64; - let sr = (ps as f64 * fps_num as f64 / fps_den.max(1) as f64).ceil() as u64; - for &(idc, max_ps, max_sr, max_br) in &LEVELS { - let max_dim = ((max_ps * 8) as f64).sqrt() as u64; - if ps <= max_ps && (width as u64) <= max_dim && (height as u64) <= max_dim && sr <= max_sr && kbps.is_none_or(|k| k as u64 <= max_br) { - return idc; - } - } - 186 -} - -fn profile_tier_level(w: &mut BitWriter, p: &SeqParams) { - w.put(0, 2); // general_profile_space - w.put(0, 1); // general_tier_flag (Main tier) - w.put(p.profile_idc as u32, 5); - w.put(compat_flags(p.profile_idc), 32); - w.put(1, 1); // general_progressive_source_flag - w.put(0, 1); // general_interlaced_source_flag - w.put(0, 1); // general_non_packed_constraint_flag - w.put(1, 1); // general_frame_only_constraint_flag - w.put(0, 32); // general_reserved_zero_43bits ... - w.put(0, 11); - w.put(0, 1); // general_inbld_flag / reserved - w.put(p.level_idc as u32, 8); - // sps_max_sub_layers_minus1 == 0: no sub-layer flags -} - -/// general_profile_compatibility_flag[j] as a 32-bit word (flag 0 is the MSB). A Main stream is also -/// Main 10 compatible. -fn compat_flags(profile_idc: u8) -> u32 { - let mut f = 1u32 << (31 - profile_idc as u32); - if profile_idc == 1 { - f |= 1 << (31 - 2); - } - f -} - -pub fn vps(p: &SeqParams) -> Vec { - let mut w = BitWriter::new(); - w.put(0, 4); // vps_video_parameter_set_id - w.put(1, 1); // vps_base_layer_internal_flag - w.put(1, 1); // vps_base_layer_available_flag - w.put(0, 6); // vps_max_layers_minus1 - w.put(0, 3); // vps_max_sub_layers_minus1 - w.put(1, 1); // vps_temporal_id_nesting_flag - w.put(0xffff, 16); - profile_tier_level(&mut w, p); - w.put(1, 1); // vps_sub_layer_ordering_info_present_flag - w.ue(1); // vps_max_dec_pic_buffering_minus1 - w.ue(0); // vps_max_num_reorder_pics - w.ue(0); // vps_max_latency_increase_plus1 - w.put(0, 6); // vps_max_layer_id - w.ue(0); // vps_num_layer_sets_minus1 - w.put(0, 1); // vps_timing_info_present_flag - w.put(0, 1); // vps_extension_flag - w.trailing(); - nal_unit(NAL_VPS, &w.into_bytes()) -} - -pub fn sps(p: &SeqParams) -> Vec { - let mut w = BitWriter::new(); - w.put(0, 4); // sps_video_parameter_set_id - w.put(0, 3); // sps_max_sub_layers_minus1 - w.put(1, 1); // sps_temporal_id_nesting_flag - profile_tier_level(&mut w, p); - w.ue(0); // sps_seq_parameter_set_id - w.ue(1); // chroma_format_idc 4:2:0 - w.ue(p.coded_width); - w.ue(p.coded_height); - let crop = p.coded_width != p.width || p.coded_height != p.height; - w.flag(crop); // conformance_window_flag - if crop { - // offsets in chroma sample units (SubWidthC = SubHeightC = 2) - w.ue(0); - w.ue((p.coded_width - p.width) / 2); - w.ue(0); - w.ue((p.coded_height - p.height) / 2); - } - w.ue(p.bit_depth - 8); // bit_depth_luma_minus8 - w.ue(p.bit_depth - 8); // bit_depth_chroma_minus8 - w.ue(LOG2_MAX_POC_LSB - 4); - w.put(1, 1); // sps_sub_layer_ordering_info_present_flag - w.ue(1); // sps_max_dec_pic_buffering_minus1 - w.ue(0); // sps_max_num_reorder_pics - w.ue(0); // sps_max_latency_increase_plus1 - w.ue(LOG2_MIN_CB - 3); - w.ue(LOG2_CTB - LOG2_MIN_CB); - w.ue(LOG2_MIN_TB - 2); - w.ue(LOG2_MAX_TB - LOG2_MIN_TB); - w.ue(0); // max_transform_hierarchy_depth_inter - w.ue(0); // max_transform_hierarchy_depth_intra - w.put(0, 1); // scaling_list_enabled_flag - w.put(0, 1); // amp_enabled_flag - w.put(0, 1); // sample_adaptive_offset_enabled_flag - w.put(0, 1); // pcm_enabled_flag - w.ue(1); // num_short_term_ref_pic_sets - // st_ref_pic_set(0): one negative picture at delta POC -1, used by the current picture - w.ue(1); // num_negative_pics - w.ue(0); // num_positive_pics - w.ue(0); // delta_poc_s0_minus1 - w.put(1, 1); // used_by_curr_pic_s0_flag - w.put(0, 1); // long_term_ref_pics_present_flag - w.put(0, 1); // sps_temporal_mvp_enabled_flag - w.put(1, 1); // strong_intra_smoothing_enabled_flag - w.put(1, 1); // vui_parameters_present_flag - vui(&mut w, p); - w.put(0, 1); // sps_extension_present_flag - w.trailing(); - nal_unit(NAL_SPS, &w.into_bytes()) -} - -fn vui(w: &mut BitWriter, p: &SeqParams) { - w.put(0, 1); // aspect_ratio_info_present_flag - w.put(0, 1); // overscan_info_present_flag - w.put(1, 1); // video_signal_type_present_flag - w.put(5, 3); // video_format: unspecified - w.flag(p.full_range); - w.put(1, 1); // colour_description_present_flag - w.put(p.colour_primaries as u32, 8); // colour_primaries - w.put(p.transfer_characteristics as u32, 8); // transfer_characteristics - w.put(p.matrix_coeffs as u32, 8); // matrix_coeffs - w.put(0, 1); // chroma_loc_info_present_flag - w.put(0, 1); // neutral_chroma_indication_flag - w.put(0, 1); // field_seq_flag - w.put(0, 1); // frame_field_info_present_flag - w.put(0, 1); // default_display_window_flag - w.put(1, 1); // vui_timing_info_present_flag - w.put(p.fps_den, 32); // vui_num_units_in_tick - w.put(p.fps_num, 32); // vui_time_scale - w.put(0, 1); // vui_poc_proportional_to_timing_flag - w.put(0, 1); // vui_hrd_parameters_present_flag - w.put(0, 1); // bitstream_restriction_flag -} - -pub fn pps() -> Vec { - let mut w = BitWriter::new(); - w.ue(0); // pps_pic_parameter_set_id - w.ue(0); // pps_seq_parameter_set_id - w.put(0, 1); // dependent_slice_segments_enabled_flag - w.put(0, 1); // output_flag_present_flag - w.put(0, 3); // num_extra_slice_header_bits - w.put(1, 1); // sign_data_hiding_enabled_flag - w.put(0, 1); // cabac_init_present_flag - w.ue(0); // num_ref_idx_l0_default_active_minus1 - w.ue(0); // num_ref_idx_l1_default_active_minus1 - w.se(0); // init_qp_minus26 - w.put(0, 1); // constrained_intra_pred_flag - w.put(0, 1); // transform_skip_enabled_flag - w.put(0, 1); // cu_qp_delta_enabled_flag - w.se(0); // pps_cb_qp_offset - w.se(0); // pps_cr_qp_offset - w.put(0, 1); // pps_slice_chroma_qp_offsets_present_flag - w.put(0, 1); // weighted_pred_flag - w.put(0, 1); // weighted_bipred_flag - w.put(0, 1); // transquant_bypass_enabled_flag - w.put(0, 1); // tiles_enabled_flag - w.put(0, 1); // entropy_coding_sync_enabled_flag - w.put(0, 1); // pps_loop_filter_across_slices_enabled_flag - w.put(1, 1); // deblocking_filter_control_present_flag - w.put(0, 1); // deblocking_filter_override_enabled_flag - w.put(0, 1); // pps_deblocking_filter_disabled_flag - w.se(0); // pps_beta_offset_div2 - w.se(0); // pps_tc_offset_div2 - w.put(0, 1); // pps_scaling_list_data_present_flag - w.put(0, 1); // lists_modification_present_flag - w.ue(0); // log2_parallel_merge_level_minus2 - w.put(0, 1); // slice_segment_header_extension_present_flag - w.put(0, 1); // pps_extension_present_flag - w.trailing(); - nal_unit(NAL_PPS, &w.into_bytes()) -} - -/// HEVCDecoderConfigurationRecord with one VPS, SPS and PPS (lengthSizeMinusOne = 3). -pub fn hvcc(p: &SeqParams, sets: &[Vec; 3]) -> Vec { - let mut o = vec![1u8]; // configurationVersion - o.push(p.profile_idc); // profile_space 0, tier 0, profile_idc - o.extend_from_slice(&compat_flags(p.profile_idc).to_be_bytes()); - // general_constraint_indicator_flags (48 bits): progressive, interlaced, non_packed, frame_only - o.extend_from_slice(&[0x90, 0, 0, 0, 0, 0]); - o.push(p.level_idc); - o.extend_from_slice(&[0xf0, 0x00]); // reserved + min_spatial_segmentation_idc 0 - o.push(0xfc); // reserved + parallelismType 0 - o.push(0xfc | 1); // reserved + chroma_format_idc 1 - o.push(0xf8 | (p.bit_depth - 8) as u8); - o.push(0xf8 | (p.bit_depth - 8) as u8); - o.extend_from_slice(&[0, 0]); // avgFrameRate unspecified - // constantFrameRate 0, numTemporalLayers 1, temporalIdNested 1, lengthSizeMinusOne 3 - o.push((1 << 3) | (1 << 2) | 3); - o.push(3); // numOfArrays - for (set, ty) in sets.iter().zip([NAL_VPS, NAL_SPS, NAL_PPS]) { - o.push(0x80 | ty); // array_completeness 1 - o.extend_from_slice(&1u16.to_be_bytes()); - o.extend_from_slice(&(set.len() as u16).to_be_bytes()); - o.extend_from_slice(set); - } - o -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn levels() { - assert_eq!(choose_level(1920, 1080, 30, 1, None), 120); - assert_eq!(choose_level(1920, 1080, 60, 1, None), 123); - assert_eq!(choose_level(3840, 2160, 30, 1, None), 150); - assert_eq!(choose_level(1280, 720, 30, 1, None), 93); - assert_eq!(choose_level(64, 64, 30, 1, None), 30); - assert_eq!(choose_level(1920, 1080, 30, 1, Some(15_000)), 123); - } -} diff --git a/crates/hevcenc/src/tables.rs b/crates/hevcenc/src/tables.rs deleted file mode 100644 index 7c2fc4470..000000000 --- a/crates/hevcenc/src/tables.rs +++ /dev/null @@ -1,236 +0,0 @@ -//! Constant tables of ITU-T H.265, mirrored from the first-party, spec-derived `filmcraft-hevc` -//! decoder (same organisation, MIT OR Apache-2.0): CABAC context layout and initValues (9.3.2.2), -//! rangeTabLPS / transIdx (Tables 9-52, 9-53), scans (6.5.3 - 6.5.5), transform matrices (8.6.4.2), -//! intra angles (Tables 8-5, 8-6), interpolation filters (8.5.3.3.3) and the chroma QP mapping (Table 8-10). - -#![allow(dead_code)] - -/// Table 9-52 (same values as H.264) rangeTabLPS[pStateIdx][qCodIRangeIdx]. -#[rustfmt::skip] -pub static RANGE_TAB_LPS: [[u8; 4]; 64] = [ - [128, 176, 208, 240], [128, 167, 197, 227], [128, 158, 187, 216], [123, 150, 178, 205], - [116, 142, 169, 195], [111, 135, 160, 185], [105, 128, 152, 175], [100, 122, 144, 166], - [95, 116, 137, 158], [90, 110, 130, 150], [85, 104, 123, 142], [81, 99, 117, 135], - [77, 94, 111, 128], [73, 89, 105, 122], [69, 85, 100, 116], [66, 80, 95, 110], - [62, 76, 90, 104], [59, 72, 86, 99], [56, 69, 81, 94], [53, 65, 77, 89], - [51, 62, 73, 85], [48, 59, 69, 80], [46, 56, 66, 76], [43, 53, 63, 72], - [41, 50, 59, 69], [39, 48, 56, 65], [37, 45, 54, 62], [35, 43, 51, 59], - [33, 41, 48, 56], [32, 39, 46, 53], [30, 37, 43, 50], [29, 35, 41, 48], - [27, 33, 39, 45], [26, 31, 37, 43], [24, 30, 35, 41], [23, 28, 33, 39], - [22, 27, 32, 37], [21, 26, 30, 35], [20, 24, 29, 33], [19, 23, 27, 31], - [18, 22, 26, 30], [17, 21, 25, 28], [16, 20, 23, 27], [15, 19, 22, 25], - [14, 18, 21, 24], [14, 17, 20, 23], [13, 16, 19, 22], [12, 15, 18, 21], - [12, 14, 17, 20], [11, 14, 16, 19], [11, 13, 15, 18], [10, 12, 15, 17], - [10, 12, 14, 16], [9, 11, 13, 15], [9, 11, 12, 14], [8, 10, 12, 14], - [8, 9, 11, 13], [7, 9, 11, 12], [7, 9, 10, 12], [7, 8, 10, 11], - [6, 8, 9, 11], [6, 7, 9, 10], [6, 7, 8, 9], [2, 2, 2, 2], -]; - -/// Table 9-53 transIdxLPS. -#[rustfmt::skip] -static TRANS_IDX_LPS: [u8; 64] = [ - 0, 0, 1, 2, 2, 4, 4, 5, 6, 7, 8, 9, 9, 11, 11, 12, - 13, 13, 15, 15, 16, 16, 18, 18, 19, 19, 21, 21, 22, 22, 23, 24, - 24, 25, 26, 26, 27, 27, 28, 29, 29, 30, 30, 30, 31, 32, 32, 33, - 33, 33, 34, 34, 35, 35, 35, 36, 36, 36, 37, 37, 37, 38, 38, 63, -]; - -/// Table 9-53 transIdxMPS. -#[rustfmt::skip] -static TRANS_IDX_MPS: [u8; 64] = [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, - 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, - 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, - 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 62, 63, -]; - -/// Combined state transition: index = (pStateIdx << 1 | valMPS) * 2 + bin_is_lps. -/// Each context is stored as `pStateIdx << 1 | valMPS`. -pub static NEXT_STATE: [[u8; 2]; 128] = { - let mut t = [[0u8; 2]; 128]; - let mut s = 0; - while s < 128 { - let p = s >> 1; - let mps = s & 1; - // MPS path - t[s][0] = (TRANS_IDX_MPS[p] << 1) | mps as u8; - // LPS path - let new_mps = if p == 0 { 1 - mps } else { mps }; - t[s][1] = (TRANS_IDX_LPS[p] << 1) | new_mps as u8; - s += 1; - } - t -}; - -/// Up-right diagonal scan (6.5.3) of a `N x N` block as (x, y) pairs. -const fn diag(n: usize) -> [(u8, u8); L] { - let mut out = [(0u8, 0u8); L]; - let mut i = 0; - let mut x: i32 = 0; - let mut y: i32 = 0; - while i < L { - while y >= 0 { - if (x as usize) < n && (y as usize) < n { - out[i] = (x as u8, y as u8); - i += 1; - } - y -= 1; - x += 1; - } - y = x; - x = 0; - } - out -} - -/// Horizontal (6.5.4, `vertical == false`) or vertical (6.5.5) scan. -const fn hv(n: usize, vertical: bool) -> [(u8, u8); L] { - let mut out = [(0u8, 0u8); L]; - let mut i = 0; - while i < L { - let (a, b) = ((i % n) as u8, (i / n) as u8); - out[i] = if vertical { (b, a) } else { (a, b) }; - i += 1; - } - out -} - -pub static SCAN_1: [[(u8, u8); 1]; 3] = [[(0, 0)], [(0, 0)], [(0, 0)]]; -pub static SCAN_2: [[(u8, u8); 4]; 3] = [diag::<4>(2), hv::<4>(2, false), hv::<4>(2, true)]; -pub static SCAN_4: [[(u8, u8); 16]; 3] = [diag::<16>(4), hv::<16>(4, false), hv::<16>(4, true)]; -pub static SCAN_8: [[(u8, u8); 64]; 3] = [diag::<64>(8), hv::<64>(8, false), hv::<64>(8, true)]; - -/// ScanOrder[log2BlockSize][scanIdx] (log2BlockSize 0..=3). -#[inline] -pub fn scan_order(log2: u32, scan_idx: usize) -> &'static [(u8, u8)] { - match log2 { - 0 => &SCAN_1[scan_idx], - 1 => &SCAN_2[scan_idx], - 2 => &SCAN_4[scan_idx], - _ => &SCAN_8[scan_idx], - } -} - -/// Up-right diagonal scan for log2 size 2 or 3. -pub fn scan_diag(log2: u32) -> &'static [(u8, u8)] { - scan_order(log2, 0) -} - -/// Table 8-10: QpC as a function of qPi (ChromaArrayType 1). -#[inline] -pub fn qpc_420(qpi: i32) -> i32 { - if qpi < 30 { - qpi - } else if qpi > 43 { - qpi - 6 - } else { - QPC_30_43[(qpi - 30) as usize] as i32 - } -} - -/// Table 9-50 ctxIdxMap for 4x4 sig_coeff_flag contexts. -pub static CTX_IDX_MAP: [u8; 16] = [0, 1, 4, 5, 2, 3, 4, 5, 6, 6, 8, 8, 7, 7, 8, 8]; - -/// levelScale (8-309). -pub static LEVEL_SCALE: [i32; 6] = [40, 45, 51, 57, 64, 72]; - -/// DST-VII 4x4 matrix (8.6.4.2, trType 1). -pub static DST_MATRIX: [[i8; 4]; 4] = [[29, 55, 74, 84], [74, 74, 0, -74], [84, -29, -74, 55], [55, -84, 74, -29]]; - - -// CABAC context layout: offset of each syntax element in the context array (Table 9-4). -pub const SAO_MERGE: usize = 0; -pub const SAO_TYPE: usize = 1; -pub const SPLIT_CU: usize = 2; -pub const CU_TRANSQUANT_BYPASS: usize = 5; -pub const CU_SKIP: usize = 6; -pub const PRED_MODE: usize = 9; -pub const PART_MODE: usize = 10; -pub const PREV_INTRA_LUMA: usize = 14; -pub const INTRA_CHROMA: usize = 15; -pub const RQT_ROOT_CBF: usize = 16; -pub const MERGE_FLAG: usize = 17; -pub const MERGE_IDX: usize = 18; -pub const INTER_PRED_IDC: usize = 19; -pub const REF_IDX: usize = 24; -pub const MVP_FLAG: usize = 26; -pub const SPLIT_TRANSFORM: usize = 27; -pub const CBF_LUMA: usize = 30; -pub const CBF_CHROMA: usize = 32; -pub const ABS_MVD_GT0: usize = 37; -pub const ABS_MVD_GT1: usize = 38; -pub const CU_QP_DELTA_ABS: usize = 39; -pub const TRANSFORM_SKIP: usize = 41; -pub const LAST_X_PREFIX: usize = 43; -pub const LAST_Y_PREFIX: usize = 61; -pub const CODED_SUB_BLOCK: usize = 79; -pub const SIG_COEFF: usize = 83; -pub const GT1: usize = 127; -pub const GT2: usize = 151; -pub const EXPLICIT_RDPCM: usize = 157; -pub const EXPLICIT_RDPCM_DIR: usize = 159; -pub const CU_CHROMA_QP_OFFSET_FLAG: usize = 161; -pub const CU_CHROMA_QP_OFFSET_IDX: usize = 162; -pub const NUM_CTX: usize = 163; - -/// initValue per initType (9.3.2.2). -pub static CABAC_INIT: [[u8; NUM_CTX]; 3] = [ - [153, 200, 139, 141, 157, 154, 154, 154, 154, 154, 184, 154, 154, 154, 184, 63, 154, 154, 154, 154, 154, 154, 154, 154, 154, 154, 154, 153, 138, 138, 111, 141, 94, 138, 182, 154, 154, 154, 154, 154, 154, 139, 139, 110, 110, 124, 125, 140, 153, 125, 127, 140, 109, 111, 143, 127, 111, 79, 108, 123, 63, 110, 110, 124, 125, 140, 153, 125, 127, 140, 109, 111, 143, 127, 111, 79, 108, 123, 63, 91, 171, 134, 141, 111, 111, 125, 110, 110, 94, 124, 108, 124, 107, 125, 141, 179, 153, 125, 107, 125, 141, 179, 153, 125, 107, 125, 141, 179, 153, 125, 140, 139, 182, 182, 152, 136, 152, 136, 153, 136, 139, 111, 136, 139, 111, 141, 111, 140, 92, 137, 138, 140, 152, 138, 139, 153, 74, 149, 92, 139, 107, 122, 152, 140, 179, 166, 182, 140, 227, 122, 197, 138, 153, 136, 167, 152, 152, 154, 154, 154, 154, 154, 154], - [153, 185, 107, 139, 126, 154, 197, 185, 201, 149, 154, 139, 154, 154, 154, 152, 79, 110, 122, 95, 79, 63, 31, 31, 153, 153, 168, 124, 138, 94, 153, 111, 149, 107, 167, 154, 154, 140, 198, 154, 154, 139, 139, 125, 110, 94, 110, 95, 79, 125, 111, 110, 78, 110, 111, 111, 95, 94, 108, 123, 108, 125, 110, 94, 110, 95, 79, 125, 111, 110, 78, 110, 111, 111, 95, 94, 108, 123, 108, 121, 140, 61, 154, 155, 154, 139, 153, 139, 123, 123, 63, 153, 166, 183, 140, 136, 153, 154, 166, 183, 140, 136, 153, 154, 166, 183, 140, 136, 153, 154, 170, 153, 123, 123, 107, 121, 107, 121, 167, 151, 183, 140, 151, 183, 140, 140, 140, 154, 196, 196, 167, 154, 152, 167, 182, 182, 134, 149, 136, 153, 121, 136, 137, 169, 194, 166, 167, 154, 167, 137, 182, 107, 167, 91, 122, 107, 167, 139, 139, 139, 139, 154, 154], - [153, 160, 107, 139, 126, 154, 197, 185, 201, 134, 154, 139, 154, 154, 183, 152, 79, 154, 137, 95, 79, 63, 31, 31, 153, 153, 168, 224, 167, 122, 153, 111, 149, 92, 167, 154, 154, 169, 198, 154, 154, 139, 139, 125, 110, 124, 110, 95, 94, 125, 111, 111, 79, 125, 126, 111, 111, 79, 108, 123, 93, 125, 110, 124, 110, 95, 94, 125, 111, 111, 79, 125, 126, 111, 111, 79, 108, 123, 93, 121, 140, 61, 154, 170, 154, 139, 153, 139, 123, 123, 63, 124, 166, 183, 140, 136, 153, 154, 166, 183, 140, 136, 153, 154, 166, 183, 140, 136, 153, 154, 170, 153, 138, 138, 122, 121, 122, 121, 167, 151, 183, 140, 151, 183, 140, 140, 140, 154, 196, 167, 167, 154, 152, 167, 182, 182, 134, 149, 136, 153, 121, 136, 122, 169, 208, 166, 167, 154, 152, 167, 182, 107, 167, 91, 107, 107, 167, 139, 139, 139, 139, 154, 154], -]; - - -/// Table 8-5: intraPredAngle for predModeIntra 0..34 (0 for planar / DC). -pub static INTRA_PRED_ANGLE: [i8; 35] = [0, 0, 32, 26, 21, 17, 13, 9, 5, 2, 0, -2, -5, -9, -13, -17, -21, -26, -32, -26, -21, -17, -13, -9, -5, -2, 0, 2, 5, 9, 13, 17, 21, 26, 32]; -/// Table 8-6: invAngle for predModeIntra 11..25 (index = mode - 11). -pub static INV_ANGLE: [i16; 15] = [-4096, -1638, -910, -630, -482, -390, -315, -256, -315, -390, -482, -630, -910, -1638, -4096]; - -/// Table 8-10: QpC for qPi = 30..43 (qPi < 30: QpC = qPi; qPi > 43: QpC = qPi - 6). -pub static QPC_30_43: [u8; 14] = [29, 30, 31, 32, 33, 33, 34, 34, 35, 35, 36, 36, 37, 37]; - - -/// Luma 8-tap interpolation filter fL[xFrac][k] (8-236..8-238), taps at offsets -3..4. -pub static LUMA_FILTER: [[i8; 8]; 4] = [[0, 0, 0, 64, 0, 0, 0, 0], [-1, 4, -10, 58, 17, -5, 1, 0], [-1, 4, -11, 40, 40, -11, 4, -1], [0, 1, -5, 17, 58, -10, 4, -1]]; -/// Chroma 4-tap interpolation filter fC[xFrac][k] (8-255..8-261), taps at offsets -1..2. -pub static CHROMA_FILTER: [[i8; 4]; 8] = [[0, 64, 0, 0], [-2, 58, 10, -2], [-4, 54, 16, -2], [-6, 46, 28, -4], [-4, 36, 36, -4], [-4, 28, 46, -6], [-2, 16, 54, -4], [-2, 10, 58, -2]]; - -/// DCT matrix (8-319, 8-321): TRANS_MATRIX[n][m] = transMatrix[m][n] (row n = basis function). -pub static TRANS_MATRIX: [[i8; 32]; 32] = [ - [64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64], - [90, 90, 88, 85, 82, 78, 73, 67, 61, 54, 46, 38, 31, 22, 13, 4, -4, -13, -22, -31, -38, -46, -54, -61, -67, -73, -78, -82, -85, -88, -90, -90], - [90, 87, 80, 70, 57, 43, 25, 9, -9, -25, -43, -57, -70, -80, -87, -90, -90, -87, -80, -70, -57, -43, -25, -9, 9, 25, 43, 57, 70, 80, 87, 90], - [90, 82, 67, 46, 22, -4, -31, -54, -73, -85, -90, -88, -78, -61, -38, -13, 13, 38, 61, 78, 88, 90, 85, 73, 54, 31, 4, -22, -46, -67, -82, -90], - [89, 75, 50, 18, -18, -50, -75, -89, -89, -75, -50, -18, 18, 50, 75, 89, 89, 75, 50, 18, -18, -50, -75, -89, -89, -75, -50, -18, 18, 50, 75, 89], - [88, 67, 31, -13, -54, -82, -90, -78, -46, -4, 38, 73, 90, 85, 61, 22, -22, -61, -85, -90, -73, -38, 4, 46, 78, 90, 82, 54, 13, -31, -67, -88], - [87, 57, 9, -43, -80, -90, -70, -25, 25, 70, 90, 80, 43, -9, -57, -87, -87, -57, -9, 43, 80, 90, 70, 25, -25, -70, -90, -80, -43, 9, 57, 87], - [85, 46, -13, -67, -90, -73, -22, 38, 82, 88, 54, -4, -61, -90, -78, -31, 31, 78, 90, 61, 4, -54, -88, -82, -38, 22, 73, 90, 67, 13, -46, -85], - [83, 36, -36, -83, -83, -36, 36, 83, 83, 36, -36, -83, -83, -36, 36, 83, 83, 36, -36, -83, -83, -36, 36, 83, 83, 36, -36, -83, -83, -36, 36, 83], - [82, 22, -54, -90, -61, 13, 78, 85, 31, -46, -90, -67, 4, 73, 88, 38, -38, -88, -73, -4, 67, 90, 46, -31, -85, -78, -13, 61, 90, 54, -22, -82], - [80, 9, -70, -87, -25, 57, 90, 43, -43, -90, -57, 25, 87, 70, -9, -80, -80, -9, 70, 87, 25, -57, -90, -43, 43, 90, 57, -25, -87, -70, 9, 80], - [78, -4, -82, -73, 13, 85, 67, -22, -88, -61, 31, 90, 54, -38, -90, -46, 46, 90, 38, -54, -90, -31, 61, 88, 22, -67, -85, -13, 73, 82, 4, -78], - [75, -18, -89, -50, 50, 89, 18, -75, -75, 18, 89, 50, -50, -89, -18, 75, 75, -18, -89, -50, 50, 89, 18, -75, -75, 18, 89, 50, -50, -89, -18, 75], - [73, -31, -90, -22, 78, 67, -38, -90, -13, 82, 61, -46, -88, -4, 85, 54, -54, -85, 4, 88, 46, -61, -82, 13, 90, 38, -67, -78, 22, 90, 31, -73], - [70, -43, -87, 9, 90, 25, -80, -57, 57, 80, -25, -90, -9, 87, 43, -70, -70, 43, 87, -9, -90, -25, 80, 57, -57, -80, 25, 90, 9, -87, -43, 70], - [67, -54, -78, 38, 85, -22, -90, 4, 90, 13, -88, -31, 82, 46, -73, -61, 61, 73, -46, -82, 31, 88, -13, -90, -4, 90, 22, -85, -38, 78, 54, -67], - [64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64], - [61, -73, -46, 82, 31, -88, -13, 90, -4, -90, 22, 85, -38, -78, 54, 67, -67, -54, 78, 38, -85, -22, 90, 4, -90, 13, 88, -31, -82, 46, 73, -61], - [57, -80, -25, 90, -9, -87, 43, 70, -70, -43, 87, 9, -90, 25, 80, -57, -57, 80, 25, -90, 9, 87, -43, -70, 70, 43, -87, -9, 90, -25, -80, 57], - [54, -85, -4, 88, -46, -61, 82, 13, -90, 38, 67, -78, -22, 90, -31, -73, 73, 31, -90, 22, 78, -67, -38, 90, -13, -82, 61, 46, -88, 4, 85, -54], - [50, -89, 18, 75, -75, -18, 89, -50, -50, 89, -18, -75, 75, 18, -89, 50, 50, -89, 18, 75, -75, -18, 89, -50, -50, 89, -18, -75, 75, 18, -89, 50], - [46, -90, 38, 54, -90, 31, 61, -88, 22, 67, -85, 13, 73, -82, 4, 78, -78, -4, 82, -73, -13, 85, -67, -22, 88, -61, -31, 90, -54, -38, 90, -46], - [43, -90, 57, 25, -87, 70, 9, -80, 80, -9, -70, 87, -25, -57, 90, -43, -43, 90, -57, -25, 87, -70, -9, 80, -80, 9, 70, -87, 25, 57, -90, 43], - [38, -88, 73, -4, -67, 90, -46, -31, 85, -78, 13, 61, -90, 54, 22, -82, 82, -22, -54, 90, -61, -13, 78, -85, 31, 46, -90, 67, 4, -73, 88, -38], - [36, -83, 83, -36, -36, 83, -83, 36, 36, -83, 83, -36, -36, 83, -83, 36, 36, -83, 83, -36, -36, 83, -83, 36, 36, -83, 83, -36, -36, 83, -83, 36], - [31, -78, 90, -61, 4, 54, -88, 82, -38, -22, 73, -90, 67, -13, -46, 85, -85, 46, 13, -67, 90, -73, 22, 38, -82, 88, -54, -4, 61, -90, 78, -31], - [25, -70, 90, -80, 43, 9, -57, 87, -87, 57, -9, -43, 80, -90, 70, -25, -25, 70, -90, 80, -43, -9, 57, -87, 87, -57, 9, 43, -80, 90, -70, 25], - [22, -61, 85, -90, 73, -38, -4, 46, -78, 90, -82, 54, -13, -31, 67, -88, 88, -67, 31, 13, -54, 82, -90, 78, -46, 4, 38, -73, 90, -85, 61, -22], - [18, -50, 75, -89, 89, -75, 50, -18, -18, 50, -75, 89, -89, 75, -50, 18, 18, -50, 75, -89, 89, -75, 50, -18, -18, 50, -75, 89, -89, 75, -50, 18], - [13, -38, 61, -78, 88, -90, 85, -73, 54, -31, 4, 22, -46, 67, -82, 90, -90, 82, -67, 46, -22, -4, 31, -54, 73, -85, 90, -88, 78, -61, 38, -13], - [9, -25, 43, -57, 70, -80, 87, -90, 90, -87, 80, -70, 57, -43, 25, -9, -9, 25, -43, 57, -70, 80, -87, 90, -90, 87, -80, 70, -57, 43, -25, 9], - [4, -13, 22, -31, 38, -46, 54, -61, 67, -73, 78, -82, 85, -88, 90, -90, 90, -90, 88, -85, 82, -78, 73, -67, 61, -54, 46, -38, 31, -22, 13, -4], -]; -/// Table 8-12: beta' for Q = 0..51. -pub static BETA_TABLE: [u8; 52] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64]; -/// Table 8-12: tC' for Q = 0..53. -pub static TC_TABLE: [u8; 54] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 6, 6, 7, 8, 9, 10, 11, 13, 14, 16, 18, 20, 22, 24]; diff --git a/crates/hevcenc/src/transform.rs b/crates/hevcenc/src/transform.rs deleted file mode 100644 index cffea75c2..000000000 --- a/crates/hevcenc/src/transform.rs +++ /dev/null @@ -1,328 +0,0 @@ -//! Forward transforms (encoder side, any integer approximation is allowed), quantisation, and the -//! normative scaling (8.6.3) and inverse transforms (8.6.4.2) the decoder applies. The inverse path -//! is exact integer arithmetic: the even/odd decomposition computes the same sums as the matrix -//! products of the specification. - -use crate::tables::{DST_MATRIX, LEVEL_SCALE, TRANS_MATRIX}; - -#[inline] -fn m(n: usize, k: usize, i: usize) -> i32 { - // k-th basis function of the n-point DCT, sample i - TRANS_MATRIX[k * (32 / n)][i] as i32 -} - -/// 1-D forward DCT of `x` (length n) into `out`. -fn fdct1d(x: &[i32], out: &mut [i32], n: usize) { - if n <= 4 { - for k in 0..n { - let mut s = 0; - for i in 0..n { - s += m(n, k, i) * x[i]; - } - out[k] = s; - } - return; - } - let h = n / 2; - let mut e = [0i32; 16]; - let mut o = [0i32; 16]; - for i in 0..h { - e[i] = x[i] + x[n - 1 - i]; - o[i] = x[i] - x[n - 1 - i]; - } - let mut ev = [0i32; 16]; - fdct1d(&e[..h], &mut ev[..h], h); - for k in 0..h { - out[2 * k] = ev[k]; - let kk = 2 * k + 1; - let mut s = 0; - for i in 0..h { - s += m(n, kk, i) * o[i]; - } - out[kk] = s; - } -} - -/// 1-D inverse DCT: x[i] = sum_k M[k][i] * c[k]. -fn idct1d(c: &[i32], out: &mut [i32], n: usize) { - if n <= 4 { - for i in 0..n { - let mut s = 0; - for k in 0..n { - s += m(n, k, i) * c[k]; - } - out[i] = s; - } - return; - } - let h = n / 2; - let mut ce = [0i32; 16]; - for k in 0..h { - ce[k] = c[2 * k]; - } - let mut e = [0i32; 16]; - idct1d(&ce[..h], &mut e[..h], h); - for i in 0..h { - let mut o = 0; - let mut k = 1; - while k < n { - let v = c[k]; - if v != 0 { - o += m(n, k, i) * v; - } - k += 2; - } - out[i] = e[i] + o; - out[n - 1 - i] = e[i] - o; - } -} - -fn fdst1d(x: &[i32], out: &mut [i32]) { - for k in 0..4 { - out[k] = (0..4).map(|i| DST_MATRIX[k][i] as i32 * x[i]).sum(); - } -} - -fn idst1d(c: &[i32], out: &mut [i32]) { - for i in 0..4 { - out[i] = (0..4).map(|k| DST_MATRIX[k][i] as i32 * c[k]).sum(); - } -} - -/// Forward 2-D transform of the n x n residual `r` (row-major) into `c`. -pub fn forward(r: &[i32], c: &mut [i32], n: usize, dst: bool, bit_depth: u32) { - let log2 = n.trailing_zeros(); - let shift1 = log2 + bit_depth - 9; - let shift2 = log2 + 6; - let (r1, r2) = (1i32 << (shift1 - 1), 1i32 << (shift2 - 1)); - let mut tmp = vec![0i32; n * n]; - let mut out = [0i32; 32]; - // rows (horizontal frequencies) - for y in 0..n { - let row = &r[y * n..y * n + n]; - if dst { - fdst1d(row, &mut out); - } else { - fdct1d(row, &mut out, n); - } - for k in 0..n { - tmp[k * n + y] = (out[k] + r1) >> shift1; // transposed: tmp[kx][y] - } - } - // columns - let mut col = [0i32; 32]; - for kx in 0..n { - col[..n].copy_from_slice(&tmp[kx * n..kx * n + n]); - if dst { - fdst1d(&col, &mut out); - } else { - fdct1d(&col, &mut out, n); - } - for ky in 0..n { - c[ky * n + kx] = (out[ky] + r2) >> shift2; - } - } -} - -/// Inverse 2-D transform (8.6.4.2) of scaled coefficients `d` in place, producing residuals. -pub fn inverse(d: &mut [i32], n: usize, dst: bool, bit_depth: u32) { - let bd_shift = 20 - bit_depth; - let mut tmp = vec![0i32; n * n]; - let mut col = [0i32; 32]; - let mut out = [0i32; 32]; - // max non-zero row, to skip empty work - let mut nz_cols = [false; 32]; - for y in 0..n { - for x in 0..n { - if d[y * n + x] != 0 { - nz_cols[x] = true; - } - } - } - // 1. columns - for x in 0..n { - if !nz_cols[x] { - continue; // tmp column stays zero - } - for j in 0..n { - col[j] = d[j * n + x]; - } - if dst { - idst1d(&col, &mut out); - } else { - idct1d(&col, &mut out, n); - } - for y in 0..n { - tmp[y * n + x] = ((out[y] + 64) >> 7).clamp(-32768, 32767); - } - } - // 2. rows - let round = 1 << (bd_shift - 1); - for y in 0..n { - let row = &tmp[y * n..y * n + n]; - if dst { - idst1d(row, &mut out); - } else { - idct1d(row, &mut out, n); - } - for x in 0..n { - d[y * n + x] = (out[x] + round) >> bd_shift; - } - } -} - -const QUANT_SCALE: [i64; 6] = [26214, 23302, 20560, 18396, 16384, 14564]; - -/// Quantise transform coefficients `c` into levels `l` with a dead-zone rounding offset -/// (`intra`: 1/3, otherwise 1/6). `qp` includes QpBdOffset. Returns the number of non-zero levels. -pub fn quantize(c: &[i32], l: &mut [i32], n: usize, qp: i32, intra: bool, bit_depth: u32) -> usize { - let log2 = n.trailing_zeros() as i32; - let tshift = 15 - bit_depth as i32 - log2; - let qbits = 14 + qp / 6 + tshift; - let scale = QUANT_SCALE[(qp % 6) as usize]; - let offset = (if intra { 171i64 } else { 85 }) << (qbits - 9); - let mut nz = 0; - for i in 0..n * n { - let v = c[i] as i64; - let a = ((v.abs() * scale + offset) >> qbits).min(32767); - if a != 0 { - nz += 1; - } - l[i] = if v < 0 { -(a as i32) } else { a as i32 }; - } - nz -} - -/// Sign data hiding (7.3.8.11 signHidden): in every 4x4 sub-block whose first and last non-zero -/// coefficients (in scan order) are more than 3 positions apart, the sign of the first one is not -/// coded but inferred from the parity of the sum of absolute levels. Fix the parity where needed by -/// the +-1 level change that adds the least quantisation error, without moving the first / last -/// non-zero positions. -pub fn sign_hiding(c: &[i32], l: &mut [i32], log2: u32, scan_idx: usize, qp: i32, bit_depth: u32) { - use crate::tables::{SCAN_4, scan_order}; - let n = 1usize << log2; - let tshift = 15 - bit_depth as i32 - log2 as i32; - let qbits = 14 + qp / 6 + tshift; - let scale = QUANT_SCALE[(qp % 6) as usize]; - let sb_scan = scan_order(log2 - 2, scan_idx); - let pos = &SCAN_4[scan_idx]; - for &(xs, ys) in sb_scan { - let idx = |p: usize| (ys as usize * 4 + pos[p].1 as usize) * n + xs as usize * 4 + pos[p].0 as usize; - let (Some(first), Some(last)) = ((0..16).find(|&p| l[idx(p)] != 0), (0..16).rev().find(|&p| l[idx(p)] != 0)) else { continue }; - if last - first <= 3 { - continue; - } - let sum: i32 = (0..16).map(|p| l[idx(p)].abs()).sum(); - let negative = l[idx(first)] < 0; - if negative == (sum & 1 == 1) { - continue; - } - // cost of +-1 changes in 1/256 units of quantisation error - let mut best: Option<(i64, usize, i32)> = None; - for p in first..=last { - let i = idx(p); - let q = ((c[i] as i64).abs() * scale) >> (qbits - 8); - let a = l[i].abs() as i64; - let err = |lv: i64| (q - (lv << 8)).abs(); - let mut consider = |cost: i64, d: i32| { - if best.is_none_or(|b| cost < b.0) { - best = Some((cost, i, d)); - } - }; - if a < 32767 && (a > 0 || (p > first && p < last)) { - consider(err(a + 1) - err(a), 1); - } - if a > 1 || (a == 1 && p > first && p < last) { - consider(err(a - 1) - err(a), -1); - } - } - if let Some((_, i, d)) = best { - let a = l[i].abs() + d; - l[i] = if c[i] < 0 { -a } else { a }; - } - } -} - -/// Scaling process (8.6.2 / 8.6.3, flat scaling matrix): levels to scaled coefficients. -pub fn dequantize(l: &[i32], d: &mut [i32], n: usize, qp: i32, bit_depth: u32) { - let log2 = n.trailing_zeros(); - let bd_shift = bit_depth + log2 - 5; - let round = 1i64 << (bd_shift - 1); - let scale = (LEVEL_SCALE[(qp % 6) as usize] as i64) << (qp / 6); - for i in 0..n * n { - let v = l[i]; - d[i] = if v == 0 { 0 } else { ((v as i64 * 16 * scale + round) >> bd_shift).clamp(-32768, 32767) as i32 }; - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Direct matrix form of the inverse transform, as written in the specification. - fn inverse_ref(c: &mut [i32], n: usize, dst: bool, bd: u32) { - let mut tmp = vec![0i32; n * n]; - let step = 32 / n; - for x in 0..n { - for y in 0..n { - let mut s = 0i32; - for j in 0..n { - let m = if dst { DST_MATRIX[j][y] as i32 } else { TRANS_MATRIX[j * step][y] as i32 }; - s += m * c[j * n + x]; - } - tmp[y * n + x] = ((s + 64) >> 7).clamp(-32768, 32767); - } - } - let bd_shift = 20 - bd; - for y in 0..n { - for x in 0..n { - let mut s = 0i32; - for j in 0..n { - let m = if dst { DST_MATRIX[j][x] as i32 } else { TRANS_MATRIX[j * step][x] as i32 }; - s += m * tmp[y * n + j]; - } - c[y * n + x] = (s + (1 << (bd_shift - 1))) >> bd_shift; - } - } - } - - #[test] - fn inverse_matches_matrix_form() { - let mut seed = 7u32; - for &(n, dst) in &[(4usize, true), (4, false), (8, false), (16, false), (32, false)] { - for _ in 0..20 { - let mut c = vec![0i32; n * n]; - for v in c.iter_mut() { - seed = seed.wrapping_mul(1664525).wrapping_add(1013904223); - if (seed >> 24).is_multiple_of(3) { - *v = ((seed >> 8) % 2001) as i32 - 1000; - } - } - let mut a = c.clone(); - let mut b = c.clone(); - inverse(&mut a, n, dst, 8); - inverse_ref(&mut b, n, dst, 8); - assert_eq!(a, b, "n {n} dst {dst}"); - } - } - } - - #[test] - fn round_trip_is_close() { - for &(n, dst) in &[(4usize, true), (8, false), (32, false)] { - for bd in [8u32, 10] { - let r: Vec = (0..n * n).map(|i| ((i * 37 + i / n * 11) % 61) as i32 - 30).collect(); - let mut c = vec![0; n * n]; - forward(&r, &mut c, n, dst, bd); - let mut l = vec![0; n * n]; - let qp = 4; - quantize(&c, &mut l, n, qp, true, bd); - let mut d = vec![0; n * n]; - dequantize(&l, &mut d, n, qp, bd); - inverse(&mut d, n, dst, bd); - let err: i32 = r.iter().zip(&d).map(|(a, b)| (a - b).abs()).max().unwrap(); - assert!(err <= 2, "n {n} bd {bd} err {err}"); - } - } - } -} diff --git a/crates/host/src/lib.rs b/crates/host/src/lib.rs index 3d1384a90..843a06123 100644 --- a/crates/host/src/lib.rs +++ b/crates/host/src/lib.rs @@ -1,6 +1,6 @@ //! A fully wired [`Session`]: footage decoding through `effectcraft-media` (FilmCraft's codecs), //! the media importer, the expression engine, JavaScript scripting (`effectcraft-script`) and -//! Render Queue export through `effectcraft-export` (FilmCraft's encoders). Frontends (desktop, +//! Render Queue export through `effectcraft-export` (craft-libs' encoders). Frontends (desktop, //! CLI, MCP, web) start here. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] @@ -65,6 +65,7 @@ impl Exporter for FileExporter { caches: job.caches.map(|c| effectcraft_export::JobCaches { gpu: &c.gpu, cpu: &c.cpu }), renderer: job.renderer, gpu_frames: job.gpu_frames, + free_memory: Some(&free_memory), }, nested_switches: job.nested_switches, }; @@ -87,6 +88,11 @@ impl Exporter for FileExporter { } } +/// The memory the system can hand out now, for an export's Auto CPU frames in flight (#1615). +fn free_memory() -> Option { + effectcraft_engine::sysinfo::memory().map(|m| m.available) +} + /// A new session with media, import, expressions, scripting and export enabled. pub fn session() -> Session { let mut s = Session { diff --git a/crates/host/src/tests.rs b/crates/host/src/tests.rs index 55240698f..687aafe77 100644 --- a/crates/host/src/tests.rs +++ b/crates/host/src/tests.rs @@ -547,6 +547,7 @@ fn script_imports_footage_and_saves() { std::fs::create_dir_all(&dir).unwrap(); let q = |n: &str| serde_json::to_string(&dir.join(n).to_string_lossy()).unwrap(); let mut s = super::session(); + s.execute("prefs.set", json!({"key": "scripting.allowScriptsWriteFiles", "value": true})).unwrap(); let code = format!( r#" var c = app.project.items.addComp("Src", 32, 32, 1, 1, 10); diff --git a/crates/host/tests/it/plugins.rs b/crates/host/tests/it/plugins.rs index 33748d366..a5fc0b8e1 100644 --- a/crates/host/tests/it/plugins.rs +++ b/crates/host/tests/it/plugins.rs @@ -276,8 +276,8 @@ pub(super) fn wat_plugin(id: &str, api: i32, render: &str) -> String { #[test] fn plugin_loading_reports_bad_modules_and_survives_failing_renders() { - let dir = std::env::temp_dir().join(format!("effectcraft-host-plugins-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); + let guard = tempfile::tempdir().unwrap(); + let dir = guard.path().to_path_buf(); let files = [ ("a-good.wasm", wat_plugin("org.test.host.good", 1, "i32.const 0").into_bytes()), ("b-garbage.wasm", b"\0asm\x01\0\0\0garbage".to_vec()), diff --git a/crates/interchange/src/export.rs b/crates/interchange/src/export.rs index 375c1bf1b..fd396b00b 100644 --- a/crates/interchange/src/export.rs +++ b/crates/interchange/src/export.rs @@ -303,16 +303,41 @@ impl Exporter<'_> { } /// The bin mirroring a Project panel folder. + /// + /// Walks up the folder's parents without recursion: a damaged or hostile project can hold a + /// parent cycle (or a very long chain), which recursed until the stack ran out. A folder seen + /// twice in one walk ends it, and the cycle's topmost folder becomes a top-level bin. fn bin(&mut self, folder: Option) -> Option { let f = folder?; - if let Some(b) = self.bins.get(&f) { - return Some(*b); + // The uncached folders from `f` up, and the bin they hang from. + let mut chain: Vec = vec![]; + let mut in_chain = std::collections::HashSet::new(); + let mut parent = None; + let mut at = Some(f); + while let Some(id) = at { + if let Some(b) = self.bins.get(&id) { + parent = Some(*b); + break; + } + if !in_chain.insert(id) { + self.warn("a folder that contains itself was exported as a top-level bin"); + break; + } + let Some(it) = self.p.item(id) else { break }; + chain.push(id); + at = it.parent; } - let it = self.p.item(f)?; - let parent = self.bin(it.parent); - let b = self.fp.add_bin(&it.name, parent); - self.bins.insert(f, b); - Some(b) + if chain.is_empty() { + // `f` is cached already (`parent` is its bin), or is not an item (`None`). + return parent; + } + for id in chain.into_iter().rev() { + let name = self.p.item(id).map(|it| it.name.clone()).unwrap_or_default(); + let b = self.fp.add_bin(&name, parent); + self.bins.insert(id, b); + parent = Some(b); + } + parent } fn footage_item(&mut self, item: ItemId, f: &Footage) -> fp::ItemId { @@ -615,12 +640,34 @@ impl Exporter<'_> { } } +/// Whether `c` or a comp nested in it (at any depth) has a layer of footage with audio. +/// +/// A walk with a stack and a visited set, as in `Project::comp_contains`: recursion never ended +/// on a precomp cycle, and re-walked a comp shared by many layers once per path (exponential +/// for a chain of comps that each use the next one twice). fn comp_has_audio(p: &Project, c: &Comp) -> bool { - c.layers.iter().any(|l| match &l.source { - LayerSource::Footage { item } => matches!(p.item(*item).map(|i| &i.kind), Some(ItemKind::Footage(f)) if f.has_audio), - LayerSource::Comp { item } => p.comp(*item).is_some_and(|c| comp_has_audio(p, c)), - _ => false, - }) + let mut seen = std::collections::HashSet::new(); + let mut stack = vec![c]; + while let Some(c) = stack.pop() { + for l in &c.layers { + match &l.source { + LayerSource::Footage { item } => { + if matches!(p.item(*item).map(|i| &i.kind), Some(ItemKind::Footage(f)) if f.has_audio) { + return true; + } + } + LayerSource::Comp { item } => { + if seen.insert(*item) + && let Some(nested) = p.comp(*item) + { + stack.push(nested); + } + } + _ => {} + } + } + } + false } fn differs(v: &Value, def: [f64; 2]) -> bool { @@ -703,3 +750,37 @@ fn media_info(name: &str, kind: MediaKind, duration: Tick, video: Option fp::MediaClip { fp::MediaClip { media, info, interpret: Default::default(), mark_in: None, mark_out: None, markers: vec![], offline: false, proxy: None, identity: None } } + +#[cfg(test)] +mod tests { + use super::*; + + /// A folder's bin is the same one each time it is asked for (a cached folder once answered + /// `None`, so a folder's second item went to the project root); a non-item has no bin. + #[test] + fn folder_bins_are_reused_once_cached() { + let mut p = Project::default(); + let a = p.add_item("A", effectcraft_color::Label::None, None, ItemKind::Folder); + let b = p.add_item("B", effectcraft_color::Label::None, Some(a), ItemKind::Folder); + let (opts, pre) = (ExportOptions::default(), HashMap::new()); + let mut ex = Exporter { + p: &p, + opts: &opts, + pre: &pre, + fp: fp::Project::new("x"), + out: ExportOutput::default(), + seqs: HashMap::new(), + media: HashMap::new(), + bins: HashMap::new(), + stack: vec![], + }; + let bb = ex.bin(Some(b)); + assert!(bb.is_some()); + assert_eq!(ex.bin(Some(b)), bb); + let ba = ex.bin(Some(a)); + assert!(ba.is_some() && ba != bb); + assert_eq!(ex.bin(Some(a)), ba); + assert_eq!(ex.bin(Some(ItemId(u64::MAX))), None); + assert_eq!(ex.bin(None), None); + } +} diff --git a/crates/interchange/src/import.rs b/crates/interchange/src/import.rs index 22e22b4f2..6faadd91a 100644 --- a/crates/interchange/src/import.rs +++ b/crates/interchange/src/import.rs @@ -177,10 +177,12 @@ pub fn import( } let folder_name = if opts.name.is_empty() { "Imported Timeline".to_string() } else { opts.name.clone() }; res.folder = project.add_item(&folder_name, Label::None, None, ItemKind::Folder); + let warned = res.warnings.iter().cloned().collect(); let mut im = Importer { src: &imported.project, project, res, + warned, items: HashMap::new(), solids: HashMap::new(), solids_folder: None, @@ -273,6 +275,8 @@ struct Importer<'a> { src: &'a fp::Project, project: &'a mut Project, res: ImportResult, + /// The warnings in `res`, to say each once without searching them all every time. + warned: HashSet, /// Source item → created item (footage, comp). items: HashMap, /// Generator / adjustment solids by (source item, width, height). @@ -290,7 +294,7 @@ struct Importer<'a> { impl Importer<'_> { fn warn(&mut self, m: impl Into) { let m = m.into(); - if !self.res.warnings.contains(&m) { + if self.warned.insert(m.clone()) { self.res.warnings.push(m); } } @@ -429,23 +433,8 @@ impl Importer<'_> { comp.markers = seq.markers.iter().map(marker_in).collect(); // Video: the top track's clips first (layer #1); later clips of a track above earlier ones // so incoming clips of dissolves cover outgoing ones. - for track in seq.video_tracks.iter().rev() { - let mut items: Vec<&fp::TrackItem> = track.items.iter().collect(); - items.sort_by_key(|i| std::cmp::Reverse(i.start)); - for ti in items { - if let Some(l) = self.clip_layer(&comp, seq, track, ti) { - comp.layers.push(l); - } - } - } - for track in &seq.audio_tracks { - let mut items: Vec<&fp::TrackItem> = track.items.iter().collect(); - items.sort_by_key(|i| std::cmp::Reverse(i.start)); - for ti in items { - if let Some(l) = self.clip_layer(&comp, seq, track, ti) { - comp.layers.push(l); - } - } + for track in seq.video_tracks.iter().rev().chain(&seq.audio_tracks) { + self.track_layers(&mut comp, seq, track); } if !seq.caption_tracks.is_empty() { self.warn("caption tracks are not imported"); @@ -508,7 +497,45 @@ impl Importer<'_> { } } - fn clip_layer(&mut self, comp: &Comp, seq: &fp::Sequence, track: &fp::Track, ti: &fp::TrackItem) -> Option { + /// A track's clips as layers of `comp`, the latest first. + fn track_layers(&mut self, comp: &mut Comp, seq: &fp::Sequence, track: &fp::Track) { + let mut items: Vec<&fp::TrackItem> = track.items.iter().collect(); + items.sort_by_key(|i| std::cmp::Reverse(i.start)); + // Each clip's transitions, found once per track: scanning every transition of the track + // for every clip made a long flat edit (many clips and many transitions) quadratic. + let mut transitions: HashMap> = HashMap::new(); + for tr in &track.transitions { + for clip in [tr.from, tr.to].into_iter().flatten() { + let list = transitions.entry(clip).or_default(); + // A transition from and to the same clip still counts once. + if !list.last().is_some_and(|last| std::ptr::eq(*last, tr)) { + list.push(tr); + } + } + } + let mut any = false; + for ti in items { + let own = transitions.get(&ti.id).map(Vec::as_slice).unwrap_or_default(); + if let Some(l) = self.clip_layer(comp, seq, track, ti, own) { + comp.layers.push(l); + any = true; + } + } + // What the track's transitions become, said once a clip of the track was imported. + if any && track.kind != fp::TrackKind::Audio { + for tr in &track.transitions { + let id = tr.effect.effect.as_str(); + if !matches!(id, "cross_dissolve" | "dip_to_black" | "dip_to_white" | "film_dissolve" | "additive_dissolve" | "non_additive_dissolve") { + self.warn(format!("transition \"{}\" was imported as a cross dissolve", tr.effect.def().map(|d| d.name).unwrap_or(id))); + } + } + } + if any && track.transitions.iter().any(|tr| tr.effect.effect == "dip_to_white") { + self.warn("Dip to White was imported as a dip to the background"); + } + } + + fn clip_layer(&mut self, comp: &Comp, seq: &fp::Sequence, track: &fp::Track, ti: &fp::TrackItem, transitions: &[&fp::Transition]) -> Option { let audio_track = track.kind == fp::TrackKind::Audio; let (source, size, has_audio) = self.source_of(ti, comp.width, comp.height)?; let is_adjustment = matches!(self.src.item(ti.item).map(|i| &i.kind), Some(fp::ItemKind::AdjustmentLayer { .. })); @@ -561,18 +588,9 @@ impl Importer<'_> { } // Transitions extend the layer over their handles. let mut fades: Vec<(Tick, Tick, f64, f64)> = vec![]; - for tr in &track.transitions { + for tr in transitions { let (ts, te) = (tick_in(tr.start), tick_in(tr.end())); - let id = tr.effect.effect.as_str(); - let dip = id.starts_with("dip_to"); - if !audio_track - && !matches!(id, "cross_dissolve" | "dip_to_black" | "dip_to_white" | "film_dissolve" | "additive_dissolve" | "non_additive_dissolve") - { - self.warn(format!("transition \"{}\" was imported as a cross dissolve", tr.effect.def().map(|d| d.name).unwrap_or(id))); - } - if dip && id == "dip_to_white" { - self.warn("Dip to White was imported as a dip to the background"); - } + let dip = tr.effect.effect.starts_with("dip_to"); let mid = ts + Tick((te - ts).0 / 2); if tr.to == Some(ti.id) { l.in_point = l.in_point.min(ts); diff --git a/crates/interchange/src/tests.rs b/crates/interchange/src/tests.rs index b451ecf81..4ce892c15 100644 --- a/crates/interchange/src/tests.rs +++ b/crates/interchange/src/tests.rs @@ -787,3 +787,98 @@ fn deeply_nested_documents_error_instead_of_overflowing_the_stack() { assert_eq!(super::import::xml_depth(b"]]>"), 2); assert_eq!(super::import::json_depth(br#"{"a": "[[[{{", "b": [[1], {"c": "\\\"["}]}"#), 3); } + +/// A hostile project: footage in a folder whose parents loop back to it. Export walked the +/// folder's parents recursively and never reached the cache insert, so the stack ran out. +#[test] +fn export_survives_a_folder_parent_cycle() { + let (mut p, cid) = ae_project(); + let a = p.add_item("A", effectcraft_color::Label::None, None, ItemKind::Folder); + let b = p.add_item("B", effectcraft_color::Label::None, Some(a), ItemKind::Folder); + p.items.get_mut(&a).unwrap().parent = Some(b); + let footage: Vec = p.items.values().filter(|i| matches!(i.kind, ItemKind::Footage(_))).map(|i| i.id).collect(); + for f in footage { + p.items.get_mut(&f).unwrap().parent = Some(a); + } + let out = export(&p, cid, &ExportOptions::default(), &HashMap::new()).unwrap(); + assert!(out.warnings.iter().any(|w| w.contains("contains itself")), "{:?}", out.warnings); +} + +/// Precomps that use each other, and a chain of comps that each use the next one twice: the +/// audio check recursed forever on the first and 2^depth times on the second. +#[test] +fn export_audio_check_ends_on_precomp_cycles_and_shared_chains() { + let mut p = Project::default(); + let a = footage_item(&mut p, "/media/a.mov"); + let comp = || Comp::new(W, H, FrameRate::new(25, 1), t(2.0)); + // A chain of 60 comps, each using the next twice; the last one is silent footage. + let mut next = { + let mut c = comp(); + let l = effectcraft_project::build::layer(&mut p, &c, "a", LayerSource::Footage { item: a }, (W, H), None); + c.layers.push(l); + comp_item(&mut p, "Leaf", c) + }; + for i in 0..60 { + let mut c = comp(); + for n in 0..2 { + let l = effectcraft_project::build::layer(&mut p, &c, &format!("use {n}"), LayerSource::Comp { item: next }, (W, H), None); + c.layers.push(l); + } + next = comp_item(&mut p, &format!("Chain {i}"), c); + } + let started = std::time::Instant::now(); + export(&p, next, &ExportOptions::default(), &HashMap::new()).unwrap(); + assert!(started.elapsed() < std::time::Duration::from_secs(30), "{:?}", started.elapsed()); + // Two comps nesting each other. + let x = comp_item(&mut p, "X", comp()); + let mut y = comp(); + let l = effectcraft_project::build::layer(&mut p, &y, "x", LayerSource::Comp { item: x }, (W, H), None); + y.layers.push(l); + let y = comp_item(&mut p, "Y", y); + let mut xc = comp(); + let l = effectcraft_project::build::layer(&mut p, &xc, "y", LayerSource::Comp { item: y }, (W, H), None); + xc.layers.push(l); + if let Some(ItemKind::Comp(c)) = p.items.get_mut(&x).map(|i| &mut i.kind) { + *c = Arc::new(xc); + } + export(&p, x, &ExportOptions::default(), &HashMap::new()).unwrap(); +} + +/// A flat edit with many clips and many transitions: each clip finds its own transitions (it +/// scanned every transition of the track per clip), and they still extend and fade it. +#[test] +fn import_finds_each_clips_transitions_without_scanning_the_track() { + let mut p = fp::Project::new("Flat"); + let a = media(&mut p, "a.png", "/media/a.mov", filmcraft_media::MediaKind::Movie, true, false, None); + let settings = fp::SequenceSettings { width: W, height: H, frame_rate: rate(), ..Default::default() }; + let main = p.new_sequence("Main", settings, 1, 0, None); + const N: usize = 500; + let mut items = Vec::with_capacity(N); + let mut transitions = Vec::with_capacity(N); + for i in 0..N { + let ti = item(&mut p, a, fp::TrackKind::Video, i as f64 * 0.4, 0.0, 0.4); + if let Some(prev) = items.last().map(|x: &fp::TrackItem| x.id) { + transitions.push(fp::Transition { + id: fp::TransitionId(p.alloc_id()), + effect: fp::find_effect("cross_dissolve").unwrap().instance(), + start: secs(i as f64 * 0.4 - 0.08), + duration: secs(0.16), + from: Some(prev), + to: Some(ti.id), + align: fp::TransitionAlign::CenterAtCut, + reverse: false, + }); + } + items.push(ti); + } + let seq = p.sequence_mut(main).unwrap(); + seq.video_tracks[0].items = items; + seq.video_tracks[0].transitions = transitions; + let (bytes, _) = fc::export(&p, main, fc::Format::Otio, &Default::default()).unwrap(); + let (q, r) = import_doc(&bytes, "Flat.otio", &mut probe); + let m = q.comp(r.comps[0]).unwrap(); + assert_eq!(m.layers.len(), N); + // A middle clip reaches over both of its transitions' handles. + let mid = m.layers.iter().find(|l| near(l.in_point, t(100.0 - 0.08))).expect("clip 250 extended into its incoming transition"); + assert!(near(mid.out_point, t(100.4 + 0.08)), "{}", mid.out_point.seconds()); +} diff --git a/crates/lottie/src/anim.rs b/crates/lottie/src/anim.rs index 5b6dabf50..40a56a7eb 100644 --- a/crates/lottie/src/anim.rs +++ b/crates/lottie/src/anim.rs @@ -21,6 +21,9 @@ use serde_json::{Value as Json, json}; use crate::Timebase; +/// Most distinct warnings an import or export reports. +const MAX_WARNINGS: usize = 200; + /// Export state shared by the whole document. pub(crate) struct Ex { pub tb: Timebase, @@ -31,7 +34,8 @@ pub(crate) struct Ex { impl Ex { pub fn warn(&mut self, w: impl Into) { let w = w.into(); - if !self.warnings.contains(&w) { + // Each warning is looked for among the earlier ones: past a screenful, more add nothing. + if self.warnings.len() < MAX_WARNINGS && !self.warnings.contains(&w) { self.warnings.push(w); } } @@ -46,7 +50,8 @@ pub(crate) struct Im { impl Im { pub fn warn(&mut self, w: impl Into) { let w = w.into(); - if !self.warnings.contains(&w) { + // Each warning is looked for among the earlier ones: past a screenful, more add nothing. + if self.warnings.len() < MAX_WARNINGS && !self.warnings.contains(&w) { self.warnings.push(w); } } diff --git a/crates/lottie/src/import.rs b/crates/lottie/src/import.rs index 57ef6427a..1e1430e12 100644 --- a/crates/lottie/src/import.rs +++ b/crates/lottie/src/import.rs @@ -1,6 +1,6 @@ //! Lottie document → EffectCraft compositions (the inverse of [`crate::export`]). -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; use std::sync::Arc; use effectcraft_color::Label; @@ -17,6 +17,9 @@ use crate::anim::{self, Im, import_prop}; use crate::export::{fx_by_id, fx_by_type, fx_params, mask_mode_from}; use crate::{LottieError, Timebase, shapes, text}; +/// Longest font family or style name kept (each text key using the font holds a copy). +const MAX_FONT_NAME_CHARS: usize = 256; + /// An embedded image asset handed to the caller to store (the returned path becomes the /// footage item's file). #[derive(Clone, Debug)] @@ -129,9 +132,12 @@ pub fn import( Ctx { im: Im { tb, warnings: vec![] }, assets: BTreeMap::new(), fonts: vec![], store, base_dir, items: vec![], folder: None, name: name.clone() }; if let Some(list) = doc.get("fonts").and_then(|f| f.get("list")).and_then(Json::as_array) { for fo in list { + // Every text key referring to the font copies its family and style: keep them to + // a font name's length. + let short = |v: &str| v.chars().take(MAX_FONT_NAME_CHARS).collect::(); let n = s(fo, "fName").unwrap_or("").to_string(); - let fam = s(fo, "fFamily").unwrap_or(&n).to_string(); - let st = s(fo, "fStyle").unwrap_or("Regular").to_string(); + let fam = short(s(fo, "fFamily").unwrap_or(&n)); + let st = short(s(fo, "fStyle").unwrap_or("Regular")); cx.fonts.push((n, fam, st)); } } @@ -150,11 +156,12 @@ pub fn import( let duration = tb.tick(op); // Pass 1: asset items (precomps empty for now, images stored). let assets = doc.get("assets").and_then(Json::as_array).cloned().unwrap_or_default(); + let layer_sizes = precomp_layer_sizes(&doc, &assets); for a in &assets { let Some(id) = s(a, "id") else { continue }; if a.get("layers").is_some() { let folder = cx.folder(project); - let layer = precomp_layer_size(&doc, &assets, id); + let layer = layer_sizes.get(id).copied(); let aw = f(a, "w").map(|v| v as u32).or(layer.map(|l| l.0)).unwrap_or(w); let ah = f(a, "h").map(|v| v as u32).or(layer.map(|l| l.1)).unwrap_or(h); let arate = f(a, "fr").map(FrameRate::from_f64).unwrap_or(rate); @@ -255,17 +262,20 @@ pub fn import( Ok(ImportResult { comp: id, items: cx.items, warnings: cx.im.warnings }) } -/// The `w`/`h` of the first precomp layer, in the main comp or in any asset, that shows asset `id`. -/// lottie-spec gives a precomposition asset no size: the layer's `w`/`h` are its clipping rect. -fn precomp_layer_size(doc: &Json, assets: &[Json], id: &str) -> Option<(u32, u32)> { +/// By asset id, the `w`/`h` of the first precomp layer, in the main comp or in any asset, that +/// shows it. lottie-spec gives a precomposition asset no size: the layer's `w`/`h` are its +/// clipping rect. One pass over all layers, not one per asset. +fn precomp_layer_sizes<'a>(doc: &'a Json, assets: &'a [Json]) -> HashMap<&'a str, (u32, u32)> { + let mut out = HashMap::new(); let lists = doc.get("layers").into_iter().chain(assets.iter().filter_map(|a| a.get("layers"))); - lists.filter_map(Json::as_array).flatten().find_map(|l| { - if l.get("ty").and_then(Json::as_u64) != Some(0) || s(l, "refId") != Some(id) { - return None; + for l in lists.filter_map(Json::as_array).flatten() { + if l.get("ty").and_then(Json::as_u64) != Some(0) { + continue; } - let (lw, lh) = f(l, "w").zip(f(l, "h"))?; - Some((lw.round().max(1.0) as u32, lh.round().max(1.0) as u32)) - }) + let (Some(id), Some((lw, lh))) = (s(l, "refId"), f(l, "w").zip(f(l, "h"))) else { continue }; + out.entry(id).or_insert((lw.round().max(1.0) as u32, lh.round().max(1.0) as u32)); + } + out } fn transform(im: &Im, ks: &Json, tr: &mut PropGroup, next: &mut u64) { @@ -411,8 +421,8 @@ fn layer_source(cx: &mut Ctx, project: &mut Project, comp: &Comp, j: &Json) -> O fn build_layers(cx: &mut Ctx, project: &mut Project, comp: &mut Comp, layers: &[Json]) { let tb = cx.im.tb; let mut by_ind: BTreeMap = BTreeMap::new(); - // (layer id, parent ind, matte: (kind, explicit tp ind), json position) - let mut pending: Vec<(LayerId, Option, Option<(MatteKind, Option)>, usize)> = vec![]; + // (layer id, its index in `comp.layers`, parent ind, matte: (kind, explicit tp ind), json position) + let mut pending: Vec<(LayerId, usize, Option, Option<(MatteKind, Option)>, usize)> = vec![]; let mut pos_ind: Vec> = vec![]; for (ji, j) in layers.iter().enumerate() { let ind = j.get("ind").and_then(Json::as_i64); @@ -463,8 +473,8 @@ fn build_layers(cx: &mut Ctx, project: &mut Project, comp: &mut Comp, layers: &[ let mut ids = Ids(&mut next); match &source { LayerSource::Shape => { - let items = j.get("shapes").and_then(Json::as_array).cloned().unwrap_or_default(); - let groups = shapes::import_items(&mut cx.im, &mut ids, &items); + let mut items = j.get("shapes").and_then(Json::as_array).into_iter().flatten(); + let groups = shapes::import_items(&mut cx.im, &mut ids, &mut items); if let Some(c) = l.props.sub_mut("contents") { c.children.extend(groups.into_iter().map(Node::Group)); } @@ -507,10 +517,10 @@ fn build_layers(cx: &mut Ctx, project: &mut Project, comp: &mut Comp, layers: &[ _ => None, } .map(|k| (k, j.get("tp").and_then(Json::as_i64))); - pending.push((l.id, parent, matte, ji)); + pending.push((l.id, comp.layers.len(), parent, matte, ji)); comp.layers.push(l); } - for (id, parent, matte, ji) in pending { + for (id, at, parent, matte, ji) in pending { let parent_id = parent.and_then(|p| by_ind.get(&p).copied()).filter(|p| *p != id); let matte_id = matte.and_then(|(k, tp)| { let target = match tp { @@ -520,7 +530,7 @@ fn build_layers(cx: &mut Ctx, project: &mut Project, comp: &mut Comp, layers: &[ }; target.filter(|t| *t != id).map(|t| TrackMatte { layer: t, kind: k }) }); - if let Some(l) = comp.layer_mut(id) { + if let Some(l) = comp.layers.get_mut(at) { l.parent = parent_id; l.track_matte = matte_id; } diff --git a/crates/lottie/src/shapes.rs b/crates/lottie/src/shapes.rs index fab8d3646..6cd1f09cc 100644 --- a/crates/lottie/src/shapes.rs +++ b/crates/lottie/src/shapes.rs @@ -436,7 +436,7 @@ fn import_shape_transform(im: &Im, j: &Json, tr: &mut PropGroup) { } /// Import Lottie shape items into shape contents groups. -pub(crate) fn import_items(im: &mut Im, ids: &mut Ids, items: &[Json]) -> Vec { +pub(crate) fn import_items(im: &mut Im, ids: &mut Ids, items: &mut dyn Iterator) -> Vec { let mut out = vec![]; for j in items { let ty = j.get("ty").and_then(Json::as_str).unwrap_or(""); @@ -447,9 +447,10 @@ pub(crate) fn import_items(im: &mut Im, ids: &mut Ids, items: &[Json]) -> Vec { - let list = j.get("it").and_then(Json::as_array).cloned().unwrap_or_default(); - let inner: Vec = list.iter().filter(|i| i.get("ty").and_then(Json::as_str) != Some("tr")).cloned().collect(); - let children = import_items(im, ids, &inner); + // Borrowed, not cloned: each nesting level copying its subtree costs memory + // quadratic in the depth. + let list = j.get("it").and_then(Json::as_array).map(Vec::as_slice).unwrap_or_default(); + let children = import_items(im, ids, &mut list.iter().filter(|i| i.get("ty").and_then(Json::as_str) != Some("tr"))); let mut g = build::shape_group(ids, "Group 1", children); if let Some(tr) = list.iter().find(|i| i.get("ty").and_then(Json::as_str) == Some("tr")) && let Some(t) = g.sub_mut("transform") diff --git a/crates/lottie/src/tests.rs b/crates/lottie/src/tests.rs index b941b4f61..8656852de 100644 --- a/crates/lottie/src/tests.rs +++ b/crates/lottie/src/tests.rs @@ -997,6 +997,53 @@ fn per_corner_roundness_exports_equal_radii_and_warns_when_mixed() { assert!(!warnings.iter().any(|w| w.contains("per-corner")), "{warnings:?}"); } +#[test] +fn many_layers_and_precomps_import_with_their_parents_and_sizes() { + // 5 000 null layers each parented to the previous, and 2 000 empty precomp assets: the + // relationship and size passes index the layers instead of searching them per layer. (Each + // has its own name: `Comp::unique_layer_name` numbering one repeated name is a separate cost.) + let n = 5_000; + let layers: Vec = (1..=n).map(|i| json!({"ty": 3, "nm": format!("N{i}"), "ind": i, "parent": i - 1, "ip": 0, "op": 30, "st": 0, "ks": {}})).collect(); + let mut assets: Vec = (0..2_000).map(|i| json!({"id": format!("c{i}"), "layers": []})).collect(); + assets.push(json!({"id": "last", "layers": []})); + let mut layers = layers; + layers.push(json!({"ty": 0, "ind": n + 1, "refId": "last", "w": 50, "h": 40, "ip": 0, "op": 30, "st": 0, "ks": {}})); + let doc = json!({"w": 200, "h": 120, "fr": 30, "ip": 0, "op": 30, "assets": assets, "layers": layers}); + let mut q = Project::default(); + let nc = import(&mut q, doc.to_string().as_bytes(), "Imported", "", &mut |_| None).unwrap().comp; + let c = q.comp(nc).unwrap(); + assert_eq!(c.layers.len(), n as usize + 1); + assert_eq!(c.layers.get(0).unwrap().parent, None); + assert_eq!(c.layers.get(1).unwrap().parent, Some(c.layers.get(0).unwrap().id)); + let LayerSource::Comp { item } = c.layers.get(n as usize).unwrap().source else { panic!("not a precomp") }; + let pc = q.comp(item).unwrap(); + assert_eq!((pc.width, pc.height), (50, 40)); +} + +#[test] +fn deeply_nested_shape_groups_and_long_font_names_import() { + let mut g = json!({"ty": "rc", "s": {"a": 0, "k": [10, 10]}, "p": {"a": 0, "k": [0, 0]}, "r": {"a": 0, "k": 0}}); + // 50 levels: each is two levels of JSON, within serde_json's recursion limit of 128. + for _ in 0..50 { + g = json!({"ty": "gr", "it": [g, {"ty": "tr", "o": {"a": 0, "k": 100}}]}); + } + let family = "F".repeat(100_000); + let keys: Vec = (0..50).map(|i| json!({"t": i, "s": {"t": "x", "f": "A", "s": 10}})).collect(); + let doc = json!({"w": 200, "h": 120, "fr": 30, "ip": 0, "op": 60, + "fonts": {"list": [{"fName": "A", "fFamily": family, "fStyle": "Regular"}]}, + "layers": [ + {"ty": 4, "ind": 1, "ip": 0, "op": 60, "st": 0, "ks": {}, "shapes": [g]}, + {"ty": 5, "ind": 2, "ip": 0, "op": 60, "st": 0, "ks": {}, "t": {"d": {"k": keys}}} + ]}); + let mut q = Project::default(); + let nc = import(&mut q, doc.to_string().as_bytes(), "Imported", "", &mut |_| None).unwrap().comp; + let c = q.comp(nc).unwrap(); + let st = c.layers.get(1).unwrap().props.prop("text/sourceText").unwrap(); + assert_eq!(st.keys.len(), 50); + let Value::Text(d) = &st.keys[0].value else { panic!("not text") }; + assert_eq!(d.font.len(), 256); +} + /// A footage item called a still whose file is not an image is not embedded. #[test] fn only_image_bytes_are_embedded() { diff --git a/crates/lottie/src/text.rs b/crates/lottie/src/text.rs index 125835c2b..4e50b461d 100644 --- a/crates/lottie/src/text.rs +++ b/crates/lottie/src/text.rs @@ -310,7 +310,8 @@ pub(crate) fn import_text(im: &mut Im, ids: &mut Ids, t: &Json, tg: &mut PropGro st.value = Value::Text(Box::new(first.clone())); } if docs.len() > 1 { - st.keys = docs.iter().map(|(f, d)| effectcraft_keyframe::Keyframe::new(im.tb.tick(*f), Value::Text(Box::new(d.clone()))).hold()).collect(); + // Moved into the keys, not cloned: the documents are not kept twice. + st.keys = docs.into_iter().map(|(f, d)| effectcraft_keyframe::Keyframe::new(im.tb.tick(f), Value::Text(Box::new(d))).hold()).collect(); } } if let Some(m) = t.get("m") diff --git a/crates/media/Cargo.toml b/crates/media/Cargo.toml index 4947d928a..76cce90fa 100644 --- a/crates/media/Cargo.toml +++ b/crates/media/Cargo.toml @@ -22,9 +22,8 @@ effectcraft-pdf = { workspace = true } effectcraft-model = { workspace = true } filmcraft-codecs = { workspace = true } filmcraft-media = { workspace = true } -filmcraft-aac = { workspace = true } -filmcraft-isobmff = { workspace = true } -filmcraft-matroska = { workspace = true } +craft-audio = { workspace = true } +craft-media = { workspace = true } filmcraft-mpegts = { workspace = true } filmcraft-frame = { workspace = true } filmcraft-color = { workspace = true } @@ -38,5 +37,6 @@ thiserror = { workspace = true } log = { workspace = true } [dev-dependencies] +tempfile = { workspace = true } # (tests render comps that read layers through effects; already a dependency of effectcraft-render) effectcraft-effects = { workspace = true } diff --git a/crates/media/src/aac.rs b/crates/media/src/aac.rs index 625c37b43..a1793b7af 100644 --- a/crates/media/src/aac.rs +++ b/crates/media/src/aac.rs @@ -6,7 +6,7 @@ use std::io; -use filmcraft_aac::AudioSpecificConfig; +use craft_audio::aac::AudioSpecificConfig; use filmcraft_media::ByteReader; /// How far into an MPEG TS / PS file the audio streams are looked for: the stream is indexed as @@ -34,15 +34,16 @@ pub(crate) fn config(header: Option<&[u8]>, file: Option<&dyn ByteReader>) -> Op /// are in its fragments, not the `moov`). fn mp4(header: &[u8], file: Option<&dyn ByteReader>) -> Option> { let mp4 = match file { - Some(f) => filmcraft_isobmff::open(Head(f, f.len())), - None => filmcraft_isobmff::open(header), + Some(f) => craft_media::isobmff::open(Head(f, f.len())), + None => craft_media::isobmff::open(header), }; let mp4 = mp4.ok()?; - let playable = - |t: &&filmcraft_isobmff::Track| t.kind == filmcraft_isobmff::TrackKind::Audio && !t.samples.is_empty() && !t.entries.is_empty() && t.timescale > 0; + let playable = |t: &&craft_media::isobmff::Track| { + t.kind == craft_media::isobmff::TrackKind::Audio && !t.samples.is_empty() && !t.entries.is_empty() && t.timescale > 0 + }; let track = mp4.tracks.iter().find(playable)?; match &track.entries.first()?.codec { - filmcraft_isobmff::CodecConfig::Aac(a) => Some(a.asc.clone()), + craft_media::isobmff::CodecConfig::Aac(a) => Some(a.asc.clone()), _ => None, } } @@ -50,9 +51,9 @@ fn mp4(header: &[u8], file: Option<&dyn ByteReader>) -> Option> { /// The track FilmCraft's Matroska source plays: the first audio track with frames, found by /// indexing the clusters as that source does. fn matroska(file: &dyn ByteReader) -> Option> { - let mkv = filmcraft_matroska::open(Head(file, file.len())).ok()?; - match &mkv.tracks.iter().find(|t| t.kind == filmcraft_matroska::TrackKind::Audio && !t.samples.is_empty())?.codec { - filmcraft_matroska::Codec::Aac { asc } => Some(asc.clone()), + let mkv = craft_media::matroska::open(Head(file, file.len())).ok()?; + match &mkv.tracks.iter().find(|t| t.kind == craft_media::matroska::TrackKind::Audio && !t.samples.is_empty())?.codec { + craft_media::matroska::Codec::Aac { asc } => Some(asc.clone()), _ => None, } } @@ -88,16 +89,7 @@ impl Head<'_> { } } -impl filmcraft_isobmff::ByteSource for Head<'_> { - fn len(&self) -> u64 { - self.1 - } - fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> { - self.read(offset, buf) - } -} - -impl filmcraft_matroska::ByteSource for Head<'_> { +impl craft_media::ByteSource for Head<'_> { fn len(&self) -> u64 { self.1 } diff --git a/crates/media/src/downmix.rs b/crates/media/src/downmix.rs index 09bb1e1f6..5e086bdaf 100644 --- a/crates/media/src/downmix.rs +++ b/crates/media/src/downmix.rs @@ -19,7 +19,7 @@ use std::f32::consts::FRAC_1_SQRT_2; use std::sync::Arc; -use filmcraft_aac::{AudioSpecificConfig, ProgramConfig}; +use craft_audio::aac::{AudioSpecificConfig, ProgramConfig}; /// A source channel's contribution to the left and right outputs, before normalisation. pub type Pair = [f32; 2]; @@ -185,7 +185,7 @@ mod tests { } } - /// AudioSpecificConfigs as ffmpeg 8.1 and FilmCraft's encoder write them (48 kHz AAC-LC). + /// AudioSpecificConfigs as ffmpeg 8.1 and craft-audio's encoder write them (48 kHz AAC-LC). const PCE_FFMPEG_61: &str = "118004c848002000c4400d4c61766336322e32382e31303256e500"; const PCE_FILMCRAFT_61: &str = "118004c845000108840000"; diff --git a/crates/media/src/probe.rs b/crates/media/src/probe.rs index 1fc945aa1..2c7b2a402 100644 --- a/crates/media/src/probe.rs +++ b/crates/media/src/probe.rs @@ -134,12 +134,12 @@ fn trim_to_edits(f: &mut Footage, shown: Option) { /// How long the video track's edit list shows its media (`elst` segments, movie timescale): /// `None` without edits, or with an empty edit (a delayed start keeps the reader's duration). fn video_shown(bytes: &[u8]) -> Option { - let file = filmcraft_isobmff::open(bytes).ok()?; - let t = file.tracks.iter().find(|t| t.kind == filmcraft_isobmff::TrackKind::Video && !t.samples.is_empty())?; + let file = craft_media::isobmff::open(bytes).ok()?; + let t = file.tracks.iter().find(|t| t.kind == craft_media::isobmff::TrackKind::Video && !t.samples.is_empty())?; edits_shown(&t.edits, file.timescale) } -fn edits_shown(edits: &[filmcraft_isobmff::Edit], timescale: u32) -> Option { +fn edits_shown(edits: &[craft_media::isobmff::Edit], timescale: u32) -> Option { if edits.is_empty() || timescale == 0 || edits.iter().any(|e| e.media_time < 0) { return None; } @@ -303,8 +303,8 @@ pub(crate) fn load_oriented(bytes: &[u8], fmt: Option) -> im /// containers have none. FilmCraft turns the frames of a pure 0/90/180/270° header itself, so this /// is only the quarter turn nearest to a header it does not turn. pub(crate) fn video_turn(bytes: &[u8]) -> Orientation { - let Ok(file) = filmcraft_isobmff::open(bytes) else { return Orientation::NoTransforms }; - let Some(track) = file.tracks.iter().find(|t| t.kind == filmcraft_isobmff::TrackKind::Video) else { + let Ok(file) = craft_media::isobmff::open(bytes) else { return Orientation::NoTransforms }; + let Some(track) = file.tracks.iter().find(|t| t.kind == craft_media::isobmff::TrackKind::Video) else { return Orientation::NoTransforms; }; // A pure rotation is already applied by FilmCraft's decoder (frames and size come out @@ -329,8 +329,8 @@ pub(crate) fn video_turn(bytes: &[u8]) -> Orientation { /// median reads as 30.303 or 58.824 fps. `None` for other containers and for movies the median /// reads right. fn video_rate(bytes: &[u8]) -> Option { - let file = filmcraft_isobmff::open(bytes).ok()?; - let t = file.tracks.iter().find(|t| t.kind == filmcraft_isobmff::TrackKind::Video && !t.samples.is_empty())?; + let file = craft_media::isobmff::open(bytes).ok()?; + let t = file.tracks.iter().find(|t| t.kind == craft_media::isobmff::TrackKind::Video && !t.samples.is_empty())?; averaged_rate(t.timescale, t.samples.iter().take(240).map(|s| s.duration)) } @@ -368,7 +368,7 @@ mod tests { /// 1000 timescale), though its media runs a frame longer; empty edits and no edits leave it. #[test] fn the_edit_list_trims_a_movie_to_the_frames_it_shows() { - let edit = |segment_duration, media_time| filmcraft_isobmff::Edit { segment_duration, media_time, media_rate: 0x0001_0000 }; + let edit = |segment_duration, media_time| craft_media::isobmff::Edit { segment_duration, media_time, media_rate: 0x0001_0000 }; let shown = edits_shown(&[edit(3000, 1024)], 1000); assert_eq!(shown, Some(Tick::from_units(3, 1))); let mut f = Footage { kind: FootageKind::Video, duration: Tick::from_units(76, 25), ..Footage::default() }; diff --git a/crates/media/tests/it/aac_layout.rs b/crates/media/tests/it/aac_layout.rs index fb3e078a6..896e4f0a7 100644 --- a/crates/media/tests/it/aac_layout.rs +++ b/crates/media/tests/it/aac_layout.rs @@ -1,5 +1,5 @@ //! Multichannel AAC reaches stereo by where each channel plays, whatever order its channel -//! configuration decodes in (#1701). Fixtures are tones encoded with FilmCraft's AAC encoder (no +//! configuration decodes in (#1701). Fixtures are tones encoded with craft-audio's AAC encoder (no //! ffmpeg needed, so nothing is skipped), with the configuration rewritten where the coded //! elements fit another: configurations 11 and 12 code the same elements as the encoder's 7- and //! 8-channel output, and a program config element can name any of them. @@ -9,9 +9,9 @@ use std::io::Cursor; use std::path::PathBuf; use std::sync::Arc; +use craft_audio::aac::{AudioSpecificConfig, Encoder, EncoderConfig, ProgramConfig}; use effectcraft_media::{MediaPool, probe, probe_bytes}; use effectcraft_time::Tick; -use filmcraft_aac::{AudioSpecificConfig, Encoder, EncoderConfig, ProgramConfig}; const RATE: u32 = 48_000; /// Tone frequencies by channel; an LFE gets [`LFE_TONE`], which its element can carry. @@ -61,7 +61,9 @@ fn lc(config: u8) -> Option { fn pce(front: Vec<(bool, u8)>, side: Vec<(bool, u8)>, back: Vec<(bool, u8)>, lfe: Vec, comment: &[u8]) -> Option { let pce = ProgramConfig { object_type: 1, sf_index: 3, front, side, back, lfe, comment: comment.to_vec(), ..Default::default() }; - Some(AudioSpecificConfig { pce: Some(pce), ..AudioSpecificConfig::lc(3, 0) }) + let mut asc = AudioSpecificConfig::lc(3, 0); + asc.pce = Some(pce); + Some(asc) } use Ch::*; @@ -115,7 +117,7 @@ fn mp4(asc: Vec, channels: u32, units: &[Vec]) -> Vec { /// As [`mp4`], after an audio track with the configuration and channel count `empty` and no /// samples, if any. fn mp4_after(empty: Option<(Vec, u32)>, asc: Vec, channels: u32, units: &[Vec]) -> Vec { - use filmcraft_isobmff::{Brand, Mp4Writer, SampleEntry, TrackConfig, WriteSample, WriterOptions}; + use craft_media::isobmff::{Brand, Mp4Writer, SampleEntry, TrackConfig, WriteSample, WriterOptions}; let mut w = Mp4Writer::new(Cursor::new(Vec::new()), WriterOptions::new(Brand::Mp4)).expect("writer"); if let Some((asc, channels)) = empty { w.add_track(TrackConfig::new(SampleEntry::aac(asc, channels, RATE), RATE)).expect("empty track"); @@ -134,7 +136,7 @@ fn mkv(asc: Vec, channels: u32, units: &[Vec]) -> Vec { /// As [`mkv`], after an audio track with the configuration and channel count `empty` and no /// frames, if any. fn mkv_after(empty: Option<(Vec, u32)>, asc: Vec, channels: u32, units: &[Vec]) -> Vec { - use filmcraft_matroska::{MkvWriter, MuxOptions, TrackKind, TrackSpec}; + use craft_media::matroska::{MkvWriter, MuxOptions, TrackKind, TrackSpec}; let track = |asc: Vec, channels: u32| { let mut t = TrackSpec::new(TrackKind::Audio, "A_AAC"); t.codec_private = asc; @@ -210,7 +212,7 @@ fn lfe_is_coded(case: &Case, bytes: Vec) { fn every_aac_layout_mixes_by_speaker_in_mp4() { for case in cases() { let (asc, units) = encode(case.content, false); - let asc = case.asc.as_ref().map_or(asc, AudioSpecificConfig::to_bytes); + let asc = case.asc.as_ref().map_or(asc, |a| a.to_bytes().expect("ASC")); let bytes = mp4(asc, case.content.len() as u32, &units); lfe_is_coded(&case, bytes.clone()); let p = write(&format!("{}.mp4", case.name), &bytes); @@ -227,7 +229,7 @@ fn every_aac_layout_mixes_by_speaker_in_mp4() { fn matroska_aac_layouts_mix_by_speaker() { for case in cases().into_iter().filter(|c| matches!(c.name, "config12" | "pce61-centre-first" | "pce61-ffmpeg")) { let (asc, units) = encode(case.content, false); - let asc = case.asc.as_ref().map_or(asc, AudioSpecificConfig::to_bytes); + let asc = case.asc.as_ref().map_or(asc, |a| a.to_bytes().expect("ASC")); let p = write(&format!("{}.mkv", case.name), &mkv(asc, case.content.len() as u32, &units)); check(case.name, case.content, &mix(&MediaPool::new(), &probe(&p).expect("probe"))); } @@ -251,9 +253,9 @@ fn an_empty_audio_track_lends_its_configuration_to_no_other() { for (name, empty) in [("config12", 7), ("pce61-centre-first", 11)] { let case = cases().into_iter().find(|c| c.name == name).expect("case"); let n = case.content.len() as u32; - let empty = Some((AudioSpecificConfig::lc(3, empty).to_bytes(), n)); + let empty = Some((AudioSpecificConfig::lc(3, empty).to_bytes().expect("ASC"), n)); let (asc, units) = encode(case.content, false); - let asc = case.asc.as_ref().map_or(asc, AudioSpecificConfig::to_bytes); + let asc = case.asc.as_ref().map_or(asc, |a| a.to_bytes().expect("ASC")); let p = write(&format!("{name}-after-empty.mp4"), &mp4_after(empty.clone(), asc.clone(), n, &units)); check(&format!("{name} MP4"), case.content, &mix(&MediaPool::new(), &probe(&p).expect("probe"))); let p = write(&format!("{name}-after-empty.mkv"), &mkv_after(empty, asc, n, &units)); diff --git a/crates/media/tests/it/downmix.rs b/crates/media/tests/it/downmix.rs index 05205ab6a..f974c6cb0 100644 --- a/crates/media/tests/it/downmix.rs +++ b/crates/media/tests/it/downmix.rs @@ -174,7 +174,7 @@ fn each_channel_reaches_stereo_at_its_bs775_gain() { fn whole_pce_entry(p: &Path, channels: usize) -> PathBuf { let mut bytes = std::fs::read(p).expect("read"); let Some((asc, 2)) = aac_entry(&bytes) else { return p.to_path_buf() }; - if channels <= 2 || filmcraft_aac::AudioSpecificConfig::parse(&asc).map(|a| a.channel_config).ok() != Some(0) { + if channels <= 2 || craft_audio::aac::AudioSpecificConfig::parse(&asc).map(|a| a.channel_config).ok() != Some(0) { return p.to_path_buf(); } // the `mp4a` entry's channelcount follows its 6 reserved bytes, data reference index and @@ -189,9 +189,9 @@ fn whole_pce_entry(p: &Path, channels: usize) -> PathBuf { /// The AudioSpecificConfig and sample entry channel count of an MP4's first AAC track. fn aac_entry(bytes: &[u8]) -> Option<(Vec, u32)> { - let file = filmcraft_isobmff::open(bytes).ok()?; + let file = craft_media::isobmff::open(bytes).ok()?; file.tracks.iter().flat_map(|t| &t.entries).find_map(|e| match &e.codec { - filmcraft_isobmff::CodecConfig::Aac(a) => Some((a.asc.clone(), e.audio.as_ref()?.channels)), + craft_media::isobmff::CodecConfig::Aac(a) => Some((a.asc.clone(), e.audio.as_ref()?.channels)), _ => None, }) } diff --git a/crates/media/tests/it/file_timecode.rs b/crates/media/tests/it/file_timecode.rs index e5cbfa92d..dae3335ba 100644 --- a/crates/media/tests/it/file_timecode.rs +++ b/crates/media/tests/it/file_timecode.rs @@ -2,8 +2,8 @@ use std::io::Cursor; +use craft_media::isobmff::{Brand, Mp4Writer, SampleEntry, TimecodeConfig, TrackConfig, WriteSample, WriterOptions}; use effectcraft_project::FootageKind; -use filmcraft_isobmff::{Brand, Mp4Writer, SampleEntry, TimecodeConfig, TrackConfig, WriteSample, WriterOptions}; /// A two-frame 24 fps Motion JPEG MOV, with a timecode track from `start_frame` when given. fn mov(start_frame: Option) -> Vec { diff --git a/crates/media/tests/it/footage.rs b/crates/media/tests/it/footage.rs index 7f0de6ae4..ed152541c 100644 --- a/crates/media/tests/it/footage.rs +++ b/crates/media/tests/it/footage.rs @@ -747,8 +747,8 @@ fn sequence_gaps_hold_the_previous_frame() { /// parallax project's 21120-px plates were decoded and resampled for every frame at Quarter). #[test] fn stills_at_reduced_sizes_are_resampled_once() { - let dir = std::env::temp_dir().join(format!("ec-still-size-{}", std::process::id())); - std::fs::create_dir_all(&dir).expect("temp dir"); + let guard = tempfile::tempdir().expect("temp dir"); + let dir = guard.path().to_path_buf(); let p = dir.join("plate.png"); let px = |x: u32, y: u32| { image::Rgba([(x * 7 % 256) as u8, (y * 13 % 256) as u8, ((x ^ y) % 256) as u8, if (x + y).is_multiple_of(5) { 0 } else { 255 - (x % 128) as u8 }]) diff --git a/crates/opusenc/Cargo.toml b/crates/opusenc/Cargo.toml index 489c648ea..db7f2b7f8 100644 --- a/crates/opusenc/Cargo.toml +++ b/crates/opusenc/Cargo.toml @@ -12,4 +12,4 @@ workspace = true [dependencies] [dev-dependencies] -filmcraft-opus = { workspace = true } +craft-audio = { workspace = true } diff --git a/crates/opusenc/README.md b/crates/opusenc/README.md index 1ebf6fedb..8a822ceac 100644 --- a/crates/opusenc/README.md +++ b/crates/opusenc/README.md @@ -85,7 +85,7 @@ recursive band splitting, greedy PVQ search. Not implemented: transient frames, pitch post-filter, intensity stereo, dynamic allocation, VBR. All-zero CELT frames become 3-byte silence packets. -## Measured (tests in `tests/`, FilmCraft decoder) +## Measured (tests in `tests/`, craft-audio's Opus decoder) Synthetic speech (glottal pulses 100–200 Hz through three moving formants, syllable envelopes, pauses) and music (three-note harmonic chords); waveform SNR against the input band-limited to @@ -129,8 +129,8 @@ stage-1/2 codebooks and prediction weights, gain, pitch, contour, LTP and pulse weights) and the decoder-side normative arithmetic the encoder must reproduce bit-exactly (CELT allocation; SILK NLSF dequantisation and stabilisation, NLSF → LPC conversion, `log2lin`, the fixed-point division/inverse helpers and the LPC analysis filter) are mirrored from FilmCraft's -first-party, spec-derived `filmcraft-opus` decoder (MIT OR Apache-2.0), which is also the -round-trip oracle in `tests/` and `crates/export/tests/it/opus_roundtrip.rs` (ffmpeg is a second, +first-party, spec-derived `filmcraft-opus` decoder (MIT OR Apache-2.0). That decoder, now +`craft_audio::opus` in craft-libs, is also the round-trip oracle in `tests/` and `crates/export/tests/it/opus_roundtrip.rs` (ffmpeg is a second, external oracle when installed). All encoder-side analysis and search (pitch, LPC, NLSF and LTP search, noise feedback, rate control, stereo prediction, the decimator) is our own design. No third-party encoder or decoder source (libopus or otherwise) was consulted. diff --git a/crates/opusenc/tests/it/common/mod.rs b/crates/opusenc/tests/it/common/mod.rs index 9b420f31b..8f00b88e9 100644 --- a/crates/opusenc/tests/it/common/mod.rs +++ b/crates/opusenc/tests/it/common/mod.rs @@ -124,9 +124,9 @@ pub fn encode(mut enc: OpusEncoder, planar: &[Vec]) -> (OpusEncoder, Vec]) -> Vec> { - let mut dec = filmcraft_opus::Decoder::new(head).expect("OpusHead"); + let mut dec = craft_audio::opus::Decoder::new(head).expect("OpusHead"); dec.set_trim_pre_skip(true); let mut out: Vec> = vec![Vec::new(); dec.channels()]; for p in packets { diff --git a/crates/path/src/interp.rs b/crates/path/src/interp.rs index 4087b26b5..89e7edc51 100644 --- a/crates/path/src/interp.rs +++ b/crates/path/src/interp.rs @@ -311,6 +311,9 @@ fn frame(v: &[P]) -> (P, f64) { (c, r) } +/// Vertex counts above this skip the shape contexts (quadratic to build) and search the starting +/// vertex coarsely; no mask comes near it (the Add Vertices option itself stops at 2000). +const MAX_CONTEXT_VERTICES: usize = 4096; const RBINS: usize = 5; const ABINS: usize = 12; @@ -412,10 +415,18 @@ pub fn correspond(a: &ShapePath, b: &ShapePath, o: &InterpOpts) -> (ShapePath, S } if !o.first_vertices_match { let n = ra.len(); - let use_sc = o.quality > 0.0; + let large = n > MAX_CONTEXT_VERTICES; + let use_sc = o.quality > 0.0 && !large; let (sa, sb) = if use_sc { (Some(shape_contexts(&ra.vertices)), Some(shape_contexts(&rb.vertices))) } else { (None, None) }; // Quality limits the offsets examined exhaustively (coarse step), then refines. - let step = if o.quality >= 50.0 { 1 } else { ((n as f64 / 24.0).ceil() as usize).max(1) }; + // (A very large shape tries about 3 √n offsets, not n, each costing a pass over it.) + let step = if large { + (n as f64).sqrt().ceil() as usize + } else if o.quality >= 50.0 { + 1 + } else { + ((n as f64 / 24.0).ceil() as usize).max(1) + }; let mut best = (f64::MAX, 0usize); let mut k = 0; while k < n { @@ -592,4 +603,22 @@ mod tests { let l2 = (lin.vertices[1][0] - lin.vertices[0][0]).hypot(lin.vertices[1][1] - lin.vertices[0][1]); assert!(l2 < 80.0); } + + /// A shape with tens of thousands of vertices is matched without the quadratic shape contexts + /// and exhaustive offset search. + #[test] + fn very_large_shapes_are_matched_in_bounded_time() { + let n = 30_000; + let circle = |rot: f64| -> ShapePath { + let pts: Vec

= (0..n) + .map(|i| [100.0 * (rot + i as f64 * std::f64::consts::TAU / n as f64).cos(), 100.0 * (rot + i as f64 * std::f64::consts::TAU / n as f64).sin()]) + .collect(); + ShapePath::polygon(&pts, true) + }; + let o = InterpOpts { one_to_one: true, quality: 100.0, ..Default::default() }; + let t = std::time::Instant::now(); + let (a, b) = correspond(&circle(0.0), &circle(1.0), &o); + assert_eq!((a.len(), b.len()), (n, n)); + assert!(t.elapsed().as_secs() < 30, "{:?}", t.elapsed()); + } } diff --git a/crates/path/src/varstroke.rs b/crates/path/src/varstroke.rs index 61e6a1ab7..da00b02d0 100644 --- a/crates/path/src/varstroke.rs +++ b/crates/path/src/varstroke.rs @@ -220,6 +220,56 @@ fn disc(out: &mut BezPath, c: Point, r: f64) { } } +/// `path` with each curve whose extent is over 10⁷ × `tol` replaced by lines flattened at 10⁻⁷ of +/// that extent. `None` when a coordinate is not finite. +fn coarsen_huge(path: &BezPath, tol: f64) -> Option { + use kurbo::Shape as _; + let mut out = BezPath::new(); + let (mut cur, mut start) = (Point::ZERO, Point::ZERO); + for el in path.elements() { + let seg = match *el { + PathEl::MoveTo(p) => { + (cur, start) = (p, p); + out.push(*el); + continue; + } + PathEl::LineTo(p) => { + cur = p; + out.push(*el); + continue; + } + PathEl::ClosePath => { + cur = start; + out.push(*el); + continue; + } + PathEl::QuadTo(_, p) | PathEl::CurveTo(_, _, p) => { + let mut one = BezPath::new(); + one.move_to(cur); + one.push(*el); + (one, p) + } + }; + let (one, end) = seg; + let bb = one.bounding_box(); + let extent = bb.width().max(bb.height()); + if !extent.is_finite() { + return None; + } + if extent * 1e-7 > tol { + kurbo::flatten(one.elements().iter().copied(), extent * 1e-7, |e| { + if let PathEl::LineTo(p) = e { + out.line_to(p); + } + }); + } else { + out.push(*el); + } + cur = end; + } + Some(out) +} + /// Outline of a tapered / waved stroke (local space; `res_scale` = device/local scale). pub fn outline(paths: &[BezPath], style: &StrokeStyle, res_scale: f64) -> Option { if style.width <= 0.0 { @@ -230,7 +280,11 @@ pub fn outline(paths: &[BezPath], style: &StrokeStyle, res_scale: f64) -> Option let half = style.width / 2.0; for path in paths { // Fine enough for the width profile: ½ device pixel, at most ~20 000 samples per path. - let rough = flatten(path, 0.1 / rs, f64::INFINITY); + // Curves with huge extents are first cut into lines at a tolerance of 10⁻⁷ of their own + // extent, so none subdivides into billions of points before the step below applies; + // every other segment (small curves in a large path included) keeps the ½-pixel tolerance. + let Some(path) = coarsen_huge(path, 0.1 / rs) else { continue }; + let rough = flatten(&path, 0.1 / rs, f64::INFINITY); for poly0 in rough { let len0 = poly0.len(); if len0 <= 0.0 { @@ -396,4 +450,37 @@ mod tests { let area: f32 = m.data.iter().sum(); assert!(area > 240.0 * 6.0 * 0.8 && area < 240.0 * 6.0 * 1.05, "{area}"); } + + /// A curve with enormous coordinates is approximated within a bounded number of points. + #[test] + fn huge_coordinates_do_not_explode_the_first_approximation() { + let mut p = BezPath::new(); + p.move_to((0.0, 0.0)); + p.curve_to((1e13, 0.0), (-1e13, 1e13), (1e13, 1e13)); + let st = StrokeStyle { + width: 4.0, + taper: Some(Taper { percent: true, start_len: 50.0, end_len: 50.0, start_width: 0.0, end_width: 0.0, start_ease: 0.0, end_ease: 0.0 }), + ..Default::default() + }; + let t = std::time::Instant::now(); + let o = outline(&[p], &st, 1.0).unwrap(); + assert!(o.elements().len() < 200_000, "{}", o.elements().len()); + assert!(t.elapsed().as_secs() < 20); + } + + /// Small curves keep their shape in a path that also has a huge segment. + #[test] + fn small_curves_survive_a_huge_segment() { + let mut p = BezPath::new(); + p.move_to((0.0, 0.0)); + p.curve_to((100.0, 0.0), (0.0, 100.0), (0.0, 0.0)); + p.line_to((1e10, 0.0)); + let c = coarsen_huge(&p, 0.1).unwrap(); + assert_eq!(c.elements().len(), p.elements().len(), "nothing here is huge enough to cut"); + let mut q = BezPath::new(); + q.move_to((0.0, 0.0)); + q.curve_to((1e13, 0.0), (-1e13, 1e13), (1e13, 1e13)); + let c = coarsen_huge(&q, 0.1).unwrap(); + assert!(c.elements().iter().skip(1).all(|e| matches!(e, PathEl::LineTo(_))) && c.elements().len() < 10_000); + } } diff --git a/crates/pdf/README.md b/crates/pdf/README.md index b38c9032e..a88adff0b 100644 --- a/crates/pdf/README.md +++ b/crates/pdf/README.md @@ -144,7 +144,7 @@ ffmpeg elsewhere) to check the code tables against an independent implementation ## Shared parser setup and bounds The workspace pins `craft-pdf` to `craft-libs` Git revision -`e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545`, with `default-features = false` and only the explicit `read` +`e7c3dda7f3e339dd2c8d40fc4eb405472be1a1e4`, with `default-features = false` and only the explicit `read` feature. This excludes the backend's bundled fonts and Adobe CMaps and preserves EffectCraft's font and asset policies. The shared parser does not change the supported drawing operators, image codecs, continuously rasterized SVG tree, editable shapes, or EPS interpreter. diff --git a/crates/pdf/src/build.rs b/crates/pdf/src/build.rs index 0b1586f89..0f295a045 100644 --- a/crates/pdf/src/build.rs +++ b/crates/pdf/src/build.rs @@ -7,6 +7,9 @@ use effectcraft_svg::{Affine, BezPath, FillRule, Geom, Group, Node, Paint, Shape /// PostScript program, or the PDF content stream. pub(crate) const MAX_PATH: usize = 1 << 20; +/// The deepest the open groups nest (the render tree and its consumers recurse through them). +const MAX_DEPTH: usize = 256; + #[derive(Clone, Copy, PartialEq, Eq)] enum Kind { Plain, @@ -20,6 +23,9 @@ pub(crate) struct Builder { pub top_layers: Vec, pub skipped: Vec, pub shapes: usize, + /// Groups the finished nodes will sit under once placed (an enclosing builder's open groups + /// and the pattern or mask groups around them), counted against [`MAX_DEPTH`]. + base: usize, } pub(crate) fn group(name: &str) -> Group { @@ -28,13 +34,25 @@ pub(crate) fn group(name: &str) -> Group { impl Builder { pub fn new() -> Builder { - Builder { stack: vec![(group("page"), Kind::Plain)], top_layers: vec![], skipped: vec![], shapes: 0 } + Builder { stack: vec![(group("page"), Kind::Plain)], top_layers: vec![], skipped: vec![], shapes: 0, base: 0 } + } + + /// A builder for content that will be placed inside this one's open groups and `wrap` more + /// groups (a pattern's `Pattern` and `Tile`, a soft mask's two), so its nesting limit counts + /// them. + pub fn nested(&self, wrap: usize) -> Builder { + Builder { base: self.base.saturating_add(self.stack.len()).saturating_add(wrap), ..Builder::new() } } pub fn depth(&self) -> usize { self.stack.len() } + /// `wrap` more groups fit under the open ones within [`MAX_DEPTH`]. + pub fn has_room(&self, wrap: usize) -> bool { + self.base.saturating_add(self.stack.len()).saturating_add(wrap) <= MAX_DEPTH + } + pub fn skip(&mut self, what: &str) { if !self.skipped.iter().any(|s| s == what) { self.skipped.push(what.to_string()); @@ -62,23 +80,33 @@ impl Builder { } } + /// Open `g` as the group drawn into from now on; past [`MAX_DEPTH`] the group is dropped + /// (what would have been drawn inside it goes to the open group) and noted as skipped. + fn push_group(&mut self, g: Group, kind: Kind) { + if self.base.saturating_add(self.stack.len()) >= MAX_DEPTH { + self.skip("group nesting limit"); + return; + } + self.stack.push((g, kind)); + } + /// Children drawn from now on are clipped by `path` (page space) until the group closes. pub fn clip(&mut self, path: BezPath, rule: FillRule) { let mut g = group("Clip Group"); g.clip.push((path, rule)); // Clipping is not a transparency group: blend modes inside reach the backdrop. g.isolated = false; - self.stack.push((g, Kind::Clip)); + self.push_group(g, Kind::Clip); } /// Open an optional-content layer (or a named group). pub fn open(&mut self, name: &str, layer: bool) { - self.stack.push((group(name), if layer { Kind::Layer } else { Kind::Plain })); + self.push_group(group(name), if layer { Kind::Layer } else { Kind::Plain }); } /// Open a group (blend mode, soft mask) closed with the graphics state. pub fn open_group(&mut self, g: Group) { - self.stack.push((g, Kind::Plain)); + self.push_group(g, Kind::Plain); } /// Add a finished node (an image, pattern tiles). diff --git a/crates/pdf/src/color.rs b/crates/pdf/src/color.rs index 6943476bc..5a7e9af86 100644 --- a/crates/pdf/src/color.rs +++ b/crates/pdf/src/color.rs @@ -215,19 +215,44 @@ pub enum PsOp { Proc(Vec), } -fn ps_parse(lx: &mut crate::object::Lexer, depth: usize) -> Vec { +/// Parse a procedure. `ops` counts the tokens read (those skipped as unsupported included); past +/// [`MAX_CALC_OPS`] the rest is not read, so an oversized program is never fully scanned or built +/// (the caller rejects it when `ops` ends past the cap). +fn ps_parse(lx: &mut crate::object::Lexer, depth: usize, ops: &mut usize) -> Vec { let mut out = vec![]; - while let Some(o) = lx.next() { - match o { - Obj::Num(n) => out.push(PsOp::Num(n)), - Obj::Bool(b) => out.push(PsOp::Num(if b { 1.0 } else { 0.0 })), - Obj::Op(op) if op == "{" && depth < 32 => out.push(PsOp::Proc(ps_parse(lx, depth + 1))), - // Nested too deep: skip the procedure's opening brace (never push it as an operator). - Obj::Op(op) if op == "{" => {} - Obj::Op(op) if op == "}" => return out, - Obj::Op(op) => out.push(PsOp::Op(op)), - _ => {} + loop { + // A calculator holds no arrays or dictionaries: one is skipped as a single token without + // being built (the lexer would allocate every element before this counted it). + if lx.at_compound() { + if *ops > MAX_CALC_OPS { + break; + } + lx.skip_compound(); + *ops += 1; + continue; } + let Some(o) = lx.next() else { break }; + if *ops > MAX_CALC_OPS { + break; + } + let op = match o { + Obj::Num(n) => PsOp::Num(n), + Obj::Bool(b) => PsOp::Num(if b { 1.0 } else { 0.0 }), + Obj::Op(op) if op == "{" && depth < 32 => PsOp::Proc(ps_parse(lx, depth + 1, ops)), + // Nested too deep: skip the procedure's opening brace (never push it as an operator). + Obj::Op(op) if op == "{" => { + *ops += 1; + continue; + } + Obj::Op(op) if op == "}" => return out, + Obj::Op(op) => PsOp::Op(op), + _ => { + *ops += 1; + continue; + } + }; + *ops += 1; + out.push(op); } out } @@ -243,6 +268,15 @@ fn ps_push(st: &mut Vec, v: f64) { } } +/// The most operators and operands a calculator program may hold (nested procedures included). +/// It has no loops, so this bounds what one evaluation runs; real programs hold a few dozen. +const MAX_CALC_OPS: usize = 4096; + +/// The operators and operands in `code`, nested procedures included. +fn ps_len(code: &[PsOp]) -> usize { + code.iter().map(|op| if let PsOp::Proc(p) = op { 1 + ps_len(p) } else { 1 }).sum() +} + fn ps_run(code: &[PsOp], st: &mut Vec, depth: usize) { if depth > 32 { return; @@ -431,6 +465,12 @@ impl Budget { Budget { nodes: MAX_NODES, samples: MAX_SAMPLES, overrun: false } } + /// Charge `n` retained values (coefficients, bounds, program operators) as nodes, eight to a + /// node, so a cached copy of a function is charged for what it holds, not just its node. + fn take_values(&mut self, n: usize) -> bool { + self.take_nodes(n.div_ceil(8)) + } + /// Charge `n` nodes; `false` (and the read is abandoned) once the cap is passed. fn take_nodes(&mut self, n: usize) -> bool { match self.nodes.checked_sub(n) { @@ -484,7 +524,8 @@ impl Func { /// [`Func::read`] nested `depth` levels deep, charging `budget`: unsupported past [`MAX_DEPTH`] /// or once the budget is spent. fn read_at(file: &File, o: &Obj, depth: usize, budget: &mut Budget) -> Func { - if depth >= MAX_DEPTH || !budget.take_nodes(1) { + // Charged first: the fallback past the depth limit is a node a copy keeps too. + if !budget.take_nodes(1) || depth >= MAX_DEPTH { return Func::Unsupported; } let o = file.resolve(o); @@ -499,12 +540,14 @@ impl Func { let dom = file.get(d, "Domain").map(|x| file.nums(x)).unwrap_or_default(); let domain = [dom.first().copied().unwrap_or(0.0), dom.get(1).copied().unwrap_or(1.0)]; match file.get_num(d, "FunctionType").unwrap_or(-1.0) as i32 { - 2 => Func::Exp { - domain, - c0: file.get(d, "C0").map(|x| file.nums(x)).unwrap_or_else(|| vec![0.0]), - c1: file.get(d, "C1").map(|x| file.nums(x)).unwrap_or_else(|| vec![1.0]), - n: file.get_num(d, "N").unwrap_or(1.0), - }, + 2 => { + let c0 = file.get(d, "C0").map(|x| file.nums(x)).unwrap_or_else(|| vec![0.0]); + let c1 = file.get(d, "C1").map(|x| file.nums(x)).unwrap_or_else(|| vec![1.0]); + if !budget.take_values(c0.len() + c1.len()) { + return Func::Unsupported; + } + Func::Exp { domain, c0, c1, n: file.get_num(d, "N").unwrap_or(1.0) } + } 3 => { // A stitching function's pieces: no element cap (a repeating gradient lists one // piece once per repeat); the budget bounds the total work. @@ -513,12 +556,12 @@ impl Func { None => Some(vec![]), }; let Some(funcs) = funcs else { return Func::Unsupported }; - Func::Stitch { - domain, - funcs, - bounds: file.get(d, "Bounds").map(|x| file.nums(x)).unwrap_or_default(), - encode: file.get(d, "Encode").map(|x| file.nums(x)).unwrap_or_default(), + let bounds = file.get(d, "Bounds").map(|x| file.nums(x)).unwrap_or_default(); + let encode = file.get(d, "Encode").map(|x| file.nums(x)).unwrap_or_default(); + if !budget.take_values(bounds.len() + encode.len()) { + return Func::Unsupported; } + Func::Stitch { domain, funcs, bounds, encode } } 0 => { let Obj::Stream(sd, raw) = o else { return Func::Unsupported }; @@ -537,6 +580,10 @@ impl Func { if !budget.take_samples(count) { return Func::Unsupported; } + // The stream must hold the table: missing bytes are not made up as zeros. + if count.checked_mul(bps).is_none_or(|bits| bits.div_ceil(8) > data.len()) { + return Func::Unsupported; + } let enc = file.get(d, "Encode").map(|x| file.nums(x)).unwrap_or_default(); let encode = [enc.first().copied().unwrap_or(0.0), enc.get(1).copied().unwrap_or(size as f64 - 1.0)]; let decode = file.get(d, "Decode").map(|x| file.nums(x)).filter(|v| v.len() >= outputs * 2).unwrap_or_else(|| range.clone()); @@ -567,10 +614,15 @@ impl Func { let Some(data) = decode_stream(file, sd, raw) else { return Func::Unsupported }; let mut lx = crate::object::Lexer::new(&data, 0); // The program is one procedure: `{ … }`. - let code = match ps_parse(&mut lx, 0).into_iter().next() { + let mut scanned = 0; + let code = match ps_parse(&mut lx, 0, &mut scanned).into_iter().next() { Some(PsOp::Proc(p)) => p, _ => return Func::Unsupported, }; + let ops = ps_len(&code); + if scanned > MAX_CALC_OPS || ops > MAX_CALC_OPS || !budget.take_values(ops) { + return Func::Unsupported; + } Func::Calc { domain: dom, range: file.get(d, "Range").map(|x| file.nums(x)).unwrap_or_default(), code } } _ => Func::Unsupported, @@ -756,12 +808,56 @@ mod tests { assert_eq!(sample_count(1 << 22, usize::MAX / (1 << 22) + 1), None); } + #[test] + fn oversized_calculator_program_is_cut_short() { + let src = format!("{{ {}}}", "1 ".repeat(MAX_CALC_OPS + 10)); + let mut lx = crate::object::Lexer::new(src.as_bytes(), 0); + let Some(PsOp::Proc(code)) = ps_parse(&mut lx, 0, &mut 0).into_iter().next() else { panic!() }; + assert_eq!(ps_len(&code), MAX_CALC_OPS + 1); + } + + #[test] + fn arrays_and_dictionaries_in_a_calculator_are_skipped_whole() { + // Strings with brackets, nested arrays and a dictionary are one skipped token each; the + // operators around them still parse. + let src = b"{ 1 [ (a]b) [1 2] << /K [3 4] /S (>>) >> 5 ] 2 add << /A 1 >> mul }"; + let mut lx = crate::object::Lexer::new(src, 0); + let Some(PsOp::Proc(code)) = ps_parse(&mut lx, 0, &mut 0).into_iter().next() else { panic!() }; + assert_eq!(code, vec![PsOp::Num(1.0), PsOp::Num(2.0), PsOp::Op("add".into()), PsOp::Op("mul".into())]); + // A huge array is skipped once the cap is passed, not read to its end. + let src = format!("{{ [ {}] 1 }}", "0 ".repeat(MAX_CALC_OPS * 100)); + let mut lx = crate::object::Lexer::new(src.as_bytes(), 0); + let mut scanned = 0; + let Some(PsOp::Proc(code)) = ps_parse(&mut lx, 0, &mut scanned).into_iter().next() else { panic!() }; + assert_eq!(code, vec![PsOp::Num(1.0)]); + // A large string inside a skipped array is passed over, not decoded: escapes, nested + // parentheses, a hexadecimal string and names all end where the lexer would end them. + let big = format!("{{ [ ({}) <{}> /{} (a\\)b(c)) 7 ] 1 }}", "x".repeat(1 << 20), "ab".repeat(1 << 19), "N".repeat(1 << 10)); + let mut lx = crate::object::Lexer::new(big.as_bytes(), 0); + let Some(PsOp::Proc(code)) = ps_parse(&mut lx, 0, &mut 0).into_iter().next() else { panic!() }; + assert_eq!(code, vec![PsOp::Num(1.0)]); + // An unclosed one ends at the end of the data. + let mut lx = crate::object::Lexer::new(b"[ 1 2 ", 0); + lx.skip_compound(); + assert_eq!(lx.pos, 6); + } + + #[test] + fn skipped_calculator_tokens_count_toward_the_cap() { + // `null` is not a calculator token: skipped, but still scanned. + let src = format!("{{ {}}}", "null ".repeat(MAX_CALC_OPS + 10)); + let mut lx = crate::object::Lexer::new(src.as_bytes(), 0); + let mut scanned = 0; + ps_parse(&mut lx, 0, &mut scanned); + assert!(scanned > MAX_CALC_OPS && scanned < MAX_CALC_OPS + 10, "parsing stops once the cap is passed ({scanned})"); + } + #[test] fn calculator_functions() { // Two inputs → three outputs: { 2 copy mul 3 1 roll add 2 div dup 0.5 gt { pop 1 } if 0 } let code = b"{ 2 copy mul 3 1 roll add 2 div dup 0.5 gt { pop 1 } { 0.25 mul } ifelse 0 }"; let mut lx = crate::object::Lexer::new(code, 0); - let Some(PsOp::Proc(code)) = ps_parse(&mut lx, 0).into_iter().next() else { panic!() }; + let Some(PsOp::Proc(code)) = ps_parse(&mut lx, 0, &mut 0).into_iter().next() else { panic!() }; let f = Func::Calc { domain: vec![0.0, 1.0, 0.0, 1.0], range: vec![0.0, 1.0, 0.0, 1.0, 0.0, 1.0], code }; assert_eq!(f.eval(&[0.5, 0.4]), vec![0.2, 0.1125, 0.0]); assert_eq!(f.eval(&[1.0, 0.8]), vec![0.8, 1.0, 0.0]); @@ -771,7 +867,7 @@ mod tests { fn run(src: &str, mut st: Vec) -> Vec { let src = format!("{{ {src} }}"); let mut lx = crate::object::Lexer::new(src.as_bytes(), 0); - let Some(PsOp::Proc(code)) = ps_parse(&mut lx, 0).into_iter().next() else { panic!() }; + let Some(PsOp::Proc(code)) = ps_parse(&mut lx, 0, &mut 0).into_iter().next() else { panic!() }; ps_run(&code, &mut st, 0); st } diff --git a/crates/pdf/src/object.rs b/crates/pdf/src/object.rs index 79a91dde8..df475a4bb 100644 --- a/crates/pdf/src/object.rs +++ b/crates/pdf/src/object.rs @@ -82,6 +82,91 @@ impl<'a> Lexer<'a> { } } + /// Whether the next token opens an array or a dictionary (skipping white space first). + pub fn at_compound(&mut self) -> bool { + self.skip_ws(); + match self.data.get(self.pos) { + Some(b'[') => true, + Some(b'<') => self.data.get(self.pos + 1) == Some(&b'<'), + _ => false, + } + } + + /// Skip the array or dictionary at the current position (see [`Lexer::at_compound`]) as + /// [`Lexer::next`] would read it, without building it: for readers that have no use for one + /// and must not allocate a vector per element of a hostile stream. + pub fn skip_compound(&mut self) { + let mut open = 0usize; + loop { + self.skip_ws(); + match self.data.get(self.pos) { + None => return, + Some(b'[') => { + open += 1; + self.pos += 1; + } + Some(b'<') if self.data.get(self.pos + 1) == Some(&b'<') => { + open += 1; + self.pos += 2; + } + Some(b']') | Some(b'>') if self.data[self.pos] == b']' || self.data.get(self.pos + 1) == Some(&b'>') => { + open = open.saturating_sub(1); + self.pos += if self.data[self.pos] == b']' { 1 } else { 2 }; + if open == 0 { + return; + } + } + // Anything else is one token (a string, name, number or word), passed over + // without building it. + Some(_) => self.skip_scalar(), + } + } + } + + /// Pass over one token that is not an array or dictionary bracket, as [`Lexer::next`] reads + /// it but without building its value. + fn skip_scalar(&mut self) { + let Some(&c) = self.data.get(self.pos) else { return }; + match c { + b'(' => { + // A literal string: balanced parentheses, a backslash escapes the next byte. + let mut depth = 0usize; + while let Some(&d) = self.data.get(self.pos) { + self.pos += 1; + match d { + b'\\' => self.pos += 1, + b'(' => depth += 1, + b')' => { + depth -= 1; + if depth == 0 { + break; + } + } + _ => {} + } + } + self.pos = self.pos.min(self.data.len()); + } + b'<' => { + // A hexadecimal string. + while self.data.get(self.pos).is_some_and(|d| *d != b'>') { + self.pos += 1; + } + self.pos = (self.pos + 1).min(self.data.len()); + } + b'/' => { + self.pos += 1; + self.word(); + } + b']' | b'{' | b'}' | b')' | b'>' => self.pos += 1, + _ => { + if self.word().is_empty() { + self.pos += 1; + } + } + } + } + fn word(&mut self) -> &'a [u8] { let s = self.pos; while self.pos < self.data.len() && !is_white(self.data[self.pos]) && !is_delim(self.data[self.pos]) { diff --git a/crates/pdf/src/page.rs b/crates/pdf/src/page.rs index 0ebc0421d..ba67b39aa 100644 --- a/crates/pdf/src/page.rs +++ b/crates/pdf/src/page.rs @@ -669,7 +669,12 @@ impl<'a> Interp<'a> { let bc = f.get(&sm, "BC").map(|b| f.nums(b)).unwrap_or_default(); let gcs = f.get_dict(&gd, "Group").and_then(|gr| f.get(gr, "CS")).map(|c| color_space(f, c, Some(res))).unwrap_or(Cs::Rgb); let backdrop = if bc.is_empty() { [0.0; 3] } else { gcs.to_rgb(&bc) }; - let saved_b = std::mem::replace(&mut self.b, Builder::new()); + // The mask it replaces closes first: its groups don't enclose the new one. + if let Some(d) = self.gs.mask_depth { + self.close_to(d); + } + let inner = self.b.nested(2); + let saved_b = std::mem::replace(&mut self.b, inner); let ctm = self.gs.ctm; let saved_gs = std::mem::replace(&mut self.gs, GState { ctm, ..GState::default() }); let saved_stack = std::mem::take(&mut self.saved); @@ -681,9 +686,6 @@ impl<'a> Interp<'a> { for s in sk { self.b.skip(&s); } - if let Some(d) = self.gs.mask_depth { - self.close_to(d); - } let mut content = Group::new("Soft Mask"); content.children = nodes; let mut grp = Group::new("Soft Mask Group"); @@ -784,8 +786,15 @@ impl<'a> Interp<'a> { if depth > 8 { return; } + // The `Pattern` and `Tile` wrappers must fit under the open groups too. + if !self.b.has_room(2) { + self.b.skip("group nesting limit"); + return; + } // The cell, drawn once in pattern space. - let saved_b = std::mem::replace(&mut self.b, Builder::new()); + // The cell ends up under the open groups and this pattern's `Pattern` and `Tile`. + let inner = self.b.nested(2); + let saved_b = std::mem::replace(&mut self.b, inner); let saved_gs = std::mem::take(&mut self.gs); let saved_stack = std::mem::take(&mut self.saved); let saved_path = std::mem::take(&mut self.path); diff --git a/crates/pdf/src/ps/builtin.rs b/crates/pdf/src/ps/builtin.rs index dcafa9417..a19aeb6da 100644 --- a/crates/pdf/src/ps/builtin.rs +++ b/crates/pdf/src/ps/builtin.rs @@ -1,4 +1,5 @@ use super::eps::find_from; +use super::exec::MAX_STACK; use super::scanner::Src; use super::text::encoding_array; use super::value::{Flow, V, array, matrix_of, matrix_val, new_dict}; @@ -181,8 +182,13 @@ impl Interp { match self.pop() { V::Num(k) => { let k = (k.max(0.0) as usize).min(self.st.len()); - let tail: Vec = self.st[self.st.len() - k..].to_vec(); - self.st.extend(tail); + // A copy past the operand limit copies nothing, and the values copied + // count as operations (one for eight): repeated doubling spends the budget. + if self.st.len().saturating_add(k) <= MAX_STACK { + self.charge(k / 8); + let tail: Vec = self.st[self.st.len() - k..].to_vec(); + self.st.extend(tail); + } } V::Array(dst) => { let s = self.pop().items().unwrap_or_default(); @@ -507,12 +513,16 @@ impl Interp { fn try_collection_op(&mut self, n: &str) -> bool { match n { // Arrays and strings. + // The storage made counts as operations (a value, or eight bytes, each), so a loop + // of large constructors spends the budget rather than the memory. "array" => { let k = self.popn().clamp(0.0, 1e6) as usize; + self.charge(k); self.push(array(vec![V::Null; k])); } "string" => { let k = self.popn().clamp(0.0, 1e7) as usize; + self.charge(k / 8); self.push(V::Str(Rc::new(RefCell::new(vec![0; k])))); } "length" => { diff --git a/crates/pdf/src/ps/eps.rs b/crates/pdf/src/ps/eps.rs index 90d60cfbe..8fb72698a 100644 --- a/crates/pdf/src/ps/eps.rs +++ b/crates/pdf/src/ps/eps.rs @@ -19,7 +19,8 @@ pub fn eps_parts(bytes: &[u8]) -> Option<(&[u8], [f64; 4])> { if v.len() == 4 && v[2] > v[0] && v[3] > v[1] { return Some([v[0], v[1], v[2], v[3]]); } - from = s; + // A rejected comment is done with: the scan goes on past its line, not over it again. + from = e; } None }; @@ -31,8 +32,15 @@ pub(super) fn find_from(hay: &[u8], needle: &[u8], from: usize) -> Option hay.get(from..)?.windows(needle.len()).position(|w| w == needle).map(|p| p + from) } -fn rfind_before(hay: &[u8], needle: &[u8], to: usize) -> Option { - hay.get(..to)?.windows(needle.len()).rposition(|w| w == needle) +/// Every position of `needle` in `hay`, in order (one pass over the file). +fn positions(hay: &[u8], needle: &[u8]) -> Vec { + let mut out = vec![]; + let mut from = 0; + while let Some(p) = find_from(hay, needle, from) { + out.push(p); + from = p + needle.len(); + } + out } /// The name after `key` (`/FontName /Name`). @@ -48,20 +56,26 @@ fn name_after(b: &[u8], key: &[u8]) -> Option { /// … cleartomark`, hexadecimal or binary) and CFF font sets (`… StartData `). pub(super) fn embedded_fonts(ps: &[u8]) -> HashMap>, bool)> { let mut out = HashMap::new(); + // The headers are found once, in one pass each; every `eexec` then takes the last header + // before it that no earlier `eexec` has used, so no prefix of the file is searched again. + let mut heads = positions(ps, b"%!PS-AdobeFont"); + heads.extend(positions(ps, b"%!FontType1")); + heads.sort_unstable(); + let names = positions(ps, b"/FontName"); let mut from = 0; + let mut used = 0; while let Some(ee) = find_from(ps, b"eexec", from) { from = ee + 5; - let start = [b"%!PS-AdobeFont".as_slice(), b"%!FontType1"] - .iter() - .filter_map(|h| rfind_before(ps, h, ee)) - .max() - .or_else(|| rfind_before(ps, b"/FontName", ee).map(|p| p.saturating_sub(512))); + let last_before = |v: &[usize]| v[..v.partition_point(|&p| p < ee)].last().copied().filter(|&p| p >= used); + // The fallback look-back stops at the previous font's end, so it cannot pick up its header. + let start = last_before(&heads).or_else(|| last_before(&names).map(|p| p.saturating_sub(512).max(used))); let Some(start) = start else { continue }; let end = find_from(ps, b"cleartomark", ee).map_or(ps.len(), |p| p + 11); if let Some(name) = name_after(&ps[start..ee], b"/FontName") { out.insert(name, (Rc::new(ps[start..end].to_vec()), false)); } from = end; + used = end; } let mut from = 0; while let Some(sd) = find_from(ps, b"StartData", from) { @@ -77,3 +91,30 @@ pub(super) fn embedded_fonts(ps: &[u8]) -> HashMap>, bool)> } out } + +#[cfg(test)] +mod tests { + use super::embedded_fonts; + + #[test] + fn font_discovery_looks_back_once_per_header() { + // 20,000 `eexec` words before any font header: each used to search the whole prefix + // for a header again. The font after them is still found. + let mut ps = b"%!PS-Adobe-3.0 EPSF-3.0\n".to_vec(); + ps.extend_from_slice("eexec ".repeat(20_000).as_bytes()); + ps.extend_from_slice(b"\n%!PS-AdobeFont-1.0: Real\n/FontName /Real def\ncurrentfile eexec\n0000\ncleartomark\n"); + let fonts = embedded_fonts(&ps); + assert_eq!(fonts.len(), 1); + assert!(fonts.contains_key("Real")); + } + + #[test] + fn header_less_font_after_a_short_one_does_not_take_its_header() { + let a = "%!PS-AdobeFont-1.0: A\n/FontName /A def\ncurrentfile eexec\n0000\ncleartomark\n"; + let b = "/FontName /B def\ncurrentfile eexec\n0000\ncleartomark\n"; + let ps = format!("%!PS-Adobe-3.0 EPSF-3.0\n{a}{b}"); + let fonts = embedded_fonts(ps.as_bytes()); + let (data, _) = fonts.get("B").expect("second font found"); + assert!(!String::from_utf8_lossy(data).contains("AdobeFont"), "{}", String::from_utf8_lossy(data)); + } +} diff --git a/crates/pdf/src/ps/exec.rs b/crates/pdf/src/ps/exec.rs index 27697527c..233ff82b0 100644 --- a/crates/pdf/src/ps/exec.rs +++ b/crates/pdf/src/ps/exec.rs @@ -6,6 +6,10 @@ use super::{Affine, BezPath, Builder, Cap, HashMap, Interp, Join}; /// The deepest procedure nesting run (deeper recursion stops the program). const MAX_CALL_DEPTH: usize = 256; +/// The most values the operand stack holds: a push past it is dropped, and `copy` copies +/// nothing rather than grow the stack past it. +pub(super) const MAX_STACK: usize = 100_000; + impl Interp { pub fn new(page: [f64; 4]) -> Interp { Interp { @@ -56,11 +60,17 @@ impl Interp { } pub(super) fn push(&mut self, v: V) { - if self.st.len() < 100_000 { + if self.st.len() < MAX_STACK { self.st.push(v); } } + /// Count `n` units of work (values copied, storage allocated) against the budget without + /// stopping the program here: once it is spent, the next operator stops it. + pub(super) fn charge(&mut self, n: usize) { + self.budget = self.budget.saturating_sub(n); + } + pub(super) fn lookup(&self, k: &str) -> Option { self.dicts.iter().rev().find_map(|d| d.borrow().get(k).cloned()) } diff --git a/crates/pdf/src/ps/paths.rs b/crates/pdf/src/ps/paths.rs index b1fe07092..30b5a7087 100644 --- a/crates/pdf/src/ps/paths.rs +++ b/crates/pdf/src/ps/paths.rs @@ -22,7 +22,7 @@ impl Interp { } /// Stop the program when a path of `len` elements would be longer than [`MAX_PATH`]. - fn path_fits(&mut self, len: usize) -> Result<(), Flow> { + pub(super) fn path_fits(&mut self, len: usize) -> Result<(), Flow> { if len <= MAX_PATH { return Ok(()); } diff --git a/crates/pdf/src/ps/text.rs b/crates/pdf/src/ps/text.rs index b138965bb..d4cee07f8 100644 --- a/crates/pdf/src/ps/text.rs +++ b/crates/pdf/src/ps/text.rs @@ -95,7 +95,14 @@ impl Interp { let ctm = self.gs.ctm; for (i, &code) in bytes.iter().enumerate() { let code = code as u32; + // Every input character costs at least one unit, so empty outlines are not free. + self.spend(1)?; if let Some(g) = font.glyph(code) { + // Each outline element counts against the path limit and the budget before it + // is copied, so a long string cannot expand into unbounded geometry. + let n = g.elements().len(); + self.path_fits(path.elements().len().saturating_add(n))?; + self.spend(n)?; path.extend(ctm * Affine::translate(p.to_vec2()) * m * (*g).clone()); } text.push_str(&font.unicode(code)); diff --git a/crates/pdf/src/shading.rs b/crates/pdf/src/shading.rs index df5e5f9c7..aa83adebf 100644 --- a/crates/pdf/src/shading.rs +++ b/crates/pdf/src/shading.rs @@ -18,6 +18,8 @@ const MAX_SIDE: f64 = 1024.0; const MAX_SCALE: f64 = 4.0; /// The most triangles one shading draws. const MAX_TRIANGLES: usize = 2_000_000; +/// How many times over the mesh raster may be covered by triangle bounding boxes in all. +const MAX_RASTER_PASSES: usize = 64; /// A mesh vertex: page-space position and its value (RGB, or `t` in `[0]` with a function). #[derive(Clone, Copy, Debug, PartialEq)] @@ -234,13 +236,17 @@ fn free_form(r: &mut Bits, l: &Layout, colors: &Colors, m: Affine) -> Vec<[V; 3] /// Type 5: a lattice of `per_row` vertices per row, two triangles per cell. fn lattice(r: &mut Bits, l: &Layout, colors: &Colors, m: Affine, per_row: usize) -> Vec<[V; 3]> { - if per_row < 2 { + if !(2..=MAX_TRIANGLES).contains(&per_row) { return vec![]; } let mut rows: Vec> = vec![]; let mut row = vec![]; while let (Some(p), Some(c)) = (l.point(r), l.comps(r)) { r.align(); + // Vertices kept, the unfinished row's included, stay within the triangle limit. + if (rows.len() * per_row).saturating_add(row.len()) >= MAX_TRIANGLES { + break; + } row.push(V { p: m * p, c: colors.value(&c) }); if row.len() == per_row { rows.push(std::mem::take(&mut row)); @@ -447,6 +453,9 @@ fn rasterize(tris: &[[V; 3]], colors: &Colors, page_box: [f64; 4]) -> Option Option usize { + 1 + g.children.iter().filter_map(|c| if let effectcraft_svg::Node::Group(g) = c { Some(depth(g)) } else { None }).max().unwrap_or(0) + } + let clips = "0 0 200 100 re W n ".repeat(250); + let cell = format!("{} 1 0 0 rg 0 0 10 10 re f", "0 0 20 20 re W n ".repeat(250)).into_bytes(); + let bytes = page_pdf( + &format!("{clips} /Pattern cs /P1 scn 0 0 100 100 re f"), + "/Pattern << /P1 10 0 R >>", + vec![(10, "<< /Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 20 20] /XStep 20 /YStep 20 /Resources << >> >>".into(), Some(cell))], + ); + let doc = parse(&bytes).unwrap(); + assert!(depth(&doc.root) <= 260, "{}", depth(&doc.root)); + assert!(doc.skipped.iter().any(|s| s == "group nesting limit"), "{:?}", doc.skipped); +} + +/// A pattern whose `Pattern` and `Tile` wrappers would pass the depth limit is skipped, not built +/// past it. +#[test] +fn a_pattern_whose_wrappers_pass_the_depth_limit_is_skipped() { + let clips = "0 0 200 100 re W n ".repeat(255); + let bytes = page_pdf( + &format!("{clips} /Pattern cs /P1 scn 0 0 100 100 re f"), + "/Pattern << /P1 10 0 R >>", + vec![( + 10, + "<< /Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 20 20] /XStep 20 /YStep 20 /Resources << >> >>".into(), + Some(b"0 0 10 10 re f".to_vec()), + )], + ); + let doc = parse(&bytes).unwrap(); + assert!(doc.skipped.iter().any(|s| s == "group nesting limit"), "{:?}", doc.skipped); +} + +/// A soft mask replacing another is nested under what is open once the old one has closed, so +/// groups inside the old mask don't count against the new mask's content. +#[test] +fn a_replaced_soft_mask_does_not_count_against_the_new_one() { + let form = |n: &str| { + ( + n.parse().unwrap(), + "<< /Type /XObject /Subtype /Form /BBox [0 0 200 100] /Group << /S /Transparency /CS /DeviceGray >> >>".to_string(), + Some(format!("{} 1 g 0 0 100 100 re f", "0 0 200 100 re W n ".repeat(250)).into_bytes()), + ) + }; + let bytes = page_pdf( + &format!("/GS1 gs {} /GS2 gs 1 0 0 rg 0 0 200 100 re f", "0 0 200 100 re W n ".repeat(250)), + "/ExtGState << /GS1 << /SMask << /S /Luminosity /G 10 0 R >> >> /GS2 << /SMask << /S /Luminosity /G 11 0 R >> >> >>", + vec![form("10"), form("11")], + ); + let doc = parse(&bytes).unwrap(); + assert!(doc.skipped.iter().all(|s| s != "group nesting limit"), "{:?}", doc.skipped); +} + /// Build isolated interpreter objects without presenting a function or colour space as a /// document catalog. Complete-document parsing is covered by the page and reader fixtures. fn object_file(objs: Objs) -> object::File { @@ -1068,6 +1125,35 @@ fn a_fanning_out_reference_graph_is_refused_within_the_budget() { assert_eq!(read_function(objs), color::Func::Unsupported); } +#[test] +fn a_sampled_function_shorter_than_its_table_is_refused() { + // 4,096 8-bit samples declared, ten bytes given: the missing ones are not made up. + let f = "<< /FunctionType 0 /Domain [0 1] /Size [4096] /Range [0 1] /BitsPerSample 8 >>"; + assert_eq!(read_function(vec![(1, f.into(), Some(vec![7; 10]))]), color::Func::Unsupported); + assert_ne!(read_function(vec![(1, f.into(), Some(vec![7; 4096]))]), color::Func::Unsupported); +} + +#[test] +fn a_calculator_program_past_the_operator_limit_is_refused() { + let f = "<< /FunctionType 4 /Domain [0 1] /Range [0 1] >>"; + let long = format!("{{ {} }}", "1 pop ".repeat(3000)); + assert_eq!(read_function(vec![(1, f.into(), Some(long.into_bytes()))]), color::Func::Unsupported); + assert_ne!(read_function(vec![(1, f.into(), Some(b"{ 1 exch sub }".to_vec()))]), color::Func::Unsupported); +} + +#[test] +fn copies_of_a_function_are_charged_for_its_coefficients() { + // A stitching function listing one object 1,000 times, whose coefficient arrays hold 2,000 + // values: each copy is charged its values, so the budget refuses it instead of copying + // two million values. + let c = format!("[{}]", "0.5 ".repeat(1000)); + let objs: Objs = vec![ + (1, format!("<< /FunctionType 3 /Domain [0 1] /Functions [{}] >>", "2 0 R ".repeat(1000)), None), + (2, format!("<< /FunctionType 2 /Domain [0 1] /C0 {c} /C1 {c} /N 1 >>"), None), + ]; + assert_eq!(read_function(objs), color::Func::Unsupported); +} + #[test] fn colour_space_chain_of_depth_3() { // Indexed → Separation → ICCBased → DeviceRGB: entry 0 is tint 1 (blue), entry 1 tint 0 (white). diff --git a/crates/pdf/src/tests/import.rs b/crates/pdf/src/tests/import.rs index c7dcbee57..516ff054d 100644 --- a/crates/pdf/src/tests/import.rs +++ b/crates/pdf/src/tests/import.rs @@ -513,6 +513,9 @@ fn paths_stop_at_the_path_limit() { // The outlines of 100,000 H's, 13 elements each. "/Helvetica findfont 10 scalefont setfont /s 100000 string def 0 1 99999 { s exch 72 put } for \ 0 0 moveto s false charpath", + // The same outlines shown: the text path is bounded like the current path. + "/Helvetica findfont 10 scalefont setfont /s 100000 string def 0 1 99999 { s exch 72 put } for \ + 0 0 moveto s show", // 210,000 rectangles of five elements each. "/a 840000 array def 0 1 839999 { a exch 0 put } for a rectfill", ] { @@ -528,6 +531,45 @@ fn paths_stop_at_the_path_limit() { assert_eq!(doc.flatten().len(), 1); } +#[test] +fn copies_past_the_operand_limit_copy_nothing() { + // `count copy` doubles the operand stack: 40 doublings would be 2^40 values. A copy that would + // pass the operand limit (100,000) copies nothing, so the program runs on to its end. + let doc = eps_soon("0 0 10 10 rectfill 1 40 { count copy } repeat clear 20 20 10 10 rectfill"); + assert!(doc.skipped.is_empty(), "{:?}", doc.skipped); + assert_eq!(doc.flatten().len(), 2); +} + +#[test] +fn storage_made_in_a_loop_runs_out_of_the_budget() { + // Each value of an array and each eight bytes of a string count as an operation when they + // are made, so a loop keeping large arrays or strings spends the budget, not the memory. + for body in ["/a 1000 array def 0 1 999 { a exch 1000000 array put } for", "/a 1000 array def 0 1 999 { a exch 10000000 string put } for"] { + runs_out_of_budget(body); + } +} + +#[test] +fn groups_stop_nesting_at_the_depth_limit() { + // Every `rectclip` opens a clipping group inside the last: past 256 open groups the group is + // dropped (and noted), and what is drawn inside goes to the open one. + let doc = eps_soon("300 { 0 0 100 100 rectclip } repeat 0 0 10 10 rectfill"); + assert_eq!(doc.skipped, ["group nesting limit"]); + assert_eq!(doc.flatten().len(), 1); +} + +#[test] +fn bounding_box_comments_are_scanned_once() { + // A rejected `%%BoundingBox:` comment is not scanned again from each later marker on its + // line: 20,000 markers on one line would otherwise take quadratic work. + let mut src = b"%!PS-Adobe-3.0 EPSF-3.0\n".to_vec(); + src.extend_from_slice("%%BoundingBox: x ".repeat(20_000).as_bytes()); + src.extend_from_slice(b"\n%%BoundingBox: 0 0 50 50\n0 0 10 10 rectfill\n"); + let doc = parse(&src).unwrap(); + assert_eq!((doc.width, doc.height), (50.0, 50.0)); + assert_eq!(doc.flatten().len(), 1); +} + #[test] fn pdf_paths_stop_at_the_path_limit() { // A content stream's current path holds at most 1,048,576 elements: `re` (five elements each) diff --git a/crates/pdf/src/tests/shading.rs b/crates/pdf/src/tests/shading.rs index 8e96193ab..4b5f9645d 100644 --- a/crates/pdf/src/tests/shading.rs +++ b/crates/pdf/src/tests/shading.rs @@ -262,3 +262,32 @@ fn corrupt_mesh_data_draws_what_parses() { let doc = parse(&bytes).unwrap(); assert!(doc.skipped.iter().any(|s| s.contains("layout")), "{:?}", doc.skipped); } + +#[test] +fn overlapping_mesh_triangles_stop_at_the_raster_budget() { + // 20,000 triangles each covering the whole page would test every sample 20,000 times; the + // raster budget stops after about 64 passes, and the shading is still drawn. + let mut m = Mesh::default(); + for _ in 0..20_000 { + m.flag(0).pt(0, 0).c(&[255, 0, 0]); + m.flag(0).pt(400, 0).c(&[255, 0, 0]); + m.flag(0).pt(0, 400).c(&[255, 0, 0]); + } + let sh = format!("<< /ShadingType 4 /ColorSpace /DeviceRGB /BitsPerCoordinate 16 /BitsPerComponent 8 /BitsPerFlag 8 {RGB_DECODE} >>"); + let bytes = page_pdf("/Sh sh", "/Shading << /Sh 10 0 R >>", vec![(10, sh, Some(m.0))]); + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || tx.send(parse(&bytes)).ok()); + let doc = rx.recv_timeout(std::time::Duration::from_secs(20)).expect("the mesh finishes").unwrap(); + assert!(render(&doc).get(50, 50)[0] > 0.99); +} + +#[test] +fn a_lattice_row_longer_than_the_triangle_limit_draws_nothing() { + let mut m = Mesh::default(); + for i in 0..8 { + m.pt(i * 10, 0).c(&[255, 0, 0]); + } + let sh = format!("<< /ShadingType 5 /ColorSpace /DeviceRGB /BitsPerCoordinate 16 /BitsPerComponent 8 /VerticesPerRow 3000000 {RGB_DECODE} >>"); + let doc = parse(&page_pdf("/Sh sh", "/Shading << /Sh 10 0 R >>", vec![(10, sh, Some(m.0))])).unwrap(); + assert!(doc.flatten().is_empty()); +} diff --git a/crates/plugin/src/wasm.rs b/crates/plugin/src/wasm.rs index 499d57397..18f3fb0d0 100644 --- a/crates/plugin/src/wasm.rs +++ b/crates/plugin/src/wasm.rs @@ -13,6 +13,8 @@ const FUEL_BASE: u64 = 50_000_000; const FUEL_PER_PIXEL: u64 = 20_000; /// The most linear memory a plug-in instance may have (a 4K float frame is ~130 MB). const MAX_MEMORY: usize = 1 << 30; +/// The most elements one table of a plug-in may hold. +const MAX_TABLE_ELEMENTS: usize = 1 << 16; /// The longest manifest read. const MAX_MANIFEST: i32 = 1 << 20; @@ -84,7 +86,10 @@ fn err(e: impl std::fmt::Display) -> String { /// Instantiate `module` in a fresh, limited store and read its memory and allocator. fn begin(engine: &Engine, module: &Module) -> Result { - let mut store = Store::new(engine, StoreLimitsBuilder::new().memory_size(MAX_MEMORY).instances(1).build()); + // One memory and a few small tables: a module declaring many of them would otherwise make + // the host allocate and initialise each before an export or fuel check could refuse it. + let limits = StoreLimitsBuilder::new().memory_size(MAX_MEMORY).memories(1).tables(4).table_elements(MAX_TABLE_ELEMENTS).instances(1).build(); + let mut store = Store::new(engine, limits); store.limiter(|l| l); store.set_fuel(FUEL_BASE).map_err(err)?; let linker = Linker::::new(engine); diff --git a/crates/project/src/comp.rs b/crates/project/src/comp.rs index d8d7cda16..c0c401c82 100644 --- a/crates/project/src/comp.rs +++ b/crates/project/src/comp.rs @@ -268,8 +268,19 @@ impl Comp { /// `base`, or the next free numbered name when a layer already has it (`Shape Layer 2`, /// `v-b1.mp4 2`; see `numbered_name`). pub fn unique_layer_name(&self, base: &str) -> String { - let taken = |n: &str| self.layers.iter().any(|l| l.name == n); - if taken(base) { crate::item::numbered_name(base, taken) } else { base.to_string() } + if !self.layers.iter().any(|l| l.name == base) { + return base.to_string(); + } + let stem = crate::item::name_stem(base); + // The suffixes in use, gathered in one pass: trying each candidate against every layer + // made importing many layers of one name cubic. + let taken: std::collections::HashSet = self + .layers + .iter() + .filter_map(|l| l.name.strip_prefix(stem)?.strip_prefix(' ')) + .filter_map(|s| s.parse::().ok().filter(|n| n.to_string() == s)) + .collect(); + (2..).find(|i| !taken.contains(i)).map(|i| format!("{stem} {i}")).unwrap_or_default() } } diff --git a/crates/project/src/item.rs b/crates/project/src/item.rs index 4fcc06ab9..b055c60ba 100644 --- a/crates/project/src/item.rs +++ b/crates/project/src/item.rs @@ -8,12 +8,18 @@ use super::{Arc, BTreeMap, Deserialize, FrameRate, Label, Project, Serialize, Ti /// another layer of the same name. A number set off by a space is a counter and is replaced /// (`Red Solid 1` → `Red Solid 2`, `shot 2` → `shot 3`); digits that end a word belong to the /// name and stay (`v-b1.mp4` → `v-b1.mp4 2`, `clip2.mov` → `clip2.mov 2`). -pub(crate) fn numbered_name(base: &str, taken: impl Fn(&str) -> bool) -> String { +/// `base` without a trailing ` N`: digits are dropped only when a space separates them from a +/// nonempty stem, so `v-b1.mp4` and `Layer1` keep theirs. +pub(crate) fn name_stem(base: &str) -> &str { let head = base.trim_end_matches(|c: char| c.is_ascii_digit()); - let stem = match head.strip_suffix(' ') { + match head.strip_suffix(' ') { Some(stem) if head.len() < base.len() && !stem.trim_end().is_empty() => stem.trim_end(), _ => base.trim_end(), - }; + } +} + +pub(crate) fn numbered_name(base: &str, taken: impl Fn(&str) -> bool) -> String { + let stem = name_stem(base); (2u64..).map(|k| format!("{stem} {k}")).find(|n| !taken(n)).unwrap_or_default() } diff --git a/crates/project/src/render_queue.rs b/crates/project/src/render_queue.rs index 5418e5ac7..b935c2fc8 100644 --- a/crates/project/src/render_queue.rs +++ b/crates/project/src/render_queue.rs @@ -421,7 +421,12 @@ impl RenderSettings { /// The output frame rate: the Frame Rate override, unless the comp preserves its frame rate /// (Composition Settings ▸ Preserve frame rate when nested or in render queue). pub fn rate(&self, comp: &Comp) -> FrameRate { - if comp.preserve_frame_rate { comp.frame_rate } else { self.frame_rate.unwrap_or(comp.frame_rate) } + if comp.preserve_frame_rate { + return comp.frame_rate; + } + // A saved override is held to the supported rates, as a comp's own rate is: its frame count + // is the export's work. + self.frame_rate.map_or(comp.frame_rate, |r| crate::limits::frame_rate(r, comp.frame_rate)) } /// First output-rate frame starting at or after `t`. fn ceil_frame(r: FrameRate, t: Tick) -> i64 { @@ -480,6 +485,8 @@ impl RenderSettings { TimeSpan::Custom { start, end } => format!("Custom ({:.3}s – {:.3}s)", start.seconds(), end.seconds()), }; let rate = match (self.frame_rate, comp) { + // A saved override outside the supported rates renders at the clamped one: say so. + (Some(r), Some(c)) if self.rate(c) != r => format!("{:.3} fps (renders at {:.3})", r.as_f64(), self.rate(c).as_f64()), (Some(r), _) => format!("{:.3} fps", r.as_f64()), (None, Some(c)) => format!("Use comp's frame rate ({:.3})", c.frame_rate.as_f64()), (None, None) => "Use comp's frame rate".into(), @@ -1713,7 +1720,22 @@ pub fn numbered_template(template: &str, n: u32) -> String { /// Make a name safe as a file name component. pub fn sanitize(name: &str) -> String { - name.chars().map(|c| if matches!(c, '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|') { '_' } else { c }).collect() + let mut s: String = name.chars().map(|c| if matches!(c, '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|') { '_' } else { c }).collect(); + // At most a file name's worth (255 UTF-8 bytes, whole characters only): a template repeating + // a long name token must not expand to gigabytes. A cut name ends in a hash of the whole + // name so long names that share a prefix stay distinct. + if s.len() > 255 { + // shortcut: 64-bit FNV-1a, not collision-resistant against crafted names; use a real hash if names become untrusted keys. + let hash = name.bytes().fold(0xcbf2_9ce4_8422_2325u64, |h, b| (h ^ u64::from(b)).wrapping_mul(0x0100_0000_01b3)); + let mut end = 255 - 17; + while !s.is_char_boundary(end) { + end -= 1; + } + s.truncate(end); + s.push_str(&format!("-{hash:016x}")); + } + // `.` and `..` would name the folder itself or its parent when the token is a path component. + if !s.is_empty() && s.chars().all(|c| c == '.') { "_".repeat(s.len()) } else { s } } #[cfg(test)] @@ -1737,10 +1759,32 @@ mod tests { use super::*; + #[test] + fn sanitize_limits_the_name_in_bytes() { + // 255 two-byte characters are cut to whole characters plus a 17-byte hash suffix. + let s = sanitize(&"é".repeat(255)); + assert!(s.len() <= 255 && s.len() > 235, "{}", s.len()); + assert!(s.starts_with("éé") && s.is_char_boundary(s.len() - 17)); + // Names that share their first 255 bytes stay distinct, and a name that fits is untouched. + let (a, b) = (sanitize(&format!("{}A", "界".repeat(85))), sanitize(&format!("{}B", "界".repeat(85)))); + assert_ne!(a, b); + assert!(a.len() <= 255 && b.len() <= 255); + assert_eq!(sanitize(&"n".repeat(255)), "n".repeat(255)); + assert_eq!(sanitize(&"é".repeat(127)), "é".repeat(127)); + } + #[test] fn templates_and_sequences() { let v = TemplateVars { comp_name: "Main/Title", project_name: "P", width: 1920, height: 1080, frame_rate: 29.97, start_frame: 0, end_frame: 9 }; assert_eq!(expand_template(DEFAULT_TEMPLATE, OutputFormat::H264, &v), "Main_Title.mp4"); + // A dot-only name is not a parent-folder component; a long one is cut to a file name. + for name in ["..", "."] { + let v = TemplateVars { comp_name: name, ..v }; + assert_eq!(expand_template("out/[compName]/x.[fileExtension]", OutputFormat::Gif, &v), format!("out/{}/x.gif", "_".repeat(name.len()))); + } + let long = "n".repeat(100_000); + let v2 = TemplateVars { comp_name: &long, ..v }; + assert_eq!(expand_template(&"[compName]".repeat(100), OutputFormat::Gif, &v2).len(), 255 * 100); assert_eq!(expand_template("[compName]_[width]x[height]@[frameRate].[fileExtension]", OutputFormat::Gif, &v), "Main_Title_1920x1080@29.97.gif"); assert_eq!(sequence_path("out/a_[#####].png", 42), "out/a_00042.png"); assert_eq!(sequence_path("a###.tif", 7), "a007.tif"); @@ -1890,6 +1934,22 @@ mod tests { assert_eq!(p.sample(&c, 3), FrameSample::Progressive(Tick::from_seconds_f64(0.12))); } + /// A saved Frame Rate override outside the supported rates is clamped, so a short comp cannot + /// ask for billions of output frames. + #[test] + fn frame_rate_override_is_held_to_the_supported_rates() { + let c = Comp::new(64, 48, FrameRate::new(25, 1), Tick::from_seconds_f64(2.0)); + let s = |r| RenderSettings { frame_rate: Some(r), time_span: TimeSpan::LengthOfComp, ..Default::default() }; + let huge = s(FrameRate::new(1_000_000_000_000, 1)); + assert_eq!(huge.rate(&c).as_f64(), crate::limits::MAX_FPS); + assert!(huge.frame_count(&c) <= 2001, "{}", huge.frame_count(&c)); + assert_eq!(s(FrameRate::new(0, 1)).rate(&c), c.frame_rate); + assert_eq!(s(FrameRate::new(60, 1)).rate(&c), FrameRate::new(60, 1)); + let shown = |r| s(r).describe(Some(&c)).into_iter().find(|l| l.0 == "Frame Rate").map(|l| l.1).unwrap_or_default(); + assert!(shown(FrameRate::new(1_000_000_000_000, 1)).contains("renders at 1000.000"), "{}", shown(FrameRate::new(1_000_000_000_000, 1))); + assert_eq!(shown(FrameRate::new(60, 1)), "60.000 fps"); + } + #[test] fn crop_and_resize_sizes() { let c = Comp::new(1920, 1080, FrameRate::new(25, 1), Tick::from_seconds_f64(1.0)); diff --git a/crates/project/src/tests.rs b/crates/project/src/tests.rs index d040c8f0b..80ba3c771 100644 --- a/crates/project/src/tests.rs +++ b/crates/project/src/tests.rs @@ -146,6 +146,18 @@ fn unique_names_and_cycles() { let c = p.comp(cid).unwrap(); assert_eq!(c.unique_layer_name("Red Solid 1"), "Red Solid 2"); assert_eq!(c.unique_layer_name("Shape Layer 1"), "Shape Layer 1"); + // A suffix written another way (`02`) is not the number taken. + let mut d = c.clone(); + for n in ["Red Solid 02", "Red Solid 3"] { + let mut l = d.layers[0].clone(); + l.name = n.into(); + d.layers.push(l); + } + assert_eq!(d.unique_layer_name("Red Solid 1"), "Red Solid 2"); + // Digits that end a word stay in the stem. + let mut e = c.clone(); + e.layers[0].name = "v-b1.mp4".into(); + assert_eq!(e.unique_layer_name("v-b1.mp4"), "v-b1.mp4 2"); assert!(p.comp_contains(cid, cid)); } diff --git a/crates/psd/src/limits.rs b/crates/psd/src/limits.rs index c1c23b390..6db366c00 100644 --- a/crates/psd/src/limits.rs +++ b/crates/psd/src/limits.rs @@ -107,7 +107,7 @@ impl Budget { /// Inflate a zlib stream to at most `limit` bytes. pub(crate) fn inflate(src: &[u8], limit: usize) -> Result> { - miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(src, limit).map_err(|e| Error::Invalid(format!("zip: {e:?}"))) + miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(src, limit).map_err(|e| Error::Invalid(format!("zip: {:?}", e.status))) } /// Give back what a channel buffer from `Psd::channel_bytes` / `Psd::decode_plane` was charged: diff --git a/crates/psd/src/memory_tests.rs b/crates/psd/src/memory_tests.rs index 108046b66..96ac5c70c 100644 --- a/crates/psd/src/memory_tests.rs +++ b/crates/psd/src/memory_tests.rs @@ -28,10 +28,11 @@ impl Psd { } 2 | 3 => { let rest = r.bytes(r.remaining())?; - let mut v = miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(rest, rowb * h).map_err(|e| Error::Invalid(format!("zip: {e:?}")))?; + let mut v = + miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(rest, rowb * h).map_err(|e| Error::Invalid(format!("zip: {:?}", e.status)))?; v.resize(rowb * h, 0); if comp == 3 { - self.unpredict(&mut v, w, h); + self.unpredict(&mut v, w, h, &mut Budget::new(u64::MAX))?; } v } @@ -128,11 +129,12 @@ impl Psd { } 2 | 3 => { let rest = r.bytes(r.remaining())?; - let v = miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(rest, want * rowb * h).map_err(|e| Error::Invalid(format!("zip: {e:?}")))?; + let v = miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(rest, want * rowb * h) + .map_err(|e| Error::Invalid(format!("zip: {:?}", e.status)))?; for (c, p) in planes.iter_mut().enumerate().take(want) { let mut plane = v.get(c * rowb * h..(c + 1) * rowb * h).ok_or(Error::Truncated)?.to_vec(); if comp == 3 { - self.unpredict(&mut plane, w, h); + self.unpredict(&mut plane, w, h, &mut Budget::new(u64::MAX))?; } *p = Some(self.ref_samples(&plane, w, h)); } diff --git a/crates/psd/src/pixels.rs b/crates/psd/src/pixels.rs index 21a5c8845..290a7b6f4 100644 --- a/crates/psd/src/pixels.rs +++ b/crates/psd/src/pixels.rs @@ -37,7 +37,7 @@ impl Psd { PlaneStream::Rle(counts) => Cow::Owned(Self::unpack_plane(&counts, 0, r, rowb, h, budget)?), PlaneStream::Zip(mut v) => { v.resize(size, 0); - self.zip_plane(comp, &mut v, 0, w, h)?; + self.zip_plane(comp, &mut v, 0, w, h, budget)?; Cow::Owned(v) } }) @@ -90,7 +90,7 @@ impl Psd { Ok(match planes { PlaneStream::Raw => Cow::Borrowed(r.bytes(size)?), PlaneStream::Rle(counts) => Cow::Owned(Self::unpack_plane(counts, c, r, rowb, h, budget)?), - PlaneStream::Zip(v) => Cow::Borrowed(self.zip_plane(comp, v, c, w, h)?), + PlaneStream::Zip(v) => Cow::Borrowed(self.zip_plane(comp, v, c, w, h, budget)?), }) } @@ -106,11 +106,11 @@ impl Psd { } /// Plane `c` of the inflated ZIP data of consecutive planes, un-predicted in place (`comp` 3). - pub(crate) fn zip_plane<'v>(&self, comp: u16, v: &'v mut [u8], c: usize, w: usize, h: usize) -> Result<&'v [u8]> { + pub(crate) fn zip_plane<'v>(&self, comp: u16, v: &'v mut [u8], c: usize, w: usize, h: usize, budget: &mut Budget) -> Result<&'v [u8]> { let rowb = self.row_bytes(w); let plane = v.get_mut(c * rowb * h..(c + 1) * rowb * h).ok_or(Error::Truncated)?; if comp == 3 { - self.unpredict(plane, w, h); + self.unpredict(plane, w, h, budget)?; } Ok(plane) } @@ -132,7 +132,8 @@ impl Psd { if self.depth == 1 { w.div_ceil(8) } else { w * self.bytes_per_sample() } } - pub(crate) fn unpredict(&self, v: &mut [u8], w: usize, h: usize) { + /// Undo ZIP prediction in place; the 32-bit scratch row is charged to `budget`. + pub(crate) fn unpredict(&self, v: &mut [u8], w: usize, h: usize, budget: &mut Budget) -> Result<()> { match self.depth { 16 => { for row in v.chunks_exact_mut(w * 2).take(h) { @@ -145,8 +146,9 @@ impl Psd { } } 32 => { - // One scratch row for the whole plane. - let mut planar = vec![0u8; w * 4]; + // One scratch row for the whole plane, charged: a wide, short layer passes the + // decode's peak check and would otherwise allocate it on top. + let mut planar = budget.filled(w.checked_mul(4).ok_or_else(|| too_large(w, h))?, 0u8)?; #[cfg(test)] counters::bump(&counters::SCRATCH); for row in v.chunks_exact_mut(w * 4).take(h) { @@ -161,6 +163,7 @@ impl Psd { } } } + budget.release(planar.len() as u64); } _ => { for row in v.chunks_exact_mut(w).take(h) { @@ -170,6 +173,7 @@ impl Psd { } } } + Ok(()) } /// Hand every sample of `raw` (`w`×`h`, row-major, in order) to `f` as a float. Samples a short @@ -483,15 +487,17 @@ pub(crate) fn unpack_bits(src: &[u8], row: usize, out: &mut Vec) -> Result<( while i < src.len() && out.len() - start < row { let n = src[i] as i8; i += 1; + // A packet is written only up to the row's end, so the reserved plane never grows. + let room = row - (out.len() - start); if n >= 0 { let k = n as usize + 1; let s = src.get(i..i + k).ok_or(Error::Truncated)?; - out.extend_from_slice(s); + out.extend_from_slice(&s[..k.min(room)]); i += k; } else if n != -128 { let k = (1 - n as isize) as usize; let v = *src.get(i).ok_or(Error::Truncated)?; - out.extend(std::iter::repeat_n(v, k)); + out.extend(std::iter::repeat_n(v, k.min(room))); i += 1; } } diff --git a/crates/psd/src/tests.rs b/crates/psd/src/tests.rs index 074976510..fd041138a 100644 --- a/crates/psd/src/tests.rs +++ b/crates/psd/src/tests.rs @@ -493,3 +493,29 @@ fn rle_composite_matches_the_raw_one() { assert_eq!(rle.layer_pixels(2, true).unwrap().data, raw.layer_pixels(2, true).unwrap().data, "{depth} bit"); } } + +#[test] +fn packbits_packets_stop_at_the_row_end() { + // A 128-byte repeat and a 128-byte literal into 3-byte rows: the reserved row is never + // outgrown (capacity stays what was reserved). + for src in [vec![0x81u8, 7], { + let mut v = vec![0x7fu8]; + v.extend([1u8; 128]); + v + }] { + let mut out = Vec::with_capacity(3); + unpack_bits(&src, 3, &mut out).unwrap(); + assert_eq!(out.len(), 3); + assert_eq!(out.capacity(), 3); + } +} + +#[test] +fn a_failed_inflate_reports_its_status_not_its_output() { + // A stream that inflates a megabyte of zeros, then breaks its checksum. + let mut z = miniz_oxide::deflate::compress_to_vec_zlib(&vec![0u8; 1 << 20], 6); + let n = z.len(); + z[n - 1] ^= 0xff; + let Err(Error::Invalid(msg)) = crate::limits::inflate(&z, 1 << 21) else { panic!("the checksum fails") }; + assert!(msg.len() < 100, "{} bytes", msg.len()); +} diff --git a/crates/psd/src/tests/opt.rs b/crates/psd/src/tests/opt.rs index 908026d07..44bcec071 100644 --- a/crates/psd/src/tests/opt.rs +++ b/crates/psd/src/tests/opt.rs @@ -69,7 +69,7 @@ fn ref_decode_channel(p: &Psd, offset: usize, len: usize, w: usize, h: usize) -> } 2 | 3 => { let rest = r.bytes(r.remaining())?; - let mut v = miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(rest, rowb * h).map_err(|e| Error::Invalid(format!("zip: {e:?}")))?; + let mut v = miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(rest, rowb * h).map_err(|e| Error::Invalid(format!("zip: {:?}", e.status)))?; v.resize(rowb * h, 0); if comp == 3 { ref_unpredict(p.depth, &mut v, w, h); @@ -112,7 +112,8 @@ fn ref_composite(p: &Psd) -> Result { } 2 | 3 => { let rest = r.bytes(r.remaining())?; - let v = miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(rest, want * rowb * h).map_err(|e| Error::Invalid(format!("zip: {e:?}")))?; + let v = + miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(rest, want * rowb * h).map_err(|e| Error::Invalid(format!("zip: {:?}", e.status)))?; for (c, pl) in planes.iter_mut().enumerate().take(want) { let mut plane = v.get(c * rowb * h..(c + 1) * rowb * h).ok_or(Error::Truncated)?.to_vec(); if comp == 3 { @@ -448,7 +449,7 @@ fn unpredict_matches_the_reference_and_allocates_one_scratch_row() { let mut a: Vec = (0..w * h * bps).map(|i| (i * 31 % 251) as u8).collect(); let mut b = a.clone(); counters::take(&SCRATCH); - p.unpredict(&mut a, w, h); + p.unpredict(&mut a, w, h, &mut Budget::new(u64::MAX)).unwrap(); let scratch = counters::take(&SCRATCH); let old_allocs = ref_unpredict(depth, &mut b, w, h); assert_eq!(a, b, "{depth} bit"); @@ -473,11 +474,11 @@ fn raw_planes_are_borrowed_and_zip_planes_are_unpredicted_in_place() { let mut v: Vec = (0..3 * 40 * 30).map(|i| (i % 255) as u8).collect(); let range = v.as_ptr_range(); for comp in [2, 3] { - let got = q.zip_plane(comp, &mut v, 1, 40, 30).unwrap(); + let got = q.zip_plane(comp, &mut v, 1, 40, 30, &mut Budget::new(u64::MAX)).unwrap(); assert_eq!(got.len(), 1200); assert!(range.contains(&got.as_ptr()), "compression {comp}"); } - assert!(q.zip_plane(2, &mut v, 3, 40, 30).is_err()); + assert!(q.zip_plane(2, &mut v, 3, 40, 30, &mut Budget::new(u64::MAX)).is_err()); } #[test] diff --git a/crates/raster/src/blur.rs b/crates/raster/src/blur.rs index d92148658..76e51f865 100644 --- a/crates/raster/src/blur.rs +++ b/crates/raster/src/blur.rs @@ -27,13 +27,25 @@ fn box_h_into(src: &[Px], out: &mut [Px], w: usize, r: usize, repeat: bool) { row[i as usize] } }; + // The starting window: the pixels inside the row once each, and the repeated edge pixels + // counted rather than visited (a radius far past the row would otherwise loop over it). let mut acc = [0.0f32; 4]; - for i in -(r as isize)..=(r as isize) { - let p = get(i); + let inside = r.min(w - 1); + for p in &row[..=inside] { for c in 0..4 { acc[c] += p[c]; } } + if repeat { + let (left, right) = (r as f32, (r - inside) as f32); + for c in 0..4 { + acc[c] += row[0][c] * left; + // A zero count adds nothing (0 * NaN would be NaN). + if right > 0.0 { + acc[c] += row[w - 1][c] * right; + } + } + } for x in 0..w { o[x] = acc.map(|v| v * norm); let add = get(x as isize + r as isize + 1); @@ -65,14 +77,27 @@ fn box_v_into(src: &[Px], out: &mut [Px], w: usize, h: usize, r: usize, repeat: out.par_chunks_mut(w * block).enumerate().for_each(|(bi, chunk)| { let ya = (bi * block) as isize; let mut acc = vec![[0.0f32; 4]; w]; - for i in ya - r as isize..=ya + r as isize { - if let Some(rw) = row(i) { - for (a, p) in acc.iter_mut().zip(rw) { - for c in 0..4 { - a[c] += p[c]; - } + // The starting window: rows inside the image once each, the repeated edge rows counted. + let (lo, hi) = (ya - r as isize, ya + r as isize); + let add = |acc: &mut [Px], rw: &[Px], k: f32| { + // A zero count adds nothing (0 * NaN would be NaN). + if k == 0.0 { + return; + } + for (a, p) in acc.iter_mut().zip(rw) { + for c in 0..4 { + a[c] += p[c] * k; } } + }; + for i in lo.max(0)..=hi.min(h as isize - 1) { + add(&mut acc, &src[i as usize * w..(i as usize + 1) * w], 1.0); + } + if repeat { + let above = (-lo).max(0) as f32; + let below = (hi - (h as isize - 1)).max(0) as f32; + add(&mut acc, &src[0..w], above); + add(&mut acc, &src[(h - 1) * w..h * w], below); } for (yy, o) in chunk.chunks_mut(w).enumerate() { let y = ya + yy as isize; @@ -360,6 +385,59 @@ mod tests { } } + #[test] + fn a_non_finite_edge_pixel_outside_every_window_is_not_counted() { + // 9 wide, radius 1: the last pixel is in no window of the first pixels; likewise the last row. + let mut src = vec![[1.0f32; 4]; 81]; + src[80] = [f32::NAN; 4]; + let mut out = vec![[0.0f32; 4]; 81]; + box_h_into(&src, &mut out, 9, 1, true); + assert!(out[0].iter().all(|v| v.is_finite()), "{:?}", out[0]); + box_v_into(&src, &mut out, 9, 9, 1, true); + assert!(out[0].iter().all(|v| v.is_finite()), "{:?}", out[0]); + } + + #[test] + fn box_windows_count_repeated_edges_and_take_any_radius() { + // Against a window summed pixel by pixel, for radii inside and far past the image. + let (w, h) = (7usize, 5usize); + let src: Vec = (0..w * h).map(|i| [i as f32, (i % 3) as f32, 1.0, 0.5]).collect(); + let at = |x: isize, y: isize, repeat: bool| -> Px { + if !repeat && (x < 0 || y < 0 || x >= w as isize || y >= h as isize) { + return [0.0; 4]; + } + src[y.clamp(0, h as isize - 1) as usize * w + x.clamp(0, w as isize - 1) as usize] + }; + for repeat in [true, false] { + for r in [1usize, 3, 6, 9, 40] { + let (mut hx, mut vx) = (vec![[0.0; 4]; w * h], vec![[0.0; 4]; w * h]); + box_h_into(&src, &mut hx, w, r, repeat); + box_v_into(&src, &mut vx, w, h, r, repeat); + for y in 0..h as isize { + for x in 0..w as isize { + let (mut eh, mut ev) = ([0.0f32; 4], [0.0f32; 4]); + for d in -(r as isize)..=r as isize { + for c in 0..4 { + eh[c] += at(x + d, y, repeat)[c]; + ev[c] += at(x, y + d, repeat)[c]; + } + } + let n = (2 * r + 1) as f32; + let i = y as usize * w + x as usize; + for c in 0..4 { + assert!((hx[i][c] - eh[c] / n).abs() < 1e-3, "h r={r} repeat={repeat}"); + assert!((vx[i][c] - ev[c] / n).abs() < 1e-3, "v r={r} repeat={repeat}"); + } + } + } + } + } + // A radius of 2^40 finishes at once. + let mut out = vec![[0.0; 4]; w * h]; + box_h_into(&src, &mut out, w, 1 << 40, true); + box_v_into(&src, &mut out.clone(), w, h, 1 << 40, true); + } + #[test] fn gaussian_preserves_mass() { let mut img = Image::new(64, 64); diff --git a/crates/script/src/prelude.js b/crates/script/src/prelude.js index 0c39c4dfe..5adab6eec 100644 --- a/crates/script/src/prelude.js +++ b/crates/script/src/prelude.js @@ -292,6 +292,34 @@ function Folder(path) { if (!(this instanceof Folder)) return new Folder(path); this.__path = path === undefined ? "" : __str(path); } + +// ECMAScript's legacy case canonicalisation (what a `/i` regular expression compares with): the +// upper case form when it is one code unit, and not an ASCII letter for a non-ASCII one. +function __canon(ch) { + var u = ch.toUpperCase(); + if (u.length !== 1) return ch; + if (ch.charCodeAt(0) >= 128 && u.charCodeAt(0) < 128) return ch; + return u; +} + +// A case-insensitive `*` / `?` file mask match in time bounded by mask × name length (a +// regular expression of `.*` runs backtracks combinatorially on overlapping wildcards). +function __globMatch(mask, name) { + // Wildcards run over the original name (lowercasing it can change its length: "İ" becomes + // two code units); literal characters compare one at a time, case-insensitively as `/i` does + // ("ς" and "σ" and "Σ" match each other). + var p = mask, s = name; + var i = 0, j = 0, star = -1, mark = 0; + while (j < s.length) { + if (i < p.length && (p[i] === "?" || p[i] === s[j] || __canon(p[i]) === __canon(s[j]))) { i++; j++; } + else if (i < p.length && p[i] === "*") { star = i++; mark = j; } + else if (star !== -1) { i = star + 1; j = ++mark; } + else return false; + } + while (i < p.length && p[i] === "*") i++; + return i === p.length; +} + Folder.prototype = { constructor: Folder, get fsName() { return this.__path; }, @@ -310,10 +338,7 @@ Folder.prototype = { var out = []; for (var i = 0; i < names.length; i++) { var n = names[i]; - if (typeof mask === "string" && mask !== "*") { - var re = new RegExp("^" + mask.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$", "i"); - if (!re.test(n.split(/[\/\\]/).pop())) continue; - } + if (typeof mask === "string" && mask !== "*" && !__globMatch(mask, n.split(/[\/\\]/).pop())) continue; var isDir = JSON.parse(__file("isDir", n)); var f = isDir ? new Folder(n) : new File(n); if (typeof mask === "function" && !mask(f)) continue; @@ -867,6 +892,12 @@ Property.prototype.property = function () { return null; }; if (!idx) return null; return this.__layer.containingComp.layer(idx).__id; } + // A string parameter (a LUT or look file, a text field) takes the text, or a File's path (#3395). + // A hidden one (Mesh Warp's mesh) stays refused by the key setters too, as by setValue. + if (info.type === "string") { + if (info.hidden) throw __err("\"" + info.name + "\" can not be set"); + return __str(v); + } if (info.type === "enum") return __num(v) - 1; if (info.type === "bool") return typeof v === "boolean" ? v : __num(v) !== 0; if (info.type === "color") { @@ -1652,7 +1683,7 @@ CompItem.prototype.constructor = CompItem; P.saveFrameToPng = function (time, file) { var f = __str(file); if (!f) throw __err("saveFrameToPng: missing file"); - __file("write", f, ""); + __file("checkWrite", f); __call("comp.saveFrameAs", { comp: this.__id, path: f, time: __num(time, "time") }); }; P.openInEssentialGraphics = function () { __call("comp.openInEssentialGraphics", { comp: this.__id }); }; @@ -1662,8 +1693,8 @@ CompItem.prototype.constructor = CompItem; if (!/\.[A-Za-z0-9]+$/.test(f)) f += ".ectemplate"; if (!overwrite && JSON.parse(__file("exists", f)) === true) return false; if (this.__controllers().length === 0) return false; - // The file gate applies to templates too. - __file("write", f, ""); + // The file gate applies to templates too (checked without touching the file). + __file("checkWrite", f); __call("essential.exportTemplate", { comp: this.__id, path: f }); return true; }; diff --git a/crates/script/src/runtime.rs b/crates/script/src/runtime.rs index c23eaefb0..28e490158 100644 --- a/crates/script/src/runtime.rs +++ b/crates/script/src/runtime.rs @@ -427,6 +427,8 @@ fn native_file(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult check_access(s, &path, true).map(|_| json!(true)), "read" | "readBinary" => { let p = check_access(s, &path, false)?; let bytes = s.services.read_file(&p.to_string_lossy()).map_err(|e| format!("Unable to read {path}: {e}"))?; @@ -530,8 +532,33 @@ fn native_eval_file(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResul r } +/// Host hooks: code a script compiles at run time (`eval`, `new Function`, …) passes the same +/// nesting check as the script itself, since the parser recurses per level and a deep string +/// built at run time would overflow the stack. +struct Hooks; + +impl boa_engine::context::HostHooks for Hooks { + fn ensure_can_compile_strings( + &self, + _realm: boa_engine::realm::Realm, + parameters: &[JsString], + body: &JsString, + _direct: bool, + _context: &mut Context, + ) -> JsResult<()> { + let mut code: String = parameters.iter().map(|p| p.to_std_string_escaped()).collect::>().join(","); + // With no parameters (`eval`) the body is the source as it stands, so a leading `#!` line + // is still its first line; with parameters it sits inside a function, where it is no comment. + if !parameters.is_empty() { + code.push('\n'); + } + code.push_str(&body.to_std_string_escaped()); + check_nesting(&code).map_err(|e| JsNativeError::syntax().with_message(e).into()) + } +} + pub(crate) fn new_context() -> Result { - let mut ctx = Context::default(); + let mut ctx = boa_engine::context::ContextBuilder::new().host_hooks(std::rc::Rc::new(Hooks)).build().map_err(|e| e.to_string())?; let limits = ctx.runtime_limits_mut(); limits.set_loop_iteration_limit(200_000_000); limits.set_recursion_limit(2000); @@ -769,19 +796,129 @@ fn run_expanded(session: &mut Session, req: &ScriptRequest) -> Outcome { /// 8 MiB thread such as the Script Console's, so deeper scripts are rejected up front. pub const MAX_NESTING: usize = 96; -/// An error when `code` nests brackets deeper than [`MAX_NESTING`] (strings, template literals -/// and comments are skipped). +/// Skip template-literal text from `*i` (just past a backtick or an interpolation's `}`). Returns +/// true when it stopped past a `${` (code follows), false at the closing backtick or the end. +fn skip_template_text(b: &[u8], i: &mut usize) -> bool { + while let Some(&d) = b.get(*i) { + match d { + b'\\' => *i += 2, + b'`' => { + *i += 1; + return false; + } + b'$' if b.get(*i + 1) == Some(&b'{') => { + *i += 2; + return true; + } + _ => *i += 1, + } + } + false +} + +/// Whether a `/` after the last significant byte `prev` (at `at` in `b`) starts a regular +/// expression literal rather than dividing: after an operator, an opening bracket, a separator, +/// a keyword that takes an expression, or at the start. After a name, a number, a string or a +/// closing bracket it divides (a regular expression after `)` is the one case this misreads). +/// `after_dot`: the word ending at `at` follows a `.` (a property name, never a keyword); a `++` +/// or `--` before the `/` is a postfix update, so it divides. +fn regex_may_start(b: &[u8], prev: u8, at: usize, after_dot: bool) -> bool { + match prev { + 0 => true, + b'+' | b'-' if at > 0 && b.get(at - 1) == Some(&prev) => false, + b'(' | b',' | b'=' | b':' | b'[' | b'!' | b'&' | b'|' | b'?' | b'{' | b';' | b'+' | b'-' | b'*' | b'%' | b'<' | b'>' | b'~' | b'^' => true, + _ if after_dot => false, + c if c.is_ascii_alphanumeric() || c == b'_' || c == b'$' => { + let start = (0..=at).rev().find(|&k| !b.get(k).is_some_and(|d| d.is_ascii_alphanumeric() || *d == b'_' || *d == b'$')).map_or(0, |k| k + 1); + let word = b.get(start..=at).unwrap_or_default(); + [&b"return"[..], b"typeof", b"instanceof", b"in", b"of", b"new", b"delete", b"void", b"throw", b"case", b"do", b"else", b"yield", b"await"] + .contains(&word) + } + _ => false, + } +} + +/// Whether the `.` at `at` is a property access: not the decimal point of a number (`1.`, `12.`) +/// that ends there. A number written `1.5.x` has a fraction before the dot, so that dot accesses. +fn dot_is_property(b: &[u8], at: usize) -> bool { + let word = |d: u8| d.is_ascii_alphanumeric() || d == b'_' || d == b'$'; + let start = (0..at).rev().find(|&k| !b.get(k).is_some_and(|d| word(*d))).map_or(0, |k| k + 1); + let before = b.get(start..at).unwrap_or_default(); + let integer = !before.is_empty() && before.iter().all(u8::is_ascii_digit); + // `1.5.x`: digits that follow another `.` are a fraction. + !(integer && !(start > 0 && b.get(start - 1) == Some(&b'.'))) +} + +/// Skip a regular expression literal that starts at `b[*i]` (a `/`), to its closing `/` (flags +/// follow as ordinary letters). A `/` inside a class `[…]` or after a backslash does not end it, +/// and neither a quote nor a backtick inside it starts a string; a line break ends it. +fn skip_regex(b: &[u8], i: &mut usize) { + *i += 1; + let mut class = false; + while let Some(&d) = b.get(*i) { + match d { + b'\\' => *i += 1, + b'[' => class = true, + b']' => class = false, + b'/' if !class => return, + b'\n' => return, + _ => {} + } + *i += 1; + } +} + +/// An error when `code` nests brackets deeper than [`MAX_NESTING`] (strings, regular expression +/// literals, template literal text and comments are skipped; the code inside `${…}` +/// interpolations is checked). pub fn check_nesting(code: &str) -> Result<(), String> { let b = code.as_bytes(); let (mut i, mut depth, mut max) = (0usize, 0usize, 0usize); + // The last significant byte (not space or comment), and where it is. + let (mut prev, mut prev_at) = (0u8, 0usize); + // A `#!` line at the very start is a comment (Boa reads it so): its text is not scanned. + if b.starts_with(b"#!") { + i = b.iter().position(|d| *d == b'\n').unwrap_or(b.len()); + } + // Whether the word that ends at `prev_at` came right after a `.` token (comments and + // whitespace between them do not matter). + let mut word_after_dot = false; + // The bracket depth of each open `${`, to tell its closing `}` from an inner one. + let mut interpolations: Vec = vec![]; while let Some(&c) = b.get(i) { match c { + b'`' => { + i += 1; + if skip_template_text(b, &mut i) { + depth += 1; + max = max.max(depth); + interpolations.push(depth); + (prev, prev_at) = (b'{', i - 1); + } else { + (prev, prev_at) = (b'`', i.saturating_sub(1)); + } + continue; + } + b'}' if interpolations.last() == Some(&depth) => { + interpolations.pop(); + depth -= 1; + i += 1; + if skip_template_text(b, &mut i) { + depth += 1; + max = max.max(depth); + interpolations.push(depth); + (prev, prev_at) = (b'{', i - 1); + } else { + (prev, prev_at) = (b'`', i.saturating_sub(1)); + } + continue; + } b'(' | b'[' | b'{' => { depth += 1; max = max.max(depth); } b')' | b']' | b'}' => depth = depth.saturating_sub(1), - b'"' | b'\'' | b'`' => { + b'"' | b'\'' => { // Skip to the closing quote (escapes skip a byte). i += 1; while let Some(&d) = b.get(i) { @@ -797,16 +934,29 @@ pub fn check_nesting(code: &str) -> Result<(), String> { while b.get(i).is_some_and(|d| *d != b'\n') { i += 1; } + continue; } b'/' if b.get(i + 1) == Some(&b'*') => { i += 2; while i < b.len() && !(b.get(i) == Some(&b'*') && b.get(i + 1) == Some(&b'/')) { i += 1; } - i += 1; + i += 2; + continue; } + b'/' if regex_may_start(b, prev, prev_at, word_after_dot) => skip_regex(b, &mut i), _ => {} } + if !c.is_ascii_whitespace() { + let word = |d: u8| d.is_ascii_alphanumeric() || d == b'_' || d == b'$'; + // A word starts where the byte before it is not a word byte (white space and comments + // end a word too): it is a property name when a `.` that is not part of a number + // comes right before it. + if word(c) && (i == 0 || !b.get(i - 1).is_some_and(|d| word(*d))) { + word_after_dot = prev == b'.' && dot_is_property(b, prev_at); + } + (prev, prev_at) = (c, i); + } i += 1; } if max > MAX_NESTING { @@ -880,6 +1030,44 @@ mod nesting_tests { assert!(check_nesting(&("(".repeat(MAX_NESTING) + &")".repeat(MAX_NESTING))).is_ok()); } + #[test] + fn regular_expression_literals_are_skipped_not_read_as_strings_or_templates() { + let deep = |n: usize| format!("{}1{}", "(".repeat(n), ")".repeat(n)); + // A backtick, a quote or a slash inside a class in a regular expression opens nothing, so + // the deep expression after it is still counted. + for re in ["/`/", "/\"/", "/'/", "/[/]/", "/\\//"] { + assert!(check_nesting(&format!("`x${{ {re}.test('x') ? {} : 0 }}`", deep(5000))).is_err(), "{re} in an interpolation"); + assert!(check_nesting(&format!("x = {re}.test(y) ? {} : 0", deep(5000))).is_err(), "{re}"); + assert!(check_nesting(&format!("x = {re}.test(y) ? {} : 0", deep(10))).is_ok(), "{re} shallow"); + } + // Division is not a regular expression: what follows it is still scanned. + assert!(check_nesting(&format!("a = 4 / 2; b = {}", deep(5000))).is_err()); + assert!(check_nesting(&format!("a = (4) / 2 / 3; b = {}", deep(5000))).is_err()); + // After a keyword that takes an expression a slash starts a regular expression. + assert!(check_nesting(&format!("typeof /`/ ; {}", deep(5000))).is_err()); + // A postfix update or a property named like a keyword is followed by a division, not a + // regular expression (the deep brackets after it must still be counted). + for head in ["i++ /", "i-- /", "obj.in /", "obj . in /", "obj . /* c */ in /", "obj.\n typeof /", "a1.in /", "1.5.in /"] { + assert!(check_nesting(&format!("x = {head} {} / 2", deep(5000))).is_err(), "{head}"); + } + // A keyword after a property name is still a keyword (`obj.in in /re/`), and the dot of a + // number does not make the word after it a property (`1. in /re/`): the regular expression + // is skipped, so the backtick inside it opens nothing and the brackets after it count. + for head in ["obj.in in", "1. in", "12. typeof"] { + assert!(check_nesting(&format!("x = {head} /`/; y = {}", deep(5000))).is_err(), "{head}"); + } + } + + #[test] + fn template_interpolations_are_checked_but_template_text_is_not() { + let deep = |n: usize| format!("`a${{{}1{}}}b`", "(".repeat(n), ")".repeat(n)); + assert!(check_nesting(&deep(5000)).is_err()); + assert!(check_nesting(&deep(10)).is_ok()); + // Brackets in the literal text, and a nested template, are fine; depth carries across `}`. + assert!(check_nesting(&format!("`{} ${{ `x${{1+(2)}}y` }} {}`", "(".repeat(500), "{".repeat(500))).is_ok()); + assert!(check_nesting(&format!("`${{1}}${{{}2{}}}`", "[".repeat(MAX_NESTING), "]".repeat(MAX_NESTING))).is_err()); + } + /// A canonical drive path and a lexical one compare once the `\\?\` prefix is gone; a UNC /// share and a Unix path are left as they are. #[test] diff --git a/crates/script/src/scriptfile.rs b/crates/script/src/scriptfile.rs index 2b6afcfbe..c3179711b 100644 --- a/crates/script/src/scriptfile.rs +++ b/crates/script/src/scriptfile.rs @@ -56,6 +56,10 @@ pub(crate) fn read(s: &Session, path: &Path) -> Result { if std::fs::metadata(path).is_ok_and(|m| m.len() > MAX_FILE_BYTES as u64) { return Err(too_big()); } + // A pipe or device reports no size and may never end: only regular files are read. + if std::fs::metadata(path).is_ok_and(|m| !m.is_file()) { + return Err(format!("Unable to read {}: not a regular file", path.display())); + } let bytes = s.services.read_file(&path.to_string_lossy()).map_err(|e| format!("Unable to read {}: {e}", path.display()))?; if bytes.len() > MAX_FILE_BYTES { return Err(too_big()); diff --git a/crates/script/src/socket.rs b/crates/script/src/socket.rs index c024fcad5..ff5d6d200 100644 --- a/crates/script/src/socket.rs +++ b/crates/script/src/socket.rs @@ -142,7 +142,7 @@ mod imp { /// Fill a stream's buffer until `done(buf)` or the timeout / end of stream. Fails only when /// the read deadline can't be set. - fn fill(s: &mut TcpStream, buf: &mut Vec, eof: &mut bool, limit: Duration, done: impl Fn(&[u8]) -> bool) -> std::io::Result<()> { + fn fill(s: &mut TcpStream, buf: &mut Vec, eof: &mut bool, limit: Duration, mut done: impl FnMut(&[u8]) -> bool) -> std::io::Result<()> { let end = Instant::now() + limit; let mut chunk = [0u8; 4096]; while !done(buf) && !*eof { @@ -322,7 +322,15 @@ mod imp { } } ("readln", Sock::Stream { s, buf, eof }) => { - if let Err(e) = fill(s, buf, eof, timeout(p), |b| b.contains(&b'\n')) { + // Only the bytes that arrived since the last look are searched: a peer + // sending no newline must not make every read rescan the whole buffer. + let mut seen = 0; + let newline = |b: &[u8]| { + let hit = b.get(seen..).is_some_and(|new| new.contains(&b'\n')); + seen = b.len(); + hit + }; + if let Err(e) = fill(s, buf, eof, timeout(p), newline) { return err(format!("cannot set the read timeout: {e}")); } let line: Vec = match buf.iter().position(|&c| c == b'\n') { diff --git a/crates/script/src/tests.rs b/crates/script/src/tests.rs index 78a16e5d3..800ccdde7 100644 --- a/crates/script/src/tests.rs +++ b/crates/script/src/tests.rs @@ -700,6 +700,56 @@ fn save_frame_to_png_writes_behind_the_file_gate() { let _ = std::fs::remove_dir_all(&dir); } +/// A script that runs the export commands itself (`app.run`, `__exec`) meets the same file-write +/// preference as the wrappers; with the preference off nothing is created or replaced. +#[test] +fn export_commands_run_directly_by_a_script_respect_the_file_gate() { + let dir = std::env::temp_dir().join(format!("ec-direct-export-{}", std::process::id())); + // A leftover dir from an interrupted run (same pid) must not feed stale absence checks. + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let png = dir.join("frame.png"); + let tpl = dir.join("card.ectemplate"); + let kept = dir.join("kept.png"); + std::fs::write(&kept, b"original").unwrap(); + let mut s = session(); + s.execute("comp.new", json!({"name": "Card", "width": 64, "height": 64, "duration": 1})).unwrap(); + s.execute("layer.newText", json!({"text": "Hi"})).unwrap(); + ok(&mut s, "var c = app.project.activeItem; c.layer(1).property('ADBE Transform Group').property('ADBE Opacity').addToMotionGraphicsTemplate(c);"); + let q = |p: &std::path::Path| serde_json::to_string(&p.to_string_lossy()).unwrap(); + let comp = s.active_comp_id().unwrap().0; + for (code, target) in [ + (format!("app.run('comp.saveFrameAs', {{comp: {comp}, path: {}}})", q(&png)), &png), + (format!("__exec('comp.saveFrameAs', JSON.stringify({{comp: {comp}, path: {}}}))", q(&png)), &png), + (format!("app.run('essential.exportTemplate', {{comp: {comp}, path: {}}})", q(&tpl)), &tpl), + (format!("app.run('comp.saveFrameAs', {{comp: {comp}, path: {}}})", q(&kept)), &kept), + ] { + let o = run_code(&mut s, &code, "direct.jsx"); + let e = o.error.unwrap_or_else(|| panic!("`{code}` was not refused")); + assert!(e.message.contains("Allow Scripts to Write Files"), "{e:?}"); + if target == &kept { + assert_eq!(std::fs::read(&kept).unwrap(), b"original"); + } else { + assert!(!target.exists(), "{} was written", target.display()); + } + } + // The wrappers check the gate without touching the file: an existing one is left as it was. + let o = run_code(&mut s, &format!("app.project.activeItem.saveFrameToPng(0, new File({}))", q(&kept)), "wrap.jsx"); + assert!(o.error.is_some()); + assert_eq!(std::fs::read(&kept).unwrap(), b"original"); + let o = run_code(&mut s, &format!("app.project.activeItem.exportAsMotionGraphicsTemplate(true, {})", q(&tpl)), "wrap.jsx"); + assert!(o.error.is_some()); + assert!(!tpl.exists()); + // The user, from the UI, is not a script: the same command writes. + s.execute("comp.saveFrameAs", json!({"comp": comp, "path": png.to_string_lossy()})).unwrap(); + assert!(png.exists()); + // With the preference on, the script's direct call writes too. + s.prefs.scripting.allow_scripts_write_files = true; + ok(&mut s, &format!("app.run('essential.exportTemplate', {{comp: {comp}, path: {}}})", q(&tpl))); + assert!(std::fs::metadata(&tpl).unwrap().len() > 100); + let _ = std::fs::remove_dir_all(&dir); +} + /// #1246: with Warn User When Executing Files on, a script that asserts `confirmed` itself still /// gets the question; only the user's answer opens the file. #[test] @@ -1949,3 +1999,77 @@ fn setting_comp_width_and_height_leaves_the_layers_where_they_are() { let (_, c) = comp_named(&s, "w"); assert_eq!(layer(c, "s").props.prop("transform/position").unwrap().value, KV::Vec3([100.0, 100.0, 0.0])); } + +#[test] +fn a_hashbang_line_does_not_hide_deep_nesting_from_eval() { + // Boa reads a leading `#!` line of eval source as a comment, so the backtick on it opens no + // template: the brackets after it are counted and refused. + let mut s = session(); + let code = "var deep = new Array(5000).join('(') + '1' + new Array(5000).join(')'); var r; \ + try { eval('#! `\\n' + deep); r = 'ran'; } catch (e) { r = 'refused'; } r"; + assert_eq!(ok(&mut s, code).result.as_str(), Some("refused")); + assert!(crate::runtime::check_nesting(&format!("#! `\n{}1{}", "(".repeat(5000), ")".repeat(5000))).is_err()); + assert!(crate::runtime::check_nesting("#! `\n(1)").is_ok()); +} + +#[test] +fn code_compiled_at_run_time_passes_the_nesting_check() { + // `eval` and `new Function` of a string nested past the limit are refused with a catchable + // error instead of overflowing the parser's stack; shallow code still compiles. + let mut s = session(); + for call in ["eval(deep)", "new Function(deep)", "Function('a', deep)"] { + let code = format!( + "var deep = new Array(5000).join('(') + '1' + new Array(5000).join(')'); var r; try {{ {call}; r = 'ran'; }} catch (e) {{ r = 'refused'; }} r" + ); + assert_eq!(ok(&mut s, &code).result.as_str(), Some("refused"), "{call}"); + } + assert_eq!(ok(&mut s, "eval('1 + (2 * 3)') + new Function('return 4')()").result.as_f64(), Some(11.0)); +} + +#[test] +fn file_masks_match_without_backtracking() { + let mut s = session(); + let r = ok( + &mut s, + "[__globMatch('*.JSX', 'a.jsx'), __globMatch('a?c*', 'abcdef'), __globMatch('*a*b', 'xaxb'), __globMatch('*.png', 'a.jsx'), __globMatch('*a*a*a*a*a*a*a*a*a*a*a*b', new Array(60).join('a'))].join()", + ); + assert_eq!(r.result.as_str(), Some("true,true,true,false,false")); + // `?` still matches one character that lowercases to two. + let r = ok(&mut s, "[__globMatch('?.txt', '\\u0130.txt'), __globMatch('\\u0130*', '\\u0130x')].join()"); + assert_eq!(r.result.as_str(), Some("true,true")); + // Greek sigma matches in every case form, as the case-insensitive regular expression did. + let r = ok( + &mut s, + "[__globMatch('\\u03bf\\u03c2.txt', '\\u039f\\u03a3.txt'), __globMatch('\\u03a3*', '\\u03c2x'), __globMatch('k', '\\u212a'), __globMatch('a', 'b')].join()", + ); + assert_eq!(r.result.as_str(), Some("true,true,false,false")); +} + +/// #3395: setValue on a string effect parameter stores the text, or a File's path, as `prop.set` +/// does. Before, every string fell through to the number check ("value must be a number"). +/// Hidden string parameters (Mesh Warp's Distortion Mesh) stay refused, keys included. +#[test] +fn set_value_stores_a_string_parameter() { + let mut s = session(); + let o = ok( + &mut s, + r#" + var c = app.project.items.addComp("Str", 64, 36, 1, 1, 24); + var l = c.layers.addSolid([1, 0, 0], "s", 64, 36, 1); + var fx = l.property("ADBE Effect Parade"); + var lut = fx.addProperty("Apply Color LUT").property("LUT"); + lut.setValue("/luts/look.cube"); + var fromString = lut.value; + lut.setValue(new File("/luts/other.cube")); + var mesh = fx.addProperty("Mesh Warp").property("Distortion Mesh"); + function attempt(f) { try { f(); return "no error"; } catch (e) { return e.message; } } + var hidden = attempt(function () { mesh.setValue("x"); }); + var keyed = attempt(function () { mesh.setValueAtTime(0, "x"); }); + [lut.propertyValueType === PropertyValueType.CUSTOM_VALUE, fromString, lut.value, hidden, keyed, mesh.numKeys] + "#, + ); + let refused = "\"Distortion Mesh\" can not be set"; + assert_eq!(o.result, json!([true, "/luts/look.cube", "/luts/other.cube", refused, refused, 0])); + let (_, c) = comp_named(&s, "Str"); + assert_eq!(layer(c, "s").props.prop("effects/#1/lut").unwrap().value, KV::Str("/luts/other.cube".into())); +} diff --git a/crates/segment/src/mobilesam.rs b/crates/segment/src/mobilesam.rs index 6599b018a..abb12fb0f 100644 --- a/crates/segment/src/mobilesam.rs +++ b/crates/segment/src/mobilesam.rs @@ -19,6 +19,9 @@ const IMG: usize = 1024; const EMB: usize = 64; const DIM: usize = 256; const LOW: usize = 256; +/// Most prompt points the decoder takes (each is an attention token, and self-attention is +/// quadratic in them); SAM is used with a handful. More are thinned evenly, keeping both ends. +const MAX_POINTS: usize = 256; const MEAN: [f32; 3] = [123.675, 116.28, 103.53]; const STD: [f32; 3] = [58.395, 57.12, 57.375]; @@ -609,7 +612,7 @@ impl MobileSam { /// Foreground probability (`w×h`) for a prompt on an embedded image, and the predicted IoU. pub fn predict(&self, e: &Embedding, prompt: &Prompt) -> (Vec, f32) { let s = e.scale; - let points: Vec<([f32; 2], bool)> = prompt.points.iter().map(|(p, fg)| ([p[0] * s, p[1] * s], *fg)).collect(); + let points: Vec<([f32; 2], bool)> = thin(&prompt.points).iter().map(|(p, fg)| ([p[0] * s, p[1] * s], *fg)).collect(); let bbox = prompt.bbox.map(|b| b.map(|v| v * s)); // A prior mask (probabilities at image size) → logits on the low-res grid. let mask: Option> = prompt.mask.as_ref().filter(|m| m.len() == e.w * e.h).map(|m| { @@ -675,3 +678,28 @@ fn sample(rgb: &[[f32; 3]], w: usize, h: usize, x: f32, y: f32, foot: f32) -> [f } s.map(|v| v / f32::max(n, 1.0)) } + +/// `points`, or `MAX_POINTS` evenly spaced ones (first and last kept) when there are more. +fn thin(points: &[([f32; 2], bool)]) -> Vec<([f32; 2], bool)> { + let n = points.len(); + if n <= MAX_POINTS { + return points.to_vec(); + } + (0..MAX_POINTS).filter_map(|i| points.get(i * (n - 1) / (MAX_POINTS - 1)).copied()).collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn many_prompt_points_are_thinned_to_the_budget() { + let few: Vec<_> = (0..MAX_POINTS).map(|i| ([i as f32, 0.0], i % 2 == 0)).collect(); + assert_eq!(thin(&few), few, "a prompt within the budget is untouched"); + let many: Vec<_> = (0..1_000_000).map(|i| ([i as f32, 0.0], i % 2 == 0)).collect(); + let t = thin(&many); + assert_eq!(t.len(), MAX_POINTS); + assert_eq!((t[0].0, t[MAX_POINTS - 1].0), (many[0].0, many[many.len() - 1].0)); + assert!(t.windows(2).all(|w| w[0].0[0] < w[1].0[0]), "spread in order"); + } +} diff --git a/crates/segment/src/tflite.rs b/crates/segment/src/tflite.rs index 34e85dfab..6aafd056f 100644 --- a/crates/segment/src/tflite.rs +++ b/crates/segment/src/tflite.rs @@ -131,6 +131,8 @@ const MAX_VALUES: u64 = 1 << 30; const MAX_LIVE: u64 = 1 << 29; /// The most constant values decoded and laid out at load (a weight shared by several tensors or /// operators counts once per use). +/// Bytes of tensor names and shapes a model may carry in all (the official ones take well under 1 MB). +const MAX_META: usize = 1 << 24; const MAX_CONSTS: usize = 1 << 27; /// Tensor types (`TensorType`). @@ -261,6 +263,7 @@ impl Model { let mut ints: Vec>> = vec![None; n]; let mut data: Vec<(usize, u8, usize, &[u8])> = vec![]; let mut consts = 0usize; + let mut meta = 0usize; for (i, t) in tensors.iter().enumerate() { let shape: Vec = fb .ints(*t, 0)? @@ -269,7 +272,13 @@ impl Model { .collect::>()?; let ty = fb.byte(*t, 1, FLOAT32)?; let buf = fb.int(*t, 2, 0)? as u32 as usize; - names.push(fb.string(*t, 3)?); + let name = fb.string(*t, 3)?; + // Names and shapes are copied per tensor: bound them in all. + meta = meta.saturating_add(name.len()).saturating_add(shape.len().saturating_mul(8)); + if meta > MAX_META { + return Err("the model's tensor metadata is too large".into()); + } + names.push(name); // Every size computed from shapes later stays in range, even a product of some of the // dimensions (a zero dimension does not let the others grow without bound). let count = shape.iter().try_fold(1usize, |a, &d| a.checked_mul(d)).ok_or("a tensor is too large")?; @@ -329,6 +338,11 @@ impl Model { && let Some(i) = ins.first().copied().flatten() && let Some(len) = floats[i].as_ref().map(Vec::len) { + // The folded constant is what the graph outputs and the run's memory estimate count: + // it must be the declared size. + if shapes[output].iter().try_fold(1usize, |a, &d| a.checked_mul(d)) != Some(len) { + return Err(format!("{}: DEQUANTIZE output size disagrees with its input", names[output])); + } spend(len)?; floats[output] = floats[i].clone(); continue; diff --git a/crates/segment/src/tflite/tests.rs b/crates/segment/src/tflite/tests.rs index 29a781d34..9ba96971f 100644 --- a/crates/segment/src/tflite/tests.rs +++ b/crates/segment/src/tflite/tests.rs @@ -334,6 +334,26 @@ fn shapes_computed_at_run_time_must_match_the_declared_ones() { refused(&zero, "tensor is too large"); } +#[test] +fn a_folded_dequantize_has_its_declared_size() { + let half = |n: usize| vec![0u8; 2 * n]; + let fold = |declared: Vec| { + model(vec![half(4)], vec![(vec![1, 4], FLOAT16, Some(0)), (declared, FLOAT32, None)], vec![(op::DEQUANTIZE, vec![0], vec![1], vec![])], vec![], vec![1]) + }; + // A declared output of one value would let the folded four-value constant be copied out per reference. + refused(&fold(vec![1, 1]), "DEQUANTIZE output size"); + assert!(Model::read(&fold(vec![1, 4])).is_ok()); +} + +#[test] +fn tensor_metadata_is_bounded_in_all() { + // A rank of 4 million dimensions of 1 is a valid shape by size, but copied per tensor. + let m = model(vec![], vec![(vec![1; 1 << 22], FLOAT32, None)], vec![], vec![], vec![]); + refused(&m, "metadata is too large"); + let ok = model(vec![], (0..70).map(|_| (vec![1, 2, 3], FLOAT32, None)).collect(), vec![], vec![], vec![]); + assert!(Model::read(&ok).is_ok()); +} + #[test] fn shared_weights_count_once_per_use() { // One 128 KB buffer decoded for 2100 tensors would be 2100 copies of 2¹⁶ floats. diff --git a/crates/svg/Cargo.toml b/crates/svg/Cargo.toml index 0e1af7719..03cacaa7b 100644 --- a/crates/svg/Cargo.toml +++ b/crates/svg/Cargo.toml @@ -16,3 +16,4 @@ effectcraft-path = { workspace = true } kurbo = { workspace = true } roxmltree = "0.21" rayon = { workspace = true } +log = { workspace = true } diff --git a/crates/svg/src/lib.rs b/crates/svg/src/lib.rs index 95c00a91a..c31617a92 100644 --- a/crates/svg/src/lib.rs +++ b/crates/svg/src/lib.rs @@ -603,8 +603,15 @@ struct Parser<'a, 'i> { /// The `` targets being expanded, outermost first: a reference back into one is a /// cycle and is skipped. using: Vec, + /// Path elements and gradient stops kept so far, `` copies and repeated gradient + /// references included; past [`MAX_RETAINED`] the import errs. + retained: std::cell::Cell, } +/// The most path elements and gradient stops an import keeps: a complex path reused many times, +/// or a gradient with many stops referenced by many shapes, is copied for each use. +const MAX_RETAINED: usize = 8_000_000; + fn is_svg_ns(n: &roxmltree::Node) -> bool { n.is_element() } @@ -628,6 +635,13 @@ impl<'a, 'i> Parser<'a, 'i> { out } + /// Charge `n` kept path elements or gradient stops; false once [`MAX_RETAINED`] is passed. + fn charge_retained(&self, n: usize) -> bool { + let r = self.retained.get().saturating_add(n); + self.retained.set(r); + r <= MAX_RETAINED + } + /// Charge `n` units of CSS work; false once the budget is spent. fn charge_css(&self, n: usize) -> bool { let w = self.css_work.get().saturating_add(n); @@ -875,6 +889,10 @@ impl<'a, 'i> Parser<'a, 'i> { // Stops: from the first element in the chain that has any. let mut stops = vec![]; if let Some(src) = chain.iter().find(|e| e.children().any(|c| c.is_element() && c.tag_name().name() == "stop")) { + // Charged before the stops are read: every reference reads and keeps them again. + if !self.charge_retained(src.children().filter(|c| c.is_element()).count()) { + return None; + } let mut last = 0.0f64; for s in src.children().filter(|c| c.is_element() && c.tag_name().name() == "stop") { let d = self.declared(s); @@ -1026,6 +1044,11 @@ impl<'a, 'i> Parser<'a, 'i> { return; } let Some(geom) = self.geometry(n, &st) else { return }; + if let Geom::Path(p) = &geom + && !self.charge_retained(p.elements().len()) + { + return; + } let fill = if matches!(tag, "line") { None } else { st.fill.as_ref().and_then(|p| self.resolve_paint(p, &st)) }; let (fill, fill_alpha) = match fill { Some((p, a)) => (Some(p), a), @@ -1304,6 +1327,7 @@ pub fn parse(bytes: &[u8]) -> Result { work: 0, budget, using: vec![], + retained: std::cell::Cell::new(0), }; let st = Style::default(); let pct_or = |name: &str, axis: char, def: f64| -> f64 { @@ -1344,6 +1368,9 @@ pub fn parse(bytes: &[u8]) -> Result { // Never a silently partial drawing. return Err(Error::Xml(format!("its references expand to more than {} elements", p.budget))); } + if p.retained.get() > MAX_RETAINED { + return Err(Error::Xml(format!("its paths and gradients, with their copies, hold more than {MAX_RETAINED} elements"))); + } if p.css_work.get() > MAX_CSS_WORK { // Never a silently unstyled drawing. return Err(Error::Xml(format!("matching its