├── .github └── FUNDING.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── demo.gif ├── examples ├── blit.hlsl ├── fft.hlsl ├── fft_delta.hlsl └── visibility.hlsl ├── install-debug.fish ├── install.fish └── src ├── effect ├── effect_param.rs ├── loaded_value.rs └── mod.rs ├── effect_fallback.effect ├── effect_template.effect ├── lib.rs ├── preprocessor.rs └── util.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Limeth] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.14.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "7c0929d69e78dd9bf5408269919fcbcaeb2e35e5d43e5815517cdc6a8e11a423" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "0.2.3" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "0.7.15" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "ansi_term" 31 | version = "0.11.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 34 | dependencies = [ 35 | "winapi", 36 | ] 37 | 38 | [[package]] 39 | name = "anyhow" 40 | version = "1.0.34" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "bf8dcb5b4bbaa28653b647d8c77bd4ed40183b48882e130c1f1ffb73de069fd7" 43 | 44 | [[package]] 45 | name = "apodize" 46 | version = "1.0.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "fca387cdc0a1f9c7a7c26556d584aa2d07fc529843082e4861003cde4ab914ed" 49 | 50 | [[package]] 51 | name = "atty" 52 | version = "0.2.14" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 55 | dependencies = [ 56 | "hermit-abi", 57 | "libc", 58 | "winapi", 59 | ] 60 | 61 | [[package]] 62 | name = "autocfg" 63 | version = "1.0.1" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 66 | 67 | [[package]] 68 | name = "backtrace" 69 | version = "0.3.55" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "ef5140344c85b01f9bbb4d4b7288a8aa4b3287ccef913a14bcc78a1063623598" 72 | dependencies = [ 73 | "addr2line", 74 | "cfg-if 1.0.0", 75 | "libc", 76 | "miniz_oxide", 77 | "object", 78 | "rustc-demangle", 79 | ] 80 | 81 | [[package]] 82 | name = "bindgen" 83 | version = "0.53.3" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "c72a978d268b1d70b0e963217e60fdabd9523a941457a6c42a7315d15c7e89e5" 86 | dependencies = [ 87 | "bitflags", 88 | "cexpr", 89 | "cfg-if 0.1.10", 90 | "clang-sys", 91 | "clap", 92 | "env_logger", 93 | "lazy_static", 94 | "lazycell", 95 | "log", 96 | "peeking_take_while", 97 | "proc-macro2", 98 | "quote", 99 | "regex", 100 | "rustc-hash", 101 | "shlex", 102 | "which", 103 | ] 104 | 105 | [[package]] 106 | name = "bitflags" 107 | version = "1.2.1" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 110 | 111 | [[package]] 112 | name = "cc" 113 | version = "1.0.65" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "95752358c8f7552394baf48cd82695b345628ad3f170d607de3ca03b8dacca15" 116 | 117 | [[package]] 118 | name = "cexpr" 119 | version = "0.4.0" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" 122 | dependencies = [ 123 | "nom", 124 | ] 125 | 126 | [[package]] 127 | name = "cfg-if" 128 | version = "0.1.10" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 131 | 132 | [[package]] 133 | name = "cfg-if" 134 | version = "1.0.0" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 137 | 138 | [[package]] 139 | name = "clang-sys" 140 | version = "0.29.3" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "fe6837df1d5cba2397b835c8530f51723267e16abbf83892e9e5af4f0e5dd10a" 143 | dependencies = [ 144 | "glob", 145 | "libc", 146 | "libloading", 147 | ] 148 | 149 | [[package]] 150 | name = "clap" 151 | version = "2.33.3" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" 154 | dependencies = [ 155 | "ansi_term", 156 | "atty", 157 | "bitflags", 158 | "strsim", 159 | "textwrap", 160 | "unicode-width", 161 | "vec_map", 162 | ] 163 | 164 | [[package]] 165 | name = "cstr" 166 | version = "0.1.7" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "19f7a08ed4ecd7e077d4cee63937473e6f7cf57b702a9114ef41751b2cbc0f60" 169 | dependencies = [ 170 | "cstr-macros", 171 | "procedural-masquerade", 172 | ] 173 | 174 | [[package]] 175 | name = "cstr-macros" 176 | version = "0.1.6" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "cd670e5ff58768ef624207fb95709ce63b8d05573fb9a05165f0eef471ea6a3a" 179 | dependencies = [ 180 | "procedural-masquerade", 181 | "syn", 182 | ] 183 | 184 | [[package]] 185 | name = "downcast-rs" 186 | version = "1.2.0" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" 189 | 190 | [[package]] 191 | name = "env_logger" 192 | version = "0.7.1" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" 195 | dependencies = [ 196 | "atty", 197 | "humantime", 198 | "log", 199 | "regex", 200 | "termcolor", 201 | ] 202 | 203 | [[package]] 204 | name = "fourier" 205 | version = "0.1.0" 206 | source = "git+https://github.com/calebzulawski/fourier?rev=dc49696345e25a91406de6b123b5ce3d24784e32#dc49696345e25a91406de6b123b5ce3d24784e32" 207 | dependencies = [ 208 | "fourier-algorithms", 209 | "fourier-macros", 210 | "num-complex", 211 | ] 212 | 213 | [[package]] 214 | name = "fourier-algorithms" 215 | version = "0.1.1" 216 | source = "git+https://github.com/calebzulawski/fourier?rev=dc49696345e25a91406de6b123b5ce3d24784e32#dc49696345e25a91406de6b123b5ce3d24784e32" 217 | dependencies = [ 218 | "multiversion", 219 | "num-complex", 220 | "num-traits", 221 | ] 222 | 223 | [[package]] 224 | name = "fourier-macros" 225 | version = "0.1.0" 226 | source = "git+https://github.com/calebzulawski/fourier?rev=dc49696345e25a91406de6b123b5ce3d24784e32#dc49696345e25a91406de6b123b5ce3d24784e32" 227 | dependencies = [ 228 | "fourier-algorithms", 229 | "num-complex", 230 | "proc-macro2", 231 | "quote", 232 | "syn", 233 | ] 234 | 235 | [[package]] 236 | name = "gimli" 237 | version = "0.23.0" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" 240 | 241 | [[package]] 242 | name = "glob" 243 | version = "0.3.0" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" 246 | 247 | [[package]] 248 | name = "hermit-abi" 249 | version = "0.1.17" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" 252 | dependencies = [ 253 | "libc", 254 | ] 255 | 256 | [[package]] 257 | name = "humantime" 258 | version = "1.3.0" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" 261 | dependencies = [ 262 | "quick-error", 263 | ] 264 | 265 | [[package]] 266 | name = "itoa" 267 | version = "0.4.6" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" 270 | 271 | [[package]] 272 | name = "lazy_static" 273 | version = "1.4.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 276 | 277 | [[package]] 278 | name = "lazycell" 279 | version = "1.3.0" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 282 | 283 | [[package]] 284 | name = "libc" 285 | version = "0.2.80" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" 288 | 289 | [[package]] 290 | name = "libloading" 291 | version = "0.5.2" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "f2b111a074963af1d37a139918ac6d49ad1d0d5e47f72fd55388619691a7d753" 294 | dependencies = [ 295 | "cc", 296 | "winapi", 297 | ] 298 | 299 | [[package]] 300 | name = "libm" 301 | version = "0.2.1" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" 304 | 305 | [[package]] 306 | name = "log" 307 | version = "0.4.11" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" 310 | dependencies = [ 311 | "cfg-if 0.1.10", 312 | ] 313 | 314 | [[package]] 315 | name = "memchr" 316 | version = "2.3.4" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" 319 | 320 | [[package]] 321 | name = "miniz_oxide" 322 | version = "0.4.3" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d" 325 | dependencies = [ 326 | "adler", 327 | "autocfg", 328 | ] 329 | 330 | [[package]] 331 | name = "multiversion" 332 | version = "0.6.1" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "025c962a3dd3cc5e0e520aa9c612201d127dcdf28616974961a649dca64f5373" 335 | dependencies = [ 336 | "multiversion-macros", 337 | ] 338 | 339 | [[package]] 340 | name = "multiversion-macros" 341 | version = "0.6.1" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "a8a3e2bde382ebf960c1f3e79689fa5941625fe9bf694a1cb64af3e85faff3af" 344 | dependencies = [ 345 | "proc-macro2", 346 | "quote", 347 | "syn", 348 | ] 349 | 350 | [[package]] 351 | name = "nom" 352 | version = "5.1.2" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" 355 | dependencies = [ 356 | "memchr", 357 | "version_check", 358 | ] 359 | 360 | [[package]] 361 | name = "num-complex" 362 | version = "0.2.4" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" 365 | dependencies = [ 366 | "autocfg", 367 | "num-traits", 368 | ] 369 | 370 | [[package]] 371 | name = "num-traits" 372 | version = "0.2.14" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 375 | dependencies = [ 376 | "autocfg", 377 | "libm", 378 | ] 379 | 380 | [[package]] 381 | name = "object" 382 | version = "0.22.0" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397" 385 | 386 | [[package]] 387 | name = "obs-shaderfilter-plus" 388 | version = "0.3.1" 389 | dependencies = [ 390 | "anyhow", 391 | "apodize", 392 | "downcast-rs", 393 | "fourier", 394 | "lazy_static", 395 | "num-complex", 396 | "obs-wrapper", 397 | "ordered-float", 398 | "paste", 399 | "regex", 400 | "smallvec", 401 | ] 402 | 403 | [[package]] 404 | name = "obs-sys" 405 | version = "0.1.2" 406 | source = "git+https://github.com/Limeth/rust-obs-plugins#d116656fef8809489f7e1205069c48e1fc77773a" 407 | dependencies = [ 408 | "bindgen", 409 | ] 410 | 411 | [[package]] 412 | name = "obs-wrapper" 413 | version = "0.1.5" 414 | source = "git+https://github.com/Limeth/rust-obs-plugins#d116656fef8809489f7e1205069c48e1fc77773a" 415 | dependencies = [ 416 | "backtrace", 417 | "cstr", 418 | "obs-sys", 419 | "paste", 420 | "safe-transmute", 421 | "serde_json", 422 | ] 423 | 424 | [[package]] 425 | name = "ordered-float" 426 | version = "1.1.0" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "3741934be594d77de1c8461ebcbbe866f585ea616a9753aa78f2bdc69f0e4579" 429 | dependencies = [ 430 | "num-traits", 431 | ] 432 | 433 | [[package]] 434 | name = "paste" 435 | version = "0.1.18" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880" 438 | dependencies = [ 439 | "paste-impl", 440 | "proc-macro-hack", 441 | ] 442 | 443 | [[package]] 444 | name = "paste-impl" 445 | version = "0.1.18" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6" 448 | dependencies = [ 449 | "proc-macro-hack", 450 | ] 451 | 452 | [[package]] 453 | name = "peeking_take_while" 454 | version = "0.1.2" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" 457 | 458 | [[package]] 459 | name = "proc-macro-hack" 460 | version = "0.5.19" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" 463 | 464 | [[package]] 465 | name = "proc-macro2" 466 | version = "1.0.24" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" 469 | dependencies = [ 470 | "unicode-xid", 471 | ] 472 | 473 | [[package]] 474 | name = "procedural-masquerade" 475 | version = "0.1.7" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "8f1383dff4092fe903ac180e391a8d4121cc48f08ccf850614b0290c6673b69d" 478 | 479 | [[package]] 480 | name = "quick-error" 481 | version = "1.2.3" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 484 | 485 | [[package]] 486 | name = "quote" 487 | version = "1.0.7" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" 490 | dependencies = [ 491 | "proc-macro2", 492 | ] 493 | 494 | [[package]] 495 | name = "regex" 496 | version = "1.4.2" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c" 499 | dependencies = [ 500 | "aho-corasick", 501 | "memchr", 502 | "regex-syntax", 503 | "thread_local", 504 | ] 505 | 506 | [[package]] 507 | name = "regex-syntax" 508 | version = "0.6.21" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189" 511 | 512 | [[package]] 513 | name = "rustc-demangle" 514 | version = "0.1.18" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" 517 | 518 | [[package]] 519 | name = "rustc-hash" 520 | version = "1.1.0" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 523 | 524 | [[package]] 525 | name = "ryu" 526 | version = "1.0.5" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 529 | 530 | [[package]] 531 | name = "safe-transmute" 532 | version = "0.11.0" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "50b8b2cd387f744f69469aaed197954ba4c0ecdb31e02edf99b023e0df11178a" 535 | 536 | [[package]] 537 | name = "serde" 538 | version = "1.0.117" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" 541 | 542 | [[package]] 543 | name = "serde_json" 544 | version = "1.0.59" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95" 547 | dependencies = [ 548 | "itoa", 549 | "ryu", 550 | "serde", 551 | ] 552 | 553 | [[package]] 554 | name = "shlex" 555 | version = "0.1.1" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" 558 | 559 | [[package]] 560 | name = "smallvec" 561 | version = "1.5.0" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "7acad6f34eb9e8a259d3283d1e8c1d34d7415943d4895f65cc73813c7396fc85" 564 | 565 | [[package]] 566 | name = "strsim" 567 | version = "0.8.0" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 570 | 571 | [[package]] 572 | name = "syn" 573 | version = "1.0.51" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "3b4f34193997d92804d359ed09953e25d5138df6bcc055a71bf68ee89fdf9223" 576 | dependencies = [ 577 | "proc-macro2", 578 | "quote", 579 | "unicode-xid", 580 | ] 581 | 582 | [[package]] 583 | name = "termcolor" 584 | version = "1.1.2" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" 587 | dependencies = [ 588 | "winapi-util", 589 | ] 590 | 591 | [[package]] 592 | name = "textwrap" 593 | version = "0.11.0" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 596 | dependencies = [ 597 | "unicode-width", 598 | ] 599 | 600 | [[package]] 601 | name = "thread_local" 602 | version = "1.0.1" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" 605 | dependencies = [ 606 | "lazy_static", 607 | ] 608 | 609 | [[package]] 610 | name = "unicode-width" 611 | version = "0.1.8" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 614 | 615 | [[package]] 616 | name = "unicode-xid" 617 | version = "0.2.1" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" 620 | 621 | [[package]] 622 | name = "vec_map" 623 | version = "0.8.2" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 626 | 627 | [[package]] 628 | name = "version_check" 629 | version = "0.9.2" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" 632 | 633 | [[package]] 634 | name = "which" 635 | version = "3.1.1" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" 638 | dependencies = [ 639 | "libc", 640 | ] 641 | 642 | [[package]] 643 | name = "winapi" 644 | version = "0.3.9" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 647 | dependencies = [ 648 | "winapi-i686-pc-windows-gnu", 649 | "winapi-x86_64-pc-windows-gnu", 650 | ] 651 | 652 | [[package]] 653 | name = "winapi-i686-pc-windows-gnu" 654 | version = "0.4.0" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 657 | 658 | [[package]] 659 | name = "winapi-util" 660 | version = "0.1.5" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 663 | dependencies = [ 664 | "winapi", 665 | ] 666 | 667 | [[package]] 668 | name = "winapi-x86_64-pc-windows-gnu" 669 | version = "0.4.0" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 672 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "obs-shaderfilter-plus" 3 | version = "0.3.1" 4 | authors = ["Jakub Hlusička "] 5 | edition = "2018" 6 | 7 | [lib] 8 | name = "obs_shaderfilter_plus" 9 | crate-type = ["cdylib"] 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | obs-wrapper = { git = "https://github.com/Limeth/rust-obs-plugins" } 15 | regex = "1" 16 | anyhow = "1.0" 17 | lazy_static = "1.4" 18 | smallvec = "1.3" 19 | fourier = { git = "https://github.com/calebzulawski/fourier", rev = "dc49696345e25a91406de6b123b5ce3d24784e32" } 20 | num-complex = "0.2" 21 | paste = "0.1.7" 22 | ordered-float = "1.0" 23 | apodize = "1.0" 24 | downcast = { package = "downcast-rs", version = "1.1" } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GPLv2 License 2 | OBS ShaderFilter Plus 3 | 4 | Copyright (C) 2020 Jakub Hlusička 5 | 6 | This program is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation; either version 2 9 | of the License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OBS ShaderFilter Plus 2 | OBS ShaderFilter Plus is a plugin for Open Broadcaster Software. 3 | It can be used to apply effects to sources using manually created GLSL/HLSL shaders. 4 | 5 | 1. Add a filter to a source by right-clicking a source, going to _Filters_, and adding _ShaderFilter Plus_. 6 | 2. Select a shader by clicking the _Browse_ button and picking the file containing the shader source code via the file browser. 7 | 3. Customize the behavior of the shader via the shader-specific user interface. 8 | 9 | Example shaders may be found in the [`examples`](examples) directory of this repository. It is a good starting point for the creation of custom effects. 10 | 11 | ![Demo](demo.gif) 12 | 13 | ## What are Shaders? 14 | Shaders are programs executed on the GPU. They can be used to apply customizable special visual effects. The shaders used by this plugin are a special subset of shaders called _fragment shaders_. These shaders are executed once for each pixel of the source, every frame. See [Usage Guide](#usage-guide) for examples. 15 | 16 | Different graphics interfaces, such as OpenGL and DirectX, use different shader languages with incompatible syntax, so it is important to be aware of the graphics interfaces OBS makes use of. 17 | 18 | * OBS on Windows uses DirectX by default, but can be forced to use OpenGL. 19 | * OBS on Linux uses OpenGL. 20 | 21 | Shaders are executed using OpenGL (GLSL shaders) or DirectX (HLSL shaders), 22 | depending on your platform. 23 | 24 | ## Writing Cross-Platform Shaders 25 | ### Cross-Platform HLSL 26 | When OBS is run with OpenGL, it performs primitive translation of HLSL sources to GLSL. However, this translation is limited and performed via basic string substitution, and therefore may not result in correct behavior. Despite these limitations, cross platform shaders could be written in HLSL, as long as they are simple. 27 | 28 | ### Cross-Platform GLSL 29 | OBS on Windows may be forced to use OpenGL by launching the program with the `--allow-opengl` [launch parameter](https://obsproject.com/wiki/Launch-Parameters). This can be done by creating a shortcut to the executable and appending the parameter to the path, for example: `"C:\Program Files\obs-studio\bin\64bit\obs64.exe" --allow-opengl`. After launching OBS this way, the OpenGL renderer must be selected in the Advanced Settings. After restarting OBS with these settings applied, GLSL shaders will work properly. 30 | 31 | ## Installation 32 | 1. Download the latest binary for your platform from [the Releases page](https://github.com/Limeth/obs-shaderfilter-plus/releases). 33 | * On Windows, download the file ending with `_windows_x64.dll` 34 | * On Linux, download the file ending with `_linux_x64.so` 35 | 2. Place it in the OBS plugin directory: 36 | * On Windows, that is usually `C:\Program Files\obs-studio\obs-plugins\64bit` 37 | * On Linux, that is usually `/usr/lib/obs-plugins` 38 | 39 | ## Usage Guide 40 | The structure of a shader is simple. All, that is required, is the following `render` function. 41 | 42 | ```hlsl 43 | float4 render(float2 uv) { 44 | // sample the source texture and return its color to be displayed 45 | return image.Sample(builtin_texture_sampler, uv); 46 | } 47 | ``` 48 | 49 | ### Builtin Variables 50 | Every shader loaded by this plugin has access to the following uniform variables. 51 | 52 | ```hlsl 53 | uniform texture2d image; // the source texture (the image we are filtering) 54 | uniform int builtin_frame; // the current frame number 55 | uniform float builtin_framerate; // the current output framerate 56 | uniform float builtin_elapsed_time; // the current elapsed time 57 | uniform float builtin_elapsed_time_previous; // the elapsed time in the previous frame 58 | uniform float builtin_elapsed_time_since_shown; // the time since the source this filter is applied to was shown 59 | uniform float builtin_elapsed_time_since_shown_previous; // the time since the source this filter is applied to was shown of the previous frame 60 | uniform float builtin_elapsed_time_since_enabled; // the time since the filter itself was shown 61 | uniform float builtin_elapsed_time_since_enabled_previous; // the time since the filter itself was shown of the previous frame 62 | uniform int2 builtin_uv_size; // the source dimensions 63 | 64 | sampler_state builtin_texture_sampler { ... }; // a texture sampler with linear filtering 65 | ``` 66 | 67 | #### On-Request Builtin Variables 68 | These uniform variables will be assigned data by the plugin. 69 | If they are not defined, they do not use up processing resources. 70 | 71 | ```hlsl 72 | uniform texture2d builtin_texture_fft_; // audio output frequency spectrum 73 | uniform texture2d builtin_texture_fft__previous; // output from the previous frame (requires builtin_texture_fft_ to be defined) 74 | ``` 75 | 76 | Builtin FFT variables have specific properties. See the the section below on properties. 77 | 78 | Example: 79 | 80 | ```hlsl 81 | #pragma shaderfilter set myfft__mix__description Main Mix/Track 82 | #pragma shaderfilter set myfft__channel__description Main Channel 83 | #pragma shaderfilter set myfft__dampening_factor_attack__step 0.0001 84 | #pragma shaderfilter set myfft__dampening_factor_attack__default 0.05 85 | #pragma shaderfilter set myfft__dampening_factor_release 0.0001 86 | uniform texture2d builtin_texture_fft_myfft; 87 | ``` 88 | 89 | See the `examples` directory for more examples. 90 | 91 | #### Custom Variables 92 | These uniform variables may be used to let the user provide values to the shader using the OBS UI. 93 | The allowed types are: 94 | * `bool`: A boolean variable 95 | * `int`: A signed 32-bit integer variable 96 | * `float`: A single precision floating point variable 97 | * `float4`/`vec4`: A color variable, shown as a color picker in the UI 98 | 99 | Example: 100 | 101 | ```hlsl 102 | #pragma shaderfilter set my_color__description My Color 103 | #pragma shaderfilter set my_color__default 7FFF00FF 104 | uniform float4 my_color; 105 | ``` 106 | 107 | See the `examples` directory for more examples. 108 | 109 | ### Defining Properties in the Source Code 110 | This plugin uses a simple preprocessor to process `#pragma shaderfilter` macros. 111 | It is not a fully-featured C preprocessor. It is executed before the shader is 112 | compiled. The shader compilation includes an actual C preprocessing step. 113 | Do not expect to be able to use macros within `#pragma shaderfilter`. 114 | 115 | Most properties can be specified in the shader source code. The syntax is as follows: 116 | ``` 117 | #pragma shaderfilter set 118 | ``` 119 | 120 | #### Universal Properties 121 | These properties can be applied to any user-defined uniform variable. 122 | * `default`: The default value of the uniform variable. 123 | * `description`: The user-facing text describing the variable. Displayed in the OBS UI. 124 | 125 | #### Integer Properties 126 | * `min` (integer): The minimum allowed value 127 | * `max` (integer): The maximum allowed value 128 | * `step` (integer): The stride when changing the value 129 | * `slider` (true/false): Whether to display a slider or not 130 | 131 | #### Float Properties 132 | * `min` (float): The minimum allowed value 133 | * `max` (float): The maximum allowed value 134 | * `step` (float): The stride when changing the value 135 | * `slider` (true/false): Whether to display a slider or not 136 | 137 | #### FFT Properties 138 | * `mix`: The Mix/Track number corresponding to checkboxes in OBS' `Advanced Audio Properties` 139 | * `channel`: The channel number (0 = Left, 1 = Right for stereo) 140 | * `dampening_factor_attack`: The linear interpolation coefficient (in percentage) used to blend the previous FFT sample with the current sample, if it is larger than the previous 141 | * `dampening_factor_release`: The linear interpolation coefficient (in percentage) used to blend the previous FFT sample with the current sample, if it is lesser than the previous 142 | 143 | 144 | 145 | ## Planned Features 146 | * Access to raw audio signal, without FFT 147 | * Specifying textures by a path to an image file 148 | 149 | ## Development 150 | ### Building 151 | #### Windows 152 | 1. Install Rust by following instructions at https://rustup.rs/ 153 | 2. Install CLang: Download and install the official pre-built binary from 154 | [LLVM download page](http://releases.llvm.org/download.html) 155 | 3. Compile OBS by following [these instructions](https://github.com/obsproject/obs-studio/wiki/Install-Instructions#windows-build-directions) 156 | * This will require the installation of _Visual Studio Build Tools 2019_, _Visual Studio Community 2019_ and _CMake_. 157 | * Visual Studio Build Tools requirements: 158 | 1. `MSVC v142 - VS 2019 C++ x64/x86 build tools (v14.25)` or later 159 | 2. `MSVC v142 - VS 2019 C++ x64/x86 Spectre-mitigated libs (v14.25)` or later 160 | * Visual Studio Community 2019 requirements: 161 | 1. `MSVC v142 - VS 2019 C++ x64/x86 build tools (v14.25)` or later 162 | 2. `MSVC v142 - VS 2019 C++ x64/x86 Spectre-mitigated libs (v14.25)` or later 163 | 3. `C++ ATL for latest v142 build tools (x86 & x64)` or later 164 | 4. `C++ ATL for latest v142 build tools with Spectre Mitigations (x86 & x64)` or later 165 | * To configure OBS via `cmake-gui`, set the following variables: 166 | * `DISABLE_PYTHON=TRUE`, unless you are not getting errors while trying to build with Python enabled 167 | 4. Compile OBS ShaderFilter Plus, replace `` with the path to the directory where you built OBS: 168 | ```bat 169 | set RUSTFLAGS=-L native=\libobs\Debug 170 | cargo build --release 171 | ``` 172 | 5. Move `target/release/libobs_shaderfilter_plus.dll` to the OBS plugin directory. 173 | #### Linux 174 | 1. Compile OBS by following [these instructions](https://github.com/obsproject/obs-studio/wiki/Install-Instructions#linux-build-directions). 175 | 2. Add the directory in which `libobs.so` resides to the `LD_LIBRARY_PATH` environment variable. 176 | 3. Install Rust (the package manager Cargo should be bundled with it) 177 | 4. Clone this repository and open it in the terminal 178 | 5. Compile using `cargo build --release` 179 | 6. Move `target/release/libobs_shaderfilter_plus.so` to the OBS plugin directory. 180 | 181 | ### Tips on building OBS (fish shell, Ubuntu) 182 | These steps should not be necessary if you just want to compile OBS ShaderFilter Plus from source. 183 | 184 | Ensure OBS is uninstalled using: 185 | ```fish 186 | sudo apt remove obs-studio 187 | ``` 188 | 189 | Compile OBS using: 190 | ```fish 191 | cmake -DUNIX_STRUCTURE=1 -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_BROWSER=ON -DCEF_ROOT_DIR="../../cef_binary_3770_linux64" ..; and make -j24; and sudo checkinstall --default --pkgname=obs-studio --fstrans=no --backup=no --pkgversion=(date +%Y%m%d)"-git" --deldoc=yes 192 | ``` 193 | 194 | Then recompile and install using: 195 | ``` 196 | make -j24; and sudo make install 197 | ``` 198 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | fn main() { 4 | let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); 5 | 6 | if target_os == "windows" { 7 | println!("cargo:rustc-link-lib=dylib=legacy_stdio_definitions"); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limeth/obs-shaderfilter-plus/21d7347e3793e0d91f160dc32f9699190b6c7102/demo.gif -------------------------------------------------------------------------------- /examples/blit.hlsl: -------------------------------------------------------------------------------- 1 | // A minimal shader example. 2 | float4 render(float2 uv) { 3 | return image.Sample(builtin_texture_sampler, uv); 4 | } 5 | -------------------------------------------------------------------------------- /examples/fft.hlsl: -------------------------------------------------------------------------------- 1 | // Configure builtin uniforms 2 | // These macros are optional, but improve the user experience 3 | #pragma shaderfilter set main__mix__description Main Mix/Track 4 | #pragma shaderfilter set main__channel__description Main Channel 5 | #pragma shaderfilter set main__dampening_factor_attack 0.0 6 | #pragma shaderfilter set main__dampening_factor_release 0.0 7 | uniform texture2d builtin_texture_fft_main; 8 | 9 | // Define configurable variables 10 | // These macros are optional, but improve the user experience 11 | #pragma shaderfilter set fft_color__description FFT Color 12 | #pragma shaderfilter set fft_color__default 7FFF00FF 13 | uniform float4 fft_color; 14 | 15 | float remap(float x, float2 from, float2 to) { 16 | float normalized = (x - from[0]) / (from[1] - from[0]); 17 | return normalized * (to[1] - to[0]) + to[0]; 18 | } 19 | 20 | float4 render(float2 uv) { 21 | float fft_frequency = uv.x; 22 | float fft_amplitude = builtin_texture_fft_main.Sample(builtin_texture_sampler, float2(fft_frequency, 0.5)).r; 23 | float fft_db = 20.0 * log(fft_amplitude / 0.5) / log(10.0); 24 | float fft_db_remapped = remap(fft_db, float2(-50, -0), float2(0, 1)); 25 | float value = float(1.0 - uv.y < fft_db_remapped); 26 | float3 color = image.Sample(builtin_texture_sampler, uv).rgb; 27 | 28 | return float4(lerp(color, fft_color.rgb, fft_color.a * value), 1.0); 29 | } 30 | -------------------------------------------------------------------------------- /examples/fft_delta.hlsl: -------------------------------------------------------------------------------- 1 | // Configure builtin uniforms 2 | // These macros are optional, but improve the user experience 3 | #pragma shaderfilter set main__mix__description Main Mix/Track 4 | #pragma shaderfilter set main__channel__description Main Channel 5 | #pragma shaderfilter set main__dampening_factor_attack 0.0 6 | #pragma shaderfilter set main__dampening_factor_release 0.0001 7 | uniform texture2d builtin_texture_fft_main; 8 | uniform texture2d builtin_texture_fft_main_previous; 9 | 10 | float remap(float x, float2 from, float2 to) { 11 | float normalized = (x - from[0]) / (from[1] - from[0]); 12 | return normalized * (to[1] - to[0]) + to[0]; 13 | } 14 | 15 | float remap_amplitude(float fft_amplitude) { 16 | float fft_db = 20.0 * log(fft_amplitude / 0.5) / log(10.0); 17 | 18 | return remap(fft_db, float2(-50, -0), float2(0, 1)); 19 | } 20 | 21 | bool below_db(float2 uv, float fft_amplitude) { 22 | return 1.0 - uv.y < remap_amplitude(fft_amplitude); 23 | } 24 | 25 | float4 render(float2 uv) { 26 | float3 color = image.Sample(builtin_texture_sampler, uv).rgb; 27 | float fft_frequency = uv.x; 28 | float fft_amplitude = builtin_texture_fft_main.Sample(builtin_texture_sampler, float2(fft_frequency, 0.5)).r; 29 | float fft_amplitude_previous = builtin_texture_fft_main_previous.Sample(builtin_texture_sampler, float2(fft_frequency, 0.5)).r; 30 | float value = float(below_db(uv, fft_amplitude)); 31 | float value_previous = float(below_db(uv, fft_amplitude_previous)); 32 | 33 | float difference = value - value_previous; 34 | float rising = float(difference > 0); 35 | float falling = float(difference < 0); 36 | 37 | float4 fft_color = float4(falling, rising, 0.0, abs(difference)); 38 | 39 | return float4(lerp(color, fft_color.rgb, fft_color.a), 1.0); 40 | } 41 | -------------------------------------------------------------------------------- /examples/visibility.hlsl: -------------------------------------------------------------------------------- 1 | // This example demonstrates the usage of `builtin_elapsed_time_since_shown` 2 | // which is the number of seconds since the source was made visible. 3 | float4 render(float2 uv) { 4 | float4 image_color = image.Sample(builtin_texture_sampler, uv); 5 | float active_time; 6 | 7 | if (uv.x < 1.0 / 3.0) { 8 | // Left third: Modulate alpha by time since shown. 9 | // The timer is reset by toggling the visibility of the source this 10 | // filter is applied to. 11 | active_time = builtin_elapsed_time_since_shown; 12 | } else if (uv.x > 2.0 / 3.0) { 13 | // Right third: Modulate alpha by time since enabled. 14 | // The timer is reset by toggling the visibility of the filter itself. 15 | active_time = builtin_elapsed_time_since_enabled; 16 | } else { 17 | // Middle third: Modulate alpha by minimum of both. 18 | active_time = min(builtin_elapsed_time_since_shown, builtin_elapsed_time_since_enabled); 19 | } 20 | 21 | float alpha_coefficient = active_time / 10.0; 22 | 23 | image_color.a *= min(1.0, alpha_coefficient); 24 | 25 | return image_color; 26 | } 27 | -------------------------------------------------------------------------------- /install-debug.fish: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env fish 2 | set DIR (realpath (dirname (status --current-filename))) 3 | 4 | if not cargo build 5 | echo "Could not build the plugin; not installing." 1>&2 6 | exit 1 7 | end 8 | 9 | if not test -f "$DIR/target/debug/libobs_shaderfilter_plus.so" 10 | echo "The binary was not built; aborting." 1>&2 11 | exit 1 12 | end 13 | 14 | sudo cp "$DIR/target/debug/libobs_shaderfilter_plus.so" /usr/lib/obs-plugins/libobs-shaderfilter-plus.so 15 | -------------------------------------------------------------------------------- /install.fish: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env fish 2 | set DIR (realpath (dirname (status --current-filename))) 3 | 4 | if not cargo build --release 5 | echo "Could not build the plugin; not installing." 1>&2 6 | exit 1 7 | end 8 | 9 | if not test -f "$DIR/target/release/libobs_shaderfilter_plus.so" 10 | echo "The binary was not built; aborting." 1>&2 11 | exit 1 12 | end 13 | 14 | sudo cp "$DIR/target/release/libobs_shaderfilter_plus.so" /usr/lib/obs-plugins/libobs-shaderfilter-plus.so 15 | -------------------------------------------------------------------------------- /src/effect/effect_param.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::borrow::Cow; 3 | use obs_wrapper::{obs_sys::MAX_AUDIO_MIXES, context::*, graphics::*, source::*}; 4 | use smallvec::{SmallVec, smallvec}; 5 | use paste::item; 6 | use crate::*; 7 | 8 | /// Used to convert cloneable values into `ShaderParamType::RustType`. 9 | pub trait EffectParamType { 10 | type ShaderParamType: ShaderParamType; 11 | type PreparedValueType: Default; 12 | 13 | fn convert_and_stage_value( 14 | prepared: Self::PreparedValueType, 15 | context: &GraphicsContext, 16 | ) -> GraphicsContextDependentDisabled<::RustType>; 17 | 18 | fn assign_staged_value<'a, 'b>( 19 | staged: &'b ::RustType, 20 | param: &'b mut EnableGuardMut<'a, 'b, GraphicsEffectParamTyped, GraphicsContext>, 21 | context: &'b FilterContext, 22 | ); 23 | } 24 | 25 | macro_rules! define_effect_param_aliases { 26 | ($($name:ident),*$(,)?) => { 27 | item! { 28 | $( 29 | #[allow(dead_code)] 30 | pub type [< EffectParam $name >] = EffectParam]>>; 31 | )* 32 | } 33 | } 34 | } 35 | 36 | define_effect_param_aliases! { 37 | Bool, Int, IVec2, IVec3, IVec4, Float, Vec2, Vec3, Vec4, Mat4, 38 | } 39 | 40 | #[derive(Clone, Debug)] 41 | pub struct TextureDescriptor { 42 | pub dimensions: [usize; 2], 43 | pub color_format: ColorFormatKind, 44 | pub levels: SmallVec<[Vec; 1]>, 45 | pub flags: u32, 46 | } 47 | 48 | impl Default for TextureDescriptor { 49 | fn default() -> Self { 50 | Self { 51 | dimensions: [1, 1], 52 | color_format: ColorFormatKind::RGBA, 53 | levels: smallvec![vec![0, 0, 0, 0]], 54 | flags: 0, 55 | } 56 | } 57 | } 58 | 59 | pub type EffectParamTexture = EffectParam; 60 | 61 | pub struct EffectParamTypeClone 62 | where T: ShaderParamType, 63 | ::RustType: Default, 64 | { 65 | __marker: std::marker::PhantomData, 66 | } 67 | 68 | impl EffectParamType for EffectParamTypeClone 69 | where T: ShaderParamType, 70 | ::RustType: Default, 71 | { 72 | type ShaderParamType = T; 73 | type PreparedValueType = ::RustType; 74 | 75 | fn convert_and_stage_value( 76 | prepared: Self::PreparedValueType, 77 | context: &GraphicsContext, 78 | ) -> GraphicsContextDependentDisabled<::RustType> { 79 | ContextDependent::new(prepared, context).disable() 80 | } 81 | 82 | fn assign_staged_value<'a, 'b>( 83 | staged: &'b ::RustType, 84 | param: &'b mut EnableGuardMut<'a, 'b, GraphicsEffectParamTyped, GraphicsContext>, 85 | context: &'b FilterContext, 86 | ) { 87 | param.set_param_value(&staged, context); 88 | } 89 | } 90 | 91 | pub struct EffectParamTypeTexture; 92 | 93 | impl EffectParamType for EffectParamTypeTexture { 94 | type ShaderParamType = ShaderParamTypeTexture; 95 | type PreparedValueType = TextureDescriptor; 96 | 97 | fn convert_and_stage_value( 98 | prepared: Self::PreparedValueType, 99 | context: &GraphicsContext, 100 | ) -> GraphicsContextDependentDisabled<::RustType> { 101 | let levels: Vec<&[u8]> = prepared.levels.iter().map(|vec| &vec[..]).collect::>(); 102 | 103 | Texture::new( 104 | prepared.dimensions, 105 | prepared.color_format, 106 | &levels, 107 | prepared.flags, 108 | context, 109 | ).disable() 110 | } 111 | 112 | fn assign_staged_value<'a, 'b>( 113 | staged: &'b ::RustType, 114 | param: &'b mut EnableGuardMut<'a, 'b, GraphicsEffectParamTyped, GraphicsContext>, 115 | context: &'b FilterContext, 116 | ) { 117 | param.set_param_value(&staged, context); 118 | } 119 | } 120 | 121 | /// This type takes care of three different tasks: 122 | /// It stores a _prepared value_ (see `prepare_value`). 123 | /// It creates a graphics resource from the _prepared value_, if it was changed, and stores the result (see `stage_value`). 124 | /// It assigns the staged values to filters (see `assign_value`). 125 | pub struct EffectParam { 126 | pub param: GraphicsContextDependentDisabled::ShaderParamType>>, 127 | pub prepared_value: Option, 128 | pub staged_value: Option::ShaderParamType as ShaderParamType>::RustType>>, 129 | } 130 | 131 | impl EffectParam { 132 | pub fn new(param: GraphicsContextDependentDisabled::ShaderParamType>>) -> Self { 133 | Self { 134 | param, 135 | prepared_value: Some(Default::default()), 136 | staged_value: None, 137 | } 138 | } 139 | 140 | /// Requests a new staged value to be generated from this prepared value 141 | pub fn prepare_value(&mut self, new_value: T::PreparedValueType) { 142 | self.prepared_value = Some(new_value); 143 | } 144 | 145 | /// If a value is prepared (not `None`), creates a graphics resource from that value, 146 | /// to be used in effect filter processing. 147 | pub fn stage_value<'a>(&mut self, graphics_context: &'a GraphicsContext) { 148 | if let Some(prepared_value) = self.prepared_value.take() { 149 | if let Some(previous) = self.staged_value.replace(::convert_and_stage_value( 150 | prepared_value, 151 | graphics_context, 152 | )) { 153 | previous.enable(graphics_context); 154 | } 155 | } 156 | } 157 | 158 | pub fn stage_value_custom<'a>(&mut self, value: GraphicsContextDependentDisabled<<::ShaderParamType as ShaderParamType>::RustType>, graphics_context: &'a GraphicsContext) { 159 | if let Some(previous) = self.staged_value.replace(value) { 160 | previous.enable(graphics_context); 161 | } 162 | } 163 | 164 | pub fn take_staged_value(&mut self) -> Option::ShaderParamType as ShaderParamType>::RustType>> { 165 | self.staged_value.take() 166 | } 167 | 168 | /// Assigns the staged value to the effect current filter. 169 | /// Keeps the staged value around. 170 | pub fn assign_value<'a>(&mut self, context: &'a FilterContext) { 171 | let staged_value = self.staged_value.as_ref() 172 | .expect("Tried to assign a value before staging it.") 173 | .as_enabled(context.graphics()); 174 | 175 | ::assign_staged_value( 176 | &staged_value, 177 | &mut self.param.as_enabled_mut(context.graphics()), 178 | context, 179 | ); 180 | } 181 | 182 | pub fn assign_value_if_staged<'a>(&mut self, context: &'a FilterContext) { 183 | if self.staged_value.is_some() { 184 | self.assign_value(context); 185 | } 186 | } 187 | 188 | pub fn enable_and_drop(self, graphics_context: &GraphicsContext) { 189 | self.param.enable(graphics_context); 190 | if let Some(staged_value) = self.staged_value { 191 | staged_value.enable(graphics_context); 192 | } 193 | } 194 | } 195 | 196 | // A helper trait to ensure most custom effect params follow the same structure. 197 | // Not all custom effect params implement this trait, however. 198 | pub trait EffectParamCustom: BindableProperty + Sized { 199 | type ShaderParamType: ShaderParamType; 200 | type PropertyDescriptorSpecialization: PropertyDescriptorSpecialization; 201 | 202 | fn new<'a>( 203 | param: GraphicsContextDependentEnabled<'a, GraphicsEffectParamTyped>, 204 | identifier: &str, 205 | settings: &mut SettingsContext, 206 | preprocess_result: &PreprocessResult, 207 | ) -> Result>; 208 | } 209 | 210 | pub struct EffectParamCustomBool { 211 | pub effect_param: EffectParamBool, 212 | pub property: LoadedValueTypeProperty 213 | } 214 | 215 | impl EffectParamCustom for EffectParamCustomBool { 216 | type ShaderParamType = ShaderParamTypeBool; 217 | type PropertyDescriptorSpecialization = PropertyDescriptorSpecializationBool; 218 | 219 | fn new<'a>( 220 | param: GraphicsContextDependentEnabled<'a, GraphicsEffectParamTyped>, 221 | identifier: &str, 222 | settings: &mut SettingsContext, 223 | preprocess_result: &PreprocessResult, 224 | ) -> Result> { 225 | let property = as LoadedValueType>::from( 226 | LoadedValueTypePropertyArgs { 227 | allow_definitions_in_source: true, 228 | default_value: *param.get_param_value_default().unwrap_or(&false), 229 | default_descriptor_specialization: PropertyDescriptorSpecializationBool {}, 230 | }, 231 | identifier, 232 | None, 233 | preprocess_result, 234 | settings, 235 | )?; 236 | let mut effect_param = EffectParam::new(param.disable()); 237 | 238 | effect_param.prepare_value(property.get_value()); 239 | 240 | Ok(Self { 241 | property, 242 | effect_param, 243 | }) 244 | } 245 | } 246 | 247 | impl BindableProperty for EffectParamCustomBool { 248 | fn add_properties(&self, properties: &mut Properties) { 249 | self.property.add_properties(properties); 250 | } 251 | 252 | fn reload_settings(&mut self, settings: &mut SettingsContext) { 253 | self.property.reload_settings(settings); 254 | self.effect_param.prepare_value(self.property.get_value()); 255 | } 256 | 257 | fn prepare_values(&mut self) {} 258 | 259 | fn stage_value<'a>(&mut self, graphics_context: &'a GraphicsContext) { 260 | self.effect_param.stage_value(graphics_context); 261 | } 262 | 263 | fn assign_value<'a>(&mut self, graphics_context: &'a FilterContext) { 264 | self.effect_param.assign_value(graphics_context); 265 | } 266 | 267 | fn enable_and_drop(self, graphics_context: &GraphicsContext) { 268 | self.effect_param.enable_and_drop(graphics_context); 269 | } 270 | } 271 | 272 | pub struct EffectParamCustomInt { 273 | pub effect_param: EffectParamInt, 274 | pub property: LoadedValueTypeProperty 275 | } 276 | 277 | impl EffectParamCustom for EffectParamCustomInt { 278 | type ShaderParamType = ShaderParamTypeInt; 279 | type PropertyDescriptorSpecialization = PropertyDescriptorSpecializationI32; 280 | 281 | fn new<'a>( 282 | param: GraphicsContextDependentEnabled<'a, GraphicsEffectParamTyped>, 283 | identifier: &str, 284 | settings: &mut SettingsContext, 285 | preprocess_result: &PreprocessResult, 286 | ) -> Result> { 287 | let property = as LoadedValueType>::from( 288 | LoadedValueTypePropertyArgs { 289 | allow_definitions_in_source: true, 290 | default_value: *param.get_param_value_default().unwrap_or(&0), 291 | default_descriptor_specialization: Self::PropertyDescriptorSpecialization { 292 | min: std::i32::MIN, 293 | max: std::i32::MAX, 294 | step: 1, 295 | slider: false, 296 | }, 297 | }, 298 | identifier, 299 | None, 300 | preprocess_result, 301 | settings, 302 | )?; 303 | let mut effect_param = EffectParam::new(param.disable()); 304 | 305 | effect_param.prepare_value(property.get_value()); 306 | 307 | Ok(Self { 308 | property, 309 | effect_param, 310 | }) 311 | } 312 | } 313 | 314 | impl BindableProperty for EffectParamCustomInt { 315 | fn add_properties(&self, properties: &mut Properties) { 316 | self.property.add_properties(properties); 317 | } 318 | 319 | fn reload_settings(&mut self, settings: &mut SettingsContext) { 320 | self.property.reload_settings(settings); 321 | self.effect_param.prepare_value(self.property.get_value()); 322 | } 323 | 324 | fn prepare_values(&mut self) {} 325 | 326 | fn stage_value<'a>(&mut self, graphics_context: &'a GraphicsContext) { 327 | self.effect_param.stage_value(graphics_context); 328 | } 329 | 330 | fn assign_value<'a>(&mut self, graphics_context: &'a FilterContext) { 331 | self.effect_param.assign_value(graphics_context); 332 | } 333 | 334 | fn enable_and_drop(self, graphics_context: &GraphicsContext) { 335 | self.effect_param.enable_and_drop(graphics_context); 336 | } 337 | } 338 | 339 | pub struct EffectParamCustomFloat { 340 | pub effect_param: EffectParamFloat, 341 | pub property: LoadedValueTypeProperty 342 | } 343 | 344 | impl EffectParamCustom for EffectParamCustomFloat { 345 | type ShaderParamType = ShaderParamTypeFloat; 346 | type PropertyDescriptorSpecialization = PropertyDescriptorSpecializationF64; 347 | 348 | fn new<'a>( 349 | param: GraphicsContextDependentEnabled<'a, GraphicsEffectParamTyped>, 350 | identifier: &str, 351 | settings: &mut SettingsContext, 352 | preprocess_result: &PreprocessResult, 353 | ) -> Result> { 354 | let property = as LoadedValueType>::from( 355 | LoadedValueTypePropertyArgs { 356 | allow_definitions_in_source: true, 357 | default_value: *param.get_param_value_default().unwrap_or(&0.0) as f64, 358 | default_descriptor_specialization: Self::PropertyDescriptorSpecialization { 359 | min: std::f64::MIN, 360 | max: std::f64::MAX, 361 | step: 0.1, 362 | slider: false, 363 | }, 364 | }, 365 | identifier, 366 | None, 367 | preprocess_result, 368 | settings, 369 | )?; 370 | let mut effect_param = EffectParam::new(param.disable()); 371 | 372 | effect_param.prepare_value(property.get_value() as f32); 373 | 374 | Ok(Self { 375 | property, 376 | effect_param, 377 | }) 378 | } 379 | } 380 | 381 | impl BindableProperty for EffectParamCustomFloat { 382 | fn add_properties(&self, properties: &mut Properties) { 383 | self.property.add_properties(properties); 384 | } 385 | 386 | fn reload_settings(&mut self, settings: &mut SettingsContext) { 387 | self.property.reload_settings(settings); 388 | self.effect_param.prepare_value(self.property.get_value() as f32); 389 | } 390 | 391 | fn prepare_values(&mut self) {} 392 | 393 | fn stage_value<'a>(&mut self, graphics_context: &'a GraphicsContext) { 394 | self.effect_param.stage_value(graphics_context); 395 | } 396 | 397 | fn assign_value<'a>(&mut self, graphics_context: &'a FilterContext) { 398 | self.effect_param.assign_value(graphics_context); 399 | } 400 | 401 | fn enable_and_drop(self, graphics_context: &GraphicsContext) { 402 | self.effect_param.enable_and_drop(graphics_context); 403 | } 404 | } 405 | 406 | pub struct EffectParamCustomColor { 407 | pub effect_param: EffectParamVec4, 408 | pub property: LoadedValueTypeProperty 409 | } 410 | 411 | impl EffectParamCustom for EffectParamCustomColor { 412 | type ShaderParamType = ShaderParamTypeVec4; 413 | type PropertyDescriptorSpecialization = PropertyDescriptorSpecializationColor; 414 | 415 | fn new<'a>( 416 | param: GraphicsContextDependentEnabled<'a, GraphicsEffectParamTyped>, 417 | identifier: &str, 418 | settings: &mut SettingsContext, 419 | preprocess_result: &PreprocessResult, 420 | ) -> Result> { 421 | let property = as LoadedValueType>::from( 422 | LoadedValueTypePropertyArgs { 423 | allow_definitions_in_source: true, 424 | default_value: Color(*param.get_param_value_default().unwrap_or(&[0.0; 4])), 425 | default_descriptor_specialization: Self::PropertyDescriptorSpecialization {}, 426 | }, 427 | identifier, 428 | None, 429 | preprocess_result, 430 | settings, 431 | )?; 432 | let mut effect_param = EffectParam::new(param.disable()); 433 | 434 | effect_param.prepare_value((property.get_value() as Color).into()); 435 | 436 | Ok(Self { 437 | property, 438 | effect_param, 439 | }) 440 | } 441 | } 442 | 443 | impl BindableProperty for EffectParamCustomColor { 444 | fn add_properties(&self, properties: &mut Properties) { 445 | self.property.add_properties(properties); 446 | } 447 | 448 | fn reload_settings(&mut self, settings: &mut SettingsContext) { 449 | self.property.reload_settings(settings); 450 | self.effect_param.prepare_value((self.property.get_value() as Color).into()); 451 | } 452 | 453 | fn prepare_values(&mut self) {} 454 | 455 | fn stage_value<'a>(&mut self, graphics_context: &'a GraphicsContext) { 456 | self.effect_param.stage_value(graphics_context); 457 | } 458 | 459 | fn assign_value<'a>(&mut self, graphics_context: &'a FilterContext) { 460 | self.effect_param.assign_value(graphics_context); 461 | } 462 | 463 | fn enable_and_drop(self, graphics_context: &GraphicsContext) { 464 | self.effect_param.enable_and_drop(graphics_context); 465 | } 466 | } 467 | 468 | pub struct EffectParamCustomFFT { 469 | pub effect_param: EffectParamTexture, 470 | pub effect_param_previous: Option, 471 | pub audio_fft: Option>, 472 | pub property_mix: LoadedValueTypeProperty, 473 | pub property_channel: LoadedValueTypeProperty, 474 | pub property_dampening_factor_attack: LoadedValueTypeProperty, 475 | pub property_dampening_factor_release: LoadedValueTypeProperty, 476 | } 477 | 478 | // Does not implement EffectParamCustom because of different argument requirements 479 | impl EffectParamCustomFFT { 480 | pub fn new<'a>( 481 | param: GraphicsContextDependentEnabled<'a, GraphicsEffectParamTyped>, 482 | param_previous: Option>>, 483 | identifier: &str, 484 | settings: &mut SettingsContext, 485 | preprocess_result: &PreprocessResult, 486 | ) -> Result> { 487 | let property_mix = as LoadedValueType>::from( 488 | LoadedValueTypePropertyArgs { 489 | allow_definitions_in_source: false, 490 | default_value: 1, 491 | default_descriptor_specialization: PropertyDescriptorSpecializationI32 { 492 | min: 1, 493 | max: MAX_AUDIO_MIXES as i32, 494 | step: 1, 495 | slider: false, 496 | }, 497 | }, 498 | identifier, 499 | Some("mix"), 500 | preprocess_result, 501 | settings, 502 | )?; 503 | let property_channel = as LoadedValueType>::from( 504 | LoadedValueTypePropertyArgs { 505 | allow_definitions_in_source: false, 506 | default_value: 1, 507 | default_descriptor_specialization: PropertyDescriptorSpecializationI32 { 508 | min: 1, 509 | max: 2, // FIXME: Causes crashes when `MAX_AUDIO_CHANNELS as i32` is used, supposedly fixed in next OBS release 510 | step: 1, 511 | slider: false, 512 | }, 513 | }, 514 | identifier, 515 | Some("channel"), 516 | preprocess_result, 517 | settings, 518 | )?; 519 | let property_dampening_factor_attack = as LoadedValueType>::from( 520 | LoadedValueTypePropertyArgs { 521 | allow_definitions_in_source: true, 522 | default_value: 0.0, 523 | default_descriptor_specialization: PropertyDescriptorSpecializationF64 { 524 | min: 0.0, 525 | max: 100.0, 526 | step: 0.01, 527 | slider: true, 528 | }, 529 | }, 530 | identifier, 531 | Some("dampening_factor_attack"), 532 | preprocess_result, 533 | settings, 534 | )?; 535 | let property_dampening_factor_release = as LoadedValueType>::from( 536 | LoadedValueTypePropertyArgs { 537 | allow_definitions_in_source: true, 538 | default_value: 0.0, 539 | default_descriptor_specialization: PropertyDescriptorSpecializationF64 { 540 | min: 0.0, 541 | max: 100.0, 542 | step: 0.01, 543 | slider: true, 544 | }, 545 | }, 546 | identifier, 547 | Some("dampening_factor_release"), 548 | preprocess_result, 549 | settings, 550 | )?; 551 | 552 | let audio_fft_descriptor = GlobalStateAudioFFTDescriptor::new( 553 | property_mix.get_value() as usize - 1, 554 | property_channel.get_value() as usize - 1, 555 | property_dampening_factor_attack.get_value() / 100.0, 556 | property_dampening_factor_release.get_value() / 100.0, 557 | // TODO: Make customizable, but provide a sane default value 558 | WindowFunction::Hanning, 559 | ); 560 | 561 | let mut result = Self { 562 | effect_param: EffectParam::new(param.disable()), 563 | effect_param_previous: param_previous.map(|param_previous| EffectParam::new(param_previous.disable())), 564 | audio_fft: None, 565 | property_mix, 566 | property_channel, 567 | property_dampening_factor_attack, 568 | property_dampening_factor_release, 569 | }; 570 | 571 | result.request_audio_fft(); 572 | 573 | Ok(result) 574 | } 575 | 576 | fn request_audio_fft(&mut self) { 577 | let audio_fft_descriptor = GlobalStateAudioFFTDescriptor::new( 578 | self.property_mix.get_value() as usize - 1, 579 | self.property_channel.get_value() as usize - 1, 580 | self.property_dampening_factor_attack.get_value() / 100.0, 581 | self.property_dampening_factor_release.get_value() / 100.0, 582 | // TODO: Make customizable, but provide a sane default value 583 | WindowFunction::Hanning, 584 | ); 585 | 586 | self.audio_fft = Some(GLOBAL_STATE.request_audio_fft(&audio_fft_descriptor)); 587 | } 588 | } 589 | 590 | impl BindableProperty for EffectParamCustomFFT { 591 | fn add_properties(&self, properties: &mut Properties) { 592 | self.property_mix.add_properties(properties); 593 | self.property_channel.add_properties(properties); 594 | self.property_dampening_factor_attack.add_properties(properties); 595 | self.property_dampening_factor_release.add_properties(properties); 596 | } 597 | 598 | fn reload_settings(&mut self, settings: &mut SettingsContext) { 599 | self.property_mix.reload_settings(settings); 600 | self.property_channel.reload_settings(settings); 601 | self.property_dampening_factor_attack.reload_settings(settings); 602 | self.property_dampening_factor_release.reload_settings(settings); 603 | self.request_audio_fft(); 604 | } 605 | 606 | fn prepare_values(&mut self) { 607 | let fft_result = if let Some(result) = self.audio_fft.as_mut().unwrap().retrieve_result() { 608 | result 609 | } else { 610 | return; 611 | }; 612 | let frequency_spectrum = &fft_result.frequency_spectrum; 613 | let texture_data = unsafe { 614 | std::slice::from_raw_parts::( 615 | frequency_spectrum.as_ptr() as *const _, 616 | frequency_spectrum.len() * std::mem::size_of::(), 617 | ) 618 | }.iter().copied().collect::>(); 619 | let texture_fft = TextureDescriptor { 620 | dimensions: [frequency_spectrum.len(), 1], 621 | color_format: ColorFormatKind::R32F, 622 | levels: smallvec![texture_data], 623 | flags: 0, 624 | }; 625 | 626 | self.effect_param.prepare_value(texture_fft); 627 | } 628 | 629 | fn stage_value<'a>(&mut self, graphics_context: &'a GraphicsContext) { 630 | if let Some(effect_param_previous) = self.effect_param_previous.as_mut() { 631 | if let Some(previous_texture_fft) = self.effect_param.take_staged_value() { 632 | effect_param_previous.stage_value_custom(previous_texture_fft, graphics_context); 633 | } 634 | } 635 | 636 | self.effect_param.stage_value(graphics_context); 637 | } 638 | 639 | fn assign_value<'a>(&mut self, graphics_context: &'a FilterContext) { 640 | if let Some(effect_param_previous) = self.effect_param_previous.as_mut() { 641 | effect_param_previous.assign_value_if_staged(graphics_context); 642 | } 643 | self.effect_param.assign_value_if_staged(graphics_context); 644 | } 645 | 646 | fn enable_and_drop(self, graphics_context: &GraphicsContext) { 647 | if let Some(effect_param_previous) = self.effect_param_previous { 648 | effect_param_previous.enable_and_drop(graphics_context); 649 | } 650 | self.effect_param.enable_and_drop(graphics_context); 651 | } 652 | } 653 | -------------------------------------------------------------------------------- /src/effect/loaded_value.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::str::FromStr; 3 | use std::borrow::Cow; 4 | use std::ffi::CString; 5 | use obs_wrapper::source::*; 6 | use crate::*; 7 | 8 | pub trait LoadedValueType: Sized { 9 | type Output; 10 | type Args; 11 | 12 | fn from_identifier( 13 | args: Self::Args, 14 | identifier: &str, 15 | preprocess_result: &PreprocessResult, 16 | settings: &mut SettingsContext, 17 | ) -> Result>; 18 | 19 | fn from( 20 | args: Self::Args, 21 | parent_identifier: &str, 22 | property_name: Option<&str>, 23 | preprocess_result: &PreprocessResult, 24 | settings: &mut SettingsContext, 25 | ) -> Result> { 26 | if let Some(property_name) = property_name { 27 | Self::from_identifier( 28 | args, 29 | &format!("{}__{}", parent_identifier, property_name), 30 | preprocess_result, 31 | settings, 32 | ) 33 | } else { 34 | Self::from_identifier( 35 | args, 36 | parent_identifier, 37 | preprocess_result, 38 | settings, 39 | ) 40 | } 41 | } 42 | 43 | fn reload_settings(&mut self, settings: &mut SettingsContext); 44 | 45 | fn add_properties(&self, properties: &mut Properties); 46 | 47 | fn get_value(&self) -> Self::Output; 48 | } 49 | 50 | pub struct LoadedValueTypeSourceArgs where T: FromStr + Clone { 51 | default_value: Option, 52 | } 53 | 54 | /// A loaded value of which the value can only be specified in the shader source code. 55 | /// Returns `Some(T)`, if a default value is specified. 56 | /// Otherwise, may return `None`. 57 | #[derive(Debug)] 58 | pub struct LoadedValueTypeSource where T: FromStr + Clone { 59 | value: Option, 60 | } 61 | 62 | impl LoadedValueType for LoadedValueTypeSource where T: FromStr + Clone { 63 | type Output = Option; 64 | type Args = LoadedValueTypeSourceArgs; 65 | 66 | fn from_identifier( 67 | args: Self::Args, 68 | identifier: &str, 69 | preprocess_result: &PreprocessResult, 70 | _settings: &mut SettingsContext, 71 | ) -> Result> { 72 | let value = preprocess_result.parse::(identifier) 73 | .transpose()? 74 | .or(args.default_value); 75 | 76 | Ok(Self { value }) 77 | } 78 | 79 | fn reload_settings(&mut self, _settings: &mut SettingsContext) { 80 | // All values are taken from the source code, nothing to reload 81 | } 82 | 83 | fn add_properties(&self, _properties: &mut Properties) { 84 | // Simple property types do not produce any UI 85 | } 86 | 87 | fn get_value(&self) -> Self::Output { 88 | self.value.clone() 89 | } 90 | } 91 | 92 | #[derive(Debug)] 93 | pub struct LoadedValueTypePropertyDescriptorArgs { 94 | default_value: T, 95 | } 96 | 97 | pub trait LoadedValueTypePropertyDescriptor: Sized { 98 | type Specialization: PropertyDescriptorSpecialization; 99 | 100 | fn new_args(specialization: Self::Specialization) -> LoadedValueTypePropertyDescriptorArgs { 101 | LoadedValueTypePropertyDescriptorArgs { 102 | default_value: specialization, 103 | } 104 | } 105 | 106 | fn from_identifier( 107 | args: LoadedValueTypePropertyDescriptorArgs, 108 | identifier: &str, 109 | preprocess_result: &PreprocessResult, 110 | settings: &mut SettingsContext, 111 | ) -> Result>; 112 | 113 | fn add_properties(&self, properties: &mut Properties); 114 | 115 | fn get_value(&self) -> PropertyDescriptor; 116 | } 117 | 118 | impl LoadedValueType for L 119 | where T: PropertyDescriptorSpecialization + Sized, 120 | L: LoadedValueTypePropertyDescriptor, 121 | { 122 | type Output = PropertyDescriptor; 123 | type Args = LoadedValueTypePropertyDescriptorArgs; 124 | 125 | fn from_identifier( 126 | args: Self::Args, 127 | identifier: &str, 128 | preprocess_result: &PreprocessResult, 129 | settings: &mut SettingsContext, 130 | ) -> Result> { 131 | ::from_identifier(args, identifier, preprocess_result, settings) 132 | } 133 | 134 | fn reload_settings(&mut self, _settings: &mut SettingsContext) { 135 | // Descriptors are loaded from the source code, not the settings 136 | } 137 | 138 | fn add_properties(&self, properties: &mut Properties) { 139 | ::add_properties(self, properties) 140 | } 141 | 142 | fn get_value(&self) -> Self::Output { 143 | ::get_value(self) 144 | } 145 | } 146 | 147 | /// A loaded value type, which loads a property descriptor from the shader source code. 148 | /// Useful for creating loaded values which retrieve their data from the effect UI. 149 | #[derive(Debug)] 150 | pub struct LoadedValueTypePropertyDescriptorF64 { 151 | descriptor: PropertyDescriptor, 152 | description: LoadedValueTypeSource, 153 | min: LoadedValueTypeSource, 154 | max: LoadedValueTypeSource, 155 | step: LoadedValueTypeSource, 156 | slider: LoadedValueTypeSource, 157 | } 158 | 159 | impl LoadedValueTypePropertyDescriptor for LoadedValueTypePropertyDescriptorF64 { 160 | type Specialization = PropertyDescriptorSpecializationF64; 161 | 162 | fn from_identifier( 163 | args: LoadedValueTypePropertyDescriptorArgs, 164 | identifier: &str, 165 | preprocess_result: &PreprocessResult, 166 | settings: &mut SettingsContext, 167 | ) -> Result> { 168 | let description = as LoadedValueType>::from( 169 | LoadedValueTypeSourceArgs { 170 | default_value: Some(identifier.to_string()), 171 | }, 172 | identifier, 173 | Some("description"), 174 | preprocess_result, 175 | settings, 176 | )?; 177 | let min = as LoadedValueType>::from( 178 | LoadedValueTypeSourceArgs { 179 | default_value: Some(args.default_value.min), 180 | }, 181 | identifier, 182 | Some("min"), 183 | preprocess_result, 184 | settings, 185 | )?; 186 | let max = as LoadedValueType>::from( 187 | LoadedValueTypeSourceArgs { 188 | default_value: Some(args.default_value.max), 189 | }, 190 | identifier, 191 | Some("max"), 192 | preprocess_result, 193 | settings, 194 | )?; 195 | let step = as LoadedValueType>::from( 196 | LoadedValueTypeSourceArgs { 197 | default_value: Some(args.default_value.step), 198 | }, 199 | identifier, 200 | Some("step"), 201 | preprocess_result, 202 | settings, 203 | )?; 204 | let slider = as LoadedValueType>::from( 205 | LoadedValueTypeSourceArgs { 206 | default_value: Some(args.default_value.slider), 207 | }, 208 | identifier, 209 | Some("slider"), 210 | preprocess_result, 211 | settings, 212 | )?; 213 | let descriptor = PropertyDescriptor { 214 | // we can safely unwrap loaded values, because default values were specified 215 | name: CString::new(identifier).unwrap(), 216 | description: CString::new(description.get_value().unwrap()).unwrap(), 217 | specialization: PropertyDescriptorSpecializationF64 { 218 | min: min.get_value().unwrap(), 219 | max: max.get_value().unwrap(), 220 | step: step.get_value().unwrap(), 221 | slider: slider.get_value().unwrap(), 222 | }, 223 | }; 224 | 225 | Ok(Self { 226 | descriptor, 227 | description, 228 | min, 229 | max, 230 | step, 231 | slider, 232 | }) 233 | } 234 | 235 | fn add_properties(&self, properties: &mut Properties) { 236 | self.min.add_properties(properties); 237 | self.max.add_properties(properties); 238 | self.step.add_properties(properties); 239 | self.slider.add_properties(properties); 240 | self.description.add_properties(properties); 241 | properties.add_property(&self.descriptor); 242 | } 243 | 244 | fn get_value(&self) -> PropertyDescriptor { 245 | self.descriptor.clone() 246 | } 247 | } 248 | 249 | #[derive(Debug)] 250 | pub struct LoadedValueTypePropertyDescriptorI32 { 251 | descriptor: PropertyDescriptor, 252 | description: LoadedValueTypeSource, 253 | min: LoadedValueTypeSource, 254 | max: LoadedValueTypeSource, 255 | step: LoadedValueTypeSource, 256 | slider: LoadedValueTypeSource, 257 | } 258 | 259 | impl LoadedValueTypePropertyDescriptor for LoadedValueTypePropertyDescriptorI32 { 260 | type Specialization = PropertyDescriptorSpecializationI32; 261 | 262 | fn from_identifier( 263 | args: LoadedValueTypePropertyDescriptorArgs, 264 | identifier: &str, 265 | preprocess_result: &PreprocessResult, 266 | settings: &mut SettingsContext, 267 | ) -> Result> { 268 | let description = as LoadedValueType>::from( 269 | LoadedValueTypeSourceArgs { 270 | default_value: Some(identifier.to_string()), 271 | }, 272 | identifier, 273 | Some("description"), 274 | preprocess_result, 275 | settings, 276 | )?; 277 | let min = as LoadedValueType>::from( 278 | LoadedValueTypeSourceArgs { 279 | default_value: Some(args.default_value.min), 280 | }, 281 | identifier, 282 | Some("min"), 283 | preprocess_result, 284 | settings, 285 | )?; 286 | let max = as LoadedValueType>::from( 287 | LoadedValueTypeSourceArgs { 288 | default_value: Some(args.default_value.max), 289 | }, 290 | identifier, 291 | Some("max"), 292 | preprocess_result, 293 | settings, 294 | )?; 295 | let step = as LoadedValueType>::from( 296 | LoadedValueTypeSourceArgs { 297 | default_value: Some(args.default_value.step), 298 | }, 299 | identifier, 300 | Some("step"), 301 | preprocess_result, 302 | settings, 303 | )?; 304 | let slider = as LoadedValueType>::from( 305 | LoadedValueTypeSourceArgs { 306 | default_value: Some(args.default_value.slider), 307 | }, 308 | identifier, 309 | Some("slider"), 310 | preprocess_result, 311 | settings, 312 | )?; 313 | let descriptor = PropertyDescriptor { 314 | // we can safely unwrap loaded values, because default values were specified 315 | name: CString::new(identifier).unwrap(), 316 | description: CString::new(description.get_value().unwrap()).unwrap(), 317 | specialization: PropertyDescriptorSpecializationI32 { 318 | min: min.get_value().unwrap(), 319 | max: max.get_value().unwrap(), 320 | step: step.get_value().unwrap(), 321 | slider: slider.get_value().unwrap(), 322 | }, 323 | }; 324 | 325 | Ok(Self { 326 | descriptor, 327 | description, 328 | min, 329 | max, 330 | step, 331 | slider, 332 | }) 333 | } 334 | 335 | fn add_properties(&self, properties: &mut Properties) { 336 | self.min.add_properties(properties); 337 | self.max.add_properties(properties); 338 | self.step.add_properties(properties); 339 | self.slider.add_properties(properties); 340 | self.description.add_properties(properties); 341 | properties.add_property(&self.descriptor); 342 | } 343 | 344 | fn get_value(&self) -> PropertyDescriptor { 345 | self.descriptor.clone() 346 | } 347 | } 348 | 349 | #[derive(Debug)] 350 | pub struct LoadedValueTypePropertyDescriptorColor { 351 | descriptor: PropertyDescriptor, 352 | description: LoadedValueTypeSource, 353 | } 354 | 355 | impl LoadedValueTypePropertyDescriptor for LoadedValueTypePropertyDescriptorColor { 356 | type Specialization = PropertyDescriptorSpecializationColor; 357 | 358 | fn from_identifier( 359 | _args: LoadedValueTypePropertyDescriptorArgs, 360 | identifier: &str, 361 | preprocess_result: &PreprocessResult, 362 | settings: &mut SettingsContext, 363 | ) -> Result> { 364 | let description = as LoadedValueType>::from( 365 | LoadedValueTypeSourceArgs { 366 | default_value: Some(identifier.to_string()), 367 | }, 368 | identifier, 369 | Some("description"), 370 | preprocess_result, 371 | settings, 372 | )?; 373 | let descriptor = PropertyDescriptor { 374 | // we can safely unwrap loaded values, because default values were specified 375 | name: CString::new(identifier).unwrap(), 376 | description: CString::new(description.get_value().unwrap()).unwrap(), 377 | specialization: PropertyDescriptorSpecializationColor {}, 378 | }; 379 | 380 | Ok(Self { 381 | descriptor, 382 | description, 383 | }) 384 | } 385 | 386 | fn add_properties(&self, properties: &mut Properties) { 387 | self.description.add_properties(properties); 388 | properties.add_property(&self.descriptor); 389 | } 390 | 391 | fn get_value(&self) -> PropertyDescriptor { 392 | self.descriptor.clone() 393 | } 394 | } 395 | 396 | #[derive(Debug)] 397 | pub struct LoadedValueTypePropertyDescriptorBool { 398 | descriptor: PropertyDescriptor, 399 | description: LoadedValueTypeSource, 400 | } 401 | 402 | impl LoadedValueTypePropertyDescriptor for LoadedValueTypePropertyDescriptorBool { 403 | type Specialization = PropertyDescriptorSpecializationBool; 404 | 405 | fn from_identifier( 406 | _args: LoadedValueTypePropertyDescriptorArgs, 407 | identifier: &str, 408 | preprocess_result: &PreprocessResult, 409 | settings: &mut SettingsContext, 410 | ) -> Result> { 411 | let description = as LoadedValueType>::from( 412 | LoadedValueTypeSourceArgs { 413 | default_value: Some(identifier.to_string()), 414 | }, 415 | identifier, 416 | Some("description"), 417 | preprocess_result, 418 | settings, 419 | )?; 420 | let descriptor = PropertyDescriptor { 421 | name: CString::new(identifier).unwrap(), 422 | description: CString::new(description.get_value().unwrap()).unwrap(), 423 | specialization: PropertyDescriptorSpecializationBool {}, 424 | }; 425 | 426 | Ok(Self { 427 | descriptor, 428 | description, 429 | }) 430 | } 431 | 432 | fn add_properties(&self, properties: &mut Properties) { 433 | self.description.add_properties(properties); 434 | properties.add_property(&self.descriptor); 435 | } 436 | 437 | fn get_value(&self) -> PropertyDescriptor { 438 | self.descriptor.clone() 439 | } 440 | } 441 | 442 | pub trait LoadedValueTypePropertyBounds = LoadedValueTypePropertyDescriptor> + Debug; 443 | 444 | pub struct LoadedValueTypePropertyArgs { 445 | pub allow_definitions_in_source: bool, 446 | pub default_descriptor_specialization: ::Specialization, 447 | pub default_value: <::Specialization as ValuePropertyDescriptorSpecialization>::ValueType, 448 | } 449 | 450 | /// Represents a hierarchically-loaded value. 451 | /// This value can be either provided by the shader source code, 452 | /// or from the effect settings properties. 453 | pub struct LoadedValueTypeProperty { 454 | loaded_value_descriptor: Option, 455 | loaded_value_default: Option::Specialization as ValuePropertyDescriptorSpecialization>::ValueType>>, 456 | default_value: Option<<::Specialization as ValuePropertyDescriptorSpecialization>::ValueType>, 457 | value: <::Specialization as ValuePropertyDescriptorSpecialization>::ValueType, 458 | } 459 | 460 | impl LoadedValueType for LoadedValueTypeProperty { 461 | type Output = <::Specialization as ValuePropertyDescriptorSpecialization>::ValueType; 462 | type Args = LoadedValueTypePropertyArgs; 463 | 464 | fn from_identifier( 465 | args: Self::Args, 466 | identifier: &str, 467 | preprocess_result: &PreprocessResult, 468 | settings: &mut SettingsContext, 469 | ) -> Result> { 470 | let hardcoded_value = preprocess_result.parse::<<::Specialization as ValuePropertyDescriptorSpecialization>::ValueType>(identifier); 471 | 472 | Ok(match hardcoded_value { 473 | Some(result) => { 474 | if !args.allow_definitions_in_source { 475 | throw!(format!("The value of the property `{}` may not be hardcoded in the shader source code.", identifier)); 476 | } 477 | 478 | Self { 479 | loaded_value_descriptor: None, 480 | loaded_value_default: None, 481 | default_value: None, 482 | value: result?, 483 | } 484 | }, 485 | None => { 486 | let (default_value, loaded_value_default) = if args.allow_definitions_in_source { 487 | let loaded_value_default = ::Specialization as ValuePropertyDescriptorSpecialization>::ValueType> as LoadedValueType>::from( 488 | LoadedValueTypeSourceArgs { 489 | default_value: Some(args.default_value), 490 | }, 491 | identifier, 492 | Some("default"), 493 | preprocess_result, 494 | settings, 495 | )?; 496 | (loaded_value_default.get_value().unwrap(), Some(loaded_value_default)) 497 | } else { 498 | let loaded_value_default = ::Specialization as ValuePropertyDescriptorSpecialization>::ValueType> as LoadedValueType>::from( 499 | LoadedValueTypeSourceArgs { 500 | default_value: None, 501 | }, 502 | identifier, 503 | Some("default"), 504 | preprocess_result, 505 | settings, 506 | ); 507 | 508 | if loaded_value_default.is_err() 509 | || loaded_value_default.unwrap().get_value().is_some() { 510 | throw!(format!("The default value of the property `{}` may not be hardcoded in the shader source code.", identifier)) 511 | } 512 | 513 | (args.default_value, None) 514 | }; 515 | let loaded_value_descriptor = ::from( 516 | ::new_args(args.default_descriptor_specialization), 517 | identifier, 518 | None, 519 | preprocess_result, 520 | settings, 521 | )?; 522 | let descriptor = loaded_value_descriptor.get_value(); 523 | let loaded_value = settings.get_property_value(&descriptor, &default_value); 524 | 525 | Self { 526 | loaded_value_descriptor: Some(loaded_value_descriptor), 527 | loaded_value_default, 528 | default_value: Some(default_value), 529 | value: loaded_value, 530 | } 531 | } 532 | }) 533 | } 534 | 535 | fn reload_settings(&mut self, settings: &mut SettingsContext) { 536 | // Assumes that it is not necessary to reload the descriptors and default values, otherwise 537 | // we would need to reload both loaded values and figure out the default value as in 538 | // `from_identifier`. 539 | if let Some(loaded_value_descriptor) = self.loaded_value_descriptor.as_ref() { 540 | let descriptor = loaded_value_descriptor.get_value(); 541 | self.value = settings.get_property_value(&descriptor, self.default_value.as_ref().unwrap()); 542 | } 543 | } 544 | 545 | fn add_properties(&self, properties: &mut Properties) { 546 | if let &Some(ref default) = &self.loaded_value_default { 547 | default.add_properties(properties); 548 | } 549 | if let &Some(ref descriptor) = &self.loaded_value_descriptor { 550 | descriptor.add_properties(properties); 551 | } 552 | } 553 | 554 | fn get_value(&self) -> <::Specialization as ValuePropertyDescriptorSpecialization>::ValueType { 555 | self.value.clone() 556 | } 557 | } 558 | -------------------------------------------------------------------------------- /src/effect/mod.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::path::PathBuf; 3 | use std::ffi::CString; 4 | use obs_wrapper::{ 5 | graphics::*, 6 | source::*, 7 | }; 8 | use downcast::{impl_downcast, Downcast}; 9 | use regex::Regex; 10 | use crate::*; 11 | 12 | mod effect_param; 13 | mod loaded_value; 14 | 15 | pub use effect_param::*; 16 | pub use loaded_value::*; 17 | 18 | /// An object representing a binding of setting-properties to graphics uniforms. 19 | pub trait BindableProperty: Downcast { 20 | fn add_properties(&self, properties: &mut Properties); 21 | fn reload_settings(&mut self, settings: &mut SettingsContext); 22 | fn prepare_values(&mut self); 23 | fn stage_value<'a>(&mut self, graphics_context: &'a GraphicsContext); 24 | fn assign_value<'a>(&mut self, graphics_context: &'a FilterContext); 25 | fn enable_and_drop(self, graphics_context: &GraphicsContext); 26 | } 27 | impl_downcast!(BindableProperty); 28 | 29 | #[derive(Default)] 30 | pub struct EffectParamsCustom { 31 | // Custom effect params sorted by their order in source 32 | pub params: Vec>, 33 | } 34 | 35 | impl EffectParamsCustom { 36 | pub fn from<'a>( 37 | mut params: HashMap>>, 38 | settings: &mut SettingsContext, 39 | preprocess_result: &PreprocessResult, 40 | ) -> Result> { 41 | use ShaderParamTypeKind::*; 42 | 43 | let mut bound_params: Vec>> = Vec::new(); 44 | 45 | let result: Result<(), Cow<'static, str>> = try { 46 | { 47 | let pattern_builtin_texture_fft = Regex::new(r"^builtin_texture_fft_(?P\w+)$").unwrap(); 48 | let pattern_field_previous = Regex::new(r"^.*_previous$").unwrap(); 49 | let param_names = params.keys().cloned().collect::>(); 50 | 51 | for param_name in ¶m_names { 52 | let captures = if let Some(captures) = pattern_builtin_texture_fft.captures(¶m_name) { 53 | captures 54 | } else { 55 | continue; 56 | }; 57 | let field_name = captures.name("field").unwrap().as_str(); 58 | 59 | if pattern_field_previous.is_match(&field_name) { 60 | continue; 61 | } 62 | 63 | let (param_index, param) = params.remove(param_name).unwrap().into_tuple(); 64 | let param_previous = params.remove(&format!("{}_previous", param_name)) 65 | .map(|indexed| indexed.into_inner()); 66 | 67 | if param.param_type() != Texture { 68 | throw!(format!("Builtin field `{}` must be of type `{}`", field_name, "texture2d")); 69 | } 70 | 71 | if let Some(ref param_previous) = param_previous.as_ref() { 72 | if param_previous.param_type() != Texture { 73 | throw!(format!("Builtin field `{}` must be of type `{}`", field_name, "texture2d")); 74 | } 75 | } 76 | 77 | bound_params.push( 78 | Indexed { 79 | index: param_index, 80 | inner: Box::new(EffectParamCustomFFT::new( 81 | param.downcast().unwrap(), 82 | param_previous.map(|param_previous| param_previous.downcast().unwrap()), 83 | field_name, 84 | settings, 85 | preprocess_result, 86 | )?), 87 | }, 88 | ); 89 | } 90 | } 91 | }; 92 | 93 | result.map_err(|err| { 94 | Cow::Owned(format!("An error occurred while binding effect uniform variable: {}", err)) 95 | })?; 96 | 97 | let pattern_param_builtin = Regex::new(r"^builtin_").unwrap(); 98 | 99 | for (_index, param) in params { 100 | let param_name = param.name().to_string(); 101 | 102 | // Ensure custom uniform properties do not conflict with builtin properties. 103 | if pattern_param_builtin.is_match(¶m_name) { 104 | throw!(format!("Unrecognized `builtin_` uniform variable type: {}", ¶m_name)); 105 | } 106 | 107 | Self::add_param(&mut bound_params, param, ¶m_name, settings, preprocess_result) 108 | .map_err(|err| { 109 | Cow::Owned(format!("An error occurred while binding effect uniform variable `{}`: {}", param_name, err)) 110 | })?; 111 | } 112 | 113 | // Ensure the properties are stored in the order they were declared 114 | bound_params.sort_unstable(); 115 | 116 | Ok(Self { 117 | params: bound_params.into_iter() 118 | .map(|indexed| indexed.into_inner()) 119 | .collect(), 120 | }) 121 | } 122 | 123 | pub fn add_param<'a>( 124 | bound_params: &mut Vec>>, 125 | param: Indexed>, 126 | param_name: &str, 127 | settings: &mut SettingsContext, 128 | preprocess_result: &PreprocessResult, 129 | ) -> Result<(), Cow<'static, str>> { 130 | use ShaderParamTypeKind::*; 131 | 132 | let bindable: Indexed> = match param.param_type() { 133 | Unknown => throw!("Cannot add an effect param of unknown type. Make sure to use HLSL type names for uniform variables."), 134 | Bool => param.map(|param| { 135 | EffectParamCustomBool::new(param.downcast().unwrap(), ¶m_name, settings, preprocess_result) 136 | .map(|param| Box::new(param) as Box) 137 | }).transpose()?, 138 | Float => param.map(|param| { 139 | EffectParamCustomFloat::new(param.downcast().unwrap(), ¶m_name, settings, preprocess_result) 140 | .map(|param| Box::new(param) as Box) 141 | }).transpose()?, 142 | Int => param.map(|param| { 143 | EffectParamCustomInt::new(param.downcast().unwrap(), ¶m_name, settings, preprocess_result) 144 | .map(|param| Box::new(param) as Box) 145 | }).transpose()?, 146 | Vec4 => param.map(|param| { 147 | EffectParamCustomColor::new(param.downcast().unwrap(), ¶m_name, settings, preprocess_result) 148 | .map(|param| Box::new(param) as Box) 149 | }).transpose()?, 150 | Vec2 | Vec3 | IVec2 | IVec3 | IVec4 | Mat4 => { 151 | throw!("Multi-component types as effect params are not yet supported."); 152 | }, 153 | String => throw!("Strings as effect params are not yet supported."), 154 | Texture => throw!("Textures as effect params are not yet supported."), 155 | }; 156 | 157 | bound_params.push(bindable); 158 | 159 | Ok(()) 160 | } 161 | 162 | pub fn reload_settings(&mut self, settings: &mut SettingsContext) { 163 | self.params.iter_mut().for_each(|param| param.reload_settings(settings)); 164 | } 165 | 166 | pub fn prepare_values(&mut self) { 167 | self.params.iter_mut().for_each(|param| param.prepare_values()); 168 | } 169 | 170 | pub fn stage_values(&mut self, graphics_context: &GraphicsContext) { 171 | self.params.iter_mut().for_each(|param| param.stage_value(graphics_context)); 172 | } 173 | 174 | pub fn assign_values(&mut self, graphics_context: &FilterContext) { 175 | self.params.iter_mut().for_each(|param| param.assign_value(graphics_context)); 176 | } 177 | 178 | pub fn enable_and_drop(self, graphics_context: &GraphicsContext) { 179 | #[allow(unused_assignments)] 180 | self.params.into_iter().for_each(|mut param| { 181 | param = match param.downcast::() { 182 | Ok(param) => return param.enable_and_drop(graphics_context), 183 | Err(param) => param, 184 | }; 185 | param = match param.downcast::() { 186 | Ok(param) => return param.enable_and_drop(graphics_context), 187 | Err(param) => param, 188 | }; 189 | param = match param.downcast::() { 190 | Ok(param) => return param.enable_and_drop(graphics_context), 191 | Err(param) => param, 192 | }; 193 | param = match param.downcast::() { 194 | Ok(param) => return param.enable_and_drop(graphics_context), 195 | Err(param) => param, 196 | }; 197 | param = match param.downcast::() { 198 | Ok(param) => return param.enable_and_drop(graphics_context), 199 | Err(param) => param, 200 | }; 201 | panic!("No registered downcast to `enable_and_drop` a `Box`. This is an implementation error."); 202 | }); 203 | } 204 | 205 | pub fn add_properties(&self, properties: &mut Properties) { 206 | self.params.iter().for_each(|param| param.add_properties(properties)); 207 | } 208 | } 209 | 210 | pub struct EffectParams { 211 | pub frame: EffectParamInt, 212 | pub framerate: EffectParamFloat, 213 | pub elapsed_time: EffectParamFloat, 214 | pub elapsed_time_previous: EffectParamFloat, 215 | pub elapsed_time_since_shown: EffectParamFloat, 216 | pub elapsed_time_since_shown_previous: EffectParamFloat, 217 | pub elapsed_time_since_enabled: EffectParamFloat, 218 | pub elapsed_time_since_enabled_previous: EffectParamFloat, 219 | pub uv_size: EffectParamIVec2, 220 | pub custom: EffectParamsCustom, 221 | } 222 | 223 | impl EffectParams { 224 | pub fn reload_settings(&mut self, settings: &mut SettingsContext) { 225 | self.custom.reload_settings(settings); 226 | } 227 | 228 | pub fn stage_values(&mut self, graphics_context: &GraphicsContext) { 229 | self.frame.stage_value(graphics_context); 230 | self.framerate.stage_value(graphics_context); 231 | self.elapsed_time.stage_value(graphics_context); 232 | self.elapsed_time_previous.stage_value(graphics_context); 233 | self.elapsed_time_since_shown.stage_value(graphics_context); 234 | self.elapsed_time_since_shown_previous.stage_value(graphics_context); 235 | self.elapsed_time_since_enabled.stage_value(graphics_context); 236 | self.elapsed_time_since_enabled_previous.stage_value(graphics_context); 237 | self.uv_size.stage_value(graphics_context); 238 | self.custom.stage_values(graphics_context); 239 | } 240 | 241 | pub fn assign_values(&mut self, graphics_context: &FilterContext) { 242 | self.frame.assign_value(graphics_context); 243 | self.framerate.assign_value(graphics_context); 244 | self.elapsed_time.assign_value(graphics_context); 245 | self.elapsed_time_previous.assign_value(graphics_context); 246 | self.elapsed_time_since_shown.assign_value(graphics_context); 247 | self.elapsed_time_since_shown_previous.assign_value(graphics_context); 248 | self.elapsed_time_since_enabled.assign_value(graphics_context); 249 | self.elapsed_time_since_enabled_previous.assign_value(graphics_context); 250 | self.uv_size.assign_value(graphics_context); 251 | self.custom.assign_values(graphics_context); 252 | } 253 | 254 | pub fn enable_and_drop(self, graphics_context: &GraphicsContext) { 255 | self.frame.enable_and_drop(graphics_context); 256 | self.framerate.enable_and_drop(graphics_context); 257 | self.elapsed_time.enable_and_drop(graphics_context); 258 | self.elapsed_time_previous.enable_and_drop(graphics_context); 259 | self.elapsed_time_since_shown.enable_and_drop(graphics_context); 260 | self.elapsed_time_since_shown_previous.enable_and_drop(graphics_context); 261 | self.elapsed_time_since_enabled.enable_and_drop(graphics_context); 262 | self.elapsed_time_since_enabled_previous.enable_and_drop(graphics_context); 263 | self.uv_size.enable_and_drop(graphics_context); 264 | self.custom.enable_and_drop(graphics_context); 265 | } 266 | 267 | pub fn add_properties(&self, properties: &mut Properties) { 268 | self.custom.add_properties(properties); 269 | } 270 | } 271 | 272 | pub struct PreparedEffect { 273 | pub effect: GraphicsContextDependentDisabled, 274 | pub shader_source: String, 275 | pub params: EffectParams, 276 | } 277 | 278 | impl PreparedEffect { 279 | pub fn enable_and_drop(self, graphics_context: &GraphicsContext) { 280 | self.effect.enable(graphics_context); 281 | self.params.enable_and_drop(graphics_context); 282 | } 283 | 284 | pub fn add_properties(&self, properties: &mut Properties) { 285 | self.params.add_properties(properties); 286 | } 287 | 288 | pub fn create_effect<'a>(shader_path: &PathBuf, shader_source: &str, graphics_context: &'a GraphicsContext) -> Result<(GraphicsContextDependentEnabled<'a, GraphicsEffect>, PreprocessResult), Cow<'static, str>> { 289 | const EFFECT_SOURCE_TEMPLATE: &'static str = include_str!("../effect_template.effect"); 290 | 291 | let (preprocess_result, effect_source) = { 292 | let pattern = Regex::new(r"(?P__SHADER__)").unwrap(); 293 | let effect_source = pattern.replace_all(EFFECT_SOURCE_TEMPLATE, shader_source); 294 | 295 | let (preprocess_result, effect_source) = preprocess(&effect_source); 296 | 297 | (preprocess_result, effect_source.into_owned()) 298 | }; 299 | 300 | let shader_path_c = CString::new( 301 | shader_path.to_str().ok_or_else(|| { 302 | "Specified shader path is not a valid UTF-8 string." 303 | })? 304 | ).map_err(|_| "Shader path cannot be converted to a C string.")?; 305 | let effect_source_c = CString::new(effect_source.clone()) 306 | .map_err(|_| "Shader contents cannot be converted to a C string.")?; 307 | 308 | let effect = { 309 | let capture = LogCaptureHandler::new(LogLevel::Error).unwrap(); 310 | let result = GraphicsEffect::from_effect_string( 311 | effect_source_c.as_c_str(), 312 | shader_path_c.as_c_str(), 313 | &graphics_context, 314 | ); 315 | 316 | result.map_err(|err| { 317 | if let Some(err) = err { 318 | Cow::Owned(format!("Could not create the effect due to the following error: {}", err)) 319 | } else { 320 | Cow::Owned(format!("Could not create the effect due to the following error:\n{}", capture.to_string())) 321 | } 322 | }) 323 | }?; 324 | 325 | Ok((effect, preprocess_result)) 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /src/effect_fallback.effect: -------------------------------------------------------------------------------- 1 | uniform float4x4 ViewProj; 2 | uniform texture2d image; 3 | 4 | uniform int builtin_frame; 5 | uniform float builtin_framerate; 6 | uniform float builtin_elapsed_time; 7 | uniform float builtin_elapsed_time_previous; 8 | uniform float builtin_elapsed_time_since_shown; 9 | uniform float builtin_elapsed_time_since_shown_previous; 10 | uniform int2 builtin_uv_size; 11 | 12 | sampler_state builtin_texture_sampler { 13 | Filter = Linear; 14 | AddressU = Border; 15 | AddressV = Border; 16 | BorderColor = 00000000; 17 | }; 18 | 19 | struct BuiltinVertData { 20 | float4 pos : POSITION; 21 | float2 uv : TEXCOORD0; 22 | }; 23 | 24 | BuiltinVertData builtin_shader_vertex(BuiltinVertData v_in) 25 | { 26 | BuiltinVertData vert_out; 27 | vert_out.pos = mul(float4(v_in.pos.xyz, 1.0), ViewProj); 28 | vert_out.uv = v_in.uv; 29 | return vert_out; 30 | } 31 | 32 | float4 builtin_shader_fragment(BuiltinVertData v_in) : TARGET { 33 | return image.Sample(builtin_texture_sampler, v_in.uv); 34 | } 35 | 36 | technique Draw 37 | { 38 | pass 39 | { 40 | vertex_shader = builtin_shader_vertex(v_in); 41 | pixel_shader = builtin_shader_fragment(v_in); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/effect_template.effect: -------------------------------------------------------------------------------- 1 | uniform float4x4 ViewProj; 2 | uniform texture2d image; 3 | 4 | uniform int builtin_frame; 5 | uniform float builtin_framerate; 6 | uniform float builtin_elapsed_time; 7 | uniform float builtin_elapsed_time_previous; 8 | uniform float builtin_elapsed_time_since_shown; 9 | uniform float builtin_elapsed_time_since_shown_previous; 10 | uniform float builtin_elapsed_time_since_enabled; 11 | uniform float builtin_elapsed_time_since_enabled_previous; 12 | uniform int2 builtin_uv_size; 13 | 14 | sampler_state builtin_texture_sampler { 15 | Filter = Linear; 16 | AddressU = Border; 17 | AddressV = Border; 18 | BorderColor = 00000000; 19 | }; 20 | 21 | struct BuiltinVertData { 22 | float4 pos : POSITION; 23 | float2 uv : TEXCOORD0; 24 | }; 25 | 26 | __SHADER__ 27 | 28 | BuiltinVertData builtin_shader_vertex(BuiltinVertData v_in) 29 | { 30 | BuiltinVertData vert_out; 31 | vert_out.pos = mul(float4(v_in.pos.xyz, 1.0), ViewProj); 32 | vert_out.uv = v_in.uv; 33 | return vert_out; 34 | } 35 | 36 | float4 builtin_shader_fragment(BuiltinVertData v_in) : TARGET { 37 | return render(v_in.uv); 38 | } 39 | 40 | technique Draw 41 | { 42 | pass 43 | { 44 | vertex_shader = builtin_shader_vertex(v_in); 45 | pixel_shader = builtin_shader_fragment(v_in); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(try_blocks)] 2 | #![feature(c_variadic)] 3 | #![feature(trait_alias)] 4 | #![feature(associated_type_bounds)] 5 | 6 | use std::collections::{HashMap, VecDeque}; 7 | use std::sync::atomic::{AtomicBool, Ordering}; 8 | use std::sync::{RwLock, Arc, Weak}; 9 | use std::borrow::Cow; 10 | use std::time::Instant; 11 | use std::path::PathBuf; 12 | use std::fs::File; 13 | use std::ffi::{CStr, CString}; 14 | use std::io::Read; 15 | use ordered_float::OrderedFloat; 16 | use lazy_static::lazy_static; 17 | use obs_wrapper::{ 18 | info::*, 19 | context::*, 20 | graphics::*, 21 | obs_register_module, 22 | prelude::*, 23 | source::*, 24 | audio::*, 25 | }; 26 | use fourier::*; 27 | use num_complex::Complex; 28 | use util::*; 29 | use effect::*; 30 | use preprocessor::*; 31 | 32 | macro_rules! throw { 33 | ($e:expr) => {{ 34 | Err($e)?; 35 | unreachable!() 36 | }} 37 | } 38 | 39 | mod util; 40 | mod effect; 41 | mod preprocessor; 42 | 43 | lazy_static! { 44 | static ref GLOBAL_STATE: GlobalState = Default::default(); 45 | } 46 | 47 | pub trait GlobalStateComponentType { 48 | type Descriptor; 49 | type Result; 50 | 51 | fn create(descriptor: &Self::Descriptor) -> Arc; 52 | fn retrieve_result(self: &Arc) -> Option; 53 | // fn register_callback(self: &Arc, callback: Box); 54 | } 55 | 56 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 57 | pub enum WindowFunction { 58 | None, 59 | Blackman, 60 | Cosine { 61 | a: OrderedFloat, 62 | b: OrderedFloat, 63 | c: OrderedFloat, 64 | d: OrderedFloat, 65 | }, 66 | Hamming, 67 | Hanning, 68 | Nuttall, 69 | Triangular, 70 | } 71 | 72 | impl WindowFunction { 73 | pub fn generate_coefficients(self, len: usize) -> Vec { 74 | use apodize::*; 75 | use WindowFunction::*; 76 | 77 | match self { 78 | None => std::iter::repeat(1.0).take(len).collect::>(), 79 | Blackman => blackman_iter(len).map(|coef| coef as f32).collect::>(), 80 | Cosine { a, b, c, d } => cosine_iter(*a, *b, *c, *d, len).map(|coef| coef as f32).collect::>(), 81 | Hamming => hamming_iter(len).map(|coef| coef as f32).collect::>(), 82 | Hanning => hanning_iter(len).map(|coef| coef as f32).collect::>(), 83 | Nuttall => nuttall_iter(len).map(|coef| coef as f32).collect::>(), 84 | Triangular => triangular_iter(len).map(|coef| coef as f32).collect::>(), 85 | } 86 | } 87 | } 88 | 89 | #[derive(Debug, Clone, Hash, PartialEq, Eq)] 90 | pub struct GlobalStateAudioFFTDescriptor { 91 | mix: usize, 92 | channel: usize, 93 | dampening_factor_attack: OrderedFloat, 94 | dampening_factor_release: OrderedFloat, 95 | window_function: WindowFunction, 96 | } 97 | 98 | impl GlobalStateAudioFFTDescriptor { 99 | pub fn new( 100 | mix: usize, 101 | channel: usize, 102 | dampening_factor_attack: f64, 103 | dampening_factor_release: f64, 104 | window_function: WindowFunction, 105 | ) -> Self { 106 | Self { 107 | mix, 108 | channel, 109 | dampening_factor_attack: OrderedFloat(dampening_factor_attack), 110 | dampening_factor_release: OrderedFloat(dampening_factor_release), 111 | window_function, 112 | } 113 | } 114 | } 115 | 116 | #[derive(Clone)] 117 | pub struct FFTResult { 118 | batch_number: usize, 119 | frequency_spectrum: Arc>, 120 | } 121 | 122 | pub struct GlobalStateAudioFFTMutable { 123 | audio_output: Option, 124 | sample_buffer: VecDeque, 125 | window: Arc>, 126 | /// Set during `retrieve_result` to indicate that the analysis of the next 127 | /// batch should be performed. 128 | next_batch_scheduled: AtomicBool, 129 | /// The result of the analysis. 130 | result: Option, 131 | } 132 | 133 | impl Default for GlobalStateAudioFFTMutable { 134 | fn default() -> Self { 135 | Self { 136 | audio_output: Default::default(), 137 | sample_buffer: Default::default(), 138 | window: Arc::new(Vec::new()), 139 | next_batch_scheduled: AtomicBool::new(true), 140 | result: None, 141 | } 142 | } 143 | } 144 | 145 | pub struct GlobalStateAudioFFT { 146 | descriptor: GlobalStateAudioFFTDescriptor, 147 | mutable: Arc>, 148 | } 149 | 150 | impl GlobalStateAudioFFT { 151 | fn get_samples_per_frame() -> usize { 152 | let audio_info = ObsAudioInfo::get() 153 | .expect("Audio info not accessible."); 154 | let video_info = ObsVideoInfo::get() 155 | .expect("Video info not accessible."); 156 | let framerate = video_info.framerate(); 157 | 158 | (audio_info.samples_per_second() as usize * framerate.denominator as usize) 159 | / framerate.numerator as usize 160 | } 161 | 162 | fn render_frames_to_time_elapsed(render_frames: usize) -> f64 { 163 | let video_info = ObsVideoInfo::get() 164 | .expect("Video info not accessible."); 165 | let framerate = video_info.framerate(); 166 | 167 | (render_frames as f64 * framerate.denominator as f64) / framerate.numerator as f64 168 | } 169 | 170 | fn perform_analysis( 171 | samples: impl Iterator + ExactSizeIterator, 172 | window: &[f32], 173 | ) -> Vec { 174 | assert_eq!(samples.len(), window.len()); 175 | 176 | let len = samples.len(); 177 | let mut fft_data: Vec> = samples.zip(window.iter()).map(|(sample, window_coefficient)| { 178 | Complex::new(sample * window_coefficient, 0.0) 179 | }).collect::>(); 180 | let fft = fourier::create_fft_f32(len); 181 | 182 | fft.transform_in_place(&mut fft_data, Transform::Fft); 183 | 184 | fft_data.into_iter().take(len / 2).map(|complex| { 185 | // normalize according to https://www.sjsu.edu/people/burford.furman/docs/me120/FFT_tutorial_NI.pdf 186 | (complex.norm() * 4.0 / len as f32).sqrt() 187 | }).collect::>() 188 | } 189 | 190 | fn process_audio_data<'a>(this: &Weak, audio_data: AudioData<'a, ()>) { 191 | let this = if let Some(this) = Weak::upgrade(this) { 192 | this 193 | } else { 194 | // The audio FFT component no longer exists, bail. 195 | return; 196 | }; 197 | 198 | let mut mutable_write = this.mutable.write().unwrap(); 199 | 200 | let current_samples = if let Some(samples) = audio_data.samples_normalized(this.descriptor.channel) { 201 | samples 202 | } else { 203 | // No samples captured, bail. 204 | return; 205 | }; 206 | 207 | mutable_write.sample_buffer.extend(current_samples); 208 | 209 | let samples_per_frame = Self::get_samples_per_frame(); 210 | let render_frames_accumulated = mutable_write.sample_buffer.len() / samples_per_frame; 211 | let render_frames_over_margin = render_frames_accumulated.saturating_sub(1); 212 | 213 | // Get rid of old data, if we lost some frames, or if the results are not being requested. 214 | if render_frames_over_margin > 0 { 215 | // println!("Skipping {} render frames in FFT calculation.", render_frames_over_margin); 216 | 217 | let samples_to_remove = samples_per_frame * render_frames_over_margin; 218 | mutable_write.sample_buffer.drain(0..samples_to_remove); 219 | } 220 | 221 | if !mutable_write.next_batch_scheduled.load(Ordering::SeqCst) { 222 | return; 223 | } 224 | 225 | if render_frames_accumulated > 0 { 226 | if mutable_write.window.len() != samples_per_frame { 227 | mutable_write.window = Arc::new(this.descriptor.window_function.generate_coefficients(samples_per_frame)); 228 | } 229 | 230 | let window = mutable_write.window.clone(); 231 | let current_accumulated_samples = mutable_write.sample_buffer.drain(0..samples_per_frame); 232 | let mut analysis_result = Self::perform_analysis(current_accumulated_samples, &window); 233 | 234 | // Dampen the result by mixing it with the result from the previous batch 235 | if *this.descriptor.dampening_factor_attack > 0.0 || *this.descriptor.dampening_factor_release > 0.0 { 236 | if let Some(previous_result) = mutable_write.result.as_ref() { 237 | let dampening_multiplier_attack = this.descriptor.dampening_factor_attack.powf( 238 | Self::render_frames_to_time_elapsed(render_frames_accumulated) 239 | ).clamp(0.0, 1.0) as f32; 240 | let dampening_multiplier_release = this.descriptor.dampening_factor_release.powf( 241 | Self::render_frames_to_time_elapsed(render_frames_accumulated) 242 | ).clamp(0.0, 1.0) as f32; 243 | 244 | analysis_result.iter_mut() 245 | .zip(previous_result.frequency_spectrum.iter()) 246 | .for_each(move |(current, previous)| { 247 | let dampening_multiplier = if *current > *previous { 248 | dampening_multiplier_attack 249 | } else { 250 | dampening_multiplier_release 251 | }; 252 | 253 | *current = dampening_multiplier * *previous + (1.0 - dampening_multiplier) * *current; 254 | }) 255 | } 256 | } 257 | 258 | let next_batch_number = mutable_write.result.as_ref() 259 | .map(|result| result.batch_number + 1).unwrap_or(0); 260 | mutable_write.result = Some(FFTResult { 261 | batch_number: next_batch_number, 262 | frequency_spectrum: Arc::new(analysis_result), 263 | }); 264 | mutable_write.next_batch_scheduled.swap(false, Ordering::SeqCst); 265 | } 266 | } 267 | } 268 | 269 | impl GlobalStateComponentType for GlobalStateAudioFFT { 270 | type Descriptor = GlobalStateAudioFFTDescriptor; 271 | type Result = FFTResult; 272 | 273 | fn create(descriptor: &Self::Descriptor) -> Arc { 274 | let audio = Audio::get(); 275 | let result = Arc::new(Self { 276 | descriptor: descriptor.clone(), 277 | mutable: Default::default(), 278 | }); 279 | 280 | let audio_output = audio.connect_output( 281 | descriptor.mix, 282 | { 283 | let self_cloned = Arc::downgrade(&result); 284 | 285 | Box::new(move |audio_data| { 286 | Self::process_audio_data(&self_cloned, audio_data); 287 | }) 288 | }, 289 | ); 290 | 291 | result.mutable.write().unwrap().audio_output = Some(audio_output); 292 | 293 | result 294 | } 295 | 296 | fn retrieve_result(self: &Arc) -> Option { 297 | let mutable_read = self.mutable.read().unwrap(); 298 | 299 | mutable_read.next_batch_scheduled.store(true, Ordering::SeqCst); 300 | mutable_read.result.clone() 301 | } 302 | } 303 | 304 | /// A component of the global state, which is dynamically allocated and 305 | /// deallocated depending on the reference count. 306 | #[derive(Default)] 307 | pub struct GlobalStateComponent { 308 | pub weak_ptr: RwLock>, 309 | pub descriptor: T::Descriptor, 310 | } 311 | 312 | impl GlobalStateComponent { 313 | pub fn new(descriptor: T::Descriptor) -> Self { 314 | Self { 315 | weak_ptr: RwLock::new(Weak::new()), 316 | descriptor, 317 | } 318 | } 319 | 320 | /// Attempts to get a strong reference to the component. 321 | /// If the component was freed, it is constructed by this function. 322 | pub fn get_component(&self) -> Arc { 323 | { 324 | let weak_ptr_read = self.weak_ptr.read().unwrap(); 325 | 326 | if let Some(strong_ptr) = weak_ptr_read.upgrade() { 327 | return strong_ptr; 328 | } 329 | } 330 | 331 | { 332 | let mut weak_ptr_write = self.weak_ptr.write().unwrap(); 333 | 334 | if let Some(strong_ptr) = weak_ptr_write.upgrade() { 335 | return strong_ptr; 336 | } 337 | 338 | let strong_ptr: Arc = T::create(&self.descriptor); 339 | 340 | *weak_ptr_write = Arc::downgrade(&strong_ptr.clone()); 341 | 342 | strong_ptr 343 | } 344 | } 345 | 346 | pub fn try_get_component(&self) -> Option> { 347 | let weak_ptr_read = self.weak_ptr.read().unwrap(); 348 | 349 | weak_ptr_read.upgrade() 350 | } 351 | } 352 | 353 | pub struct GlobalState { 354 | pub audio_ffts: RwLock>>, 355 | } 356 | 357 | impl Default for GlobalState { 358 | fn default() -> Self { 359 | Self { 360 | audio_ffts: Default::default(), 361 | } 362 | } 363 | } 364 | 365 | impl GlobalState { 366 | fn request_audio_fft(&self, descriptor: &GlobalStateAudioFFTDescriptor) -> Arc { 367 | { 368 | let audio_ffts_read = self.audio_ffts.read().unwrap(); 369 | 370 | if let Some(audio_fft) = audio_ffts_read.get(descriptor) { 371 | return audio_fft.get_component(); 372 | } 373 | } 374 | 375 | { 376 | let mut audio_ffts_write = self.audio_ffts.write().unwrap(); 377 | 378 | if let Some(audio_fft) = audio_ffts_write.get(descriptor) { 379 | return audio_fft.get_component(); 380 | } 381 | 382 | let component_wrapper = GlobalStateComponent::new(descriptor.clone()); 383 | let component = component_wrapper.get_component(); 384 | 385 | audio_ffts_write.retain(|_, component| component.try_get_component().is_some()); 386 | audio_ffts_write.insert(descriptor.clone(), component_wrapper); 387 | 388 | component 389 | } 390 | } 391 | } 392 | 393 | // use crossbeam_channel::{unbounded, Receiver, Sender}; 394 | 395 | struct Data { 396 | source: SourceContext, 397 | effect: Option, 398 | effect_fallback_blit: GraphicsContextDependentDisabled, 399 | 400 | signal_callback_enable: EnableSignalCallbackHandle, 401 | 402 | creation: Instant, 403 | shown_at: Option, 404 | enabled_at: Option, 405 | next_frame: u32, 406 | elapsed_time_previous: Option, 407 | elapsed_time_since_shown_previous: Option, 408 | elapsed_time_since_enabled_previous: Option, 409 | 410 | property_shader: PropertyDescriptor, 411 | property_shader_reload: PropertyDescriptor, 412 | property_message: PropertyDescriptor, 413 | property_message_display: bool, 414 | 415 | settings_update_requested: Arc, 416 | shown: bool, 417 | enabled: Arc, 418 | } 419 | 420 | impl Data { 421 | pub fn new(source: SourceContext) -> Self { 422 | let settings_update_requested = Arc::new(AtomicBool::new(true)); 423 | let enabled = Arc::new(AtomicBool::new(false)); 424 | let enabled_clone = enabled.clone(); 425 | 426 | Self { 427 | signal_callback_enable: source.on_signal_enable(Box::new(move |enabled_new| { 428 | enabled_clone.store(enabled_new, Ordering::SeqCst); 429 | })), 430 | source, 431 | effect: None, 432 | effect_fallback_blit: { 433 | const EFFECT_SOURCE_FALLBACK: &'static str = include_str!("effect_fallback.effect"); 434 | 435 | let graphics_context = GraphicsContext::enter() 436 | .expect("Could not enter the graphics context to initialize the fallback effect."); 437 | 438 | let shader_path_c = CString::new("effect_fallback.effect").unwrap(); 439 | let effect_source_c = CString::new(EFFECT_SOURCE_FALLBACK) 440 | .expect("Shader contents cannot be converted to a C string."); 441 | 442 | GraphicsEffect::from_effect_string( 443 | effect_source_c.as_c_str(), 444 | shader_path_c.as_c_str(), 445 | &graphics_context, 446 | ).unwrap().disable() 447 | }, 448 | creation: Instant::now(), 449 | shown_at: None, 450 | enabled_at: None, 451 | next_frame: 0, 452 | elapsed_time_previous: None, 453 | elapsed_time_since_shown_previous: None, 454 | elapsed_time_since_enabled_previous: None, 455 | property_shader: PropertyDescriptor { 456 | name: CString::new("builtin_ui_shader").unwrap(), 457 | description: CString::new("The shader to use.").unwrap(), 458 | specialization: PropertyDescriptorSpecializationPath { 459 | path_type: PathType::File, 460 | filter: CString::from(cstr!("*.hlsl *.glsl *.frag *.fragment ;; All File Types | *.*")), 461 | default_path: CString::from(cstr!("")), 462 | }, 463 | }, 464 | property_shader_reload: PropertyDescriptor { 465 | name: CString::new("builtin_ui_shader_reload").unwrap(), 466 | description: CString::new("Reload Shader").unwrap(), 467 | specialization: PropertyDescriptorSpecializationButton::new( 468 | Box::new({ 469 | let settings_update_requested = settings_update_requested.clone(); 470 | move || { 471 | settings_update_requested.store(true, Ordering::SeqCst); 472 | false 473 | } 474 | }), 475 | ) 476 | }, 477 | property_message: PropertyDescriptor { 478 | name: CString::new("builtin_ui_message").unwrap(), 479 | description: CString::new("").unwrap(), 480 | specialization: PropertyDescriptorSpecializationString { 481 | string_type: StringType::Multiline, 482 | } 483 | }, 484 | property_message_display: false, 485 | settings_update_requested, 486 | shown: false, 487 | enabled, 488 | } 489 | } 490 | } 491 | 492 | impl Drop for Data { 493 | fn drop(&mut self) { 494 | // self.send.send(FilterMessage::CloseConnection).unwrap_or(()); 495 | if let Some(prepared_effect) = self.effect.take() { 496 | let graphics_context = GraphicsContext::enter().unwrap(); 497 | prepared_effect.enable_and_drop(&graphics_context); 498 | } 499 | } 500 | } 501 | 502 | struct ShaderFilterPlus { 503 | context: ModuleContext, 504 | } 505 | 506 | impl Sourceable for ShaderFilterPlus { 507 | fn get_id() -> &'static CStr { 508 | cstr!("obs-shaderfilter-plus") 509 | } 510 | fn get_type() -> SourceType { 511 | SourceType::FILTER 512 | } 513 | } 514 | 515 | impl GetNameSource for ShaderFilterPlus { 516 | fn get_name() -> &'static CStr { 517 | cstr!("ShaderFilter Plus") 518 | } 519 | } 520 | 521 | impl GetPropertiesSource for ShaderFilterPlus { 522 | /// Convention for naming properties, so that they do not conflict: 523 | /// `builtin_ui_*` -- UI-only properties (not sent to the GPU), like the shader path 524 | /// `builtin_*` -- Properties either sent to the GPU or influencing other properties which are sent to the GPU 525 | /// `*` -- Custom uniform properties defined by the user, must not begin with `builtin_` 526 | fn get_properties(context: PluginContext) -> Properties { 527 | let data = context.data().as_ref().unwrap(); 528 | let mut properties = Properties::new(); 529 | 530 | properties.add_property(&data.property_shader); 531 | properties.add_property(&data.property_shader_reload); 532 | 533 | if data.property_message_display { 534 | properties.add_property(&data.property_message); 535 | } 536 | 537 | if let Some(effect) = data.effect.as_ref() { 538 | effect.add_properties(&mut properties); 539 | } 540 | 541 | properties 542 | } 543 | } 544 | 545 | impl VideoTickSource for ShaderFilterPlus { 546 | fn video_tick(mut context: PluginContext, _seconds: f32) { 547 | let (data, settings) = context.data_settings_mut(); 548 | let data = if let Some(data) = data.as_mut() { 549 | data 550 | } else { 551 | return; 552 | }; 553 | 554 | let frame = data.next_frame; 555 | data.next_frame += 1; 556 | let framerate = ObsVideoInfo::get().map(|info| info.framerate().as_f32()).unwrap_or(0.0); 557 | let elapsed_time = data.creation.elapsed().as_secs_f32(); 558 | let elapsed_time_previous = data.elapsed_time_previous.replace(elapsed_time) 559 | .unwrap_or(elapsed_time); 560 | let now = Instant::now(); 561 | let elapsed_time_since_shown = if data.shown { 562 | if let Some(shown_at) = data.shown_at.as_ref() { 563 | shown_at.elapsed().as_secs_f32() 564 | } else { 565 | data.shown_at = Some(now); 566 | 0.0 567 | } 568 | } else { 569 | if data.shown_at.is_some() { 570 | data.shown_at = None; 571 | } 572 | 573 | 0.0 574 | }; 575 | let elapsed_time_since_enabled = if data.enabled.load(Ordering::SeqCst) { 576 | if let Some(enabled_at) = data.enabled_at.as_ref() { 577 | enabled_at.elapsed().as_secs_f32() 578 | } else { 579 | data.enabled_at = Some(now); 580 | 0.0 581 | } 582 | } else { 583 | if data.enabled_at.is_some() { 584 | data.enabled_at = None; 585 | } 586 | 587 | 0.0 588 | }; 589 | let elapsed_time_since_shown_previous = data.elapsed_time_since_shown_previous 590 | .replace(elapsed_time_since_shown) 591 | .unwrap_or(elapsed_time_since_shown); 592 | let elapsed_time_since_enabled_previous = data.elapsed_time_since_enabled_previous 593 | .replace(elapsed_time_since_enabled) 594 | .unwrap_or(elapsed_time_since_enabled); 595 | 596 | if let Some(effect) = data.effect.as_mut() { 597 | let params = &mut effect.params; 598 | 599 | params.frame.prepare_value(frame as i32); 600 | params.framerate.prepare_value(framerate); 601 | params.elapsed_time.prepare_value(elapsed_time); 602 | params.elapsed_time_previous.prepare_value(elapsed_time_previous); 603 | params.elapsed_time_since_shown.prepare_value(elapsed_time_since_shown); 604 | params.elapsed_time_since_shown_previous.prepare_value(elapsed_time_since_shown_previous); 605 | params.elapsed_time_since_enabled.prepare_value(elapsed_time_since_enabled); 606 | params.elapsed_time_since_enabled_previous.prepare_value(elapsed_time_since_enabled_previous); 607 | params.uv_size.prepare_value([ 608 | data.source.get_base_width() as i32, 609 | data.source.get_base_height() as i32, 610 | ]); 611 | 612 | params.custom.prepare_values(); 613 | 614 | { 615 | let graphics_context = GraphicsContext::enter().unwrap(); 616 | params.stage_values(&graphics_context); 617 | } 618 | } 619 | 620 | if data.settings_update_requested.compare_and_swap(true, false, Ordering::SeqCst) { 621 | data.source.update_source_settings(settings); 622 | } 623 | } 624 | } 625 | 626 | impl VideoRenderSource for ShaderFilterPlus { 627 | fn video_render( 628 | mut context: PluginContext, 629 | graphics_context: &mut GraphicsContext, 630 | ) { 631 | let data = if let Some(data) = context.data_mut().as_mut() { 632 | data 633 | } else { 634 | return; 635 | }; 636 | 637 | let source = &mut data.source; 638 | 639 | let mut cx: u32 = 1; 640 | let mut cy: u32 = 1; 641 | 642 | source.do_with_target(|target| { 643 | cx = target.get_base_width(); 644 | cy = target.get_base_height(); 645 | }); 646 | 647 | let prepared_effect = if let Some(effect) = data.effect.as_mut() { 648 | effect 649 | } else { // use the fallback effect 650 | source.process_filter( 651 | &mut data.effect_fallback_blit.as_enabled_mut(graphics_context), 652 | (cx, cy), 653 | ColorFormatKind::RGBA, 654 | GraphicsAllowDirectRendering::NoDirectRendering, 655 | |_context, _effect| {}, 656 | ); 657 | return; 658 | }; 659 | 660 | let effect = &mut prepared_effect.effect.as_enabled_mut(graphics_context); 661 | let params = &mut prepared_effect.params; 662 | 663 | source.process_filter( 664 | effect, 665 | (cx, cy), 666 | ColorFormatKind::RGBA, 667 | GraphicsAllowDirectRendering::NoDirectRendering, 668 | |context, _effect| { 669 | params.assign_values(&context); 670 | // image.set_next_sampler(context, sampler); 671 | }, 672 | ); 673 | } 674 | } 675 | 676 | impl CreatableSource for ShaderFilterPlus { 677 | fn create(_settings: &mut SettingsContext, source: SourceContext) -> Data { 678 | Data::new(source) 679 | } 680 | } 681 | 682 | impl UpdateSource for ShaderFilterPlus { 683 | fn update( 684 | mut context: PluginContext, 685 | ) { 686 | let result: Result<(), Cow> = try { 687 | let (data, mut settings) = context.data_settings_mut(); 688 | let data = data.as_mut().ok_or_else(|| "Could not access the data.")?; 689 | 690 | let shader_path = settings.get_property_value(&data.property_shader, &PathBuf::new()); 691 | 692 | if shader_path.as_path().as_os_str().is_empty() { 693 | throw!("Please specify the shader source file."); 694 | } 695 | 696 | let mut shader_file = File::open(&shader_path) 697 | .map_err(|_| { 698 | if let Some(effect) = data.effect.take() { 699 | let graphics_context = GraphicsContext::enter() 700 | .expect("Could not enter a graphics context."); 701 | 702 | effect.enable_and_drop(&graphics_context); 703 | } 704 | 705 | format!("Shader not found at the specified path: {:?}", &shader_path) 706 | })?; 707 | let shader_source = { 708 | let mut shader_source = String::new(); 709 | shader_file.read_to_string(&mut shader_source).expect("Could not read the shader at the given path."); 710 | shader_source 711 | }; 712 | let old_shader_source = data.effect.as_ref().map(|old_effect| { 713 | old_effect.shader_source.clone() 714 | }); 715 | 716 | if old_shader_source.is_some() && old_shader_source.unwrap() == shader_source { 717 | // Only update the params, if the shader stayed the same 718 | let effect = data.effect.as_mut().unwrap(); 719 | effect.params.reload_settings(&mut settings); 720 | return; 721 | } 722 | 723 | println!("Shader source changed, recreating effect."); 724 | 725 | // If shader source changed, create a new effect and request to update properties 726 | let graphics_context = GraphicsContext::enter().unwrap(); 727 | let (effect, preprocess_result) = PreparedEffect::create_effect(&shader_path, &shader_source, &graphics_context)?; 728 | let mut builtin_param_names = vec!["ViewProj", "image"]; 729 | 730 | macro_rules! builtin_effect { 731 | ($path:expr) => {{ 732 | builtin_param_names.push($path); 733 | EffectParam::new( 734 | effect.get_param_by_name(cstr!($path)) 735 | .ok_or_else(|| { 736 | format!("Could not access built in effect parameter `{}`.", $path) 737 | })? 738 | .downcast() 739 | .ok_or_else(|| { 740 | format!("Incompatible effect parameter type `{}`.", $path) 741 | })? 742 | .disable() 743 | ) 744 | }} 745 | } 746 | 747 | let mut params = EffectParams { 748 | frame: builtin_effect!("builtin_frame"), 749 | framerate: builtin_effect!("builtin_framerate"), 750 | elapsed_time: builtin_effect!("builtin_elapsed_time"), 751 | elapsed_time_previous: builtin_effect!("builtin_elapsed_time_previous"), 752 | elapsed_time_since_shown: builtin_effect!("builtin_elapsed_time_since_shown"), 753 | elapsed_time_since_shown_previous: builtin_effect!("builtin_elapsed_time_since_shown_previous"), 754 | elapsed_time_since_enabled: builtin_effect!("builtin_elapsed_time_since_enabled"), 755 | elapsed_time_since_enabled_previous: builtin_effect!("builtin_elapsed_time_since_enabled_previous"), 756 | uv_size: builtin_effect!("builtin_uv_size"), 757 | custom: Default::default(), 758 | }; 759 | 760 | let custom_params = effect.params_iter() 761 | .filter(|item| { 762 | !builtin_param_names.contains(&item.name()) 763 | }) 764 | .enumerate() 765 | .map(|(index, param)| { 766 | (param.name().to_string(), Indexed::from((index, param))) 767 | }) 768 | .collect::>(); 769 | 770 | params.custom = EffectParamsCustom::from(custom_params, settings, &preprocess_result)?; 771 | 772 | let effect = PreparedEffect { 773 | effect: effect.disable(), 774 | shader_source: shader_source.clone(), 775 | params, 776 | }; 777 | 778 | // Drop old effect before the new one is created. 779 | if let Some(old_effect) = data.effect.replace(effect) { 780 | let graphics_context = GraphicsContext::enter() 781 | .expect("Could not enter a graphics context."); 782 | old_effect.enable_and_drop(&graphics_context); 783 | } 784 | 785 | data.property_message_display = false; 786 | 787 | settings.set_property_value(&data.property_message, CString::new("").unwrap()); 788 | data.source.update_source_properties(); 789 | }; 790 | 791 | if let Err(error_message) = result { 792 | println!("An error occurred while updating a ShaderFilter Plus filter: {}", error_message); 793 | 794 | let (data, settings) = context.data_settings_mut(); 795 | 796 | if let Some(data) = data.as_mut() { 797 | data.property_message_display = true; 798 | 799 | settings.set_property_value( 800 | &data.property_message, 801 | CString::new(error_message.as_ref()).unwrap(), 802 | ); 803 | data.source.update_source_properties(); 804 | } 805 | } 806 | } 807 | } 808 | 809 | impl HideSource for ShaderFilterPlus { 810 | fn hide(mut context: PluginContext) { 811 | if let Some(data) = context.data_mut().as_mut() { 812 | data.shown = false; 813 | } 814 | } 815 | } 816 | 817 | impl ShowSource for ShaderFilterPlus { 818 | fn show(mut context: PluginContext) { 819 | if let Some(data) = context.data_mut().as_mut() { 820 | data.shown = true; 821 | } 822 | } 823 | } 824 | 825 | impl Module for ShaderFilterPlus { 826 | fn new(context: ModuleContext) -> Self { 827 | Self { context } 828 | } 829 | 830 | fn get_ctx(&self) -> &ModuleContext { 831 | &self.context 832 | } 833 | 834 | fn load(&mut self, load_context: &mut LoadContext) -> bool { 835 | let source = load_context 836 | .create_source_builder::() 837 | .enable_get_name() 838 | .enable_create() 839 | .enable_get_properties() 840 | .enable_update() 841 | .enable_video_render() 842 | .enable_video_tick() 843 | .enable_hide() 844 | .enable_show() 845 | .build(); 846 | 847 | load_context.register_source(source); 848 | 849 | true 850 | } 851 | 852 | fn description() -> &'static CStr { 853 | cstr!("A plugin to provide a way of specifying effects using shaders.") 854 | } 855 | 856 | fn name() -> &'static CStr { 857 | cstr!("OBS ShaderFilter Plus") 858 | } 859 | 860 | fn author() -> &'static CStr { 861 | cstr!("Jakub \"Limeth\" Hlusička, Charles Fettinger, NLeseul") 862 | } 863 | } 864 | 865 | obs_register_module!(ShaderFilterPlus); 866 | -------------------------------------------------------------------------------- /src/preprocessor.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | use std::borrow::Cow; 3 | use std::collections::HashMap; 4 | use regex::{Regex, Replacer, Captures}; 5 | use anyhow::Result; 6 | 7 | #[derive(Default)] 8 | pub struct PreprocessResult { 9 | map: HashMap, 10 | } 11 | 12 | impl PreprocessResult { 13 | pub fn parse(&self, identifier: &str) -> Option>> { 14 | self.map.get(identifier) 15 | .map(|raw| { 16 | raw.parse::().map_err(|_| { 17 | Cow::Owned(format!( 18 | "Could not parse property `{}` of type `{}`.", 19 | identifier, 20 | std::any::type_name::(), 21 | )) 22 | }) 23 | }) 24 | } 25 | 26 | pub fn parse_default(&self, identifier: &str, default: Option) -> Result> { 27 | self.parse::(identifier) 28 | .or_else(|| { 29 | default.map(|default| Ok(default)) 30 | }) 31 | .ok_or_else(|| { 32 | Cow::Owned(format!( 33 | "Property `{}` is missing a definition.", 34 | identifier, 35 | )) 36 | }) 37 | .and_then(|result| result) 38 | } 39 | } 40 | 41 | impl<'a> Replacer for &'a mut PreprocessResult { 42 | // Appends the replacement string to `dst`. 43 | // In our case, however, we want to get rid of all #pragma macros, not replace them. 44 | fn replace_append(&mut self, caps: &Captures, _dst: &mut String) { 45 | let result: Result<(), Cow> = try { 46 | let identifier = caps.name("identifier").ok_or_else(|| "Missing property identifier.")?.as_str().to_string(); 47 | let value = caps.name("value").ok_or_else(|| "Missing value.")?.as_str().to_string(); 48 | 49 | self.map.insert(identifier, value); 50 | }; 51 | 52 | if let Err(result) = result { 53 | eprintln!("Could not parse `#pragma shaderfilter`: {} 54 | Make sure the macro usage follows the form `#pragma shaderfilter `.", result); 55 | } 56 | } 57 | } 58 | 59 | pub fn preprocess(source: &str) -> (PreprocessResult, Cow) { 60 | let mut result = PreprocessResult::default(); 61 | // Matches on macros: 62 | // #pragma shaderfilter 63 | let pattern = Regex::new(r"(?m)^\s*#pragma\s+shaderfilter\s+set\s+(?P\w+)\s+(?P[^\s].*?)\s*$").unwrap(); 64 | let string = pattern.replace_all(source, &mut result); 65 | 66 | (result, string) 67 | } 68 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::sync::Mutex; 4 | use std::ffi::{VaList, CStr}; 5 | use std::os::raw::{c_int, c_char, c_void, c_ulong}; 6 | use std::sync::atomic::{self, AtomicBool}; 7 | use obs_wrapper::obs_sys::{ 8 | LOG_ERROR, LOG_WARNING, LOG_INFO, LOG_DEBUG, 9 | }; 10 | use std::cmp::Ordering; 11 | use std::ops::{Deref, DerefMut}; 12 | 13 | pub struct Indexed { 14 | pub index: usize, 15 | pub inner: T, 16 | } 17 | 18 | impl Indexed { 19 | pub fn into_tuple(self) -> (usize, T) { 20 | (self.index, self.inner) 21 | } 22 | 23 | pub fn into_inner(self) -> T { 24 | self.inner 25 | } 26 | 27 | pub fn map(self, map: impl FnOnce(T) -> R) -> Indexed { 28 | Indexed { 29 | index: self.index, 30 | inner: (map)(self.inner), 31 | } 32 | } 33 | } 34 | 35 | impl Indexed> { 36 | pub fn transpose(self) -> Option> { 37 | let Indexed { index, inner } = self; 38 | inner.map(|inner| { 39 | Indexed { index, inner } 40 | }) 41 | } 42 | } 43 | 44 | impl Indexed> { 45 | pub fn transpose(self) -> Result, E> { 46 | let Indexed { index, inner } = self; 47 | inner.map(|inner| { 48 | Indexed { index, inner } 49 | }) 50 | } 51 | } 52 | 53 | impl From<(usize, T)> for Indexed { 54 | fn from((index, inner): (usize, T)) -> Self { 55 | Self { index, inner } 56 | } 57 | } 58 | 59 | impl Deref for Indexed { 60 | type Target = T; 61 | 62 | fn deref(&self) -> &Self::Target { 63 | &self.inner 64 | } 65 | } 66 | 67 | impl DerefMut for Indexed { 68 | fn deref_mut(&mut self) -> &mut Self::Target { 69 | &mut self.inner 70 | } 71 | } 72 | 73 | impl PartialEq for Indexed { 74 | fn eq(&self, other: &Self) -> bool { 75 | PartialEq::eq(&self.index, &other.index) 76 | } 77 | } 78 | 79 | impl Eq for Indexed { 80 | } 81 | 82 | impl PartialOrd for Indexed { 83 | fn partial_cmp(&self, other: &Self) -> Option { 84 | PartialOrd::partial_cmp(&self.index, &other.index) 85 | } 86 | } 87 | 88 | impl Ord for Indexed { 89 | fn cmp(&self, other: &Self) -> Ordering { 90 | Ord::cmp(&self.index, &other.index) 91 | } 92 | } 93 | 94 | #[allow(non_camel_case_types)] 95 | pub type log_handler_t = ::std::option::Option< 96 | unsafe extern "C" fn( 97 | lvl: ::std::os::raw::c_int, 98 | msg: *const ::std::os::raw::c_char, 99 | args: VaList<'static, 'static>, 100 | p: *mut ::std::os::raw::c_void, 101 | ), 102 | >; 103 | 104 | extern "C" { 105 | pub fn base_get_log_handler( 106 | handler: *mut log_handler_t, 107 | param: *mut *mut ::std::os::raw::c_void, 108 | ); 109 | } 110 | 111 | extern "C" { 112 | pub fn base_set_log_handler(handler: log_handler_t, param: *mut ::std::os::raw::c_void); 113 | } 114 | 115 | extern "C" { 116 | pub fn vsnprintf<'a>( 117 | str: *mut ::std::os::raw::c_char, 118 | size: ::std::os::raw::c_ulong, 119 | format: *const ::std::os::raw::c_char, 120 | ap: VaList<'a, 'static>, 121 | ) -> ::std::os::raw::c_int; 122 | } 123 | 124 | pub type RedirectLogCallback = Box)>; 125 | 126 | #[repr(C)] 127 | #[derive(Clone, Copy, Eq, PartialEq)] 128 | pub enum LogLevel { 129 | Error = LOG_ERROR as isize, 130 | Warning = LOG_WARNING as isize, 131 | Info = LOG_INFO as isize, 132 | Debug = LOG_DEBUG as isize, 133 | } 134 | 135 | static LOG_HANDLER_LOCK: AtomicBool = AtomicBool::new(false); 136 | 137 | lazy_static::lazy_static! { 138 | static ref LOG_CAPTURE_HANDLER: Mutex> = Mutex::new(None); 139 | } 140 | 141 | pub struct LogCaptureHandlerGlobal { 142 | handler_previous: log_handler_t, 143 | param_previous: *mut c_void, 144 | callback_ptr: *mut RedirectLogCallback, 145 | captured_log: String, 146 | } 147 | 148 | unsafe impl Send for LogCaptureHandlerGlobal {} 149 | unsafe impl Sync for LogCaptureHandlerGlobal {} 150 | 151 | unsafe extern "C" fn global_redirect_log_handler( 152 | lvl: c_int, 153 | msg: *const c_char, 154 | args: VaList<'static, 'static>, 155 | p: *mut c_void, 156 | ) { 157 | let callback = Box::from_raw(p as *mut RedirectLogCallback); 158 | 159 | (callback)(lvl, msg, args); 160 | 161 | std::mem::forget(callback); 162 | } 163 | 164 | /// Stores its state in LOG_CAPTURE_HANDLER 165 | pub struct LogCaptureHandler; 166 | 167 | impl LogCaptureHandler { 168 | pub fn new(min_log_level: LogLevel) -> Option { 169 | if LOG_HANDLER_LOCK.compare_and_swap(false, true, atomic::Ordering::SeqCst) { 170 | return None; 171 | } 172 | 173 | let mut handler_previous: log_handler_t = None; 174 | let mut param_previous = std::ptr::null_mut(); 175 | let handler_previous_ptr: *mut log_handler_t = &mut handler_previous as *mut _; 176 | let param_previous_ptr: *mut *mut c_void = &mut param_previous as *mut _; 177 | 178 | unsafe { 179 | base_get_log_handler(handler_previous_ptr, param_previous_ptr); 180 | } 181 | 182 | let callback_ptr = Box::into_raw(Box::new(Box::new({ 183 | move |log_level, format, args: VaList<'static, 'static>| { 184 | if let Some(handler_previous) = handler_previous.clone() { 185 | unsafe { 186 | args.with_copy(move |args| { 187 | if log_level <= min_log_level as i32 { 188 | const SIZE: usize = 4096; 189 | let mut formatted = [0 as c_char; SIZE]; 190 | let formatted_ptr = &mut formatted[0] as *mut c_char; 191 | 192 | vsnprintf(formatted_ptr, SIZE as c_ulong, format, args); 193 | 194 | let formatted = CStr::from_ptr(formatted_ptr); 195 | let mut capture_handler = LOG_CAPTURE_HANDLER.lock().unwrap(); 196 | let capture_handler = capture_handler.as_mut().unwrap(); 197 | let captured_log = &mut capture_handler.captured_log; 198 | 199 | *captured_log = format!("{}{}\n", captured_log.clone(), formatted.to_string_lossy()); 200 | } 201 | }); 202 | 203 | // Call the original handler 204 | (handler_previous)(log_level, format, args, param_previous); 205 | } 206 | } 207 | } 208 | }) as RedirectLogCallback)); 209 | 210 | unsafe { 211 | base_set_log_handler(Some(global_redirect_log_handler), callback_ptr as *mut _); 212 | } 213 | 214 | *LOG_CAPTURE_HANDLER.lock().unwrap() = Some(LogCaptureHandlerGlobal { 215 | handler_previous, 216 | param_previous, 217 | callback_ptr, 218 | captured_log: String::new(), 219 | }); 220 | 221 | Some(Self) 222 | } 223 | 224 | pub fn to_string(self) -> String { 225 | let captured_log = { 226 | let capture_handler = LOG_CAPTURE_HANDLER.lock().unwrap(); 227 | let capture_handler = capture_handler.as_ref().unwrap(); 228 | 229 | capture_handler.captured_log.clone() 230 | }; 231 | 232 | std::mem::drop(self); 233 | 234 | captured_log 235 | } 236 | } 237 | 238 | impl Drop for LogCaptureHandler { 239 | fn drop(&mut self) { 240 | let capture_handler = LOG_CAPTURE_HANDLER.lock().unwrap().take().unwrap(); 241 | 242 | unsafe { 243 | base_set_log_handler(capture_handler.handler_previous, capture_handler.param_previous); 244 | 245 | std::mem::drop(Box::from_raw(capture_handler.callback_ptr as *mut RedirectLogCallback)); 246 | } 247 | 248 | LOG_HANDLER_LOCK.store(false, atomic::Ordering::SeqCst); 249 | } 250 | } 251 | --------------------------------------------------------------------------------