mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-12 21:32:52 +00:00
Merge remote-tracking branch 'upstream/main' into fix/3087-aep-effect-placeholders
# Conflicts: # docs/aep-import.md
This commit is contained in:
197 files changed
+5255
-17474
No files matched your search
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+88
-89
@@ -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",
|
||||
|
||||
+22
-43
@@ -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" }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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<Vec<Option<u32>>> {
|
||||
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<Vec<Option<u32>>
|
||||
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(),
|
||||
)
|
||||
|
||||
@@ -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")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>,
|
||||
/// 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<String>) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<u8> = [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::<String>::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<u8> {
|
||||
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);
|
||||
}
|
||||
@@ -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<Vec<Lookup>> {
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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.
|
||||
@@ -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<u8>,
|
||||
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<u8> {
|
||||
self.buf
|
||||
}
|
||||
}
|
||||
|
||||
/// `leb128()` encoding of an OBU size.
|
||||
pub(crate) fn leb128(mut v: usize, out: &mut Vec<u8>) {
|
||||
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<u8>) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
@@ -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<Cdfs> {
|
||||
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<const N: usize> ClearCount for [u16; N] {
|
||||
fn clear_count(&mut self) {
|
||||
self[N - 1] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ClearCount, const M: usize> ClearCount for [T; M] {
|
||||
fn clear_count(&mut self) {
|
||||
for e in self.iter_mut() {
|
||||
e.clear_count();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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<u8>,
|
||||
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<u8>,
|
||||
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<u8> {
|
||||
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<u8>,
|
||||
frame_num: u64,
|
||||
refr: Option<RefFrame>,
|
||||
rc: Option<RateState>,
|
||||
}
|
||||
|
||||
impl Encoder {
|
||||
pub fn new(cfg: EncoderConfig) -> Result<Encoder, Error> {
|
||||
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<u8> {
|
||||
self.seq_obu.clone()
|
||||
}
|
||||
|
||||
/// AV1CodecConfigurationRecord (`av1C` payload, also the WebM CodecPrivate).
|
||||
pub fn av1c(&self) -> Vec<u8> {
|
||||
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<u8> {
|
||||
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<u8>, 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<u8>, 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<u8>, 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<u32> {
|
||||
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<u16>; 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
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<u16>,
|
||||
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::<i32>() + above[..w].iter().sum::<i32>() + ((w + h) >> 1) as i32;
|
||||
sum / (w + h) as i32
|
||||
} else if e.have_left {
|
||||
(left[..h].iter().sum::<i32>() + (h >> 1) as i32) >> log2
|
||||
} else if e.have_above {
|
||||
(above[..w].iter().sum::<i32>() + (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<u16> {
|
||||
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<u16>; 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<u16> = 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<u8>,
|
||||
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<u8> {
|
||||
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<u8> = 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<bool> = (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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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<const N: u32>(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<const N: u32>(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<const N: u32>(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<const LOG2: u32>(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<f32>; 4],
|
||||
adst: [Vec<f32>; 3],
|
||||
/// The same matrices transposed (`[i][k]`).
|
||||
dct_t: [Vec<f32>; 4],
|
||||
adst_t: [Vec<f32>; 3],
|
||||
}
|
||||
|
||||
fn transpose(m: &[f32]) -> Vec<f32> {
|
||||
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<Bases> = 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<const N: usize, const NN: usize>(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<i32> = (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<i32> = 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,6 +0,0 @@
|
||||
//! The crate's integration tests, one binary (each module is a former `tests/<name>.rs`).
|
||||
|
||||
mod bench;
|
||||
mod common;
|
||||
mod ffmpeg_oracle;
|
||||
mod roundtrip;
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -394,7 +394,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
||||
};
|
||||
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]);
|
||||
|
||||
@@ -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<f32> {
|
||||
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 {
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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<Image>, scale: f64) -> Option<(Vec<f32>, Vec<u8>, 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]);
|
||||
|
||||
@@ -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)]
|
||||
|
||||
+72
-13
@@ -562,13 +562,22 @@ struct SimEntry<S> {
|
||||
/// 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<S: Clone> SimCache<S> {
|
||||
pub const fn new(cap: usize) -> SimCache<S> {
|
||||
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<S> {
|
||||
self.run_where(key, target, init, step, |_, _| true)
|
||||
}
|
||||
@@ -586,6 +595,7 @@ impl<S: Clone> SimCache<S> {
|
||||
valid: impl Fn(u64, &S) -> bool,
|
||||
) -> std::sync::Arc<S> {
|
||||
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<S: Clone> SimCache<S> {
|
||||
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<S: Clone> SimCache<S> {
|
||||
};
|
||||
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<u64> = 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<u64> = 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<u64> = 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<u64> = 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<u64> = 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]];
|
||||
|
||||
@@ -344,7 +344,9 @@ pub fn load_lut(src: &str) -> Option<Arc<Lut>> {
|
||||
{
|
||||
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();
|
||||
|
||||
@@ -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<String> {
|
||||
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)) {
|
||||
|
||||
@@ -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<String> {
|
||||
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}",
|
||||
|
||||
@@ -395,6 +395,9 @@ fn collect_files(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// 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<String, usize> = 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<Value> {
|
||||
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).
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
//! premultiplied): the composite as `R`, `G`, `B`, `A` plus one `<layer name>.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>) -> 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<String>, next: &mut HashMap<String, usize>) -> 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<CommandSpec> {
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
#[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<String> = ["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);
|
||||
}
|
||||
}
|
||||
@@ -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<Value> {
|
||||
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<String, std::result::Result<Footage, String>> = 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<Value> {
|
||||
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();
|
||||
|
||||
@@ -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<i64, String> {
|
||||
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(';');
|
||||
|
||||
@@ -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<String>, 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<String> {
|
||||
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<String, HashMap<RunKey, Vec<String>>>) -> Vec<String> {
|
||||
let Some(key) = run_key(path, alphabetical) else { return vec![] };
|
||||
let mut files: Vec<String> = 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<RunKey, Vec<String>> = 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<Str
|
||||
/// and (with the Sequence option, or [`SequenceOptions::folders`] for a folder's files) numbered
|
||||
/// stills of one run one [`Source::Sequence`].
|
||||
pub fn group(services: &dyn Services, paths: &[String], opts: SequenceOptions) -> Vec<Source> {
|
||||
// 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<String, HashMap<RunKey, Vec<String>>> = 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<String> =
|
||||
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<RunKey, Vec<String>> = 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<String> = 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<String>)> = 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<String>)> = vec![];
|
||||
let mut run_of: HashMap<RunKey, usize> = HashMap::new();
|
||||
let mut out: Vec<Option<Source>> = vec![];
|
||||
let mut slot: Vec<usize> = 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<Vec<u8>> {
|
||||
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<String> {
|
||||
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<String> = 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<Vec<u8>> {
|
||||
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<String> {
|
||||
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]
|
||||
|
||||
@@ -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<u8>)], dir: &str) -> Result<Vec<String>> {
|
||||
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/<item>/<file>` → 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<u8>)
|
||||
// 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}")))?;
|
||||
|
||||
@@ -477,35 +477,51 @@ pub fn split_at_images(doc: &Doc) -> Vec<Doc> {
|
||||
})
|
||||
.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<Node>) -> 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<Node>)> = 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<Option<effectcraft_svg::Group>> = 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<Sh
|
||||
}
|
||||
l
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use effectcraft_svg::{Geom, Group, Image, Shape};
|
||||
|
||||
fn shape(name: &str) -> 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>) -> Node {
|
||||
let mut g = Group::new(name);
|
||||
g.children = children;
|
||||
Node::Group(g)
|
||||
}
|
||||
fn names(g: &Group, out: &mut Vec<String>) {
|
||||
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<Vec<String>> = segs
|
||||
.iter()
|
||||
.map(|d| {
|
||||
let mut v = vec![];
|
||||
names(&d.root, &mut v);
|
||||
v
|
||||
})
|
||||
.collect();
|
||||
let want: Vec<Vec<&str>> =
|
||||
vec![vec!["a"], vec!["<g1", "b", "g1>"], vec!["<g1", "c", "<g2", "d", "g2>", "g1>"], vec!["<g1", "<g2", "e", "g2>", "g1>"], vec![], vec!["f"]];
|
||||
assert_eq!(got, want);
|
||||
assert!(segs.iter().all(|d| d.root.name == "root" && d.width == 10.0));
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
@@ -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<f64> = 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<usize> = (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::<f64>()
|
||||
};
|
||||
let rect = |x: f64, y: f64, w: f64, h: f64| PathSeg::R { x, y, w, h };
|
||||
let rects: Vec<PathSeg> = (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<PathSeg> = (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<PathSeg> = (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));
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -125,7 +125,13 @@ fn utc_parts(secs: u64, nanos: u32) -> (i64, chrono::NaiveDateTime) {
|
||||
|
||||
/// One log line (without the newline): `<timestamp> <LEVEL> [<thread>] <target>: <message>`.
|
||||
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"));
|
||||
}
|
||||
}
|
||||
@@ -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"]);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<Vec<f64>>);
|
||||
impl crate::render::FootageSource for Vector {
|
||||
fn frame(&self, _: ItemId, _: &Footage, _: Tick) -> Option<Arc<Image>> {
|
||||
panic!("the full-size frame was decoded");
|
||||
}
|
||||
fn vector_frame(&self, _: ItemId, f: &Footage, scale: f64) -> Option<Arc<Image>> {
|
||||
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<Arc<Image>> {
|
||||
None
|
||||
}
|
||||
fn vector_frame(&self, _: ItemId, _: &Footage, _: f64) -> Option<Arc<Image>> {
|
||||
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);
|
||||
}
|
||||
@@ -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 }
|
||||
+26
-26
@@ -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<H264> {
|
||||
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<filmcraft_h264enc::Packet>) -> Vec<Packet> {
|
||||
fn packets(&self, ps: Vec<craft_video::h264::Packet>) -> Vec<Packet> {
|
||||
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<Vec<Packet>> {
|
||||
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<Vec<Packet>> {
|
||||
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<filmcraft_aac::Encoder>),
|
||||
Aac(Box<craft_audio::aac::Encoder>),
|
||||
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))
|
||||
|
||||
@@ -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<Vec<Packet>> {
|
||||
@@ -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<Vec<Packet>> {
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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<usize>,
|
||||
/// 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<u64> + Sync)>,
|
||||
}
|
||||
|
||||
/// The layer caches of a Render Queue run (see [`JobOptions::caches`]): one per compositor,
|
||||
|
||||
@@ -137,6 +137,17 @@ pub(crate) fn cpu_inflight(var: Option<&str>) -> Option<usize> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<u64>, free: Option<u64>, threads: usize) -> Option<usize> {
|
||||
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<Option<(usize, String)>>,
|
||||
/// 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<Option<Option<usize>>>,
|
||||
/// (output frame, seconds) of every rendered frame (Plus Per Frame Info logs).
|
||||
pub frame_times: Option<Mutex<Vec<(u64, f64)>>>,
|
||||
/// 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<usize> {
|
||||
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<usize> {
|
||||
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<u64>| 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);
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
|
||||
})
|
||||
},
|
||||
|
||||
@@ -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<Vec<Pic>> {
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
+5
-5
@@ -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<u32> {
|
||||
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");
|
||||
@@ -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;
|
||||
+5
-5
@@ -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<Pic> = (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");
|
||||
@@ -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!(
|
||||
@@ -1,10 +1,12 @@
|
||||
//! The crate's integration tests, one binary (each module is a former `tests/<name>.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;
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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<ColorSpace>, [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");
|
||||
|
||||
@@ -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<u32> {
|
||||
/// 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<u8> = (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());
|
||||
|
||||
@@ -20,9 +20,9 @@ fn encode(pcm: &[f32], channels: u8, bitrate: u32) -> (OpusEncoder, Vec<Vec<u8>>
|
||||
(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<u8>], len: usize) -> Vec<Vec<f32>> {
|
||||
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<f32>> = 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"])
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<Vec<u8>> {
|
||||
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.
|
||||
|
||||
@@ -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<u8> {
|
||||
@@ -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<u8>], recon: &[[Vec<u8>; 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")
|
||||
|
||||
@@ -133,7 +133,7 @@ fn radial_fast(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
}
|
||||
|
||||
fn camera_lens(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 }
|
||||
@@ -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.
|
||||
@@ -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<u8>,
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<u16>; 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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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::<u32>() + r.left[1..=n].iter().map(|&v| v as u32).sum::<u32>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<u8>,
|
||||
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<u8>,
|
||||
/// 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<f64>; 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<u8>; 3],
|
||||
frames: u64,
|
||||
/// POC of the previous picture (relative to the last IDR).
|
||||
poc: u32,
|
||||
reference: Option<[Vec<u16>; 3]>,
|
||||
recon: Option<[Vec<u16>; 3]>,
|
||||
rc: Option<RateCtl>,
|
||||
}
|
||||
|
||||
impl Encoder {
|
||||
pub fn new(cfg: EncoderConfig) -> Result<Encoder, Error> {
|
||||
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<u8>; 3] {
|
||||
self.sets.clone()
|
||||
}
|
||||
|
||||
/// HEVCDecoderConfigurationRecord (`hvcC` payload) with the parameter sets.
|
||||
pub fn hvcc(&self) -> Vec<u8> {
|
||||
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<u16>; 3]> {
|
||||
self.recon.as_ref()
|
||||
}
|
||||
|
||||
/// Copy the input into coded-size planes, replicating the right / bottom edges.
|
||||
fn pad_input(&self, f: &Frame) -> [Vec<u16>; 3] {
|
||||
let max = (1u32 << self.seq.bit_depth) - 1;
|
||||
let mut out: [Vec<u16>; 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<u16> { (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 }
|
||||
}
|
||||
}
|
||||
@@ -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<u16>; 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<u16> {
|
||||
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<u16> {
|
||||
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::<u32>();
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<u32>) -> 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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8>; 3]) -> Vec<u8> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<const L: usize>(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<const L: usize>(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];
|
||||
@@ -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<i32> = (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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<u64> {
|
||||
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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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<ItemId>) -> Option<fp::BinId> {
|
||||
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<ItemId> = 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<VideoSp
|
||||
fn media_clip(media: fp::MediaRef, info: MediaInfo) -> 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);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 197 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user