Add media foundation crates: time, decode, and render.
Introduce exact rational time, a portable decode boundary with a fake driver, and wgpu offscreen rendering so a future editor core can describe and execute visual composition without platform decode drivers yet.
This commit is contained in:
parent
ded03d609c
commit
d5fe49ca8d
|
|
@ -60,6 +60,15 @@ dependencies = [
|
|||
"equator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.104"
|
||||
|
|
@ -528,6 +537,9 @@ name = "bitflags"
|
|||
version = "2.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitstream-io"
|
||||
|
|
@ -1108,7 +1120,7 @@ dependencies = [
|
|||
"core-graphics2",
|
||||
"io-surface",
|
||||
"libc",
|
||||
"metal",
|
||||
"metal 0.29.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1252,6 +1264,16 @@ version = "0.3.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
|
||||
|
||||
[[package]]
|
||||
name = "decode"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"decode",
|
||||
"thiserror 2.0.19",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deflate64"
|
||||
version = "0.1.12"
|
||||
|
|
@ -1370,6 +1392,15 @@ dependencies = [
|
|||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "document-features"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
|
||||
dependencies = [
|
||||
"litrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "downcast-rs"
|
||||
version = "1.2.1"
|
||||
|
|
@ -1978,6 +2009,17 @@ dependencies = [
|
|||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gl_generator"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d"
|
||||
dependencies = [
|
||||
"khronos_api",
|
||||
"log",
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.4"
|
||||
|
|
@ -2021,6 +2063,15 @@ dependencies = [
|
|||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glutin_wgl_sys"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e"
|
||||
dependencies = [
|
||||
"gl_generator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpu-alloc"
|
||||
version = "0.6.2"
|
||||
|
|
@ -2051,6 +2102,38 @@ dependencies = [
|
|||
"bitflags 2.13.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpu-allocator"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd"
|
||||
dependencies = [
|
||||
"log",
|
||||
"presser",
|
||||
"thiserror 1.0.69",
|
||||
"windows 0.57.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpu-descriptor"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"gpu-descriptor-types",
|
||||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpu-descriptor-types"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpui"
|
||||
version = "0.2.2"
|
||||
|
|
@ -2101,7 +2184,7 @@ dependencies = [
|
|||
"libc",
|
||||
"log",
|
||||
"lyon",
|
||||
"metal",
|
||||
"metal 0.29.0",
|
||||
"naga",
|
||||
"num_cpus",
|
||||
"objc",
|
||||
|
|
@ -2218,7 +2301,7 @@ dependencies = [
|
|||
"core-video",
|
||||
"ctor",
|
||||
"foreign-types",
|
||||
"metal",
|
||||
"metal 0.29.0",
|
||||
"objc",
|
||||
]
|
||||
|
||||
|
|
@ -2802,6 +2885,34 @@ version = "1.0.18"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
|
||||
dependencies = [
|
||||
"jni-sys 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
|
||||
dependencies = [
|
||||
"jni-sys-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys-macros"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.35"
|
||||
|
|
@ -2831,8 +2942,15 @@ checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76"
|
|||
dependencies = [
|
||||
"libc",
|
||||
"libloading",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "khronos_api"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
||||
|
||||
[[package]]
|
||||
name = "kurbo"
|
||||
version = "0.11.3"
|
||||
|
|
@ -2942,6 +3060,12 @@ version = "0.8.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||
|
||||
[[package]]
|
||||
name = "litrs"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
|
|
@ -3102,6 +3226,21 @@ dependencies = [
|
|||
"paste",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "metal"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"block",
|
||||
"core-graphics-types 0.1.3",
|
||||
"foreign-types",
|
||||
"log",
|
||||
"objc",
|
||||
"paste",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
|
|
@ -3195,6 +3334,15 @@ dependencies = [
|
|||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk-sys"
|
||||
version = "0.5.0+25.2.9519653"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691"
|
||||
dependencies = [
|
||||
"jni-sys 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "new_debug_unreachable"
|
||||
version = "1.0.6"
|
||||
|
|
@ -3586,6 +3734,15 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
|
|
@ -3773,6 +3930,18 @@ version = "0.2.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7"
|
||||
|
||||
[[package]]
|
||||
name = "pollster"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3"
|
||||
|
||||
[[package]]
|
||||
name = "postage"
|
||||
version = "0.5.0"
|
||||
|
|
@ -3785,7 +3954,7 @@ dependencies = [
|
|||
"log",
|
||||
"parking_lot",
|
||||
"pin-project",
|
||||
"pollster",
|
||||
"pollster 0.2.5",
|
||||
"static_assertions",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
|
@ -3808,6 +3977,12 @@ dependencies = [
|
|||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "presser"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
|
|
@ -4111,6 +4286,12 @@ dependencies = [
|
|||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "range-alloc"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08"
|
||||
|
||||
[[package]]
|
||||
name = "rangemap"
|
||||
version = "1.7.1"
|
||||
|
|
@ -4309,6 +4490,24 @@ version = "0.8.11"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "render"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"decode",
|
||||
"pollster 0.4.0",
|
||||
"thiserror 2.0.19",
|
||||
"time",
|
||||
"wgpu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "renderdoc-sys"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
|
||||
|
||||
[[package]]
|
||||
name = "resvg"
|
||||
version = "0.45.1"
|
||||
|
|
@ -5361,6 +5560,15 @@ dependencies = [
|
|||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.19",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-keccak"
|
||||
version = "2.0.2"
|
||||
|
|
@ -6124,6 +6332,153 @@ version = "0.1.12"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||
|
||||
[[package]]
|
||||
name = "wgpu"
|
||||
version = "25.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec8fb398f119472be4d80bc3647339f56eb63b2a331f6a3d16e25d8144197dd9"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bitflags 2.13.1",
|
||||
"cfg_aliases",
|
||||
"document-features",
|
||||
"hashbrown 0.15.5",
|
||||
"js-sys",
|
||||
"log",
|
||||
"naga",
|
||||
"parking_lot",
|
||||
"portable-atomic",
|
||||
"profiling",
|
||||
"raw-window-handle",
|
||||
"smallvec",
|
||||
"static_assertions",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"wgpu-core",
|
||||
"wgpu-hal",
|
||||
"wgpu-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wgpu-core"
|
||||
version = "25.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7b882196f8368511d613c6aeec80655160db6646aebddf8328879a88d54e500"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bit-set",
|
||||
"bit-vec",
|
||||
"bitflags 2.13.1",
|
||||
"cfg_aliases",
|
||||
"document-features",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"log",
|
||||
"naga",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"portable-atomic",
|
||||
"profiling",
|
||||
"raw-window-handle",
|
||||
"rustc-hash 1.1.0",
|
||||
"smallvec",
|
||||
"thiserror 2.0.19",
|
||||
"wgpu-core-deps-apple",
|
||||
"wgpu-core-deps-emscripten",
|
||||
"wgpu-core-deps-windows-linux-android",
|
||||
"wgpu-hal",
|
||||
"wgpu-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wgpu-core-deps-apple"
|
||||
version = "25.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfd488b3239b6b7b185c3b045c39ca6bf8af34467a4c5de4e0b1a564135d093d"
|
||||
dependencies = [
|
||||
"wgpu-hal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wgpu-core-deps-emscripten"
|
||||
version = "25.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f09ad7aceb3818e52539acc679f049d3475775586f3f4e311c30165cf2c00445"
|
||||
dependencies = [
|
||||
"wgpu-hal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wgpu-core-deps-windows-linux-android"
|
||||
version = "25.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cba5fb5f7f9c98baa7c889d444f63ace25574833df56f5b817985f641af58e46"
|
||||
dependencies = [
|
||||
"wgpu-hal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wgpu-hal"
|
||||
version = "25.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f968767fe4d3d33747bbd1473ccd55bf0f6451f55d733b5597e67b5deab4ad17"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"arrayvec",
|
||||
"ash",
|
||||
"bit-set",
|
||||
"bitflags 2.13.1",
|
||||
"block",
|
||||
"bytemuck",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"core-graphics-types 0.1.3",
|
||||
"glow",
|
||||
"glutin_wgl_sys",
|
||||
"gpu-alloc",
|
||||
"gpu-allocator",
|
||||
"gpu-descriptor",
|
||||
"hashbrown 0.15.5",
|
||||
"js-sys",
|
||||
"khronos-egl",
|
||||
"libc",
|
||||
"libloading",
|
||||
"log",
|
||||
"metal 0.31.0",
|
||||
"naga",
|
||||
"ndk-sys",
|
||||
"objc",
|
||||
"ordered-float",
|
||||
"parking_lot",
|
||||
"portable-atomic",
|
||||
"profiling",
|
||||
"range-alloc",
|
||||
"raw-window-handle",
|
||||
"renderdoc-sys",
|
||||
"smallvec",
|
||||
"thiserror 2.0.19",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
"wgpu-types",
|
||||
"windows 0.58.0",
|
||||
"windows-core 0.58.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wgpu-types"
|
||||
version = "25.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2aa49460c2a8ee8edba3fca54325540d904dd85b2e086ada762767e17d06e8bc"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"bytemuck",
|
||||
"js-sys",
|
||||
"log",
|
||||
"thiserror 2.0.19",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "6.0.3"
|
||||
|
|
@ -6177,6 +6532,16 @@ dependencies = [
|
|||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
|
||||
dependencies = [
|
||||
"windows-core 0.58.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.61.3"
|
||||
|
|
@ -6224,6 +6589,19 @@ dependencies = [
|
|||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
|
||||
dependencies = [
|
||||
"windows-implement 0.58.0",
|
||||
"windows-interface 0.58.0",
|
||||
"windows-result 0.2.0",
|
||||
"windows-strings 0.1.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.2"
|
||||
|
|
@ -6259,6 +6637,17 @@ dependencies = [
|
|||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
|
|
@ -6281,6 +6670,17 @@ dependencies = [
|
|||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
|
|
@ -6345,6 +6745,15 @@ dependencies = [
|
|||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
|
|
@ -6354,6 +6763,16 @@ dependencies = [
|
|||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
||||
dependencies = [
|
||||
"windows-result 0.2.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.3.1"
|
||||
|
|
@ -6761,6 +7180,12 @@ version = "0.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
||||
|
||||
[[package]]
|
||||
name = "xml-rs"
|
||||
version = "0.8.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
|
||||
|
||||
[[package]]
|
||||
name = "xmlwriter"
|
||||
version = "0.1.0"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
resolver = "3"
|
||||
members = [
|
||||
'apps/desktop',
|
||||
# 'crates/*',
|
||||
'crates/*',
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
|
@ -12,3 +12,9 @@ license = "MIT"
|
|||
|
||||
[workspace.dependencies]
|
||||
gpui = "0.2.2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
thiserror = "2"
|
||||
bytemuck = { version = "1", features = ["derive"] }
|
||||
wgpu = "25.0.2"
|
||||
time = { path = "crates/time" }
|
||||
decode = { path = "crates/decode" }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "decode"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
fake = []
|
||||
|
||||
[dependencies]
|
||||
time = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
bytemuck = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
decode = { path = ".", features = ["fake"] }
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
use crate::{Frame, Source, SourceId, SourceStream, SourceStreamId};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error, PartialEq, Eq)]
|
||||
pub enum DecodeError {
|
||||
#[error("source not found")]
|
||||
SourceNotFound,
|
||||
#[error("stream not found")]
|
||||
StreamNotFound,
|
||||
#[error("unsupported format")]
|
||||
UnsupportedFormat,
|
||||
#[error("decoding failed: {0}")]
|
||||
DecodingFailed(String),
|
||||
#[error("end of stream")]
|
||||
EndOfStream,
|
||||
}
|
||||
|
||||
pub trait Decoder {
|
||||
fn open(&mut self, source: &Source) -> Result<SourceId, DecodeError>;
|
||||
fn streams(&self, source: SourceId) -> Result<Vec<SourceStream>, DecodeError>;
|
||||
fn read_frame(
|
||||
&mut self,
|
||||
source: SourceId,
|
||||
stream: SourceStreamId,
|
||||
time: time::RationalTime,
|
||||
) -> Result<Frame, DecodeError>;
|
||||
fn close(&mut self, source: SourceId) -> Result<(), DecodeError>;
|
||||
}
|
||||
|
|
@ -0,0 +1,260 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
DecodeError, Decoder, Frame, PixelFormat, Source, SourceId, SourceStream, SourceStreamId,
|
||||
};
|
||||
use time::{FrameRate, RationalTime};
|
||||
|
||||
/// Configuration applied to each source opened by [`FakeDecoder`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FakeDecoderConfig {
|
||||
pub frame_rate: FrameRate,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub duration: RationalTime,
|
||||
pub format: PixelFormat,
|
||||
pub color: [u8; 4],
|
||||
}
|
||||
|
||||
impl Default for FakeDecoderConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
frame_rate: FrameRate::new(30, 1).expect("valid default frame rate"),
|
||||
width: 64,
|
||||
height: 64,
|
||||
duration: RationalTime::new(1, 1).expect("valid default duration"),
|
||||
format: PixelFormat::Rgba8,
|
||||
color: [255, 0, 0, 255],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct OpenSource {
|
||||
stream: SourceStream,
|
||||
config: FakeDecoderConfig,
|
||||
}
|
||||
|
||||
pub struct FakeDecoder {
|
||||
next_id: u64,
|
||||
default_config: FakeDecoderConfig,
|
||||
sources: HashMap<u64, OpenSource>,
|
||||
}
|
||||
|
||||
impl FakeDecoder {
|
||||
pub fn new(config: FakeDecoderConfig) -> Self {
|
||||
Self {
|
||||
next_id: 1,
|
||||
default_config: config,
|
||||
sources: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn lookup(&self, source: SourceId) -> Result<&OpenSource, DecodeError> {
|
||||
self.sources
|
||||
.get(&source.raw())
|
||||
.ok_or(DecodeError::SourceNotFound)
|
||||
}
|
||||
|
||||
fn lookup_mut(&mut self, source: SourceId) -> Result<&mut OpenSource, DecodeError> {
|
||||
self.sources
|
||||
.get_mut(&source.raw())
|
||||
.ok_or(DecodeError::SourceNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder for FakeDecoder {
|
||||
fn open(&mut self, _source: &Source) -> Result<SourceId, DecodeError> {
|
||||
let id = SourceId::new(self.next_id);
|
||||
self.next_id += 1;
|
||||
|
||||
let config = self.default_config.clone();
|
||||
let stream = SourceStream {
|
||||
id: SourceStreamId::new(1),
|
||||
frame_rate: config.frame_rate,
|
||||
width: config.width,
|
||||
height: config.height,
|
||||
duration: config.duration,
|
||||
};
|
||||
|
||||
self.sources.insert(
|
||||
id.raw(),
|
||||
OpenSource {
|
||||
stream,
|
||||
config,
|
||||
},
|
||||
);
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
fn streams(&self, source: SourceId) -> Result<Vec<SourceStream>, DecodeError> {
|
||||
Ok(vec![self.lookup(source)?.stream.clone()])
|
||||
}
|
||||
|
||||
fn read_frame(
|
||||
&mut self,
|
||||
source: SourceId,
|
||||
stream: SourceStreamId,
|
||||
time: RationalTime,
|
||||
) -> Result<Frame, DecodeError> {
|
||||
let open = self.lookup_mut(source)?;
|
||||
if open.stream.id != stream {
|
||||
return Err(DecodeError::StreamNotFound);
|
||||
}
|
||||
|
||||
if !time.lt(&open.stream.duration) {
|
||||
return Err(DecodeError::EndOfStream);
|
||||
}
|
||||
|
||||
let frame_index = open
|
||||
.stream
|
||||
.frame_rate
|
||||
.frame_index_floor(time)
|
||||
.map_err(|_| DecodeError::DecodingFailed("frame index overflow".into()))?;
|
||||
let frame_time = open
|
||||
.stream
|
||||
.frame_rate
|
||||
.frame_start(frame_index)
|
||||
.map_err(|_| DecodeError::DecodingFailed("frame time overflow".into()))?;
|
||||
|
||||
let len = Frame::expected_byte_len(open.stream.width, open.stream.height, open.config.format);
|
||||
let mut pixels = vec![0u8; len].into_boxed_slice();
|
||||
fill_solid(&mut pixels, open.config.format, open.config.color);
|
||||
|
||||
Ok(Frame {
|
||||
source,
|
||||
stream,
|
||||
time: frame_time,
|
||||
width: open.stream.width,
|
||||
height: open.stream.height,
|
||||
format: open.config.format,
|
||||
pixels,
|
||||
})
|
||||
}
|
||||
|
||||
fn close(&mut self, source: SourceId) -> Result<(), DecodeError> {
|
||||
self.sources
|
||||
.remove(&source.raw())
|
||||
.map(|_| ())
|
||||
.ok_or(DecodeError::SourceNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
fn fill_solid(pixels: &mut [u8], format: PixelFormat, color: [u8; 4]) {
|
||||
let (r, g, b, a) = (color[0], color[1], color[2], color[3]);
|
||||
match format {
|
||||
PixelFormat::Rgba8 => {
|
||||
for chunk in pixels.chunks_exact_mut(4) {
|
||||
chunk.copy_from_slice(&[r, g, b, a]);
|
||||
}
|
||||
}
|
||||
PixelFormat::Bgra8 => {
|
||||
for chunk in pixels.chunks_exact_mut(4) {
|
||||
chunk.copy_from_slice(&[b, g, r, a]);
|
||||
}
|
||||
}
|
||||
PixelFormat::Rgb8 => {
|
||||
for chunk in pixels.chunks_exact_mut(3) {
|
||||
chunk.copy_from_slice(&[r, g, b]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn decoder() -> FakeDecoder {
|
||||
FakeDecoder::new(FakeDecoderConfig {
|
||||
frame_rate: FrameRate::new(30, 1).unwrap(),
|
||||
width: 4,
|
||||
height: 4,
|
||||
duration: RationalTime::new(1, 1).unwrap(),
|
||||
format: PixelFormat::Rgba8,
|
||||
color: [10, 20, 30, 255],
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_frame_time_is_floor_of_requested_time() {
|
||||
let mut decoder = decoder();
|
||||
let source = decoder
|
||||
.open(&Source::Bytes(Arc::from([0u8; 0])))
|
||||
.unwrap();
|
||||
let stream = decoder.streams(source).unwrap()[0].id;
|
||||
let requested = RationalTime::new(1, 30).unwrap();
|
||||
let frame = decoder.read_frame(source, stream, requested).unwrap();
|
||||
assert_eq!(frame.time, RationalTime::new(1, 30).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn streams_report_declared_metadata() {
|
||||
let mut decoder = decoder();
|
||||
let source = decoder
|
||||
.open(&Source::Bytes(Arc::from([0u8; 0])))
|
||||
.unwrap();
|
||||
let streams = decoder.streams(source).unwrap();
|
||||
assert_eq!(streams.len(), 1);
|
||||
assert_eq!(streams[0].width, 4);
|
||||
assert_eq!(streams[0].height, 4);
|
||||
assert_eq!(streams[0].frame_rate, FrameRate::new(30, 1).unwrap());
|
||||
assert_eq!(streams[0].duration, RationalTime::new(1, 1).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_source_errors() {
|
||||
let mut decoder = decoder();
|
||||
let missing = SourceId::new(999);
|
||||
assert_eq!(decoder.streams(missing), Err(DecodeError::SourceNotFound));
|
||||
assert_eq!(
|
||||
decoder.read_frame(missing, SourceStreamId::new(1), RationalTime::new(0, 1).unwrap()),
|
||||
Err(DecodeError::SourceNotFound)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_stream_errors() {
|
||||
let mut decoder = decoder();
|
||||
let source = decoder
|
||||
.open(&Source::Bytes(Arc::from([0u8; 0])))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
decoder.read_frame(
|
||||
source,
|
||||
SourceStreamId::new(99),
|
||||
RationalTime::new(0, 1).unwrap()
|
||||
),
|
||||
Err(DecodeError::StreamNotFound)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_frame_after_close_errors() {
|
||||
let mut decoder = decoder();
|
||||
let source = decoder
|
||||
.open(&Source::Bytes(Arc::from([0u8; 0])))
|
||||
.unwrap();
|
||||
let stream = decoder.streams(source).unwrap()[0].id;
|
||||
decoder.close(source).unwrap();
|
||||
assert_eq!(
|
||||
decoder.read_frame(source, stream, RationalTime::new(0, 1).unwrap()),
|
||||
Err(DecodeError::SourceNotFound)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_frame_at_or_past_duration_errors() {
|
||||
let mut decoder = decoder();
|
||||
let source = decoder
|
||||
.open(&Source::Bytes(Arc::from([0u8; 0])))
|
||||
.unwrap();
|
||||
let stream = decoder.streams(source).unwrap()[0].id;
|
||||
let duration = RationalTime::new(1, 1).unwrap();
|
||||
assert_eq!(
|
||||
decoder.read_frame(source, stream, duration),
|
||||
Err(DecodeError::EndOfStream)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
use crate::{SourceId, SourceStreamId};
|
||||
|
||||
/// Pixel layout the decoder can hand to the renderer.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PixelFormat {
|
||||
Rgba8,
|
||||
Bgra8,
|
||||
Rgb8,
|
||||
}
|
||||
|
||||
/// A decoded video frame.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Frame {
|
||||
pub source: SourceId,
|
||||
pub stream: SourceStreamId,
|
||||
pub time: time::RationalTime,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub format: PixelFormat,
|
||||
pub pixels: Box<[u8]>,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
pub fn expected_byte_len(width: u32, height: u32, format: PixelFormat) -> usize {
|
||||
let bpp = match format {
|
||||
PixelFormat::Rgba8 | PixelFormat::Bgra8 => 4,
|
||||
PixelFormat::Rgb8 => 3,
|
||||
};
|
||||
width as usize * height as usize * bpp
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
//! Portable media decode boundary: sources, frames, and the decoder driver socket.
|
||||
|
||||
mod driver;
|
||||
mod frame;
|
||||
mod source;
|
||||
|
||||
#[cfg(feature = "fake")]
|
||||
mod fake;
|
||||
|
||||
pub use driver::{DecodeError, Decoder};
|
||||
pub use frame::{Frame, PixelFormat};
|
||||
pub use source::{Source, SourceId, SourceStream, SourceStreamId};
|
||||
|
||||
#[cfg(feature = "fake")]
|
||||
pub use fake::{FakeDecoder, FakeDecoderConfig};
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A decodable media input.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Source {
|
||||
Path(PathBuf),
|
||||
Bytes(Arc<[u8]>),
|
||||
}
|
||||
|
||||
/// Opaque id assigned when a source is opened.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct SourceId(u64);
|
||||
|
||||
impl SourceId {
|
||||
pub fn new(id: u64) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
|
||||
pub fn raw(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Opaque id for one stream within a source.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct SourceStreamId(u64);
|
||||
|
||||
impl SourceStreamId {
|
||||
pub fn new(id: u64) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
|
||||
pub fn raw(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// One video stream of a decoded source.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SourceStream {
|
||||
pub id: SourceStreamId,
|
||||
pub frame_rate: time::FrameRate,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub duration: time::RationalTime,
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
[package]
|
||||
name = "render"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
wgpu-tests = []
|
||||
|
||||
[dependencies]
|
||||
time = { workspace = true }
|
||||
decode = { workspace = true }
|
||||
wgpu = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
bytemuck = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
decode = { workspace = true, features = ["fake"] }
|
||||
pollster = "0.4"
|
||||
|
||||
[[test]]
|
||||
name = "compose"
|
||||
required-features = ["wgpu-tests"]
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
use crate::plan::{Affine, Blend, Crop, GpuError, Opacity, Output, OutputFormat, OutputTexture, RenderError};
|
||||
use crate::pixels::output_format_to_wgpu;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Layer {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub pixels: Vec<u8>,
|
||||
pub crop: Crop,
|
||||
pub transform: Affine,
|
||||
pub opacity: Opacity,
|
||||
pub blend: Blend,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Composer;
|
||||
|
||||
impl Composer {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn compose(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
output: &Output,
|
||||
layers: &[Layer],
|
||||
) -> Result<OutputTexture, RenderError> {
|
||||
let mut buffer = vec![0u8; output_pixel_len(output)];
|
||||
|
||||
for layer in layers {
|
||||
composite_layer(&mut buffer, output, layer);
|
||||
}
|
||||
|
||||
reorder_for_output_format(&mut buffer, output.format);
|
||||
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("render_output"),
|
||||
size: wgpu::Extent3d {
|
||||
width: output.width,
|
||||
height: output.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: output_format_to_wgpu(output.format),
|
||||
usage: wgpu::TextureUsages::COPY_SRC
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING
|
||||
| wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
});
|
||||
|
||||
queue.write_texture(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &texture,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
&buffer,
|
||||
wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(output.width * 4),
|
||||
rows_per_image: Some(output.height),
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width: output.width,
|
||||
height: output.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
|
||||
Ok(OutputTexture {
|
||||
texture,
|
||||
output: *output,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn output_pixel_len(output: &Output) -> usize {
|
||||
output.width as usize * output.height as usize * 4
|
||||
}
|
||||
|
||||
fn reorder_for_output_format(buffer: &mut [u8], format: OutputFormat) {
|
||||
if matches!(format, OutputFormat::Rgba8Premultiplied) {
|
||||
return;
|
||||
}
|
||||
for px in buffer.chunks_exact_mut(4) {
|
||||
px.swap(0, 2);
|
||||
}
|
||||
}
|
||||
|
||||
fn composite_layer(dst: &mut [u8], output: &Output, layer: &Layer) {
|
||||
let out_w = output.width as i32;
|
||||
let out_h = output.height as i32;
|
||||
let inv = match invert_affine(layer.transform) {
|
||||
Ok(v) => v,
|
||||
Err(()) => return,
|
||||
};
|
||||
|
||||
for oy in 0..out_h {
|
||||
for ox in 0..out_w {
|
||||
let (sx, sy) = apply_affine(inv, ox as f32, oy as f32);
|
||||
if sx < layer.crop.x as f32
|
||||
|| sy < layer.crop.y as f32
|
||||
|| sx >= (layer.crop.x + layer.crop.w) as f32
|
||||
|| sy >= (layer.crop.y + layer.crop.h) as f32
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let src_x = sx.floor() as u32;
|
||||
let src_y = sy.floor() as u32;
|
||||
if src_x >= layer.width || src_y >= layer.height {
|
||||
continue;
|
||||
}
|
||||
|
||||
let src = sample_premul(&layer.pixels, layer.width, src_x, src_y);
|
||||
let src = scale_premul(src, layer.opacity.value());
|
||||
|
||||
let dst_idx = ((oy * out_w + ox) * 4) as usize;
|
||||
let dst_px = [
|
||||
dst[dst_idx],
|
||||
dst[dst_idx + 1],
|
||||
dst[dst_idx + 2],
|
||||
dst[dst_idx + 3],
|
||||
];
|
||||
|
||||
let out = match layer.blend {
|
||||
Blend::SourceOver => source_over_premul(dst_px, src),
|
||||
};
|
||||
|
||||
dst[dst_idx..dst_idx + 4].copy_from_slice(&out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_affine(affine: Affine, x: f32, y: f32) -> (f32, f32) {
|
||||
let [a, b, c, d, e, f] = affine.m;
|
||||
(a * x + c * y + e, b * x + d * y + f)
|
||||
}
|
||||
|
||||
fn invert_affine(affine: Affine) -> Result<Affine, ()> {
|
||||
let [a, b, c, d, e, f] = affine.m;
|
||||
let det = a * d - b * c;
|
||||
if det.abs() < f32::EPSILON {
|
||||
return Err(());
|
||||
}
|
||||
let inv_det = 1.0 / det;
|
||||
let ia = d * inv_det;
|
||||
let ib = -b * inv_det;
|
||||
let ic = -c * inv_det;
|
||||
let id = a * inv_det;
|
||||
let ie = -(ia * e + ic * f);
|
||||
let iff = -(ib * e + id * f);
|
||||
Ok(Affine {
|
||||
m: [ia, ib, ic, id, ie, iff],
|
||||
})
|
||||
}
|
||||
|
||||
fn sample_premul(pixels: &[u8], width: u32, x: u32, y: u32) -> [u8; 4] {
|
||||
let idx = ((y * width + x) * 4) as usize;
|
||||
[
|
||||
pixels[idx],
|
||||
pixels[idx + 1],
|
||||
pixels[idx + 2],
|
||||
pixels[idx + 3],
|
||||
]
|
||||
}
|
||||
|
||||
fn scale_premul(px: [u8; 4], opacity: f32) -> [u8; 4] {
|
||||
[
|
||||
(px[0] as f32 * opacity).round() as u8,
|
||||
(px[1] as f32 * opacity).round() as u8,
|
||||
(px[2] as f32 * opacity).round() as u8,
|
||||
(px[3] as f32 * opacity).round() as u8,
|
||||
]
|
||||
}
|
||||
|
||||
fn source_over_premul(dst: [u8; 4], src: [u8; 4]) -> [u8; 4] {
|
||||
let src_a = src[3] as f32 / 255.0;
|
||||
let inv = 1.0 - src_a;
|
||||
[
|
||||
(src[0] as f32 + dst[0] as f32 * inv).round() as u8,
|
||||
(src[1] as f32 + dst[1] as f32 * inv).round() as u8,
|
||||
(src[2] as f32 + dst[2] as f32 * inv).round() as u8,
|
||||
(src[3] as f32 + dst[3] as f32 * inv).round().min(255.0) as u8,
|
||||
]
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn map_wgpu_err(_: wgpu::Error) -> RenderError {
|
||||
RenderError::Gpu(GpuError::Internal)
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
//! Product-neutral visual rendering against wgpu.
|
||||
|
||||
mod compose;
|
||||
mod pixels;
|
||||
mod plan;
|
||||
mod source;
|
||||
mod validate;
|
||||
|
||||
pub use plan::{
|
||||
Affine, Blend, Crop, GpuError, Node, NodeId, Opacity, OpacityError, Output, OutputFormat,
|
||||
OutputTexture, PlanError, RenderError, RenderPlan, SourceRef,
|
||||
};
|
||||
pub use validate::validate_plan;
|
||||
|
||||
use compose::Composer;
|
||||
use decode::{DecodeError, Decoder, Source, SourceId};
|
||||
use source::{FrameCache, SourceRegistry};
|
||||
|
||||
/// Offscreen renderer: validates plans, resolves frames through a decoder, composes into textures.
|
||||
pub struct Renderer<'a> {
|
||||
decoder: &'a mut dyn Decoder,
|
||||
device: &'a wgpu::Device,
|
||||
queue: &'a wgpu::Queue,
|
||||
registry: SourceRegistry,
|
||||
cache: FrameCache,
|
||||
composer: Composer,
|
||||
}
|
||||
|
||||
impl<'a> Renderer<'a> {
|
||||
pub fn new(
|
||||
decoder: &'a mut dyn Decoder,
|
||||
device: &'a wgpu::Device,
|
||||
queue: &'a wgpu::Queue,
|
||||
) -> Self {
|
||||
Self {
|
||||
decoder,
|
||||
device,
|
||||
queue,
|
||||
registry: SourceRegistry::new(),
|
||||
cache: FrameCache::new(),
|
||||
composer: Composer::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_source(&mut self, source: &Source) -> Result<SourceId, RenderError> {
|
||||
let id = self.decoder.open(source).map_err(RenderError::Decode)?;
|
||||
let streams = self
|
||||
.decoder
|
||||
.streams(id)
|
||||
.map_err(RenderError::Decode)?;
|
||||
self.registry.insert(id, streams);
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub fn release_source(&mut self, source: SourceId) -> Result<(), RenderError> {
|
||||
self.cache.flush_source(source);
|
||||
self.registry.remove(source);
|
||||
self.decoder.close(source).map_err(RenderError::Decode)
|
||||
}
|
||||
|
||||
pub fn render(&mut self, plan: &RenderPlan) -> Result<OutputTexture, RenderError> {
|
||||
validate::validate_for_render(plan, &self.registry)?;
|
||||
|
||||
let mut layers = Vec::with_capacity(plan.nodes.len());
|
||||
for node in &plan.nodes {
|
||||
if !self.registry.contains(node.source.source) {
|
||||
return Err(RenderError::UnknownSource);
|
||||
}
|
||||
|
||||
let streams = self
|
||||
.registry
|
||||
.streams(node.source.source)
|
||||
.ok_or(RenderError::UnknownSource)?;
|
||||
let stream = streams
|
||||
.iter()
|
||||
.find(|s| s.id == node.source.stream)
|
||||
.cloned()
|
||||
.ok_or(RenderError::UnknownStream)?;
|
||||
|
||||
let frame = self.resolve_frame(node, &stream)?;
|
||||
let premul = pixels::to_linear_premultiplied(&frame)?;
|
||||
layers.push(compose::Layer {
|
||||
width: frame.width,
|
||||
height: frame.height,
|
||||
pixels: premul,
|
||||
crop: node.crop,
|
||||
transform: node.transform,
|
||||
opacity: node.opacity,
|
||||
blend: node.blend,
|
||||
});
|
||||
}
|
||||
|
||||
self.composer.compose(
|
||||
self.device,
|
||||
self.queue,
|
||||
&plan.output,
|
||||
&layers,
|
||||
)
|
||||
}
|
||||
|
||||
fn resolve_frame(
|
||||
&mut self,
|
||||
node: &Node,
|
||||
stream: &decode::SourceStream,
|
||||
) -> Result<decode::Frame, RenderError> {
|
||||
let cache_key_time = stream
|
||||
.frame_rate
|
||||
.frame_index_floor(node.source.time)
|
||||
.map_err(|_| RenderError::Decode(DecodeError::DecodingFailed("time overflow".into())))?;
|
||||
let cache_key = stream
|
||||
.frame_rate
|
||||
.frame_start(cache_key_time)
|
||||
.map_err(|_| RenderError::Decode(DecodeError::DecodingFailed("time overflow".into())))?;
|
||||
|
||||
if let Some(frame) = self.cache.get(node.source.source, node.source.stream, cache_key) {
|
||||
return Ok(frame.clone());
|
||||
}
|
||||
|
||||
let frame = self
|
||||
.decoder
|
||||
.read_frame(node.source.source, node.source.stream, node.source.time)
|
||||
.map_err(RenderError::Decode)?;
|
||||
|
||||
self.cache.insert(
|
||||
node.source.source,
|
||||
node.source.stream,
|
||||
cache_key,
|
||||
frame.clone(),
|
||||
);
|
||||
Ok(frame)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
use decode::{Frame, PixelFormat};
|
||||
|
||||
use crate::plan::{GpuError, RenderError};
|
||||
|
||||
pub fn to_linear_premultiplied(frame: &Frame) -> Result<Vec<u8>, RenderError> {
|
||||
let len = frame.width as usize * frame.height as usize * 4;
|
||||
let mut out = vec![0u8; len];
|
||||
|
||||
match frame.format {
|
||||
PixelFormat::Rgba8 => rgba8_to_premul(&frame.pixels, &mut out),
|
||||
PixelFormat::Bgra8 => bgra8_to_premul(&frame.pixels, &mut out),
|
||||
PixelFormat::Rgb8 => rgb8_to_premul(&frame.pixels, &mut out),
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub(crate) fn output_format_to_wgpu(format: crate::plan::OutputFormat) -> wgpu::TextureFormat {
|
||||
match format {
|
||||
crate::plan::OutputFormat::Rgba8Premultiplied => wgpu::TextureFormat::Rgba8Unorm,
|
||||
crate::plan::OutputFormat::Bgra8Premultiplied => wgpu::TextureFormat::Bgra8Unorm,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "wgpu-tests"))]
|
||||
pub fn readback_rgba8(
|
||||
texture: &wgpu::Texture,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<Vec<u8>, RenderError> {
|
||||
let bytes_per_row = width * 4;
|
||||
let padded_bytes_per_row = align_to(bytes_per_row, 256);
|
||||
let buffer_size = padded_bytes_per_row as u64 * height as u64;
|
||||
|
||||
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("readback"),
|
||||
size: buffer_size,
|
||||
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("readback_encoder"),
|
||||
});
|
||||
encoder.copy_texture_to_buffer(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &buffer,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(padded_bytes_per_row),
|
||||
rows_per_image: Some(height),
|
||||
},
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
queue.submit(Some(encoder.finish()));
|
||||
|
||||
let slice = buffer.slice(..);
|
||||
slice.map_async(wgpu::MapMode::Read, |_| {});
|
||||
device.poll(wgpu::PollType::Wait).map_err(map_poll_error)?;
|
||||
|
||||
let data = slice.get_mapped_range();
|
||||
let mut pixels = vec![0u8; (width * height * 4) as usize];
|
||||
for row in 0..height as usize {
|
||||
let src_start = row * padded_bytes_per_row as usize;
|
||||
let dst_start = row * bytes_per_row as usize;
|
||||
pixels[dst_start..dst_start + bytes_per_row as usize]
|
||||
.copy_from_slice(&data[src_start..src_start + bytes_per_row as usize]);
|
||||
}
|
||||
drop(data);
|
||||
buffer.unmap();
|
||||
|
||||
Ok(pixels)
|
||||
}
|
||||
|
||||
fn rgba8_to_premul(src: &[u8], dst: &mut [u8]) {
|
||||
for (s, d) in src.chunks_exact(4).zip(dst.chunks_exact_mut(4)) {
|
||||
premul_pixel(s[0], s[1], s[2], s[3], d);
|
||||
}
|
||||
}
|
||||
|
||||
fn bgra8_to_premul(src: &[u8], dst: &mut [u8]) {
|
||||
for (s, d) in src.chunks_exact(4).zip(dst.chunks_exact_mut(4)) {
|
||||
premul_pixel(s[2], s[1], s[0], s[3], d);
|
||||
}
|
||||
}
|
||||
|
||||
fn rgb8_to_premul(src: &[u8], dst: &mut [u8]) {
|
||||
for (s, d) in src.chunks_exact(3).zip(dst.chunks_exact_mut(4)) {
|
||||
premul_pixel(s[0], s[1], s[2], 255, d);
|
||||
}
|
||||
}
|
||||
|
||||
fn premul_pixel(r: u8, g: u8, b: u8, a: u8, dst: &mut [u8]) {
|
||||
let af = a as f32 / 255.0;
|
||||
dst[0] = (r as f32 * af).round() as u8;
|
||||
dst[1] = (g as f32 * af).round() as u8;
|
||||
dst[2] = (b as f32 * af).round() as u8;
|
||||
dst[3] = a;
|
||||
}
|
||||
|
||||
fn align_to(value: u32, alignment: u32) -> u32 {
|
||||
((value + alignment - 1) / alignment) * alignment
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn map_poll_error(_err: wgpu::PollError) -> RenderError {
|
||||
RenderError::Gpu(GpuError::Internal)
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
use decode::{SourceId, SourceStreamId};
|
||||
use time::RationalTime;
|
||||
|
||||
/// An immutable, fully-resolved description of one composition.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct RenderPlan {
|
||||
pub output: Output,
|
||||
pub nodes: Vec<Node>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Output {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub format: OutputFormat,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum OutputFormat {
|
||||
Rgba8Premultiplied,
|
||||
Bgra8Premultiplied,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Node {
|
||||
pub id: NodeId,
|
||||
pub source: SourceRef,
|
||||
pub crop: Crop,
|
||||
pub transform: Affine,
|
||||
pub opacity: Opacity,
|
||||
pub blend: Blend,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct NodeId(u64);
|
||||
|
||||
impl NodeId {
|
||||
pub fn new(id: u64) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct SourceRef {
|
||||
pub source: SourceId,
|
||||
pub stream: SourceStreamId,
|
||||
pub time: RationalTime,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Crop {
|
||||
pub x: u32,
|
||||
pub y: u32,
|
||||
pub w: u32,
|
||||
pub h: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Affine {
|
||||
pub m: [f32; 6],
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Opacity(f32);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum OpacityError {
|
||||
OutOfRange(f32),
|
||||
}
|
||||
|
||||
impl Opacity {
|
||||
pub fn new(value: f32) -> Result<Self, OpacityError> {
|
||||
if !(0.0..=1.0).contains(&value) || !value.is_finite() {
|
||||
Err(OpacityError::OutOfRange(value))
|
||||
} else {
|
||||
Ok(Self(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn value(self) -> f32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Blend {
|
||||
SourceOver,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum RenderError {
|
||||
InvalidPlan(PlanError),
|
||||
UnknownSource,
|
||||
UnknownStream,
|
||||
UnsupportedPixelFormat,
|
||||
Decode(decode::DecodeError),
|
||||
Gpu(GpuError),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum PlanError {
|
||||
DanglingSourceRef,
|
||||
DuplicateNodeId,
|
||||
DimensionZero,
|
||||
OutOfRangeCrop,
|
||||
InvalidAffine,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum GpuError {
|
||||
OutOfMemory,
|
||||
Validation,
|
||||
Internal,
|
||||
}
|
||||
|
||||
pub struct OutputTexture {
|
||||
pub texture: wgpu::Texture,
|
||||
pub output: Output,
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use decode::{SourceId, SourceStream, SourceStreamId};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SourceRegistry {
|
||||
sources: HashMap<SourceId, Vec<SourceStream>>,
|
||||
}
|
||||
|
||||
impl SourceRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, id: SourceId, streams: Vec<SourceStream>) {
|
||||
self.sources.insert(id, streams);
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, id: SourceId) -> bool {
|
||||
self.sources.remove(&id).is_some()
|
||||
}
|
||||
|
||||
pub fn contains(&self, id: SourceId) -> bool {
|
||||
self.sources.contains_key(&id)
|
||||
}
|
||||
|
||||
pub fn streams(&self, id: SourceId) -> Option<&[SourceStream]> {
|
||||
self.sources.get(&id).map(Vec::as_slice)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct FrameCache {
|
||||
entries: HashMap<(SourceId, SourceStreamId, time::RationalTime), decode::Frame>,
|
||||
}
|
||||
|
||||
impl FrameCache {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn get(
|
||||
&self,
|
||||
source: SourceId,
|
||||
stream: SourceStreamId,
|
||||
time: time::RationalTime,
|
||||
) -> Option<&decode::Frame> {
|
||||
self.entries.get(&(source, stream, time))
|
||||
}
|
||||
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
source: SourceId,
|
||||
stream: SourceStreamId,
|
||||
time: time::RationalTime,
|
||||
frame: decode::Frame,
|
||||
) {
|
||||
self.entries.insert((source, stream, time), frame);
|
||||
}
|
||||
|
||||
pub fn flush_source(&mut self, source: SourceId) {
|
||||
self.entries.retain(|(sid, _, _), _| *sid != source);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,288 @@
|
|||
use std::collections::HashSet;
|
||||
|
||||
use decode::SourceStream;
|
||||
|
||||
use crate::plan::{Affine, Crop, PlanError, RenderError, RenderPlan};
|
||||
use crate::source::SourceRegistry;
|
||||
|
||||
pub fn validate_plan(plan: &RenderPlan, registry: &SourceRegistry) -> Result<(), RenderError> {
|
||||
validate_topology(plan)?;
|
||||
|
||||
for node in &plan.nodes {
|
||||
if !registry.contains(node.source.source) {
|
||||
return Err(RenderError::InvalidPlan(PlanError::DanglingSourceRef));
|
||||
}
|
||||
|
||||
let streams = registry
|
||||
.streams(node.source.source)
|
||||
.ok_or(RenderError::InvalidPlan(PlanError::DanglingSourceRef))?;
|
||||
let stream = streams
|
||||
.iter()
|
||||
.find(|s| s.id == node.source.stream)
|
||||
.ok_or(RenderError::InvalidPlan(PlanError::DanglingSourceRef))?;
|
||||
|
||||
validate_crop(node.crop, stream)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn validate_for_render(
|
||||
plan: &RenderPlan,
|
||||
registry: &SourceRegistry,
|
||||
) -> Result<(), RenderError> {
|
||||
validate_topology(plan)?;
|
||||
|
||||
for node in &plan.nodes {
|
||||
let streams = registry
|
||||
.streams(node.source.source)
|
||||
.ok_or(RenderError::UnknownSource)?;
|
||||
let stream = streams
|
||||
.iter()
|
||||
.find(|s| s.id == node.source.stream)
|
||||
.ok_or(RenderError::UnknownStream)?;
|
||||
|
||||
validate_crop(node.crop, stream)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_topology(plan: &RenderPlan) -> Result<(), RenderError> {
|
||||
if plan.output.width == 0 || plan.output.height == 0 {
|
||||
return Err(RenderError::InvalidPlan(PlanError::DimensionZero));
|
||||
}
|
||||
|
||||
let mut seen = HashSet::new();
|
||||
for node in &plan.nodes {
|
||||
if !seen.insert(node.id) {
|
||||
return Err(RenderError::InvalidPlan(PlanError::DuplicateNodeId));
|
||||
}
|
||||
|
||||
if !affine_is_finite(node.transform) {
|
||||
return Err(RenderError::InvalidPlan(PlanError::InvalidAffine));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_crop(crop: Crop, stream: &SourceStream) -> Result<(), RenderError> {
|
||||
let width = stream.width;
|
||||
let height = stream.height;
|
||||
|
||||
if crop.w == 0
|
||||
|| crop.h == 0
|
||||
|| crop.x.saturating_add(crop.w) > width
|
||||
|| crop.y.saturating_add(crop.h) > height
|
||||
{
|
||||
return Err(RenderError::InvalidPlan(PlanError::OutOfRangeCrop));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn affine_is_finite(affine: Affine) -> bool {
|
||||
affine.m.iter().all(|v| v.is_finite())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use decode::{SourceId, SourceStream, SourceStreamId};
|
||||
use time::{FrameRate, RationalTime};
|
||||
|
||||
use super::*;
|
||||
use crate::plan::{
|
||||
Affine, Blend, Crop, Node, NodeId, Opacity, Output, OutputFormat, RenderPlan, SourceRef,
|
||||
};
|
||||
|
||||
fn test_output() -> Output {
|
||||
Output {
|
||||
width: 4,
|
||||
height: 4,
|
||||
format: OutputFormat::Rgba8Premultiplied,
|
||||
}
|
||||
}
|
||||
|
||||
fn registry_with(source: SourceId, streams: Vec<SourceStream>) -> SourceRegistry {
|
||||
let mut registry = SourceRegistry::new();
|
||||
registry.insert(source, streams);
|
||||
registry
|
||||
}
|
||||
|
||||
fn sample_stream() -> SourceStream {
|
||||
SourceStream {
|
||||
id: SourceStreamId::new(1),
|
||||
frame_rate: FrameRate::new(30, 1).unwrap(),
|
||||
width: 4,
|
||||
height: 4,
|
||||
duration: RationalTime::new(10, 1).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dangling_source_ref_rejected() {
|
||||
let plan = RenderPlan {
|
||||
output: test_output(),
|
||||
nodes: vec![Node {
|
||||
id: NodeId::new(1),
|
||||
source: SourceRef {
|
||||
source: SourceId::new(42),
|
||||
stream: SourceStreamId::new(1),
|
||||
time: RationalTime::new(0, 1).unwrap(),
|
||||
},
|
||||
crop: Crop {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 4,
|
||||
h: 4,
|
||||
},
|
||||
transform: Affine {
|
||||
m: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
|
||||
},
|
||||
opacity: Opacity::new(1.0).unwrap(),
|
||||
blend: Blend::SourceOver,
|
||||
}],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
validate_plan(&plan, &SourceRegistry::new()),
|
||||
Err(RenderError::InvalidPlan(PlanError::DanglingSourceRef))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_node_id_rejected() {
|
||||
let source = SourceId::new(1);
|
||||
let stream = SourceStreamId::new(1);
|
||||
let registry = registry_with(source, vec![sample_stream()]);
|
||||
let node = Node {
|
||||
id: NodeId::new(1),
|
||||
source: SourceRef {
|
||||
source,
|
||||
stream,
|
||||
time: RationalTime::new(0, 1).unwrap(),
|
||||
},
|
||||
crop: Crop {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 4,
|
||||
h: 4,
|
||||
},
|
||||
transform: Affine {
|
||||
m: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
|
||||
},
|
||||
opacity: Opacity::new(1.0).unwrap(),
|
||||
blend: Blend::SourceOver,
|
||||
};
|
||||
|
||||
let plan = RenderPlan {
|
||||
output: test_output(),
|
||||
nodes: vec![node.clone(), node],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
validate_plan(&plan, ®istry),
|
||||
Err(RenderError::InvalidPlan(PlanError::DuplicateNodeId))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_range_crop_rejected() {
|
||||
let source = SourceId::new(1);
|
||||
let stream = SourceStreamId::new(1);
|
||||
let registry = registry_with(source, vec![sample_stream()]);
|
||||
let plan = RenderPlan {
|
||||
output: test_output(),
|
||||
nodes: vec![Node {
|
||||
id: NodeId::new(1),
|
||||
source: SourceRef {
|
||||
source,
|
||||
stream,
|
||||
time: RationalTime::new(0, 1).unwrap(),
|
||||
},
|
||||
crop: Crop {
|
||||
x: 2,
|
||||
y: 2,
|
||||
w: 4,
|
||||
h: 4,
|
||||
},
|
||||
transform: Affine {
|
||||
m: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
|
||||
},
|
||||
opacity: Opacity::new(1.0).unwrap(),
|
||||
blend: Blend::SourceOver,
|
||||
}],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
validate_plan(&plan, ®istry),
|
||||
Err(RenderError::InvalidPlan(PlanError::OutOfRangeCrop))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_finite_affine_rejected() {
|
||||
let source = SourceId::new(1);
|
||||
let stream = SourceStreamId::new(1);
|
||||
let registry = registry_with(source, vec![sample_stream()]);
|
||||
|
||||
for bad in [
|
||||
[f32::NAN, 0.0, 0.0, 1.0, 0.0, 0.0],
|
||||
[1.0, f32::INFINITY, 0.0, 1.0, 0.0, 0.0],
|
||||
[1.0, 0.0, 0.0, f32::NEG_INFINITY, 0.0, 0.0],
|
||||
] {
|
||||
let plan = RenderPlan {
|
||||
output: test_output(),
|
||||
nodes: vec![Node {
|
||||
id: NodeId::new(1),
|
||||
source: SourceRef {
|
||||
source,
|
||||
stream,
|
||||
time: RationalTime::new(0, 1).unwrap(),
|
||||
},
|
||||
crop: Crop {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 4,
|
||||
h: 4,
|
||||
},
|
||||
transform: Affine { m: bad },
|
||||
opacity: Opacity::new(1.0).unwrap(),
|
||||
blend: Blend::SourceOver,
|
||||
}],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
validate_plan(&plan, ®istry),
|
||||
Err(RenderError::InvalidPlan(PlanError::InvalidAffine))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dimension_zero_rejected() {
|
||||
let plan = RenderPlan {
|
||||
output: Output {
|
||||
width: 0,
|
||||
height: 4,
|
||||
format: OutputFormat::Rgba8Premultiplied,
|
||||
},
|
||||
nodes: vec![],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
validate_plan(&plan, &SourceRegistry::new()),
|
||||
Err(RenderError::InvalidPlan(PlanError::DimensionZero))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_node_list_accepted() {
|
||||
let plan = RenderPlan {
|
||||
output: test_output(),
|
||||
nodes: vec![],
|
||||
};
|
||||
|
||||
assert!(validate_plan(&plan, &SourceRegistry::new()).is_ok());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,330 @@
|
|||
#![cfg(feature = "wgpu-tests")]
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use decode::{
|
||||
FakeDecoder, FakeDecoderConfig, PixelFormat, Source, SourceStreamId,
|
||||
};
|
||||
use render::pixels::readback_rgba8;
|
||||
use render::{
|
||||
Affine, Blend, Crop, Node, NodeId, Opacity, Output, OutputFormat, RenderPlan, Renderer,
|
||||
SourceRef,
|
||||
};
|
||||
use time::{FrameRate, RationalTime};
|
||||
|
||||
fn headless_gpu() -> (wgpu::Device, wgpu::Queue) {
|
||||
pollster::block_on(async {
|
||||
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||
backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
|
||||
..Default::default()
|
||||
});
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::LowPower,
|
||||
force_fallback_adapter: true,
|
||||
compatible_surface: None,
|
||||
})
|
||||
.await
|
||||
.expect("adapter");
|
||||
adapter
|
||||
.request_device(&wgpu::DeviceDescriptor {
|
||||
label: Some("compose_test_device"),
|
||||
required_features: wgpu::Features::empty(),
|
||||
required_limits: wgpu::Limits::downlevel_defaults(),
|
||||
memory_hints: wgpu::MemoryHints::Performance,
|
||||
trace: wgpu::Trace::Off,
|
||||
})
|
||||
.await
|
||||
.expect("device")
|
||||
})
|
||||
}
|
||||
|
||||
fn output_8x8() -> Output {
|
||||
Output {
|
||||
width: 8,
|
||||
height: 8,
|
||||
format: OutputFormat::Rgba8Premultiplied,
|
||||
}
|
||||
}
|
||||
|
||||
fn fill_affine(src_w: u32, src_h: u32, out_w: u32, out_h: u32) -> Affine {
|
||||
Affine {
|
||||
m: [
|
||||
out_w as f32 / src_w as f32,
|
||||
0.0,
|
||||
0.0,
|
||||
out_h as f32 / src_h as f32,
|
||||
0.0,
|
||||
0.0,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn decoder_with_color(color: [u8; 4], size: u32) -> FakeDecoder {
|
||||
FakeDecoder::new(FakeDecoderConfig {
|
||||
frame_rate: FrameRate::new(30, 1).unwrap(),
|
||||
width: size,
|
||||
height: size,
|
||||
duration: RationalTime::new(10, 1).unwrap(),
|
||||
format: PixelFormat::Rgba8,
|
||||
color,
|
||||
})
|
||||
}
|
||||
|
||||
fn center_pixel(pixels: &[u8], width: u32, height: u32) -> [u8; 4] {
|
||||
let x = width / 2;
|
||||
let y = height / 2;
|
||||
let idx = ((y * width + x) * 4) as usize;
|
||||
[
|
||||
pixels[idx],
|
||||
pixels[idx + 1],
|
||||
pixels[idx + 2],
|
||||
pixels[idx + 3],
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires GPU; run with --features wgpu-tests -- --ignored"]
|
||||
fn single_opaque_node_fills_output_with_source_color() {
|
||||
let (device, queue) = headless_gpu();
|
||||
let mut decoder = decoder_with_color([200, 40, 60, 255], 8);
|
||||
let mut renderer = Renderer::new(&mut decoder, &device, &queue);
|
||||
let source = renderer
|
||||
.register_source(&Source::Bytes(Arc::from([0u8; 0])))
|
||||
.unwrap();
|
||||
let stream = decoder.streams(source).unwrap()[0].id;
|
||||
|
||||
let plan = RenderPlan {
|
||||
output: output_8x8(),
|
||||
nodes: vec![Node {
|
||||
id: NodeId::new(1),
|
||||
source: SourceRef {
|
||||
source,
|
||||
stream,
|
||||
time: RationalTime::new(0, 1).unwrap(),
|
||||
},
|
||||
crop: Crop {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 8,
|
||||
h: 8,
|
||||
},
|
||||
transform: fill_affine(8, 8, 8, 8),
|
||||
opacity: Opacity::new(1.0).unwrap(),
|
||||
blend: Blend::SourceOver,
|
||||
}],
|
||||
};
|
||||
|
||||
let out = renderer.render(&plan).unwrap();
|
||||
let pixels = readback_rgba8(&out.texture, &device, &queue, 8, 8).unwrap();
|
||||
let px = center_pixel(&pixels, 8, 8);
|
||||
assert_eq!(px, [200, 40, 60, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires GPU; run with --features wgpu-tests -- --ignored"]
|
||||
fn two_stacked_nodes_blend_with_half_opacity() {
|
||||
let (device, queue) = headless_gpu();
|
||||
let mut bottom = decoder_with_color([100, 0, 0, 255], 8);
|
||||
let mut top = decoder_with_color([0, 0, 200, 255], 8);
|
||||
|
||||
struct TwoDecoder {
|
||||
bottom: FakeDecoder,
|
||||
top: FakeDecoder,
|
||||
mapping: std::collections::HashMap<u64, bool>,
|
||||
}
|
||||
|
||||
impl decode::Decoder for TwoDecoder {
|
||||
fn open(&mut self, source: &Source) -> Result<decode::SourceId, decode::DecodeError> {
|
||||
if self.mapping.is_empty() {
|
||||
let id = self.bottom.open(source)?;
|
||||
self.mapping.insert(id.raw(), false);
|
||||
Ok(id)
|
||||
} else {
|
||||
let id = self.top.open(source)?;
|
||||
self.mapping.insert(id.raw(), true);
|
||||
Ok(id)
|
||||
}
|
||||
}
|
||||
|
||||
fn streams(
|
||||
&self,
|
||||
source: decode::SourceId,
|
||||
) -> Result<Vec<decode::SourceStream>, decode::DecodeError> {
|
||||
if *self.mapping.get(&source.raw()).unwrap_or(&false) {
|
||||
self.top.streams(source)
|
||||
} else {
|
||||
self.bottom.streams(source)
|
||||
}
|
||||
}
|
||||
|
||||
fn read_frame(
|
||||
&mut self,
|
||||
source: decode::SourceId,
|
||||
stream: decode::SourceStreamId,
|
||||
time: time::RationalTime,
|
||||
) -> Result<decode::Frame, decode::DecodeError> {
|
||||
if *self.mapping.get(&source.raw()).unwrap_or(&false) {
|
||||
self.top.read_frame(source, stream, time)
|
||||
} else {
|
||||
self.bottom.read_frame(source, stream, time)
|
||||
}
|
||||
}
|
||||
|
||||
fn close(&mut self, source: decode::SourceId) -> Result<(), decode::DecodeError> {
|
||||
if *self.mapping.get(&source.raw()).unwrap_or(&false) {
|
||||
self.top.close(source)
|
||||
} else {
|
||||
self.bottom.close(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut decoder = TwoDecoder {
|
||||
bottom,
|
||||
top,
|
||||
mapping: std::collections::HashMap::new(),
|
||||
};
|
||||
let mut renderer = Renderer::new(&mut decoder, &device, &queue);
|
||||
let bottom_source = renderer
|
||||
.register_source(&Source::Bytes(Arc::from([1u8; 0])))
|
||||
.unwrap();
|
||||
let top_source = renderer
|
||||
.register_source(&Source::Bytes(Arc::from([2u8; 0])))
|
||||
.unwrap();
|
||||
let bottom_stream = decoder.streams(bottom_source).unwrap()[0].id;
|
||||
let top_stream = decoder.streams(top_source).unwrap()[0].id;
|
||||
|
||||
let plan = RenderPlan {
|
||||
output: output_8x8(),
|
||||
nodes: vec![
|
||||
Node {
|
||||
id: NodeId::new(1),
|
||||
source: SourceRef {
|
||||
source: bottom_source,
|
||||
stream: bottom_stream,
|
||||
time: RationalTime::new(0, 1).unwrap(),
|
||||
},
|
||||
crop: Crop {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 8,
|
||||
h: 8,
|
||||
},
|
||||
transform: fill_affine(8, 8, 8, 8),
|
||||
opacity: Opacity::new(1.0).unwrap(),
|
||||
blend: Blend::SourceOver,
|
||||
},
|
||||
Node {
|
||||
id: NodeId::new(2),
|
||||
source: SourceRef {
|
||||
source: top_source,
|
||||
stream: top_stream,
|
||||
time: RationalTime::new(0, 1).unwrap(),
|
||||
},
|
||||
crop: Crop {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 8,
|
||||
h: 8,
|
||||
},
|
||||
transform: fill_affine(8, 8, 8, 8),
|
||||
opacity: Opacity::new(0.5).unwrap(),
|
||||
blend: Blend::SourceOver,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let out = renderer.render(&plan).unwrap();
|
||||
let pixels = readback_rgba8(&out.texture, &device, &queue, 8, 8).unwrap();
|
||||
let px = center_pixel(&pixels, 8, 8);
|
||||
|
||||
// premul source-over: top(0,0,200,255)*0.5 over bottom(100,0,0,255)
|
||||
assert_eq!(px[0], 50);
|
||||
assert_eq!(px[2], 100);
|
||||
assert_eq!(px[3], 255);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires GPU; run with --features wgpu-tests -- --ignored"]
|
||||
fn cropped_node_renders_only_cropped_region() {
|
||||
let (device, queue) = headless_gpu();
|
||||
let mut decoder = decoder_with_color([0, 180, 0, 255], 8);
|
||||
let mut renderer = Renderer::new(&mut decoder, &device, &queue);
|
||||
let source = renderer
|
||||
.register_source(&Source::Bytes(Arc::from([0u8; 0])))
|
||||
.unwrap();
|
||||
let stream = decoder.streams(source).unwrap()[0].id;
|
||||
|
||||
let plan = RenderPlan {
|
||||
output: output_8x8(),
|
||||
nodes: vec![Node {
|
||||
id: NodeId::new(1),
|
||||
source: SourceRef {
|
||||
source,
|
||||
stream,
|
||||
time: RationalTime::new(0, 1).unwrap(),
|
||||
},
|
||||
crop: Crop {
|
||||
x: 4,
|
||||
y: 4,
|
||||
w: 4,
|
||||
h: 4,
|
||||
},
|
||||
transform: fill_affine(4, 4, 8, 8),
|
||||
opacity: Opacity::new(1.0).unwrap(),
|
||||
blend: Blend::SourceOver,
|
||||
}],
|
||||
};
|
||||
|
||||
let out = renderer.render(&plan).unwrap();
|
||||
let pixels = readback_rgba8(&out.texture, &device, &queue, 8, 8).unwrap();
|
||||
|
||||
let corner = pixels[0..4].try_into().unwrap();
|
||||
let center = center_pixel(&pixels, 8, 8);
|
||||
assert_eq!(corner, [0, 0, 0, 0]);
|
||||
assert_eq!(center, [0, 180, 0, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires GPU; run with --features wgpu-tests -- --ignored"]
|
||||
fn affine_transform_maps_source_to_expected_output_rectangle() {
|
||||
let (device, queue) = headless_gpu();
|
||||
let mut decoder = decoder_with_color([10, 20, 30, 255], 8);
|
||||
let mut renderer = Renderer::new(&mut decoder, &device, &queue);
|
||||
let source = renderer
|
||||
.register_source(&Source::Bytes(Arc::from([0u8; 0])))
|
||||
.unwrap();
|
||||
let stream = decoder.streams(source).unwrap()[0].id;
|
||||
|
||||
let plan = RenderPlan {
|
||||
output: output_8x8(),
|
||||
nodes: vec![Node {
|
||||
id: NodeId::new(1),
|
||||
source: SourceRef {
|
||||
source,
|
||||
stream,
|
||||
time: RationalTime::new(0, 1).unwrap(),
|
||||
},
|
||||
crop: Crop {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 8,
|
||||
h: 8,
|
||||
},
|
||||
transform: Affine {
|
||||
m: [0.5, 0.0, 0.0, 0.5, 2.0, 2.0],
|
||||
},
|
||||
opacity: Opacity::new(1.0).unwrap(),
|
||||
blend: Blend::SourceOver,
|
||||
}],
|
||||
};
|
||||
|
||||
let out = renderer.render(&plan).unwrap();
|
||||
let pixels = readback_rgba8(&out.texture, &device, &queue, 8, 8).unwrap();
|
||||
|
||||
let inside = center_pixel(&pixels, 8, 8);
|
||||
let outside_idx = (0 * 8 + 0) * 4;
|
||||
assert_eq!(inside, [10, 20, 30, 255]);
|
||||
assert_eq!(pixels[outside_idx..outside_idx + 4], [0, 0, 0, 0]);
|
||||
}
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use decode::{
|
||||
DecodeError, Decoder, FakeDecoder, FakeDecoderConfig, PixelFormat, Source, SourceId,
|
||||
SourceStreamId,
|
||||
};
|
||||
use render::{
|
||||
Affine, Blend, Crop, Node, NodeId, Opacity, Output, OutputFormat, RenderError, RenderPlan,
|
||||
Renderer, SourceRef,
|
||||
};
|
||||
use time::{FrameRate, RationalTime};
|
||||
|
||||
fn test_decoder(color: [u8; 4]) -> FakeDecoder {
|
||||
FakeDecoder::new(FakeDecoderConfig {
|
||||
frame_rate: FrameRate::new(30, 1).unwrap(),
|
||||
width: 4,
|
||||
height: 4,
|
||||
duration: RationalTime::new(10, 1).unwrap(),
|
||||
format: PixelFormat::Rgba8,
|
||||
color,
|
||||
})
|
||||
}
|
||||
|
||||
fn test_output() -> Output {
|
||||
Output {
|
||||
width: 4,
|
||||
height: 4,
|
||||
format: OutputFormat::Rgba8Premultiplied,
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_to_output_affine(crop_w: u32, crop_h: u32, out_w: u32, out_h: u32) -> Affine {
|
||||
Affine {
|
||||
m: [
|
||||
out_w as f32 / crop_w as f32,
|
||||
0.0,
|
||||
0.0,
|
||||
out_h as f32 / crop_h as f32,
|
||||
0.0,
|
||||
0.0,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
struct HeadlessGpu {
|
||||
device: wgpu::Device,
|
||||
queue: wgpu::Queue,
|
||||
}
|
||||
|
||||
fn headless_gpu() -> HeadlessGpu {
|
||||
pollster::block_on(async {
|
||||
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||
backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
|
||||
..Default::default()
|
||||
});
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::LowPower,
|
||||
force_fallback_adapter: true,
|
||||
compatible_surface: None,
|
||||
})
|
||||
.await
|
||||
.expect("adapter");
|
||||
let (device, queue) = adapter
|
||||
.request_device(&wgpu::DeviceDescriptor {
|
||||
label: Some("test_device"),
|
||||
required_features: wgpu::Features::empty(),
|
||||
required_limits: wgpu::Limits::downlevel_defaults(),
|
||||
memory_hints: wgpu::MemoryHints::Performance,
|
||||
trace: wgpu::Trace::Off,
|
||||
})
|
||||
.await
|
||||
.expect("device");
|
||||
HeadlessGpu { device, queue }
|
||||
})
|
||||
}
|
||||
|
||||
fn node(id: u64, source: SourceId, stream: SourceStreamId, color_time: RationalTime) -> Node {
|
||||
Node {
|
||||
id: NodeId::new(id),
|
||||
source: SourceRef {
|
||||
source,
|
||||
stream,
|
||||
time: color_time,
|
||||
},
|
||||
crop: Crop {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 4,
|
||||
h: 4,
|
||||
},
|
||||
transform: scale_to_output_affine(4, 4, 4, 4),
|
||||
opacity: Opacity::new(1.0).unwrap(),
|
||||
blend: Blend::SourceOver,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unregistered_source_fails_unknown_source() {
|
||||
let gpu = headless_gpu();
|
||||
let mut decoder = test_decoder([255, 0, 0, 255]);
|
||||
let mut renderer = Renderer::new(&mut decoder, &gpu.device, &gpu.queue);
|
||||
|
||||
let plan = RenderPlan {
|
||||
output: test_output(),
|
||||
nodes: vec![Node {
|
||||
id: NodeId::new(1),
|
||||
source: SourceRef {
|
||||
source: SourceId::new(99),
|
||||
stream: SourceStreamId::new(1),
|
||||
time: RationalTime::new(0, 1).unwrap(),
|
||||
},
|
||||
crop: Crop {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 4,
|
||||
h: 4,
|
||||
},
|
||||
transform: scale_to_output_affine(4, 4, 4, 4),
|
||||
opacity: Opacity::new(1.0).unwrap(),
|
||||
blend: Blend::SourceOver,
|
||||
}],
|
||||
};
|
||||
|
||||
assert!(matches!(
|
||||
renderer.render(&plan),
|
||||
Err(RenderError::UnknownSource)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registered_source_resolves_with_floored_frame_time() {
|
||||
let gpu = headless_gpu();
|
||||
let mut decoder = test_decoder([255, 0, 0, 255]);
|
||||
let mut renderer = Renderer::new(&mut decoder, &gpu.device, &gpu.queue);
|
||||
let source = renderer
|
||||
.register_source(&Source::Bytes(Arc::from([0u8; 0])))
|
||||
.unwrap();
|
||||
let stream = SourceStreamId::new(1);
|
||||
|
||||
let requested = RationalTime::new(1, 30).unwrap();
|
||||
let plan = RenderPlan {
|
||||
output: test_output(),
|
||||
nodes: vec![node(1, source, stream, requested)],
|
||||
};
|
||||
|
||||
renderer.render(&plan).unwrap();
|
||||
drop(renderer);
|
||||
|
||||
let frame = decoder.read_frame(source, stream, requested).unwrap();
|
||||
assert_eq!(frame.time, RationalTime::new(1, 30).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_source_invalidates_cache_and_fails_unknown_source() {
|
||||
let gpu = headless_gpu();
|
||||
let mut decoder = test_decoder([255, 0, 0, 255]);
|
||||
let mut renderer = Renderer::new(&mut decoder, &gpu.device, &gpu.queue);
|
||||
let source = renderer
|
||||
.register_source(&Source::Bytes(Arc::from([0u8; 0])))
|
||||
.unwrap();
|
||||
let stream = SourceStreamId::new(1);
|
||||
|
||||
let plan = RenderPlan {
|
||||
output: test_output(),
|
||||
nodes: vec![node(1, source, stream, RationalTime::new(0, 1).unwrap())],
|
||||
};
|
||||
renderer.render(&plan).unwrap();
|
||||
renderer.release_source(source).unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
renderer.render(&plan),
|
||||
Err(RenderError::UnknownSource)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn end_of_stream_propagates_as_decode_error() {
|
||||
let gpu = headless_gpu();
|
||||
let mut decoder = FakeDecoder::new(FakeDecoderConfig {
|
||||
frame_rate: FrameRate::new(30, 1).unwrap(),
|
||||
width: 4,
|
||||
height: 4,
|
||||
duration: RationalTime::new(1, 30).unwrap(),
|
||||
format: PixelFormat::Rgba8,
|
||||
color: [255, 0, 0, 255],
|
||||
});
|
||||
let mut renderer = Renderer::new(&mut decoder, &gpu.device, &gpu.queue);
|
||||
let source = renderer
|
||||
.register_source(&Source::Bytes(Arc::from([0u8; 0])))
|
||||
.unwrap();
|
||||
let stream = SourceStreamId::new(1);
|
||||
|
||||
let plan = RenderPlan {
|
||||
output: test_output(),
|
||||
nodes: vec![node(
|
||||
1,
|
||||
source,
|
||||
stream,
|
||||
RationalTime::new(1, 1).unwrap(),
|
||||
)],
|
||||
};
|
||||
|
||||
assert!(matches!(
|
||||
renderer.render(&plan),
|
||||
Err(RenderError::Decode(DecodeError::EndOfStream))
|
||||
));
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
[package]
|
||||
name = "time"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
floats = []
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1"
|
||||
|
||||
[[test]]
|
||||
name = "floats"
|
||||
required-features = ["floats"]
|
||||
|
|
@ -0,0 +1,348 @@
|
|||
//! Exact rational time and frame rate primitives.
|
||||
//!
|
||||
//! ## Serialization stability
|
||||
//!
|
||||
//! `RationalTime` and `FrameRate` serialize as objects with fixed field names:
|
||||
//! - `RationalTime`: `{ "n": i64, "d": u64 }`
|
||||
//! - `FrameRate`: `{ "n": u64, "d": u64 }`
|
||||
//!
|
||||
//! Field names and types are additive-only; renames or removals are breaking.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Exact rational time as a reduced fraction.
|
||||
///
|
||||
/// Invariant: `d > 0`, `gcd(|n|, d) == 1`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct RationalTime {
|
||||
n: i64,
|
||||
d: u64,
|
||||
}
|
||||
|
||||
/// Frames per second as an exact positive rational (e.g. 29.97 = 30000/1001).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct FrameRate {
|
||||
n: u64,
|
||||
d: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error, PartialEq, Eq)]
|
||||
pub enum TimeError {
|
||||
#[error("denominator must be non-zero")]
|
||||
ZeroDenominator,
|
||||
#[error("arithmetic overflow")]
|
||||
Overflow,
|
||||
}
|
||||
|
||||
impl RationalTime {
|
||||
pub fn new(n: i64, d: u64) -> Result<Self, TimeError> {
|
||||
if d == 0 {
|
||||
return Err(TimeError::ZeroDenominator);
|
||||
}
|
||||
Ok(Self::from_parts_unchecked(n, d))
|
||||
}
|
||||
|
||||
pub fn numer(&self) -> i64 {
|
||||
self.n
|
||||
}
|
||||
|
||||
pub fn denom(&self) -> u64 {
|
||||
self.d
|
||||
}
|
||||
|
||||
pub fn is_zero(&self) -> bool {
|
||||
self.n == 0
|
||||
}
|
||||
|
||||
pub fn add(self, other: Self) -> Result<Self, TimeError> {
|
||||
let lcm = lcm_u64(self.d, other.d).ok_or(TimeError::Overflow)?;
|
||||
let left = (self.n as i128)
|
||||
.checked_mul((lcm / self.d) as i128)
|
||||
.ok_or(TimeError::Overflow)?;
|
||||
let right = (other.n as i128)
|
||||
.checked_mul((lcm / other.d) as i128)
|
||||
.ok_or(TimeError::Overflow)?;
|
||||
let sum = left.checked_add(right).ok_or(TimeError::Overflow)?;
|
||||
Self::from_i128(sum, lcm as u128)
|
||||
}
|
||||
|
||||
pub fn sub(self, other: Self) -> Result<Self, TimeError> {
|
||||
let lcm = lcm_u64(self.d, other.d).ok_or(TimeError::Overflow)?;
|
||||
let left = (self.n as i128)
|
||||
.checked_mul((lcm / self.d) as i128)
|
||||
.ok_or(TimeError::Overflow)?;
|
||||
let right = (other.n as i128)
|
||||
.checked_mul((lcm / other.d) as i128)
|
||||
.ok_or(TimeError::Overflow)?;
|
||||
let diff = left.checked_sub(right).ok_or(TimeError::Overflow)?;
|
||||
Self::from_i128(diff, lcm as u128)
|
||||
}
|
||||
|
||||
pub fn mul(self, scalar: i64) -> Result<Self, TimeError> {
|
||||
if scalar == 0 || self.n == 0 {
|
||||
return Ok(Self { n: 0, d: 1 });
|
||||
}
|
||||
let n = (self.n as i128)
|
||||
.checked_mul(scalar as i128)
|
||||
.ok_or(TimeError::Overflow)?;
|
||||
Self::from_i128(n, self.d as u128)
|
||||
}
|
||||
|
||||
pub fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
(self.n as i128 * other.d as i128).cmp(&(other.n as i128 * self.d as i128))
|
||||
}
|
||||
|
||||
pub fn ge(&self, other: &Self) -> bool {
|
||||
self.cmp(other) != std::cmp::Ordering::Less
|
||||
}
|
||||
|
||||
pub fn lt(&self, other: &Self) -> bool {
|
||||
self.cmp(other) == std::cmp::Ordering::Less
|
||||
}
|
||||
|
||||
fn from_parts_unchecked(n: i64, d: u64) -> Self {
|
||||
if n == 0 {
|
||||
return Self { n: 0, d: 1 };
|
||||
}
|
||||
|
||||
let g = gcd_u64(n.unsigned_abs(), d);
|
||||
let n = n / g as i64;
|
||||
let d = d / g;
|
||||
|
||||
// Keep sign on the numerator; denominator stays positive.
|
||||
if n == 0 {
|
||||
Self { n: 0, d: 1 }
|
||||
} else {
|
||||
Self { n, d }
|
||||
}
|
||||
}
|
||||
|
||||
fn from_i128(n: i128, d: u128) -> Result<Self, TimeError> {
|
||||
if d == 0 {
|
||||
return Err(TimeError::ZeroDenominator);
|
||||
}
|
||||
if n == 0 {
|
||||
return Ok(Self { n: 0, d: 1 });
|
||||
}
|
||||
|
||||
let g = gcd_u128(n.unsigned_abs(), d);
|
||||
let n = n / g as i128;
|
||||
let d = (d / g) as u64;
|
||||
|
||||
if n < i64::MIN as i128 || n > i64::MAX as i128 {
|
||||
return Err(TimeError::Overflow);
|
||||
}
|
||||
if d == 0 {
|
||||
return Err(TimeError::Overflow);
|
||||
}
|
||||
|
||||
Ok(Self::from_parts_unchecked(n as i64, d))
|
||||
}
|
||||
}
|
||||
|
||||
impl FrameRate {
|
||||
pub fn new(n: u64, d: u64) -> Result<Self, TimeError> {
|
||||
if n == 0 || d == 0 {
|
||||
return Err(TimeError::ZeroDenominator);
|
||||
}
|
||||
Ok(Self { n, d })
|
||||
}
|
||||
|
||||
pub fn numer(&self) -> u64 {
|
||||
self.n
|
||||
}
|
||||
|
||||
pub fn denom(&self) -> u64 {
|
||||
self.d
|
||||
}
|
||||
|
||||
/// Duration of one frame as an exact rational time.
|
||||
pub fn period(&self) -> RationalTime {
|
||||
RationalTime::from_parts_unchecked(self.d as i64, self.n)
|
||||
}
|
||||
|
||||
pub fn frame_index_floor(&self, time: RationalTime) -> Result<i64, TimeError> {
|
||||
frame_index_at(time, self.n, self.d, FrameSnap::Floor)
|
||||
}
|
||||
|
||||
pub fn frame_index_ceil(&self, time: RationalTime) -> Result<i64, TimeError> {
|
||||
frame_index_at(time, self.n, self.d, FrameSnap::Ceil)
|
||||
}
|
||||
|
||||
/// Start time of `index` under this frame rate.
|
||||
pub fn frame_start(&self, index: i64) -> Result<RationalTime, TimeError> {
|
||||
self.period().mul(index)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "floats")]
|
||||
pub mod floats {
|
||||
use super::{FrameRate, RationalTime, TimeError};
|
||||
|
||||
impl RationalTime {
|
||||
pub fn from_seconds_floor(seconds: f64, max_denominator: u64) -> Result<Self, TimeError> {
|
||||
from_seconds(seconds, max_denominator, SecondsSnap::Floor)
|
||||
}
|
||||
|
||||
pub fn from_seconds_ceil(seconds: f64, max_denominator: u64) -> Result<Self, TimeError> {
|
||||
from_seconds(seconds, max_denominator, SecondsSnap::Ceil)
|
||||
}
|
||||
|
||||
pub fn from_seconds_round(seconds: f64, max_denominator: u64) -> Result<Self, TimeError> {
|
||||
from_seconds(seconds, max_denominator, SecondsSnap::Round)
|
||||
}
|
||||
|
||||
pub fn to_seconds(self) -> f64 {
|
||||
self.n as f64 / self.d as f64
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl FrameRate {
|
||||
pub fn from_fps_float(fps: f64, max_denominator: u64) -> Result<Self, TimeError> {
|
||||
let seconds = RationalTime::from_seconds_round(1.0 / fps, max_denominator)?;
|
||||
Ok(Self {
|
||||
n: seconds.denom(),
|
||||
d: seconds.numer().unsigned_abs(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
enum SecondsSnap {
|
||||
Floor,
|
||||
Ceil,
|
||||
Round,
|
||||
}
|
||||
|
||||
fn from_seconds(seconds: f64, max_denominator: u64, snap: SecondsSnap) -> Result<RationalTime, TimeError> {
|
||||
if !seconds.is_finite() {
|
||||
return Err(TimeError::Overflow);
|
||||
}
|
||||
if max_denominator == 0 {
|
||||
return Err(TimeError::ZeroDenominator);
|
||||
}
|
||||
|
||||
let scaled = match snap {
|
||||
SecondsSnap::Floor => (seconds * max_denominator as f64).floor(),
|
||||
SecondsSnap::Ceil => (seconds * max_denominator as f64).ceil(),
|
||||
SecondsSnap::Round => (seconds * max_denominator as f64).round(),
|
||||
};
|
||||
|
||||
if !scaled.is_finite() {
|
||||
return Err(TimeError::Overflow);
|
||||
}
|
||||
|
||||
RationalTime::new(scaled as i64, max_denominator)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum FrameSnap {
|
||||
Floor,
|
||||
Ceil,
|
||||
}
|
||||
|
||||
fn frame_index_at(time: RationalTime, rate_n: u64, rate_d: u64, snap: FrameSnap) -> Result<i64, TimeError> {
|
||||
if time.n == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let numerator = (time.n as i128)
|
||||
.checked_mul(rate_n as i128)
|
||||
.ok_or(TimeError::Overflow)?;
|
||||
let denominator = (time.d as i128)
|
||||
.checked_mul(rate_d as i128)
|
||||
.ok_or(TimeError::Overflow)?;
|
||||
if denominator == 0 {
|
||||
return Err(TimeError::ZeroDenominator);
|
||||
}
|
||||
|
||||
let (mut q, r) = (numerator / denominator, numerator % denominator);
|
||||
|
||||
match snap {
|
||||
FrameSnap::Floor => {
|
||||
if r != 0 && numerator < 0 {
|
||||
q -= 1;
|
||||
}
|
||||
}
|
||||
FrameSnap::Ceil => {
|
||||
if r != 0 && numerator > 0 {
|
||||
q += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if q < i64::MIN as i128 || q > i64::MAX as i128 {
|
||||
return Err(TimeError::Overflow);
|
||||
}
|
||||
Ok(q as i64)
|
||||
}
|
||||
|
||||
fn gcd_u64(mut a: u64, mut b: u64) -> u64 {
|
||||
while b != 0 {
|
||||
a %= b;
|
||||
std::mem::swap(&mut a, &mut b);
|
||||
}
|
||||
a
|
||||
}
|
||||
|
||||
fn gcd_u128(mut a: u128, mut b: u128) -> u128 {
|
||||
while b != 0 {
|
||||
a %= b;
|
||||
std::mem::swap(&mut a, &mut b);
|
||||
}
|
||||
a
|
||||
}
|
||||
|
||||
fn lcm_u64(a: u64, b: u64) -> Option<u64> {
|
||||
let g = gcd_u64(a, b);
|
||||
a.checked_div(g)?.checked_mul(b)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn reduction_is_canonical() {
|
||||
let a = RationalTime::new(2, 4).unwrap();
|
||||
let b = RationalTime::new(1, 2).unwrap();
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_numerator_reduces() {
|
||||
let a = RationalTime::new(-4, 8).unwrap();
|
||||
let b = RationalTime::new(-1, 2).unwrap();
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_denominator_rejected() {
|
||||
assert_eq!(RationalTime::new(1, 0), Err(TimeError::ZeroDenominator));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_sub_mul_exactness() {
|
||||
let a = RationalTime::new(1, 3).unwrap();
|
||||
let b = RationalTime::new(1, 6).unwrap();
|
||||
assert_eq!(a.add(b).unwrap(), RationalTime::new(1, 2).unwrap());
|
||||
assert_eq!(a.sub(b).unwrap(), RationalTime::new(1, 6).unwrap());
|
||||
assert_eq!(a.mul(2).unwrap(), RationalTime::new(2, 3).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overflow_surfaces() {
|
||||
let large = RationalTime::new(i64::MAX, 1).unwrap();
|
||||
assert_eq!(large.add(large), Err(TimeError::Overflow));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_index_floor_matches_media_semantics() {
|
||||
let fps = FrameRate::new(30, 1).unwrap();
|
||||
let t = RationalTime::new(1, 30).unwrap();
|
||||
assert_eq!(fps.frame_index_floor(t).unwrap(), 1);
|
||||
assert_eq!(fps.frame_index_floor(RationalTime::new(0, 1).unwrap()).unwrap(), 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
use time::RationalTime;
|
||||
|
||||
#[test]
|
||||
fn ntsc_period_from_seconds_exact() {
|
||||
let t = RationalTime::from_seconds_round(1001.0 / 30000.0, 30000).unwrap();
|
||||
assert_eq!(t, RationalTime::new(1001, 30000).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn film_period_from_seconds_exact() {
|
||||
let t = RationalTime::from_seconds_round(1001.0 / 24000.0, 24000).unwrap();
|
||||
assert_eq!(t, RationalTime::new(1001, 24000).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_seconds_within_tolerance() {
|
||||
let t = RationalTime::new(30000, 1001).unwrap();
|
||||
let seconds = t.to_seconds();
|
||||
assert!((seconds - 29.97002997002997).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_seconds_floor_ceil_round() {
|
||||
let max_d = 1000;
|
||||
assert_eq!(
|
||||
RationalTime::from_seconds_floor(0.3334, max_d).unwrap(),
|
||||
RationalTime::new(333, 1000).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
RationalTime::from_seconds_ceil(0.3334, max_d).unwrap(),
|
||||
RationalTime::new(334, 1000).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
RationalTime::from_seconds_round(0.3335, max_d).unwrap(),
|
||||
RationalTime::new(334, 1000).unwrap()
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
use time::{FrameRate, RationalTime, TimeError};
|
||||
|
||||
#[test]
|
||||
fn reduction_to_canonical_form() {
|
||||
assert_eq!(
|
||||
RationalTime::new(2, 4).unwrap(),
|
||||
RationalTime::new(1, 2).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_denominator_normalization() {
|
||||
assert_eq!(
|
||||
RationalTime::new(-2, 4).unwrap(),
|
||||
RationalTime::new(-1, 2).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_denominator_rejected() {
|
||||
assert_eq!(RationalTime::new(1, 0), Err(TimeError::ZeroDenominator));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ntsc_frame_rate_round_trips_exactly() {
|
||||
let fps = FrameRate::new(30_000, 1001).unwrap();
|
||||
assert_eq!(fps.numer(), 30_000);
|
||||
assert_eq!(fps.denom(), 1001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn film_frame_rate_round_trips_exactly() {
|
||||
let fps = FrameRate::new(24000, 1001).unwrap();
|
||||
assert_eq!(fps.numer(), 24000);
|
||||
assert_eq!(fps.denom(), 1001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_sub_mul_exactness_and_overflow() {
|
||||
let a = RationalTime::new(1, 3).unwrap();
|
||||
let b = RationalTime::new(1, 6).unwrap();
|
||||
assert_eq!(a.add(b).unwrap(), RationalTime::new(1, 2).unwrap());
|
||||
assert_eq!(a.sub(b).unwrap(), RationalTime::new(1, 6).unwrap());
|
||||
assert_eq!(a.mul(2).unwrap(), RationalTime::new(2, 3).unwrap());
|
||||
|
||||
let large = RationalTime::new(i64::MAX, 1).unwrap();
|
||||
assert_eq!(large.add(large), Err(TimeError::Overflow));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_index_floor_for_known_fps() {
|
||||
let fps = FrameRate::new(24, 1).unwrap();
|
||||
let half_second = RationalTime::new(1, 2).unwrap();
|
||||
assert_eq!(fps.frame_index_floor(half_second).unwrap(), 12);
|
||||
|
||||
let fps2997 = FrameRate::new(30000, 1001).unwrap();
|
||||
let one_second = RationalTime::new(1001, 1000).unwrap();
|
||||
assert_eq!(fps2997.frame_index_floor(one_second).unwrap(), 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialization_stability_fixture() {
|
||||
let json = r#"{"n":30000,"d":1001}"#;
|
||||
let t: RationalTime = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(t, RationalTime::new(30000, 1001).unwrap());
|
||||
}
|
||||
Loading…
Reference in New Issue