From fac32bd41dce00c86c1aa81aea9198250e4747ba Mon Sep 17 00:00:00 2001 From: modeco80 Date: Wed, 4 Dec 2024 03:20:06 -0500 Subject: [PATCH] Revert "replace video with letsplay_av_ffmpeg" Unfortunately, I think this was a bit too brash of a move to make for right now. This reverts commit 0eb2aaa8c12c441ab4a58be760638b5344c76d8c. --- server/Cargo.lock | 299 +++++++++++------- server/Cargo.toml | 4 +- server/src/main.rs | 21 +- server/src/retro_thread.rs | 14 +- server/src/video/cuda_gl/bindgen.sh | 27 ++ server/src/video/cuda_gl/gl.h | 1 + server/src/video/cuda_gl/mod.rs | 15 + server/src/video/cuda_gl/safe.rs | 151 +++++++++ server/src/video/cuda_gl/sys.rs | 73 +++++ server/src/video/encoder_thread.rs | 471 ++++++++++++++++++++++++++++ server/src/video/h264_encoder.rs | 354 +++++++++++++++++++++ server/src/video/hwdevice.rs | 88 ++++++ server/src/video/hwframe.rs | 121 +++++++ server/src/video/mod.rs | 22 ++ 14 files changed, 1520 insertions(+), 141 deletions(-) create mode 100755 server/src/video/cuda_gl/bindgen.sh create mode 100644 server/src/video/cuda_gl/gl.h create mode 100644 server/src/video/cuda_gl/mod.rs create mode 100644 server/src/video/cuda_gl/safe.rs create mode 100644 server/src/video/cuda_gl/sys.rs create mode 100644 server/src/video/encoder_thread.rs create mode 100644 server/src/video/h264_encoder.rs create mode 100644 server/src/video/hwdevice.rs create mode 100644 server/src/video/hwframe.rs create mode 100644 server/src/video/mod.rs diff --git a/server/Cargo.lock b/server/Cargo.lock index 2598662..adb5d8e 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -4,18 +4,18 @@ version = 3 [[package]] name = "addr2line" -version = "0.24.2" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] [[package]] -name = "adler2" -version = "2.0.0" +name = "adler" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.89" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "async-trait" @@ -45,15 +45,15 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.4.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "axum" -version = "0.7.7" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" dependencies = [ "async-trait", "axum-core", @@ -89,9 +89,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.5" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" dependencies = [ "async-trait", "bytes", @@ -102,7 +102,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper 1.0.1", + "sync_wrapper 0.1.2", "tower-layer", "tower-service", "tracing", @@ -110,10 +110,11 @@ dependencies = [ [[package]] name = "axum-macros" -version = "0.4.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" dependencies = [ + "heck", "proc-macro2", "quote", "syn", @@ -121,30 +122,30 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", + "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", - "windows-targets", ] [[package]] name = "base64" -version = "0.22.1" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "bindgen" -version = "0.69.5" +version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ "bitflags", "cexpr", @@ -183,15 +184,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.2" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "cc" -version = "1.1.30" +version = "1.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" +checksum = "e9d013ecb737093c0e86b151a7b837993cf9ec6c502946cfb44bedc392421e0b" dependencies = [ "shlex", ] @@ -224,9 +225,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.14" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" dependencies = [ "libc", ] @@ -318,9 +319,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -333,9 +334,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -343,15 +344,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -360,15 +361,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", @@ -377,21 +378,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -428,9 +429,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "gl" @@ -464,6 +465,12 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -506,9 +513,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.5" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "httpdate" @@ -518,9 +525,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.5.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", @@ -537,9 +544,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.9" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" +checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" dependencies = [ "bytes", "futures-util", @@ -548,7 +555,16 @@ dependencies = [ "hyper", "pin-project-lite", "tokio", - "tower-service", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", ] [[package]] @@ -594,20 +610,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" -[[package]] -name = "letsplay_av_ffmpeg" -version = "0.1.0" -dependencies = [ - "anyhow", - "cudarc", - "ffmpeg-next", - "gl", - "letsplay_gpu", - "libloading", - "tokio", - "tracing", -] - [[package]] name = "letsplay_gpu" version = "0.1.0" @@ -618,9 +620,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.159" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libloading" @@ -683,11 +685,11 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ - "adler2", + "adler", ] [[package]] @@ -734,18 +736,18 @@ dependencies = [ [[package]] name = "object" -version = "0.36.5" +version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.20.2" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "overload" @@ -782,6 +784,26 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -796,9 +818,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "ppv-lite86" @@ -811,9 +833,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.88" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -859,18 +881,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.11.0" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", @@ -880,9 +902,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", @@ -891,9 +913,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "retro_frontend" @@ -903,12 +925,19 @@ dependencies = [ "libc", "libloading", "libretro-sys", + "rgb565", "serde", "thiserror", "toml", "tracing", ] +[[package]] +name = "rgb565" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43e85498d0bb728f77a88b4313eaf4ed21673f3f8a05c36e835cf6c9c0d066" + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -923,9 +952,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustversion" -version = "1.0.18" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" @@ -941,18 +970,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", @@ -1064,9 +1093,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.79" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -1087,18 +1116,18 @@ checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", @@ -1115,6 +1144,21 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.40.0" @@ -1146,9 +1190,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.24.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" dependencies = [ "futures-util", "log", @@ -1192,14 +1236,14 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.1" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", + "pin-project", "pin-project-lite", - "sync_wrapper 0.1.2", "tokio", "tower-layer", "tower-service", @@ -1278,9 +1322,9 @@ dependencies = [ [[package]] name = "tungstenite" -version = "0.24.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" dependencies = [ "byteorder", "bytes", @@ -1291,6 +1335,7 @@ dependencies = [ "rand", "sha1", "thiserror", + "url", "utf-8", ] @@ -1301,10 +1346,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] -name = "unicode-ident" -version = "1.0.13" +name = "unicode-bidi" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] [[package]] name = "utf-8" @@ -1338,10 +1409,10 @@ dependencies = [ "async-trait", "axum", "cudarc", + "ffmpeg-next", "futures", "futures-util", "gl", - "letsplay_av_ffmpeg", "letsplay_gpu", "libloading", "rand", diff --git a/server/Cargo.toml b/server/Cargo.toml index 3198be6..875e15a 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,6 @@ anyhow = "1.0.86" # Libretro Sex letsplay_gpu.path = "/home/lily/source/lets-play/crates/letsplay_gpu" retro_frontend.path = "/home/lily/source/lets-play/crates/retro_frontend" -letsplay_av_ffmpeg.path = "/home/lily/source/lets-play/crates/letsplay_av_ffmpeg" gl = "0.14.0" # async @@ -21,7 +20,8 @@ axum = { version = "0.7.5", features = ["ws", "macros"] } futures = "0.3" futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } - +# ffmpeg +ffmpeg = { version = "7.0.0", package = "ffmpeg-next" } # misc stuff rand = "0.8.5" diff --git a/server/src/main.rs b/server/src/main.rs index d506f1a..498607d 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,9 +1,7 @@ mod retro_thread; mod surface; mod types; -//mod video; - -use letsplay_av_ffmpeg as video; +mod video; mod transport; @@ -179,7 +177,7 @@ async fn main() -> anyhow::Result<()> { let resource = Arc::new(Mutex::new(GraphicsResource::new(&device))); - let (mut encoder_rx, encoder_tx) = video::encoder_thread::hardware_frame::spawn( + let (mut encoder_rx, encoder_tx) = encoder_thread::encoder_thread_spawn_hwframe( &device.clone(), &resource.clone(), &egl_ctx.clone(), @@ -207,24 +205,13 @@ async fn main() -> anyhow::Result<()> { )); */ - /* let _ = retro_input_event_tx.blocking_send(retro_thread::RetroInEvent::LoadCore( - "cores/pcsx2_new_libretro.so".into(), + "cores/pcsx2_libretro.so".into(), )); let _ = retro_input_event_tx.blocking_send(retro_thread::RetroInEvent::LoadGame( - "/data/sda/lily/ISOs/Sony PlayStation 2/Jonny Moseley Mad Trix (USA).bin".into(), + "/data/sda/lily/ISOs/Sony PlayStation 2/ztx-hl.bin".into(), )); - */ - let _ = retro_input_event_tx.blocking_send(retro_thread::RetroInEvent::LoadCore( - "cores/swanstation_libretro.so".into(), - )); - let _ = retro_input_event_tx.blocking_send(retro_thread::RetroInEvent::LoadGame( - // "/data/sda/lily/ISOs/Nintendo GameCube/Dave Mirra Freestyle BMX 2 (USA).ciso".into(), -// "roms/smb1.nes".into(), - "roms/merged/nmv2/jagb/nmv2jagb.cue".into(), - )); - // start the libretro thread looping now that we're alive let _ = retro_input_event_tx.blocking_send(retro_thread::RetroInEvent::Start); diff --git a/server/src/retro_thread.rs b/server/src/retro_thread.rs index a9cbb7d..9be7211 100644 --- a/server/src/retro_thread.rs +++ b/server/src/retro_thread.rs @@ -17,9 +17,7 @@ use retro_frontend::{ use gpu::egl_helpers::DeviceContext; use letsplay_gpu as gpu; -use letsplay_av_ffmpeg::types::*; - -use crate::{surface::Surface, video::cuda_gl::safe::GraphicsResource}; +use crate::{surface::Surface, types::Size, video::cuda_gl::safe::GraphicsResource}; /// Called by OpenGL. We use this to dump errors. extern "system" fn opengl_message_callback( @@ -185,19 +183,19 @@ impl RetroState { let size = framebuffer.size.clone(); let buffer = framebuffer.get_buffer(); - //let has_disconnected_pitch = pitch != size.width as u32; + let has_disconnected_pitch = pitch != size.width as u32; for _y in 0..size.height { // Trick to flip the buffer the way OpenGL likes. let y = (size.height - 1) - _y; let src_line_off = (y as u32 * pitch) as usize; - let dest_line_off = (_y as u32 * size.width) as usize; + let mut dest_line_off = (_y as u32 * size.width) as usize; // copy only - //if has_disconnected_pitch { + if has_disconnected_pitch { //dest_line_off = (y * pitch) as usize; - //} + } // Create slices repressenting each part let src_slice = &slice[src_line_off..src_line_off + size.width as usize]; @@ -219,7 +217,7 @@ impl FrontendInterface for RetroState { self.get_frontend().set_gl_fbo(raw); if !self.gl_rendering { - self.software_framebuffer.resize(crate::types::Size { width, height }); + self.software_framebuffer.resize(Size { width, height }); } // register the FBO's texture to our cuda interop resource diff --git a/server/src/video/cuda_gl/bindgen.sh b/server/src/video/cuda_gl/bindgen.sh new file mode 100755 index 0000000..1f9dade --- /dev/null +++ b/server/src/video/cuda_gl/bindgen.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Does bindgen for CUDA cudaGL. (needs postprocessing) +set -exu + +# --allowlist-type="^CU.*" \ +# --allowlist-type="^cuuint(32|64)_t" \ +# --allowlist-type="^cudaError_enum" \ +# --allowlist-type="^cu.*Complex$" \ +# --allowlist-type="^cuda.*" \ +# --allowlist-type="^libraryPropertyType.*" \ +# --allowlist-var="^CU.*" \ + +echo "use cudarc::sys::*; /* Hack :3 */" > ./sys.rs + +bindgen \ + --allowlist-type="" \ + --allowlist-function="^cuGraphicsGL.*" \ + --default-enum-style=rust \ + --no-doc-comments \ + --with-derive-default \ + --with-derive-eq \ + --with-derive-hash \ + --with-derive-ord \ + --use-core \ + --dynamic-loading Lib \ + gl.h -- -I/opt/cuda/include \ + >> ./sys.rs \ No newline at end of file diff --git a/server/src/video/cuda_gl/gl.h b/server/src/video/cuda_gl/gl.h new file mode 100644 index 0000000..31e1454 --- /dev/null +++ b/server/src/video/cuda_gl/gl.h @@ -0,0 +1 @@ +#include "cudaGL.h" \ No newline at end of file diff --git a/server/src/video/cuda_gl/mod.rs b/server/src/video/cuda_gl/mod.rs new file mode 100644 index 0000000..c8781b4 --- /dev/null +++ b/server/src/video/cuda_gl/mod.rs @@ -0,0 +1,15 @@ +#[allow(non_snake_case)] +pub mod sys; +use sys::*; + +pub mod safe; + +pub unsafe fn lib() -> &'static Lib { + static LIB: std::sync::OnceLock = std::sync::OnceLock::new(); + LIB.get_or_init(|| { + if let Ok(lib) = Lib::new(libloading::library_filename("cuda")) { + return lib; + } + panic!("cuda library doesn't exist."); + }) +} \ No newline at end of file diff --git a/server/src/video/cuda_gl/safe.rs b/server/src/video/cuda_gl/safe.rs new file mode 100644 index 0000000..6064104 --- /dev/null +++ b/server/src/video/cuda_gl/safe.rs @@ -0,0 +1,151 @@ +use cudarc::driver::{result as cuda_result, sys as cuda_sys, CudaDevice}; + +use std::sync::Arc; + +pub struct MappedGraphicsResource { + resource: cuda_sys::CUgraphicsResource, +} + +impl MappedGraphicsResource { + fn new(resource: cuda_sys::CUgraphicsResource) -> Self { + Self { resource } + } + + pub fn map(&mut self) -> Result<(), cuda_result::DriverError> { + unsafe { + cuda_sys::lib() + .cuGraphicsMapResources(1, &mut self.resource, std::ptr::null_mut()) + .result()?; + } + Ok(()) + } + + pub fn unmap(&mut self) -> Result<(), cuda_result::DriverError> { + unsafe { + cuda_sys::lib() + .cuGraphicsUnmapResources(1, &mut self.resource, std::ptr::null_mut()) + .result()?; + } + + Ok(()) + } + + pub fn get_mapped_array(&mut self) -> Result { + assert!( + !self.resource.is_null(), + "do not call GraphicsResource::get_mapped_array if no resource is actually registered" + ); + + let mut array: cuda_sys::CUarray = std::ptr::null_mut(); + + unsafe { + cuda_sys::lib() + .cuGraphicsSubResourceGetMappedArray(&mut array, self.resource, 0, 0) + .result()?; + } + + Ok(array) + } + + pub fn get_device_pointer( + &mut self, + ) -> Result { + assert!( + !self.resource.is_null(), + "do not call GraphicsResource::get_mapped_array if no resource is actually registered" + ); + + let mut array: cuda_sys::CUdeviceptr = 0; + let mut size: usize = 0; + + unsafe { + cuda_sys::lib() + .cuGraphicsResourceGetMappedPointer_v2(&mut array, &mut size, self.resource) + .result()?; + } + + Ok(array) + } +} + +impl Drop for MappedGraphicsResource { + fn drop(&mut self) { + let _ = self.unmap(); + } +} + +/// Wrapper over cuGraphicsGL* apis +pub struct GraphicsResource { + context: Arc, + resource: cuda_sys::CUgraphicsResource, +} + +impl GraphicsResource { + pub fn new(device: &Arc) -> Self { + Self { + context: device.clone(), + resource: std::ptr::null_mut(), + } + } + + pub fn device(&self) -> Arc { + self.context.clone() + } + + /// Maps this resource. + pub fn map(&mut self) -> Result { + let mut res = MappedGraphicsResource::new(self.resource); + res.map()?; + + Ok(res) + } + + pub fn register( + &mut self, + texture_id: gl::types::GLuint, + texture_kind: gl::types::GLuint, + ) -> Result<(), cuda_result::DriverError> { + // better to be safe than leak memory? idk. + if !self.resource.is_null() { + self.unregister()?; + } + + unsafe { + super::lib() + .cuGraphicsGLRegisterImage(&mut self.resource, texture_id, texture_kind, 1) + .result()?; + } + + Ok(()) + } + + pub fn is_registered(&self) -> bool { + !self.resource.is_null() + } + + pub fn unregister(&mut self) -> Result<(), cuda_result::DriverError> { + assert!( + !self.resource.is_null(), + "do not call if no resource is actually registered" + ); + + unsafe { + cuda_sys::lib() + .cuGraphicsUnregisterResource(self.resource) + .result()?; + } + + self.resource = std::ptr::null_mut(); + Ok(()) + } +} + +impl Drop for GraphicsResource { + fn drop(&mut self) { + if self.is_registered() { + let _ = self.unregister(); + } + } +} + +unsafe impl Send for GraphicsResource {} diff --git a/server/src/video/cuda_gl/sys.rs b/server/src/video/cuda_gl/sys.rs new file mode 100644 index 0000000..f0c3313 --- /dev/null +++ b/server/src/video/cuda_gl/sys.rs @@ -0,0 +1,73 @@ +use cudarc::driver::sys::*; /* Hack :3 */ +use gl::types::{GLenum, GLuint}; +/* automatically generated by rust-bindgen 0.69.4 */ + +pub struct Lib { + __library: ::libloading::Library, + pub cuGraphicsGLRegisterBuffer: Result< + unsafe extern "C" fn( + pCudaResource: *mut CUgraphicsResource, + buffer: GLuint, + Flags: ::core::ffi::c_uint, + ) -> CUresult, + ::libloading::Error, + >, + pub cuGraphicsGLRegisterImage: Result< + unsafe extern "C" fn( + pCudaResource: *mut CUgraphicsResource, + image: GLuint, + target: GLenum, + Flags: ::core::ffi::c_uint, + ) -> CUresult, + ::libloading::Error, + >, +} +impl Lib { + pub unsafe fn new

(path: P) -> Result + where + P: AsRef<::std::ffi::OsStr>, + { + let library = ::libloading::Library::new(path)?; + Self::from_library(library) + } + pub unsafe fn from_library(library: L) -> Result + where + L: Into<::libloading::Library>, + { + let __library = library.into(); + let cuGraphicsGLRegisterBuffer = __library + .get(b"cuGraphicsGLRegisterBuffer\0") + .map(|sym| *sym); + let cuGraphicsGLRegisterImage = __library + .get(b"cuGraphicsGLRegisterImage\0") + .map(|sym| *sym); + Ok(Lib { + __library, + cuGraphicsGLRegisterBuffer, + cuGraphicsGLRegisterImage, + }) + } + pub unsafe fn cuGraphicsGLRegisterBuffer( + &self, + pCudaResource: *mut CUgraphicsResource, + buffer: GLuint, + Flags: ::core::ffi::c_uint, + ) -> CUresult { + (self + .cuGraphicsGLRegisterBuffer + .as_ref() + .expect("Expected function, got error."))(pCudaResource, buffer, Flags) + } + pub unsafe fn cuGraphicsGLRegisterImage( + &self, + pCudaResource: *mut CUgraphicsResource, + image: GLuint, + target: GLenum, + Flags: ::core::ffi::c_uint, + ) -> CUresult { + (self + .cuGraphicsGLRegisterImage + .as_ref() + .expect("Expected function, got error."))(pCudaResource, image, target, Flags) + } +} diff --git a/server/src/video/encoder_thread.rs b/server/src/video/encoder_thread.rs new file mode 100644 index 0000000..c3f5fcc --- /dev/null +++ b/server/src/video/encoder_thread.rs @@ -0,0 +1,471 @@ +use anyhow::Context; +use cudarc::{ + driver::{ + sys::{CUdeviceptr, CUmemorytype}, + CudaDevice, CudaSlice, DevicePtr, LaunchAsync, + }, + nvrtc::CompileOptions, +}; +use letsplay_gpu::egl_helpers::DeviceContext; +use std::{ + sync::{Arc, Mutex}, + time::Duration, +}; +use tokio::sync::mpsc::{self, error::TryRecvError}; + +use super::h264_encoder::H264Encoder; +use super::{cuda_gl::safe::GraphicsResource, ffmpeg}; + +pub enum EncodeThreadInput { + Init { size: crate::types::Size }, + ForceKeyframe, + SendFrame, +} + +#[derive(Clone)] +pub enum EncodeThreadOutput { + Frame { packet: ffmpeg::Packet }, +} + +struct EncoderState { + encoder: Option, + frame: Arc>>, + packet: ffmpeg::Packet, +} + +impl EncoderState { + fn new(frame: Arc>>) -> Self { + Self { + encoder: None, + frame: frame, + packet: ffmpeg::Packet::empty(), + } + } + + fn init(&mut self, size: crate::types::Size) -> anyhow::Result<()> { + self.encoder = Some(H264Encoder::new_nvenc_swframe( + size.clone(), + 60, + 2 * (1024 * 1024), + )?); + + // replace packet + self.packet = ffmpeg::Packet::empty(); + + Ok(()) + } + + //fn frame(&mut self) -> Arc>> { + // self.frame.clone() + //} + + fn send_frame(&mut self, pts: u64, force_keyframe: bool) -> Option { + let mut lk = self.frame.lock().expect("fuck"); + let frame = lk.as_mut().unwrap(); + let encoder = self.encoder.as_mut().unwrap(); + + // set frame metadata + unsafe { + if force_keyframe { + (*frame.as_mut_ptr()).pict_type = ffmpeg::sys::AVPictureType::AV_PICTURE_TYPE_I; + (*frame.as_mut_ptr()).flags = ffmpeg::sys::AV_FRAME_FLAG_KEY; + (*frame.as_mut_ptr()).key_frame = 1; + } else { + (*frame.as_mut_ptr()).pict_type = ffmpeg::sys::AVPictureType::AV_PICTURE_TYPE_NONE; + (*frame.as_mut_ptr()).flags = 0i32; + (*frame.as_mut_ptr()).key_frame = 0; + } + + (*frame.as_mut_ptr()).pts = pts as i64; + } + + encoder.send_frame(&*frame); + encoder + .receive_packet(&mut self.packet) + .expect("Failed to recieve packet"); + + unsafe { + if !self.packet.is_empty() { + return Some(self.packet.clone()); + } + } + + return None; + } +} + +struct EncoderStateHW { + encoder: Option, + frame: ffmpeg::frame::Video, + packet: ffmpeg::Packet, +} + +impl EncoderStateHW { + fn new() -> Self { + Self { + encoder: None, + frame: ffmpeg::frame::Video::empty(), + packet: ffmpeg::Packet::empty(), + } + } + + fn init(&mut self, device: &Arc, size: crate::types::Size) -> anyhow::Result<()> { + self.encoder = Some(H264Encoder::new_nvenc_hwframe( + &device, + size.clone(), + 60, + 2 * (1024 * 1024), + )?); + + // replace packet + self.packet = ffmpeg::Packet::empty(); + self.frame = self.encoder.as_mut().unwrap().create_frame()?; + + Ok(()) + } + + #[inline] + fn frame(&mut self) -> &mut ffmpeg::frame::Video { + &mut self.frame + } + + fn send_frame(&mut self, pts: u64, force_keyframe: bool) -> Option { + let frame = &mut self.frame; + let encoder = self.encoder.as_mut().unwrap(); + + // set frame metadata + unsafe { + if force_keyframe { + (*frame.as_mut_ptr()).pict_type = ffmpeg::sys::AVPictureType::AV_PICTURE_TYPE_I; + (*frame.as_mut_ptr()).flags = ffmpeg::sys::AV_FRAME_FLAG_KEY; + (*frame.as_mut_ptr()).key_frame = 1; + } else { + (*frame.as_mut_ptr()).pict_type = ffmpeg::sys::AVPictureType::AV_PICTURE_TYPE_NONE; + (*frame.as_mut_ptr()).flags = 0i32; + (*frame.as_mut_ptr()).key_frame = 0; + } + + (*frame.as_mut_ptr()).pts = pts as i64; + } + + encoder.send_frame(&*frame); + encoder + .receive_packet(&mut self.packet) + .expect("Failed to recieve packet"); + + unsafe { + if !self.packet.is_empty() { + return Some(self.packet.clone()); + } + } + + return None; + } +} + +fn encoder_thread_swframe_main( + mut rx: mpsc::Receiver, + tx: mpsc::Sender, + frame: &Arc>>, +) -> anyhow::Result<()> { + // FIXME: for HW frame support + //let dev = cudarc::driver::CudaDevice::new(0)?; + + let mut frame_number = 0u64; + let mut force_keyframe = false; + + let mut encoder = EncoderState::new(frame.clone()); + + loop { + match rx.try_recv() { + Ok(msg) => match msg { + EncodeThreadInput::Init { size } => { + frame_number = 0; + + if force_keyframe { + force_keyframe = false; + } + + encoder.init(size).expect("encoder init failed"); + } + + EncodeThreadInput::ForceKeyframe => { + force_keyframe = true; + } + + EncodeThreadInput::SendFrame => { + if let Some(pkt) = encoder.send_frame(frame_number as u64, force_keyframe) { + // A bit less clear than ::empty(), but it's "Safe" + if let Some(_) = pkt.data() { + let _ = tx.blocking_send(EncodeThreadOutput::Frame { + packet: pkt.clone(), + }); + } + + frame_number += 1; + } + + if force_keyframe { + force_keyframe = false; + } + } + }, + + Err(TryRecvError::Disconnected) => break, + Err(TryRecvError::Empty) => { + std::thread::sleep(Duration::from_millis(1)); + } + } + } + + Ok(()) +} + +pub fn encoder_thread_spawn_swframe( + frame: &Arc>>, +) -> ( + mpsc::Receiver, + mpsc::Sender, +) { + let (in_tx, in_rx) = mpsc::channel(1); + let (out_tx, out_rx) = mpsc::channel(1); + + let clone = Arc::clone(frame); + + std::thread::spawn(move || encoder_thread_swframe_main(in_rx, out_tx, &clone)); + + (out_rx, in_tx) +} + +/// Source for the kernel used to flip OpenGL framebuffers right-side up. +const OPENGL_FLIP_KERNEL_SRC: &str = " +extern \"C\" __global__ void flip_opengl( + const unsigned* pSrc, + unsigned* pDest, + int width, + int height +) { + const unsigned x = blockIdx.x * blockDim.x + threadIdx.x; + const unsigned y = blockIdx.y * blockDim.y + threadIdx.y; + + if (x < width && y < height) { + unsigned reversed_y = (height - 1) - y; + ((unsigned*)pDest)[y * width + x] = ((unsigned*)pSrc)[reversed_y * width + x]; + } +}"; + +fn encoder_thread_hwframe_main( + mut rx: mpsc::Receiver, + tx: mpsc::Sender, + + cuda_device: &Arc, + cuda_resource: &Arc>, + gl_context: &Arc>, +) -> anyhow::Result<()> { + let mut frame_number = 0u64; + let mut force_keyframe = false; + + let mut encoder = EncoderStateHW::new(); + + // :) + cuda_device.bind_to_thread()?; + + // Compile the support kernel + let ptx = cudarc::nvrtc::compile_ptx_with_opts( + &OPENGL_FLIP_KERNEL_SRC, + CompileOptions { + //options: vec!["--gpu-architecture=compute_50".into()], + ..Default::default() + }, + ) + .with_context(|| "compiling support kernel")?; + + // pop it in + cuda_device.load_ptx(ptx, "module", &["flip_opengl"])?; + + let mut memcpy = cudarc::driver::sys::CUDA_MEMCPY2D_st::default(); + + // setup the things that won't change about the cuda memcpy + + // src + memcpy.srcXInBytes = 0; + memcpy.srcY = 0; + memcpy.srcMemoryType = CUmemorytype::CU_MEMORYTYPE_ARRAY; + + // dest + memcpy.dstXInBytes = 0; + memcpy.dstY = 0; + memcpy.dstMemoryType = CUmemorytype::CU_MEMORYTYPE_DEVICE; + + // Temporary buffer used for opengl flip on the GPU. We copy to this buffer, + // then copy the flipped version (using the launched support kernel) to the CUDA device memory ffmpeg + // allocated. + let mut temp_buffer: CudaSlice = cuda_device.alloc_zeros::(48).expect("over"); + + loop { + match rx.blocking_recv() { + Some(msg) => match msg { + EncodeThreadInput::Init { size } => { + frame_number = 0; + + if force_keyframe { + force_keyframe = false; + } + + temp_buffer = cuda_device + .alloc_zeros::((size.width * size.height) as usize) + .expect("oh youre fucked anyways"); + + encoder + .init(cuda_device, size) + .expect("encoder init failed"); + } + + EncodeThreadInput::ForceKeyframe => { + force_keyframe = true; + } + + EncodeThreadInput::SendFrame => { + // benchmarking + //use std::time::Instant; + //let start = Instant::now(); + + // copy gl frame *ON THE GPU* to ffmpeg frame + { + let gl_ctx = gl_context.lock().expect("you dumb fuck"); + let mut gl_resource = + cuda_resource.lock().expect("couldnt lock GL resource!"); + + gl_ctx.make_current(); + + let mut mapped = gl_resource + .map() + .expect("couldnt map graphics resource. Its joever"); + + let array = mapped + .get_mapped_array() + .expect("well its all over anyways"); + + let frame = encoder.frame(); + + // setup the cuMemcpy2D operation to copy to the temporary buffer + // (we should probably abstract source and provide a way to elide this, + // and instead feed ffmpeg directly. for now it's *just* used with gl so /shrug) + { + memcpy.srcArray = array; + + unsafe { + let frame_ptr = frame.as_mut_ptr(); + memcpy.dstDevice = temp_buffer.device_ptr().clone(); + memcpy.dstPitch = (*frame_ptr).linesize[0] as usize; + memcpy.WidthInBytes = ((*frame_ptr).width * 4) as usize; + memcpy.Height = (*frame_ptr).height as usize; + } + } + + // copy to the temporary buffer and synchronize + unsafe { + cudarc::driver::sys::lib() + .cuMemcpy2DAsync_v2(&memcpy, std::ptr::null_mut()) + .result() + .expect("cuMemcpy2D fail epic"); + + cudarc::driver::sys::lib() + .cuStreamSynchronize(std::ptr::null_mut()) + .result()?; + } + + // launch kernel to flip the opengl framebuffer right-side up + { + let width = frame.width(); + let height = frame.height(); + + let launch_config = cudarc::driver::LaunchConfig { + grid_dim: (width / 16 + 1, height / 2 + 1, 1), + block_dim: (16, 2, 1), + shared_mem_bytes: 0, + }; + + let flip_opengl = cuda_device.get_func("module", "flip_opengl").expect( + "for some reason we couldn't get the support kenrel function", + ); + + unsafe { + let frame_ptr = frame.as_mut_ptr(); + + let mut slice = cuda_device.upgrade_device_ptr::( + (*frame_ptr).data[0] as CUdeviceptr, + (width * height) as usize * 4usize, + ); + + flip_opengl.launch( + launch_config, + (&mut temp_buffer, &mut slice, width, height), + )?; + + // leak so it doesn't free the memory + // (the device pointer we convert into a slice is owned by ffmpeg, so we shouldn't be the ones + // trying to free it!) + let _ = slice.leak(); + + // Synchronize for the final time + cudarc::driver::sys::lib() + .cuStreamSynchronize(std::ptr::null_mut()) + .result()?; + } + } + + // FIXME: ideally this would work on-drop but it doesn't. + mapped.unmap().expect("fuck you asshole"); + gl_ctx.release(); + } + + if let Some(pkt) = encoder.send_frame(frame_number as u64, force_keyframe) { + // A bit less clear than ::empty(), but it's "Safe" + if let Some(_) = pkt.data() { + let _ = tx.blocking_send(EncodeThreadOutput::Frame { + packet: pkt.clone(), + }); + } + + frame_number += 1; + } + + if force_keyframe { + force_keyframe = false; + } + + //tracing::info!("encoding frame {frame_number} took {:2?}", start.elapsed()); + } + }, + + None => break, + } + } + + //std::thread::sleep(Duration::from_millis(1)); + + Ok(()) +} + +pub fn encoder_thread_spawn_hwframe( + cuda_device: &Arc, + cuda_resource: &Arc>, + gl_context: &Arc>, +) -> ( + mpsc::Receiver, + mpsc::Sender, +) { + let (in_tx, in_rx) = mpsc::channel(1); + let (out_tx, out_rx) = mpsc::channel(1); + + let dev_clone = Arc::clone(cuda_device); + let rsrc_clone = Arc::clone(cuda_resource); + let gl_clone = Arc::clone(gl_context); + + std::thread::spawn(move || { + encoder_thread_hwframe_main(in_rx, out_tx, &dev_clone, &rsrc_clone, &gl_clone) + }); + + (out_rx, in_tx) +} diff --git a/server/src/video/h264_encoder.rs b/server/src/video/h264_encoder.rs new file mode 100644 index 0000000..aa9875a --- /dev/null +++ b/server/src/video/h264_encoder.rs @@ -0,0 +1,354 @@ +use super::ffmpeg; +use super::hwframe::HwFrameContext; +use anyhow::Context; +use cudarc::driver::CudaDevice; +use ffmpeg::error::EAGAIN; + +use ffmpeg::codec as lavc; // lavc + +use crate::types::Size; + +/// this is required for libx264 to like. Work +pub fn create_context_from_codec(codec: ffmpeg::Codec) -> Result { + unsafe { + let context = ffmpeg::sys::avcodec_alloc_context3(codec.as_ptr()); + if context.is_null() { + return Err(ffmpeg::Error::Unknown); + } + + let context = lavc::Context::wrap(context, None); + Ok(context) + } +} + +fn create_context_and_set_common_parameters( + codec: &str, + size: &Size, + max_framerate: u32, + bitrate: usize, +) -> anyhow::Result<(ffmpeg::Codec, ffmpeg::encoder::video::Video)> { + let encoder = ffmpeg::encoder::find_by_name(codec) + .expect(&format!("could not find the codec \"{codec}\"")); + + let mut video_encoder_context = create_context_from_codec(encoder)?.encoder().video()?; + + video_encoder_context.set_width(size.width); + video_encoder_context.set_height(size.height); + video_encoder_context.set_frame_rate(Some(ffmpeg::Rational(1, max_framerate as i32))); + + video_encoder_context.set_bit_rate(bitrate / 4); + video_encoder_context.set_max_bit_rate(bitrate); + + // qp TODO: + //video_encoder_context.set_qmax(30); + //video_encoder_context.set_qmin(35); + + video_encoder_context.set_time_base(ffmpeg::Rational(1, max_framerate as i32).invert()); + video_encoder_context.set_format(ffmpeg::format::Pixel::YUV420P); + + // The GOP here is setup to balance keyframe retransmission with bandwidth. + //video_encoder_context.set_gop((max_framerate * 4) as u32); + video_encoder_context.set_gop(i32::MAX as u32); + video_encoder_context.set_max_b_frames(0); + + unsafe { + (*video_encoder_context.as_mut_ptr()).delay = 0; + (*video_encoder_context.as_mut_ptr()).refs = 0; + } + + Ok((encoder, video_encoder_context)) +} + +/// A simple H.264 encoder. Currently software only, however +/// pieces are being put in place to eventually allow HW encoding. +pub enum H264Encoder { + Software { + encoder: ffmpeg::encoder::video::Encoder, + }, + + /// Hardware encoding, with frames uploaded to the GPU by ffmpeg. + NvencSWFrame { + encoder: ffmpeg::encoder::video::Encoder, + }, + + /// Hardware encoding, with frames already on the GPU. + NvencHWFrame { + encoder: ffmpeg::encoder::video::Encoder, + hw_context: HwFrameContext, + }, +} + +impl H264Encoder { + /// Creates a new software encoder. + pub fn new_software(size: Size, max_framerate: u32, bitrate: usize) -> anyhow::Result { + // Create the libx264 context + let (encoder, mut video_encoder_context) = + create_context_and_set_common_parameters("libx264", &size, max_framerate, bitrate)?; + + video_encoder_context.set_format(ffmpeg::format::Pixel::YUV420P); + + let threads = std::thread::available_parallelism().expect("ggg").get() / 8; + + // FIXME: tracing please. + println!("H264Encoder::new_software(): Using {threads} threads to encode"); + + // Frame-level threading causes [N] frames of latency + // so we use slice-level threading to reduce the latency + // as much as possible while still allowing threading + video_encoder_context.set_threading(ffmpeg::threading::Config { + kind: ffmpeg::threading::Type::Slice, + count: threads, + }); + + // Set libx264 applicable dictionary options + let mut dict = ffmpeg::Dictionary::new(); + dict.set("tune", "zerolatency"); + dict.set("preset", "veryfast"); + + // This could probably be moved but then it would mean returning the dictionary too + // which is fine I guess it just seems a bit rickity + dict.set("profile", "main"); + + // TODO: + dict.set("crf", "43"); + dict.set("crf_max", "48"); + + dict.set("forced-idr", "1"); + + let encoder = video_encoder_context + .open_as_with(encoder, dict) + .with_context(|| "While opening x264 video codec")?; + + Ok(Self::Software { encoder: encoder }) + } + + /// Creates a new hardware (NVIDIA NVENC) encoder, which encodes + /// frames from software input. FFmpeg handles uploading frames to the GPU. + pub fn new_nvenc_swframe( + size: Size, + max_framerate: u32, + bitrate: usize, + ) -> anyhow::Result { + let (encoder, mut video_encoder_context) = + create_context_and_set_common_parameters("h264_nvenc", &size, max_framerate, bitrate) + .with_context(|| "while trying to create encoder")?; + + video_encoder_context.set_format(ffmpeg::format::Pixel::ZRGB32); + + video_encoder_context.set_qmin(37); + video_encoder_context.set_qmax(33); + + // set h264_nvenc options + let mut dict = ffmpeg::Dictionary::new(); + + dict.set("tune", "ull"); + dict.set("preset", "p1"); + + dict.set("profile", "main"); + + // TODO: + dict.set("rc", "vbr"); + dict.set("qp", "35"); + + dict.set("forced-idr", "1"); + + // damn you + dict.set("delay", "0"); + dict.set("zerolatency", "1"); + + let encoder = video_encoder_context + .open_as_with(encoder, dict) + .with_context(|| "While opening h264_nvenc video codec")?; + + Ok(Self::NvencSWFrame { encoder: encoder }) + } + + /// Creates a new hardware (NVIDIA NVENC) encoder, which encodes + /// frames from GPU memory, via CUDA. + /// FFmpeg handles uploading frames to the GPU. + /// You are expected to handle uploading or otherwise working with a frame on the GPU. + pub fn new_nvenc_hwframe( + cuda_device: &CudaDevice, + size: Size, + max_framerate: u32, + bitrate: usize, + ) -> anyhow::Result { + let cuda_device_context = super::hwdevice::CudaDeviceContextBuilder::new()? + .set_cuda_context((*cuda_device.cu_primary_ctx()) as *mut _) + .build() + .with_context(|| "while trying to create CUDA device context")?; + + let mut hw_frame_context = super::hwframe::HwFrameContextBuilder::new(cuda_device_context)? + .set_width(size.width) + .set_height(size.height) + .set_sw_format(ffmpeg::format::Pixel::ZBGR32) + .set_format(ffmpeg::format::Pixel::CUDA) + .build() + .with_context(|| "while trying to create CUDA frame context")?; + + let (encoder, mut video_encoder_context) = + create_context_and_set_common_parameters("h264_nvenc", &size, max_framerate, bitrate) + .with_context(|| "while trying to create encoder")?; + + video_encoder_context.set_format(ffmpeg::format::Pixel::CUDA); + + video_encoder_context.set_qmin(35); + video_encoder_context.set_qmax(38); + + unsafe { + // FIXME: this currently breaks the avbufferref system a bit + (*video_encoder_context.as_mut_ptr()).hw_frames_ctx = + ffmpeg::sys::av_buffer_ref(hw_frame_context.as_raw_mut()); + (*video_encoder_context.as_mut_ptr()).hw_device_ctx = + ffmpeg::sys::av_buffer_ref(hw_frame_context.as_device_context_mut()); + } + + // set h264_nvenc options + let mut dict = ffmpeg::Dictionary::new(); + + dict.set("tune", "ull"); + dict.set("preset", "p1"); + + dict.set("profile", "main"); + + // TODO: + dict.set("rc", "vbr"); + dict.set("qp", "35"); + + dict.set("forced-idr", "1"); + + // damn you + dict.set("delay", "0"); + dict.set("zerolatency", "1"); + + let encoder = video_encoder_context + .open_as_with(encoder, dict) + .with_context(|| "While opening h264_nvenc video codec")?; + + Ok(Self::NvencHWFrame { + encoder: encoder, + hw_context: hw_frame_context, + }) + } + + // NOTE: It's a bit pointless to have this have a mut borrow, + // but you'll probably have a mutable borrow on this already.. + pub fn is_hardware(&mut self) -> bool { + match self { + Self::Software { .. } => false, + Self::NvencSWFrame { .. } => true, + Self::NvencHWFrame { .. } => true, + } + } + + //pub fn get_hw_context(&mut self) -> &mut HwFrameContext { + // match self { + // Self::Nvenc { encoder: _, hw_context } => hw_context, + // _ => panic!("should not use H264Encoder::get_hw_context() on a Software encoder") + // } + //} + + pub fn create_frame(&mut self) -> anyhow::Result { + match self { + Self::Software { encoder } | Self::NvencSWFrame { encoder } => { + return Ok(ffmpeg::frame::Video::new( + encoder.format(), + encoder.width(), + encoder.height(), + )); + } + + Self::NvencHWFrame { + encoder, + hw_context, + } => { + let mut frame = ffmpeg::frame::Video::empty(); + + unsafe { + (*frame.as_mut_ptr()).format = ffmpeg::format::Pixel::CUDA as i32; + (*frame.as_mut_ptr()).width = encoder.width() as i32; + (*frame.as_mut_ptr()).height = encoder.height() as i32; + (*frame.as_mut_ptr()).hw_frames_ctx = hw_context.as_raw_mut(); + + hw_context.get_buffer(&mut frame)?; + + (*frame.as_mut_ptr()).linesize[0] = (*frame.as_ptr()).width * 4; + + return Ok(frame); + } + } + } + } + + pub fn send_frame(&mut self, frame: &ffmpeg::Frame) { + match self { + Self::Software { encoder } => { + encoder.send_frame(frame).unwrap(); + } + + Self::NvencSWFrame { encoder } => { + encoder.send_frame(frame).unwrap(); + } + + Self::NvencHWFrame { + encoder, + hw_context: _, + } => { + encoder.send_frame(frame).unwrap(); + } + } + } + + pub fn send_eof(&mut self) { + match self { + Self::Software { encoder } => { + encoder.send_eof().unwrap(); + } + + Self::NvencSWFrame { encoder } => { + // Realistically this should be the same right? + encoder.send_eof().unwrap(); + // todo!("Requires support."); + } + + Self::NvencHWFrame { + encoder, + hw_context: _, + } => { + encoder.send_eof().unwrap(); + } + } + } + + fn receive_packet_impl(&mut self, packet: &mut ffmpeg::Packet) -> Result<(), ffmpeg::Error> { + return match self { + Self::Software { encoder } => encoder.receive_packet(packet), + Self::NvencSWFrame { encoder } => encoder.receive_packet(packet), + Self::NvencHWFrame { + encoder, + hw_context: _, + } => encoder.receive_packet(packet), + }; + } + + // Shuold this return a Result so we can make it easier to know when to continue? + pub fn receive_packet(&mut self, packet: &mut ffmpeg::Packet) -> anyhow::Result<()> { + loop { + match self.receive_packet_impl(packet) { + Ok(_) => break, + Err(ffmpeg::Error::Other { errno }) => { + if errno != EAGAIN { + return Err(ffmpeg::Error::Other { errno: errno }.into()); + } else { + // EAGAIN is not fatal, and simply means + // we should just try again + break; + } + } + Err(e) => return Err(e.into()), + } + } + + Ok(()) + } +} diff --git a/server/src/video/hwdevice.rs b/server/src/video/hwdevice.rs new file mode 100644 index 0000000..a358dbe --- /dev/null +++ b/server/src/video/hwdevice.rs @@ -0,0 +1,88 @@ +use std::ptr::null_mut; + +use super::check_ret; + +use super::ffmpeg; + +pub struct CudaDeviceContext { + buffer: *mut ffmpeg::sys::AVBufferRef, +} + +impl CudaDeviceContext { + fn new(buffer: *mut ffmpeg::sys::AVBufferRef) -> Self { + Self { buffer } + } + + // pub fn as_device_mut(&mut self) -> &mut ffmpeg::sys::AVHWDeviceContext { + // unsafe { &mut *((*self.buffer).data as *mut ffmpeg::sys::AVHWDeviceContext) } + // } + + // pub fn as_device(&self) -> &ffmpeg::sys::AVHWDeviceContext { + // unsafe { &*((*self.buffer).data as *const ffmpeg::sys::AVHWDeviceContext) } + // } + + pub fn as_raw_mut(&mut self) -> &mut ffmpeg::sys::AVBufferRef { + unsafe { &mut *self.buffer } + } + + // pub fn as_raw(&self) -> &ffmpeg::sys::AVBufferRef { + // unsafe { &*self.buffer } + // } +} + +impl Drop for CudaDeviceContext { + fn drop(&mut self) { + unsafe { + if !self.buffer.is_null() { + ffmpeg::sys::av_buffer_unref(&mut self.buffer); + } + } + } +} + +pub struct CudaDeviceContextBuilder { + buffer: *mut ffmpeg::sys::AVBufferRef, +} + +impl CudaDeviceContextBuilder { + pub fn new() -> anyhow::Result { + let buffer = unsafe { ffmpeg::sys::av_hwdevice_ctx_alloc(ffmpeg::sys::AVHWDeviceType::AV_HWDEVICE_TYPE_CUDA) }; + if buffer.is_null() { + return Err(anyhow::anyhow!("could not allocate a hwdevice".to_string())); + } + + Ok(Self { buffer }) + } + + pub fn build(mut self) -> Result { + check_ret(unsafe { ffmpeg::sys::av_hwdevice_ctx_init(self.buffer) })?; + let result = Ok(CudaDeviceContext::new(self.buffer)); + self.buffer = null_mut(); + + result + } + + pub fn set_cuda_context(mut self, context: ffmpeg::sys::CUcontext) -> Self { + unsafe { + (*(self.as_device_mut().hwctx as *mut ffmpeg::sys::AVCUDADeviceContext)).cuda_ctx = context; + } + + self + } + + pub fn as_device_mut(&mut self) -> &mut ffmpeg::sys::AVHWDeviceContext { + unsafe { &mut *((*self.buffer).data as *mut ffmpeg::sys::AVHWDeviceContext) } + } + + // pub fn as_device(&self) -> &ffmpeg::sys::AVHWDeviceContext { + // unsafe { &*((*self.buffer).data as *const ffmpeg::sys::AVHWDeviceContext) } + // } + + // pub fn as_raw_mut(&mut self) -> &mut ffmpeg::sys::AVBufferRef { + // unsafe { &mut *self.buffer } + // } + + // pub fn as_raw(&self) -> &ffmpeg::sys::AVBufferRef { + // unsafe { &*self.buffer } + // } +} \ No newline at end of file diff --git a/server/src/video/hwframe.rs b/server/src/video/hwframe.rs new file mode 100644 index 0000000..d08932d --- /dev/null +++ b/server/src/video/hwframe.rs @@ -0,0 +1,121 @@ +use std::ptr::null_mut; + +use super::ffmpeg; + +use ffmpeg::format::Pixel; + +use super::{check_ret, hwdevice::CudaDeviceContext}; + +pub struct HwFrameContext { + _cuda_device_context: CudaDeviceContext, + buffer: *mut ffmpeg::sys::AVBufferRef, +} + +impl HwFrameContext { + fn new(cuda_device_context: CudaDeviceContext, buffer: *mut ffmpeg::sys::AVBufferRef) -> Self { + Self { + _cuda_device_context: cuda_device_context, + buffer, + } + } + + // pub fn as_context_mut(&mut self) -> &mut ffmpeg::sys::AVHWFramesContext { + // unsafe { &mut *((*self.buffer).data as *mut ffmpeg::sys::AVHWFramesContext) } + // } + + // pub fn as_context(&self) -> &ffmpeg::sys::AVHWFramesContext { + // unsafe { &*((*self.buffer).data as *const ffmpeg::sys::AVHWFramesContext) } + // } + + pub fn as_raw_mut(&mut self) -> &mut ffmpeg::sys::AVBufferRef { + unsafe { &mut *self.buffer } + } + + pub fn as_device_context_mut(&mut self) -> &mut ffmpeg::sys::AVBufferRef { + self._cuda_device_context.as_raw_mut() + } + + /// call once to allocate frame + pub fn get_buffer(&mut self, frame: &mut ffmpeg::frame::Video) -> Result<(), ffmpeg::Error> { + unsafe { + super::check_ret(ffmpeg::sys::av_hwframe_get_buffer( + self.buffer, + frame.as_mut_ptr(), + 0, + ))?; + } + + Ok(()) + } + + // pub fn as_raw(&self) -> &ffmpeg::sys::AVBufferRef { + // unsafe { &*self.buffer } + // } +} + +unsafe impl Send for HwFrameContext {} + +impl Drop for HwFrameContext { + fn drop(&mut self) { + unsafe { + if !self.buffer.is_null() { + ffmpeg::sys::av_buffer_unref(&mut self.buffer); + } + } + } +} + +pub struct HwFrameContextBuilder { + cuda_device_context: CudaDeviceContext, + buffer: *mut ffmpeg::sys::AVBufferRef, +} + +impl HwFrameContextBuilder { + pub fn new(mut cuda_device_context: CudaDeviceContext) -> anyhow::Result { + let buffer = unsafe { ffmpeg::sys::av_hwframe_ctx_alloc(cuda_device_context.as_raw_mut()) }; + if buffer.is_null() { + return Err(anyhow::anyhow!("could not allocate a hwframe context")); + } + + Ok(Self { + cuda_device_context, + buffer, + }) + } + + pub fn build(mut self) -> Result { + check_ret(unsafe { ffmpeg::sys::av_hwframe_ctx_init(self.buffer) })?; + let result = Ok(HwFrameContext::new(self.cuda_device_context, self.buffer)); + self.buffer = null_mut(); + + result + } + + pub fn set_width(mut self, width: u32) -> Self { + self.as_frame_mut().width = width as i32; + self + } + + pub fn set_height(mut self, height: u32) -> Self { + self.as_frame_mut().height = height as i32; + self + } + + pub fn set_sw_format(mut self, sw_format: Pixel) -> Self { + self.as_frame_mut().sw_format = sw_format.into(); + self + } + + pub fn set_format(mut self, format: Pixel) -> Self { + self.as_frame_mut().format = format.into(); + self + } + + pub fn as_frame_mut(&mut self) -> &mut ffmpeg::sys::AVHWFramesContext { + unsafe { &mut *((*self.buffer).data as *mut ffmpeg::sys::AVHWFramesContext) } + } + + // pub fn as_frame(&self) -> &ffmpeg::sys::AVHWFramesContext { + // unsafe { &*((*self.buffer).data as *const ffmpeg::sys::AVHWFramesContext) } + // } +} diff --git a/server/src/video/mod.rs b/server/src/video/mod.rs new file mode 100644 index 0000000..382e9f2 --- /dev/null +++ b/server/src/video/mod.rs @@ -0,0 +1,22 @@ +pub mod h264_encoder; +//pub mod lc_muxer; + +/// Re-export of `ffmpeg` crate. +pub use ffmpeg as ffmpeg; + +pub mod hwdevice; +pub mod hwframe; + +#[allow(unused)] // FIXME +pub mod encoder_thread; + +pub mod cuda_gl; + +// from hgaiser/moonshine +pub fn check_ret(error_code: i32) -> Result<(), ffmpeg::Error> { + if error_code != 0 { + return Err(ffmpeg::Error::from(error_code)); + } + + Ok(()) +} \ No newline at end of file