diff --git a/.gitignore b/.gitignore
index f00bd6b..725a2c7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,10 @@ models/
*.zip
*.msi
*.userprefs
+tmp/
+third_party/Practical-RIFE/
+third_party/rife-ncnn-vulkan/
+third_party/vs-mlrt/
+src/AnimeEnhancer.App/Assets/Beta/faye-cutout.png
+src/AnimeEnhancer.App/Assets/Beta/spike-cutout.png
+src/AnimeEnhancer.App/Assets/Beta/original-character-source.png
diff --git a/Directory.Build.props b/Directory.Build.props
index 341bcfc..4a5484a 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -10,9 +10,9 @@
true
latest
true
- 1.0.17
- 1.0.17.0
- 1.0.17.0
+ 1.1.0-beta.1
+ 1.1.0.0
+ 1.1.0.0
picki
picki
AnimeEnhancer
diff --git a/README.md b/README.md
index d25b53e..9dfe965 100644
--- a/README.md
+++ b/README.md
@@ -30,7 +30,7 @@ AnimeEnhancer also performs actions that heuristic antivirus scanners watch clos
Before running the app:
1. Download it only from the [official GitHub Releases page](../../releases/latest).
-2. Compare the file's SHA-256 hash with the included `SHA256SUMS.txt`. In PowerShell, run `Get-FileHash .\AnimeEnhancer-1.0.17-win-x64.zip -Algorithm SHA256`.
+2. Compare the file's SHA-256 hash with the included `SHA256SUMS.txt`. For the beta ZIP, run `Get-FileHash .\AnimeEnhancer-1.1.0-beta.1-win-x64.zip -Algorithm SHA256` in PowerShell.
3. You can inspect the complete source code here or build it yourself.
4. If the hash matches and SmartScreen shows the blue warning, select **More info**, confirm the publisher is listed as unknown, then choose **Run anyway**.
@@ -48,9 +48,15 @@ Do not disable Microsoft Defender or add a broad antivirus exclusion. If Defende
Saturation and contrast update live. Stream Cleanup and Smooth Gradients are optional and disabled by default, since they help compressed video but can soften clean sources. Split view makes it easy to compare the original and enhanced image.
+## AnimeFlow 60 FPS beta
+
+The 1.1 beta adds optional RIFE/ncnn Vulkan frame interpolation for NVIDIA RTX systems. AnimeFlow derives presentation timing from the captured video timestamps, skips already-high-frame-rate content automatically, and keeps the normal Anime4K reconstruction path active for interpolated frames.
+
+AnimeFlow is still experimental. Fast cuts, subtitles, hair, particles, and heavy camera motion can produce temporary warping or uneven motion. Leave it disabled for the most predictable image quality, and please report the source site, GPU, display refresh rate, and a short description of the scene when filing feedback.
+
## Lossless Scaling Frame Generation
-Version 1.0.17 includes an **LS FrameGen output** mode. It makes AnimeEnhancer's processed output visible to external capture without repeatedly forcing the enhancement overlay above Lossless Scaling or stealing focus from the source video.
+AnimeEnhancer includes an **LS FrameGen output** mode. It makes AnimeEnhancer's processed output visible to external capture without repeatedly forcing the enhancement overlay above Lossless Scaling or stealing focus from the source video.
1. Enable **LS FrameGen output** in AnimeEnhancer.
2. Start AnimeEnhancer and wait for the captureable `AnimeEnhancer LS Source` window to appear.
@@ -83,7 +89,7 @@ Automatic mode detects fullscreen windows by geometry rather than a browser allo
You need the .NET 10 SDK and Git. Clone with submodules, then run:
```powershell
-git clone --recurse-submodules https://github.com/yohay2012/AnimeEnhancer.git
+git clone --recurse-submodules https://github.com/picki1/AnimeEnhancer.git
cd AnimeEnhancer
powershell -ExecutionPolicy Bypass -File scripts/bootstrap.ps1
powershell -ExecutionPolicy Bypass -File scripts/build.ps1
diff --git a/docs/SOURCES.md b/docs/SOURCES.md
index 5103694..8512557 100644
--- a/docs/SOURCES.md
+++ b/docs/SOURCES.md
@@ -3,6 +3,9 @@
Retrieval date: 2026-08-01.
- Anime4K v4.0.1, commit `4029bf701ecaa15f163cdc49cffe5501c1acf410`, MIT, . Vendored under `third_party/Anime4K`; complete Restore CNN S and Upscale CNN x2 S weights are mechanically translated to HLSL. Official `GLSL_Instructions.md` supplies Mode A/B/C orders.
+- rife-ncnn-vulkan by nihui, MIT, . The AnimeFlow native bridge uses its Vulkan/ncnn inference implementation and the bundled RIFE v4.6 model.
+- ncnn by Tencent, BSD 3-Clause with bundled third-party notices, . Used by the AnimeFlow native runtime.
+- Practical-RIFE by hzwer, MIT, . RIFE architecture and model provenance.
- Microsoft Windows.UI.Composition-Win32-Samples, commit `ee50e2ea137dcef7b82ba504eff7435e5ebf5294`, MIT, . Reference-only clone; `BasicCapture.cs` informed WGC lifecycle. No SharpDX dependency or copied implementation.
- Vortice.Windows.Samples, commit `c7dd7a7bbf1f71e54698630f8fa5aa27343d9014`, MIT, . Reference-only for current device and swap-chain usage.
- Vortice.Direct3D11, Vortice.DXGI, and Vortice.D3DCompiler 3.8.3, MIT, NuGet packages. Binding and shader compiler layer.
diff --git a/docs/releases/1.1.0-beta.1.md b/docs/releases/1.1.0-beta.1.md
new file mode 100644
index 0000000..df3da19
--- /dev/null
+++ b/docs/releases/1.1.0-beta.1.md
@@ -0,0 +1,28 @@
+# AnimeEnhancer 1.1.0 Beta 1
+
+This is the first public beta of the redesigned AnimeEnhancer interface and the new AnimeFlow motion engine.
+
+## What's new
+
+- New compact neon/anime interface with original project artwork
+- AnimeFlow 60 FPS experimental RIFE/ncnn Vulkan interpolation for RTX GPUs
+- Timestamp-paced presentation to avoid bursts of stale generated frames
+- Automatic passthrough for content that is already running at a high frame rate
+- Anime4K reconstruction remains active on interpolated frames
+- Lower-latency frame queues, scene-cut protection, and adaptive cadence
+- GPU selection, saturation, contrast, Stream Cleanup, Smooth Gradients, split view, LS FrameGen output, screenshots, and diagnostics
+- Fullscreen side notification, pointer-friendly browser controls, and a visible close control
+
+## Beta warning
+
+AnimeFlow is optional and experimental. Fast cuts, subtitles, hair, particles, and complex camera motion can still show temporary warping or uneven pacing. If a scene looks worse, turn AnimeFlow off; the normal Anime4K enhancement path is unchanged.
+
+Start with a 720p source at minimum. A clean 1080p source gives the best reconstruction.
+
+## Downloads
+
+- `AnimeEnhancer-1.1.0-beta.1-win-x64.zip` — portable version; extract and run, no installation required
+- `AnimeEnhancer-1.1.0-beta.1-x64.msi` — optional Windows installer
+- `SHA256SUMS.txt` — hashes for both packages
+
+Windows SmartScreen may show an unknown-publisher warning because the binaries are not commercially code-signed. Download only from this repository and verify the SHA-256 hash before running.
diff --git a/installer/AnimeEnhancer.Installer.wixproj b/installer/AnimeEnhancer.Installer.wixproj
index 7dc3665..8eee49d 100644
--- a/installer/AnimeEnhancer.Installer.wixproj
+++ b/installer/AnimeEnhancer.Installer.wixproj
@@ -1,4 +1,4 @@
- Packagex64AnimeEnhancer-1.0.17-x64ICE57
- Publish
+ Packagex64AnimeEnhancer-1.1.0-beta.1-x64ICE57
+ Publish
diff --git a/installer/Package.wxs b/installer/Package.wxs
index 317f799..db5d265 100644
--- a/installer/Package.wxs
+++ b/installer/Package.wxs
@@ -1,5 +1,5 @@
-
+
diff --git a/licenses/NCNN-LICENSE.txt b/licenses/NCNN-LICENSE.txt
new file mode 100644
index 0000000..2eb0363
--- /dev/null
+++ b/licenses/NCNN-LICENSE.txt
@@ -0,0 +1,86 @@
+Tencent is pleased to support the open source community by making ncnn available.
+Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved.
+If you have downloaded a copy of the ncnn binary from Tencent, please note that the ncnn binary is licensed under the BSD 3-Clause License.
+If you have downloaded a copy of the ncnn source code from Tencent, please note that ncnn source code is licensed under the BSD 3-Clause License, except for the third-party components listed below which are subject to different license terms. Your integration of ncnn into your own projects may require compliance with the BSD 3-Clause License, as well as the other licenses applicable to the third-party components included within ncnn.
+A copy of the BSD 3-Clause License is included in this file.
+
+Other dependencies and licenses:
+
+Open Source Software Licensed Under the zlib License:
+The below software in this distribution may have been modified by THL A29 Limited (“Tencent Modifications”). All Tencent Modifications are Copyright (C) 2017 THL A29 Limited.
+----------------------------------------------------------------------------------------
+1. neon_mathfun.h
+Copyright (C) 2011 Julien Pommier
+
+2. sse_mathfun.h
+Copyright (C) 2007 Julien Pommier
+
+3. avx_mathfun.h
+Copyright (C) 2012 Giovanni Garberoglio
+Interdisciplinary Laboratory for Computational Science (LISC)
+Fondazione Bruno Kessler and University of Trento
+via Sommarive, 18
+I-38123 Trento (Italy)
+
+
+Terms of the zlib License:
+---------------------------------------------------
+Copyright (c)
+
+This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software.
+
+Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
+
+1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
+2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
+3. This notice may not be removed or altered from any source distribution.
+
+
+
+Open Source Software Licensed Under the BSD 2-Clause License:
+The below software in this distribution may have been modified by THL A29 Limited (“Tencent Modifications”). All Tencent Modifications are Copyright (C) 2017 THL A29 Limited.
+----------------------------------------------------------------------------------------
+1. squeezenet 1.1
+Copyright (c) 2016 Forrest N. Iandola and Matthew W. Moskewicz and Khalid Ashraf and Song Han and William J. Dally and Kurt Keutzer
+All rights reserved.
+
+2. caffe.proto master
+All contributions by the University of California:
+Copyright (c) 2014-2017 The Regents of the University of California (Regents)
+All rights reserved.
+
+All other contributions:
+Copyright (c) 2014-2017, the respective contributors
+All rights reserved.
+
+
+Terms of the BSD 2-Clause License:
+--------------------------------------------------------------------
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+
+Open Source Software Licensed Under the BSD 3-Clause License:
+The below software in this distribution may have been modified by THL A29 Limited (“Tencent Modifications”). All Tencent Modifications are Copyright (C) 2017 THL A29 Limited.
+----------------------------------------------------------------------------------------
+1. android.toolchain.cmake master
+Copyright (c) 2010-2011, Ethan Rublee
+Copyright (c) 2011-2014, Andrey Kamaev
+All rights reserved.
+
+
+Terms of the BSD 3-Clause License:
+--------------------------------------------------------------------
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+Neither the name of [copyright holder] nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/licenses/NVIDIA/NOTICE.txt b/licenses/NVIDIA/NOTICE.txt
new file mode 100644
index 0000000..f9f2c97
--- /dev/null
+++ b/licenses/NVIDIA/NOTICE.txt
@@ -0,0 +1,4 @@
+RTX Motion uses NVIDIA Optical Flow FRUC runtime components from the
+NVIDIA Optical Flow SDK 5.0.7 under the included NVIDIA DesignWorks SDK license.
+
+This software contains source code provided by NVIDIA Corporation.
diff --git a/licenses/NVIDIA/NVIDIA-DesignWorks-SDK-License.pdf b/licenses/NVIDIA/NVIDIA-DesignWorks-SDK-License.pdf
new file mode 100644
index 0000000..af46309
Binary files /dev/null and b/licenses/NVIDIA/NVIDIA-DesignWorks-SDK-License.pdf differ
diff --git a/licenses/PRACTICAL-RIFE-LICENSE.txt b/licenses/PRACTICAL-RIFE-LICENSE.txt
new file mode 100644
index 0000000..d6c5c55
--- /dev/null
+++ b/licenses/PRACTICAL-RIFE-LICENSE.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 hzwer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/licenses/RIFE-NCNN-VULKAN-LICENSE.txt b/licenses/RIFE-NCNN-VULKAN-LICENSE.txt
new file mode 100644
index 0000000..21c2dd1
--- /dev/null
+++ b/licenses/RIFE-NCNN-VULKAN-LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2020 nihui
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/licenses/THIRD-PARTY-NOTICES.md b/licenses/THIRD-PARTY-NOTICES.md
index 143ac0c..8044d37 100644
--- a/licenses/THIRD-PARTY-NOTICES.md
+++ b/licenses/THIRD-PARTY-NOTICES.md
@@ -2,4 +2,4 @@
AnimeEnhancer's original application code, interface, branding, and packaging are Copyright © 2026 picki. All rights reserved. The following third-party components are not owned by picki and retain their original licenses.
-Anime4K is MIT licensed by bloc97 and contributors. Vortice.Windows is MIT licensed by Amer Koleci and contributors. The Microsoft composition samples are MIT licensed. Serilog is Apache-2.0 licensed. See NuGet packages and `docs/SOURCES.md` for exact versions.
+Anime4K is MIT licensed by bloc97 and contributors. rife-ncnn-vulkan is MIT licensed by nihui. Practical-RIFE is MIT licensed by hzwer. ncnn is BSD 3-Clause licensed by Tencent and includes additional third-party notices. Vortice.Windows is MIT licensed by Amer Koleci and contributors. The Microsoft composition samples are MIT licensed. Serilog is Apache-2.0 licensed. See NuGet packages, the license files in this directory, and `docs/SOURCES.md` for exact sources.
diff --git a/native/AnimeFlow/README.md b/native/AnimeFlow/README.md
new file mode 100644
index 0000000..3ea2455
--- /dev/null
+++ b/native/AnimeFlow/README.md
@@ -0,0 +1,7 @@
+# AnimeFlow native bridge
+
+`rife_native.cpp` is the small C ABI used by `RifeMotionEngine.cs`. The release DLL is built against the official [rife-ncnn-vulkan](https://github.com/nihui/rife-ncnn-vulkan) source with its recursive ncnn dependencies and Vulkan enabled.
+
+To reproduce the native DLL, clone rife-ncnn-vulkan recursively, copy `rife_native.cpp` into its `src` directory, and add a shared-library target containing `rife_native.cpp`, `rife.cpp`, and `warp.cpp`, linked to the same libraries as the upstream executable. The shipped model directory is `rife-v4.6`.
+
+The native runtime and model remain subject to their original third-party licenses; see `licenses/` and `docs/SOURCES.md`.
diff --git a/native/AnimeFlow/rife_native.cpp b/native/AnimeFlow/rife_native.cpp
new file mode 100644
index 0000000..4c3ae97
--- /dev/null
+++ b/native/AnimeFlow/rife_native.cpp
@@ -0,0 +1,92 @@
+#include "rife.h"
+#include "net.h"
+
+#include
+#include
+#include
+
+namespace
+{
+ struct RifeHandle
+ {
+ explicit RifeHandle(int gpu) : engine(gpu, false, false, false, 1, false, true) {}
+ RIFE engine;
+ };
+
+ std::atomic handle_count{0};
+}
+
+extern "C" __declspec(dllexport) void* __cdecl rife_create(int gpu_id, const wchar_t* model_directory)
+{
+ if (!model_directory) return nullptr;
+ if (handle_count.fetch_add(1) == 0) ncnn::create_gpu_instance();
+
+ RifeHandle* handle = new (std::nothrow) RifeHandle(gpu_id);
+ if (!handle || handle->engine.load(model_directory) != 0)
+ {
+ delete handle;
+ if (handle_count.fetch_sub(1) == 1) ncnn::destroy_gpu_instance();
+ return nullptr;
+ }
+ return handle;
+}
+
+extern "C" __declspec(dllexport) int __cdecl rife_default_gpu()
+{
+ ncnn::create_gpu_instance();
+ const int result = ncnn::get_default_gpu_index();
+ if (handle_count.load() == 0) ncnn::destroy_gpu_instance();
+ return result;
+}
+
+extern "C" __declspec(dllexport) int __cdecl rife_process_bgra(
+ void* opaque,
+ const unsigned char* first,
+ const unsigned char* second,
+ int width,
+ int height,
+ int input_stride,
+ float timestep,
+ unsigned char* output,
+ int output_stride)
+{
+ if (!opaque || !first || !second || !output || width <= 0 || height <= 0) return -1;
+ auto* handle = static_cast(opaque);
+ std::vector bgr0((size_t)width * height * 3);
+ std::vector bgr1((size_t)width * height * 3);
+ for (int y = 0; y < height; y++)
+ {
+ const unsigned char* row0 = first + (size_t)y * input_stride;
+ const unsigned char* row1 = second + (size_t)y * input_stride;
+ unsigned char* dst0 = bgr0.data() + (size_t)y * width * 3;
+ unsigned char* dst1 = bgr1.data() + (size_t)y * width * 3;
+ for (int x = 0; x < width; x++)
+ {
+ dst0[x * 3] = row0[x * 4]; dst0[x * 3 + 1] = row0[x * 4 + 1]; dst0[x * 3 + 2] = row0[x * 4 + 2];
+ dst1[x * 3] = row1[x * 4]; dst1[x * 3 + 1] = row1[x * 4 + 1]; dst1[x * 3 + 2] = row1[x * 4 + 2];
+ }
+ }
+ ncnn::Mat frame0(width, height, bgr0.data(), (size_t)3, 3);
+ ncnn::Mat frame1(width, height, bgr1.data(), (size_t)3, 3);
+ ncnn::Mat interpolated(width, height, (size_t)3, 3);
+ const int result = handle->engine.process_v4(frame0, frame1, timestep, interpolated);
+ if (result != 0 || interpolated.empty()) return result != 0 ? result : -2;
+ const unsigned char* bgr = static_cast(interpolated.data);
+ for (int y = 0; y < height; y++)
+ {
+ const unsigned char* src = bgr + (size_t)y * width * 3;
+ unsigned char* dst = output + (size_t)y * output_stride;
+ for (int x = 0; x < width; x++)
+ {
+ dst[x * 4] = src[x * 3]; dst[x * 4 + 1] = src[x * 3 + 1]; dst[x * 4 + 2] = src[x * 3 + 2]; dst[x * 4 + 3] = 255;
+ }
+ }
+ return 0;
+}
+
+extern "C" __declspec(dllexport) void __cdecl rife_destroy(void* opaque)
+{
+ if (!opaque) return;
+ delete static_cast(opaque);
+ if (handle_count.fetch_sub(1) == 1) ncnn::destroy_gpu_instance();
+}
diff --git a/scripts/package.ps1 b/scripts/package.ps1
index 9ff0a75..413e764 100644
--- a/scripts/package.ps1
+++ b/scripts/package.ps1
@@ -1,14 +1,14 @@
$ErrorActionPreference='Stop';$root=Split-Path $PSScriptRoot -Parent;& (Join-Path $PSScriptRoot 'publish.ps1');if($LASTEXITCODE){exit $LASTEXITCODE}
-$portable=Join-Path $root 'artifacts\portable\AnimeEnhancer-1.0.17';$zip=Join-Path $root 'artifacts\portable\AnimeEnhancer-1.0.17-win-x64.zip';if(Test-Path $zip){Remove-Item -LiteralPath $zip -Force};Compress-Archive -Path "$portable\*" -DestinationPath $zip
+$portable=Join-Path $root 'artifacts\portable\AnimeEnhancer-1.1.0-beta.1';$zip=Join-Path $root 'artifacts\portable\AnimeEnhancer-1.1.0-beta.1-win-x64.zip';if(Test-Path $zip){Remove-Item -LiteralPath $zip -Force};Compress-Archive -Path "$portable\*" -DestinationPath $zip
$checks=Join-Path $root 'artifacts\checksums';New-Item -ItemType Directory -Force $checks|Out-Null;(Get-FileHash $zip -Algorithm SHA256).Hash.ToLower()+" "+(Split-Path $zip -Leaf)|Set-Content (Join-Path $checks 'SHA256SUMS.txt')
-$manifest=[ordered]@{version='1.0.17';runtime='win-x64';framework='net10.0-windows10.0.26100.0';minimumWindows='10.0.19041.0';created=(Get-Date).ToUniversalTime().ToString('o')};$manifest|ConvertTo-Json|Set-Content (Join-Path $checks 'version.json')
+$manifest=[ordered]@{version='1.1.0-beta.1';runtime='win-x64';framework='net10.0-windows10.0.26100.0';minimumWindows='10.0.19041.0';created=(Get-Date).ToUniversalTime().ToString('o')};$manifest|ConvertTo-Json|Set-Content (Join-Path $checks 'version.json')
$dotnet=Get-Command dotnet -ErrorAction SilentlyContinue
if(-not $dotnet){$workspace=Split-Path (Split-Path $root -Parent) -Parent;$dotnet=Join-Path $workspace 'work\.dotnet\dotnet.exe'}
$installerOut=Join-Path $root 'artifacts\installer';New-Item -ItemType Directory -Force $installerOut|Out-Null
& $dotnet build (Join-Path $root 'installer\AnimeEnhancer.Installer.wixproj') -c Release
if($LASTEXITCODE){throw 'Installer build failed.'}
$builtMsi=Get-ChildItem (Join-Path $root 'installer') -Recurse -Filter *.msi|Where-Object Name -NotLike '*_validate.msi'|Sort-Object LastWriteTime -Descending|Select-Object -First 1;if(-not $builtMsi){throw 'Installer MSI was not produced.'}
-$msiPath=Join-Path $installerOut 'AnimeEnhancer-1.0.17-x64.msi';$copied=$false
+$msiPath=Join-Path $installerOut 'AnimeEnhancer-1.1.0-beta.1-x64.msi';$copied=$false
for($attempt=0;$attempt -lt 20 -and -not $copied;$attempt++){try{Copy-Item $builtMsi.FullName $msiPath -Force;$copied=$true}catch{Start-Sleep -Milliseconds 250}}
if(-not $copied){throw 'Installer MSI remained locked after build.'};$msi=Get-Item $msiPath
$msiLine=(Get-FileHash $msi.FullName -Algorithm SHA256).Hash.ToLower()+" "+$msi.Name;Add-Content -LiteralPath (Join-Path $checks 'SHA256SUMS.txt') -Value $msiLine
diff --git a/scripts/publish.ps1 b/scripts/publish.ps1
index 562be9c..0fd835a 100644
--- a/scripts/publish.ps1
+++ b/scripts/publish.ps1
@@ -1,5 +1,5 @@
$ErrorActionPreference='Stop';$root=Split-Path $PSScriptRoot -Parent;$dotnet=Get-Command dotnet -ErrorAction SilentlyContinue;if(-not $dotnet){$workspace=Split-Path (Split-Path $root -Parent) -Parent;$dotnet=Join-Path $workspace 'work\.dotnet\dotnet.exe'}
& (Join-Path $PSScriptRoot 'build.ps1') -Configuration Release;if($LASTEXITCODE){exit $LASTEXITCODE}
-$out=Join-Path $root 'artifacts\portable\AnimeEnhancer-1.0.17';New-Item -ItemType Directory -Force $out|Out-Null
+$out=Join-Path $root 'artifacts\portable\AnimeEnhancer-1.1.0-beta.1';New-Item -ItemType Directory -Force $out|Out-Null
& $dotnet publish (Join-Path $root 'src\AnimeEnhancer.App\AnimeEnhancer.App.csproj') -c Release -r win-x64 --self-contained true -o $out --no-build
if($LASTEXITCODE){exit $LASTEXITCODE}
diff --git a/scripts/verify-release.ps1 b/scripts/verify-release.ps1
index d054615..6354d75 100644
--- a/scripts/verify-release.ps1
+++ b/scripts/verify-release.ps1
@@ -1,5 +1,5 @@
-$ErrorActionPreference='Stop';$root=Split-Path $PSScriptRoot -Parent;$p=Join-Path $root 'artifacts\portable\AnimeEnhancer-1.0.17'
-$required=@('AnimeEnhancer.exe','AnimeEnhancer.dll','Vortice.Direct3D11.dll','Vortice.DXGI.dll','shaders\shaders.json','licenses\Anime4K-LICENSE.txt','docs\PRIVACY.md')
+$ErrorActionPreference='Stop';$root=Split-Path $PSScriptRoot -Parent;$p=Join-Path $root 'artifacts\portable\AnimeEnhancer-1.1.0-beta.1'
+$required=@('AnimeEnhancer.exe','AnimeEnhancer.dll','animeflow-rife.dll','AnimeFlow\rife-v4.6\flownet.bin','Vortice.Direct3D11.dll','Vortice.DXGI.dll','shaders\shaders.json','licenses\Anime4K-LICENSE.txt','licenses\RIFE-NCNN-VULKAN-LICENSE.txt','licenses\NCNN-LICENSE.txt','docs\PRIVACY.md')
foreach($f in $required){if(-not(Test-Path (Join-Path $p $f))){throw "Missing release file: $f"}}
if(Get-ChildItem $p -Recurse -Directory|Where-Object Name -eq '.reference'){throw '.reference was packaged'}
$manifest=Get-Content (Join-Path $p 'shaders\shaders.json') -Raw|ConvertFrom-Json;foreach($s in $manifest.shaders){if(-not(Test-Path (Join-Path $p ('shaders\'+$s.bytecodeFile)))){throw "Missing shader bytecode: $($s.bytecodeFile)"}}
diff --git a/shaders/Presentation/Copy.hlsl b/shaders/Presentation/Copy.hlsl
new file mode 100644
index 0000000..2d44045
--- /dev/null
+++ b/shaders/Presentation/Copy.hlsl
@@ -0,0 +1,8 @@
+struct VSOutput { float4 Position : SV_Position; float2 UV : TEXCOORD0; };
+Texture2D SourceTexture : register(t0);
+SamplerState LinearSampler : register(s0);
+
+float4 PSMain(VSOutput input) : SV_Target
+{
+ return SourceTexture.SampleLevel(LinearSampler, input.UV, 0);
+}
diff --git a/shaders/Presentation/MotionStabilize.hlsl b/shaders/Presentation/MotionStabilize.hlsl
new file mode 100644
index 0000000..8536c26
--- /dev/null
+++ b/shaders/Presentation/MotionStabilize.hlsl
@@ -0,0 +1,63 @@
+struct VSOutput { float4 Position : SV_Position; float2 UV : TEXCOORD0; };
+Texture2D MotionFrame : register(t0);
+Texture2D RealFrame : register(t1);
+SamplerState LinearSampler : register(s0);
+
+cbuffer Params : register(b0)
+{
+ float2 SourceSize;
+ float2 OutputSize;
+ float Strength;
+ float Split;
+ uint Comparison;
+ float Saturation;
+ float Contrast;
+ float3 Padding;
+};
+
+float Luma(float3 color) { return dot(color, float3(0.2126, 0.7152, 0.0722)); }
+float MaxChannel(float3 value) { return max(value.r, max(value.g, value.b)); }
+
+float4 PSMain(VSOutput input) : SV_Target
+{
+ float2 px = 1.0 / SourceSize;
+ float3 generated = MotionFrame.SampleLevel(LinearSampler, input.UV, 0).rgb;
+ float3 generatedNorth = MotionFrame.SampleLevel(LinearSampler, input.UV + float2(0, -px.y), 0).rgb;
+ float3 generatedSouth = MotionFrame.SampleLevel(LinearSampler, input.UV + float2(0, px.y), 0).rgb;
+ float3 generatedEast = MotionFrame.SampleLevel(LinearSampler, input.UV + float2(px.x, 0), 0).rgb;
+ float3 generatedWest = MotionFrame.SampleLevel(LinearSampler, input.UV + float2(-px.x, 0), 0).rgb;
+ float3 real = RealFrame.SampleLevel(LinearSampler, input.UV, 0).rgb;
+ float3 north = RealFrame.SampleLevel(LinearSampler, input.UV + float2(0, -px.y), 0).rgb;
+ float3 south = RealFrame.SampleLevel(LinearSampler, input.UV + float2(0, px.y), 0).rgb;
+ float3 east = RealFrame.SampleLevel(LinearSampler, input.UV + float2(px.x, 0), 0).rgb;
+ float3 west = RealFrame.SampleLevel(LinearSampler, input.UV + float2(-px.x, 0), 0).rgb;
+
+ float3 localMin = min(real, min(min(north, south), min(east, west)));
+ float3 localMax = max(real, max(max(north, south), max(east, west)));
+ float realEdge = max(max(abs(Luma(north) - Luma(south)), abs(Luma(east) - Luma(west))),
+ max(abs(Luma(real) - Luma(north)), abs(Luma(real) - Luma(east))));
+ float generatedEdge = max(max(abs(Luma(generatedNorth) - Luma(generatedSouth)), abs(Luma(generatedEast) - Luma(generatedWest))),
+ max(abs(Luma(generated) - Luma(generatedNorth)), abs(Luma(generated) - Luma(generatedEast))));
+ float disagreement = MaxChannel(abs(generated - real));
+
+ // Large disagreement is normal on moving fills, but disagreement directly
+ // on strong anime line art is where FRUC produces curls, tears and text
+ // fragments. Clamp only those suspicious pixels to the real-frame
+ // neighbourhood and retain most of the interpolated motion everywhere else.
+ float lineDisagreement = smoothstep(0.08, 0.28, disagreement) * smoothstep(0.018, 0.11, realEdge);
+ float edgeMismatch = smoothstep(0.025, 0.14, abs(generatedEdge - realEdge));
+ float inventedTexture = smoothstep(0.025, 0.12, generatedEdge - realEdge) * smoothstep(0.045, 0.24, disagreement);
+ float suspicious = saturate(lineDisagreement + edgeMismatch * 0.72 + inventedTexture * 0.92);
+ float room = lerp(0.075, 0.035, smoothstep(0.02, 0.14, realEdge));
+ float3 bounded = clamp(generated, localMin - room, localMax + room);
+ float3 stable = lerp(generated, bounded, suspicious * 0.94);
+ stable = lerp(stable, real, suspicious * 0.30);
+
+ // FRUC's warp can soften otherwise-correct lines slightly. A restrained
+ // edge-only correction keeps generated and real frames equally crisp.
+ float3 neighbourhood = (north + south + east + west) * 0.25;
+ float3 detail = real - neighbourhood;
+ stable += detail * (0.10 * smoothstep(0.02, 0.12, realEdge) * (1.0 - suspicious));
+
+ return float4(saturate(stable), 1.0);
+}
diff --git a/src/AnimeEnhancer.App/AnimeEnhancer.App.csproj b/src/AnimeEnhancer.App/AnimeEnhancer.App.csproj
index 73e8b13..c2039a8 100644
--- a/src/AnimeEnhancer.App/AnimeEnhancer.App.csproj
+++ b/src/AnimeEnhancer.App/AnimeEnhancer.App.csproj
@@ -7,6 +7,7 @@
+
@@ -19,4 +20,10 @@
+
+
+
+
+
+
diff --git a/src/AnimeEnhancer.App/App.xaml b/src/AnimeEnhancer.App/App.xaml
index eabd6d5..678c703 100644
--- a/src/AnimeEnhancer.App/App.xaml
+++ b/src/AnimeEnhancer.App/App.xaml
@@ -1,13 +1,19 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AnimeEnhancer.App/App.xaml.cs b/src/AnimeEnhancer.App/App.xaml.cs
index a803103..14181df 100644
--- a/src/AnimeEnhancer.App/App.xaml.cs
+++ b/src/AnimeEnhancer.App/App.xaml.cs
@@ -13,7 +13,7 @@ protected override void OnStartup(StartupEventArgs e)
var otherCopies = Process.GetProcessesByName("AnimeEnhancer").Count(p => p.Id != Environment.ProcessId);
if (!ownsMutex || otherCopies > 0)
{
- System.Windows.MessageBox.Show($"AnimeEnhancer found {Math.Max(1, otherCopies)} other running copy/copies.\n\nExit every old copy from the tray icon, then start AnimeEnhancer 1.0.17 again. Multiple capture overlays cannot run together.", "Close old AnimeEnhancer copies", MessageBoxButton.OK, MessageBoxImage.Warning);
+ System.Windows.MessageBox.Show($"AnimeEnhancer found {Math.Max(1, otherCopies)} other running copy/copies.\n\nExit every old copy from the tray icon, then start AnimeEnhancer 1.1 Beta again. Multiple capture overlays cannot run together.", "Close old AnimeEnhancer copies", MessageBoxButton.OK, MessageBoxImage.Warning);
Shutdown(); return;
}
LocalLog.Initialize(); DispatcherUnhandledException += (_, a) => { Log.Fatal(a.Exception, "Unhandled UI exception"); System.Windows.MessageBox.Show(a.Exception.Message, "AnimeEnhancer error"); a.Handled = true; };
diff --git a/src/AnimeEnhancer.App/Assets/Beta/original-character.png b/src/AnimeEnhancer.App/Assets/Beta/original-character.png
new file mode 100644
index 0000000..91ba7d9
Binary files /dev/null and b/src/AnimeEnhancer.App/Assets/Beta/original-character.png differ
diff --git a/src/AnimeEnhancer.App/MainWindow.xaml b/src/AnimeEnhancer.App/MainWindow.xaml
index 961e38c..673dc34 100644
--- a/src/AnimeEnhancer.App/MainWindow.xaml
+++ b/src/AnimeEnhancer.App/MainWindow.xaml
@@ -1,29 +1,41 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
-
+
-
+
-
+
-
+
-
+
@@ -36,37 +48,56 @@
-
+
-
+
-
+
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
+
diff --git a/src/AnimeEnhancer.App/MainWindow.xaml.cs b/src/AnimeEnhancer.App/MainWindow.xaml.cs
index d6a0d2d..a9b7fb0 100644
--- a/src/AnimeEnhancer.App/MainWindow.xaml.cs
+++ b/src/AnimeEnhancer.App/MainWindow.xaml.cs
@@ -44,6 +44,14 @@ public partial class MainWindow : Window
private NativeMethods.Rect _outputRect;
private bool _borderlessCapture;
private bool _borderlessAccessChecked;
+ private int _snapshotRequested;
+ private string? _automaticSnapshotPath = Environment.GetEnvironmentVariable("ANIMEENHANCER_AUTOSNAPSHOT_PATH");
+ private int _automaticSnapshotFrames;
+ private long _lastMotionLogCount;
+ private RifeMotionEngine? _rifeMotion;
+ private CancellationTokenSource? _motionPresenterStop;
+ private Task? _motionPresenter;
+ private PixelSize _motionSize;
public MainWindow()
{
@@ -59,6 +67,7 @@ public MainWindow()
if (_settings.Preset == EnhancementPreset.Ultra4X) _settings.Preset = EnhancementPreset.UltraDetail;
if (_settings.SettingsVersion < 14) { _settings.SettingsVersion = 14; _settingsStore.Save(_settings); }
if (_settings.SettingsVersion < 15) { _settings.SettingsVersion = 15; _settings.LosslessScalingCompatibility = false; _settingsStore.Save(_settings); }
+ if (_settings.SettingsVersion < 16) { _settings.SettingsVersion = 16; _settings.RtxMotion60 = false; _settingsStore.Save(_settings); }
if (_settings.Preset is EnhancementPreset.Off or EnhancementPreset.Performance or EnhancementPreset.Auto or EnhancementPreset.Custom or EnhancementPreset.AnimeClean) _settings.Preset = EnhancementPreset.Quality;
BalancedPreset.IsChecked = _settings.Preset == EnhancementPreset.Balanced;
QualityPreset.IsChecked = _settings.Preset == EnhancementPreset.Quality;
@@ -72,6 +81,7 @@ public MainWindow()
CompressionCleanupToggle.IsChecked = _settings.CompressionCleanup;
DebandingToggle.IsChecked = _settings.Debanding;
LosslessScalingToggle.IsChecked = _settings.LosslessScalingCompatibility;
+ RtxMotionToggle.IsChecked = _settings.RtxMotion60;
var adapters = new List { "Auto (recommended)" };
try { adapters.AddRange(GpuPipeline.EnumerateAdapters()); } catch (Exception ex) { Log.Warning(ex, "GPU enumeration failed"); }
GpuList.ItemsSource = adapters;
@@ -95,6 +105,7 @@ private void OnLoaded(object sender, RoutedEventArgs e)
_watcher = new FullscreenWatcher(new WindowInteropHelper(this).Handle);
_watcher.Changed += hwnd => Dispatcher.BeginInvoke(async () => await FullscreenChangedAsync(hwnd));
CreateTray(); _statsTimer.Start();
+ if (Environment.GetEnvironmentVariable("ANIMEENHANCER_AUTORUN") == "1") Dispatcher.BeginInvoke(() => Start_Click(this, new RoutedEventArgs()));
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041)) System.Windows.MessageBox.Show("AnimeEnhancer requires Windows 10 version 2004 or newer.", "Unsupported Windows");
}
@@ -111,6 +122,7 @@ private async void Start_Click(object sender, RoutedEventArgs e)
SourceText.Text = "Open a video and enter fullscreen";
GpuButton.IsEnabled = false;
LosslessScalingToggle.IsEnabled = false;
+ RtxMotionToggle.IsEnabled = false;
if (!AutoFullscreen.IsChecked.GetValueOrDefault())
{
var hwnd = NativeMethods.GetForegroundWindow();
@@ -154,6 +166,8 @@ private async Task ActivateAsync(nint hwnd)
_outputRect = rect; _overlay!.Cover(rect); _overlay.SetInputTarget(hwnd);
if (_overlay.ExternalCaptureEnabled != _settings.LosslessScalingCompatibility) throw new InvalidOperationException("Restart AnimeEnhancer to apply the LS output mode.");
_gpu.Attach(_overlay.Handle, new PixelSize(rect.Width, rect.Height));
+ _gpu.ConfigureRtxMotion(false, out _);
+ if (_settings.RtxMotion60) StartRifeMotion(rect.Width, rect.Height);
if (_toast is not null) _ = _toast.ShowForAsync("GPU ready • waiting for video", rect);
await _state.TransitionAsync(EnhancementState.InitializingCapture, token);
@@ -212,9 +226,40 @@ private void OnFrame(Windows.Graphics.DirectX.Direct3D11.IDirect3DSurface surfac
try
{
using var texture = WinRtDirect3DDevice.GetTexture(surface); _captureSize = size;
- _gpu!.Process(texture, size, _settings.Preset, _settings.Comparison, .5, _settings.VSync, (float)_settings.Saturation, (float)_settings.Contrast, _settings.Debanding, _settings.CompressionCleanup);
- _firstFrame?.TrySetResult(true); Interlocked.Increment(ref _processed);
- Interlocked.Increment(ref _presentedTotal);
+ var motionPassthrough = false;
+ if (_settings.RtxMotion60 && _rifeMotion is not null)
+ {
+ motionPassthrough = _rifeMotion.Submit(_gpu!.CaptureMotionSource(texture, size, _motionSize), timestamp);
+ if (motionPassthrough)
+ _gpu.Process(texture, size, _settings.Preset, _settings.Comparison, .5, _settings.VSync,
+ (float)_settings.Saturation, (float)_settings.Contrast, _settings.Debanding, _settings.CompressionCleanup, false);
+ }
+ else
+ _gpu!.Process(texture, size, _settings.Preset, _settings.Comparison, .5, _settings.VSync, (float)_settings.Saturation, (float)_settings.Contrast, _settings.Debanding, _settings.CompressionCleanup, false);
+ var generated = _rifeMotion?.GeneratedFrames ?? 0;
+ if (generated >= _lastMotionLogCount + 60)
+ {
+ _lastMotionLogCount = generated;
+ Log.Information("AnimeFlow generated {Frames} RIFE frames", generated);
+ }
+ var automaticPath = _automaticSnapshotPath;
+ if (automaticPath is not null && Interlocked.Increment(ref _automaticSnapshotFrames) >= 3)
+ {
+ _automaticSnapshotPath = null;
+ var captured = _gpu.CapturePresentedFrame();
+ _ = Task.Run(() => SaveSnapshot(captured, automaticPath));
+ }
+ else if (Interlocked.Exchange(ref _snapshotRequested, 0) == 1)
+ {
+ var captured = _gpu.CapturePresentedFrame();
+ _ = Task.Run(() => SaveSnapshot(captured));
+ }
+ _firstFrame?.TrySetResult(true);
+ if (!_settings.RtxMotion60 || motionPassthrough)
+ {
+ Interlocked.Increment(ref _processed);
+ Interlocked.Increment(ref _presentedTotal);
+ }
if (Interlocked.Exchange(ref _overlayVisible, 1) == 0)
{
Dispatcher.BeginInvoke(async () =>
@@ -225,6 +270,7 @@ private void OnFrame(Windows.Graphics.DirectX.Direct3D11.IDirect3DSurface surfac
var mode = _settings.Preset switch { EnhancementPreset.RtxQuality => "RTX Quality", EnhancementPreset.UltraDetail => "Ultra Detail", _ => _settings.Preset.ToString() };
var label = _outputRect.Width >= 3800 && _outputRect.Height >= 2100 ? $"{mode} • 4K enhancement active" : $"{mode} active • {_outputRect.Width}×{_outputRect.Height}";
if (_settings.LosslessScalingCompatibility) label += " • LSFG ready";
+ if (_settings.RtxMotion60) label += " • AnimeFlow 60 FPS active";
SetStatus(label, "#65D6C7"); if (_toast is not null) await _toast.ShowForAsync(label, _outputRect);
});
}
@@ -245,14 +291,107 @@ private async Task StopAsync()
try
{
StopCaptureOnly(); await ReturnToIdleStateAsync();
- StartButton.Content = "Start enhancement"; StopButton.IsEnabled = false; GpuButton.IsEnabled = true; LosslessScalingToggle.IsEnabled = true; SourceText.Text = "Ready for fullscreen video";
+ StartButton.Content = "Start enhancement"; StopButton.IsEnabled = false; GpuButton.IsEnabled = true; LosslessScalingToggle.IsEnabled = true; RtxMotionToggle.IsEnabled = true; SourceText.Text = "Ready for fullscreen video";
}
finally { _lifecycle.Release(); }
}
+ private void StartRifeMotion(int sourceWidth, int sourceHeight)
+ {
+ StopRifeMotion();
+ // 960-wide inference keeps two generated frames inside a 24 fps source
+ // interval on an RTX 4060. Reconstruction still runs at up to 1080p.
+ var width = Math.Clamp(sourceWidth, 640, 960);
+ var height = Math.Max(180, (int)Math.Round(width * (double)sourceHeight / sourceWidth));
+ height = (height + 1) & ~1;
+ _motionSize = new PixelSize(width, height);
+ var model = System.IO.Path.Combine(AppContext.BaseDirectory, "AnimeFlow", "rife-v4.6");
+ if (!System.IO.Directory.Exists(model)) throw new System.IO.DirectoryNotFoundException("AnimeFlow RIFE model is missing.");
+ _rifeMotion = new RifeMotionEngine(width, height, model);
+ _motionPresenterStop = new CancellationTokenSource();
+ var token = _motionPresenterStop.Token;
+ _motionPresenter = Task.Factory.StartNew(() => MotionPresentationLoop(token), token,
+ TaskCreationOptions.LongRunning, TaskScheduler.Default);
+ _lastMotionLogCount = 0;
+ Log.Information("AnimeFlow RIFE started at {Width}x{Height}", width, height);
+ }
+
+ private void MotionPresentationLoop(CancellationToken token)
+ {
+ const double presentationBufferMilliseconds = 38.0;
+ var clock = System.Diagnostics.Stopwatch.StartNew();
+ long sourceAnchor = 0;
+ double wallAnchor = 0;
+ try
+ {
+ while (!token.IsCancellationRequested)
+ {
+ var engine = _rifeMotion;
+ if (engine?.Error is string error) throw new InvalidOperationException(error);
+ if (engine?.PassthroughRecommended == true)
+ {
+ Thread.Sleep(1); sourceAnchor = 0; wallAnchor = 0; continue;
+ }
+ if (engine is null || !engine.TryGetFrame(out var frame, out var sourceTimestamp)) { Thread.Sleep(1); continue; }
+
+ if (sourceAnchor == 0 || sourceTimestamp < sourceAnchor || sourceTimestamp - sourceAnchor > TimeSpan.TicksPerSecond)
+ {
+ sourceAnchor = sourceTimestamp;
+ wallAnchor = clock.Elapsed.TotalMilliseconds + presentationBufferMilliseconds;
+ }
+ var target = wallAnchor + (sourceTimestamp - sourceAnchor) * 1000.0 / TimeSpan.TicksPerSecond;
+ var lateness = clock.Elapsed.TotalMilliseconds - target;
+ if (lateness > 18)
+ {
+ // Never catch up by bursting old frames; re-anchor on a large
+ // miss so motion stays evenly paced after a temporary GPU spike.
+ if (lateness > 55)
+ {
+ sourceAnchor = sourceTimestamp;
+ wallAnchor = clock.Elapsed.TotalMilliseconds + 3;
+ target = wallAnchor;
+ }
+ else continue;
+ }
+
+ var remaining = target - clock.Elapsed.TotalMilliseconds;
+ if (remaining > 2) Thread.Sleep(Math.Max(0, (int)remaining - 1));
+ while (clock.Elapsed.TotalMilliseconds < target && !token.IsCancellationRequested) Thread.SpinWait(80);
+ if (token.IsCancellationRequested) break;
+ if (engine.PassthroughRecommended) { sourceAnchor = 0; wallAnchor = 0; continue; }
+
+ _gpu?.PresentMotionFrame(frame, _motionSize, _settings.Preset, _settings.Comparison, .5,
+ (float)_settings.Saturation, (float)_settings.Contrast, _settings.Debanding, _settings.CompressionCleanup);
+ Interlocked.Increment(ref _presentedTotal);
+ Interlocked.Increment(ref _processed);
+
+ }
+ }
+ catch (OperationCanceledException) { }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "AnimeFlow presentation stopped");
+ Dispatcher.BeginInvoke(() =>
+ {
+ if (!_settings.RtxMotion60) return;
+ _settings.RtxMotion60 = false; RtxMotionToggle.IsChecked = false; Save();
+ SetStatus("Upscaling active • AnimeFlow stopped safely", "#F4BF62");
+ });
+ }
+ }
+
+ private void StopRifeMotion()
+ {
+ var stop = _motionPresenterStop; _motionPresenterStop = null;
+ if (stop is not null) { stop.Cancel(); try { _motionPresenter?.Wait(1200); } catch { } stop.Dispose(); }
+ _motionPresenter = null;
+ _rifeMotion?.Dispose(); _rifeMotion = null; _motionSize = default;
+ }
+
private void StopCaptureOnly()
{
_capture?.Dispose(); _capture = null; _firstFrame = null; _activeSourceHwnd = 0;
+ StopRifeMotion();
_pointerBridge?.Deactivate();
_overlay?.Hide(); _toast?.Hide(); _exitControl?.Hide(); Interlocked.Exchange(ref _overlayVisible, 0); Interlocked.Exchange(ref _presentedTotal, 0);
}
@@ -261,9 +400,10 @@ private void UpdateStats()
{
var now = DateTime.UtcNow; var elapsed = (now - _fpsSince).TotalSeconds;
if (elapsed >= 1) { var count = Interlocked.Exchange(ref _processed, 0); _processingFps = count / elapsed; _fpsSince = now; }
- var ultraSignal = _settings.Preset == EnhancementPreset.UltraDetail ? "\nULTRA DETAIL 2 clarity passes" : string.Empty;
- var lsSignal = _settings.LosslessScalingCompatibility ? "\nLSFG OUTPUT external capture enabled" : string.Empty;
- StatsText.Text = $"{_processingFps:F0} FPS {_gpu?.LastGpuMilliseconds ?? 0:F1} ms {_captureSize}\n{_gpu?.AdapterName ?? "GPU initializes on first video"}{ultraSignal}{lsSignal}";
+ var ultraSignal = _settings.Preset == EnhancementPreset.UltraDetail ? " • Ultra Detail" : string.Empty;
+ var lsSignal = _settings.LosslessScalingCompatibility ? " • LSFG" : string.Empty;
+ var motionSignal = _settings.RtxMotion60 ? $" • AnimeFlow {_rifeMotion?.GeneratedFrames ?? 0}" : string.Empty;
+ StatsText.Text = $"{_processingFps:F0} FPS • {_gpu?.LastGpuMilliseconds ?? 0:F1} ms • {_captureSize} • {_gpu?.AdapterName ?? "GPU initializes on first video"}{ultraSignal}{lsSignal}{motionSignal}";
}
private void Preset_Checked(object sender, RoutedEventArgs e)
@@ -293,6 +433,11 @@ private void LosslessScaling_Checked(object sender, RoutedEventArgs e)
{
if (!_uiReady) return;
_settings.LosslessScalingCompatibility = LosslessScalingToggle.IsChecked == true;
+ if (_settings.LosslessScalingCompatibility && RtxMotionToggle.IsChecked == true)
+ {
+ RtxMotionToggle.IsChecked = false;
+ _settings.RtxMotion60 = false;
+ }
Save();
if (_armed) { SetStatus("Restart enhancement to apply LS mode", "#F4BF62"); return; }
_pointerBridge?.Dispose();
@@ -300,6 +445,23 @@ private void LosslessScaling_Checked(object sender, RoutedEventArgs e)
_overlay = new NativeOverlayWindow(_settings.LosslessScalingCompatibility);
_pointerBridge = new OverlayPointerBridge(_overlay);
}
+ private void RtxMotion_Checked(object sender, RoutedEventArgs e)
+ {
+ if (!_uiReady) return;
+ _settings.RtxMotion60 = RtxMotionToggle.IsChecked == true;
+ if (_settings.RtxMotion60 && LosslessScalingToggle.IsChecked == true)
+ {
+ LosslessScalingToggle.IsChecked = false;
+ _settings.LosslessScalingCompatibility = false;
+ }
+ Save();
+ if (_armed) SetStatus("Restart enhancement to apply RTX Motion", "#F4BF62");
+ else
+ {
+ _winRtDevice?.Dispose(); _winRtDevice = null;
+ _gpu?.Dispose(); _gpu = null;
+ }
+ }
private void UpdateColorLabels()
{
if (SaturationValue is null || ContrastValue is null) return;
@@ -322,18 +484,66 @@ private void Export_Click(object sender, RoutedEventArgs e)
DiagnosticsExporter.Export(_settings, file); System.Windows.MessageBox.Show($"Diagnostics exported to:\n{file}", "AnimeEnhancer");
}
+ private void Snapshot_Click(object sender, RoutedEventArgs e) => RequestSnapshot();
+ private void RequestSnapshot()
+ {
+ if (_state.State != EnhancementState.Running)
+ {
+ SetStatus("Start enhancement before saving a snapshot", "#F4BF62");
+ return;
+ }
+ Interlocked.Exchange(ref _snapshotRequested, 1);
+ SetStatus("Saving GPU snapshot…", "#FF78C6");
+ }
+
+ private void SaveSnapshot(CapturedFrame frame, string? requestedPath = null)
+ {
+ try
+ {
+ var desktop = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
+ var path = requestedPath ?? System.IO.Path.Combine(desktop, $"AnimeEnhancer-Comparison-{DateTime.Now:yyyyMMdd-HHmmss}.png");
+ var folder = System.IO.Path.GetDirectoryName(path);
+ if (!string.IsNullOrEmpty(folder)) System.IO.Directory.CreateDirectory(folder);
+ using var bitmap = new System.Drawing.Bitmap(frame.Width, frame.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
+ var area = new System.Drawing.Rectangle(0, 0, frame.Width, frame.Height);
+ var data = bitmap.LockBits(area, System.Drawing.Imaging.ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
+ try
+ {
+ var rowBytes = frame.Width * 4;
+ for (var y = 0; y < frame.Height; y++)
+ Marshal.Copy(frame.BgraPixels, y * rowBytes, data.Scan0 + y * data.Stride, rowBytes);
+ }
+ finally { bitmap.UnlockBits(data); }
+ bitmap.Save(path, System.Drawing.Imaging.ImageFormat.Png);
+ Dispatcher.BeginInvoke(() =>
+ {
+ SetStatus("Comparison PNG saved", "#65D6C7");
+ if (_toast is not null) _ = _toast.ShowForAsync("Comparison PNG saved to Desktop", _outputRect);
+ });
+ Log.Information("GPU snapshot saved to {Path}", path);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "GPU snapshot failed");
+ Dispatcher.BeginInvoke(() => SetStatus("Could not save comparison PNG", "#FF718A"));
+ }
+ }
+
private void OnSourceInitialized(object? sender, EventArgs e)
{
var source = HwndSource.FromHwnd(new WindowInteropHelper(this).Handle); source.AddHook(WndProc); var hwnd = source.Handle;
Register(hwnd, 1, NativeMethods.ModControl | NativeMethods.ModAlt | NativeMethods.ModNoRepeat, 'U');
Register(hwnd, 2, NativeMethods.ModControl | NativeMethods.ModAlt | NativeMethods.ModShift | NativeMethods.ModNoRepeat, 'Q');
+ Register(hwnd, 3, NativeMethods.ModControl | NativeMethods.ModAlt | NativeMethods.ModNoRepeat, 'P');
}
private static void Register(nint hwnd, int id, uint mods, char key) { if (!NativeMethods.RegisterHotKey(hwnd, id, mods, key)) Log.Warning("Hotkey {Id} registration failed: {Error}", id, Marshal.GetLastWin32Error()); }
private nint WndProc(nint hwnd, int msg, nint wParam, nint lParam, ref bool handled)
{
if (msg != 0x0312) return 0; handled = true;
if (wParam == 1) Dispatcher.BeginInvoke(async () => { if (_armed) await StopAsync(); else Start_Click(this, new RoutedEventArgs()); });
- if (wParam == 2) Dispatcher.BeginInvoke(async () => await StopAsync()); return 0;
+ if (wParam == 2) Dispatcher.BeginInvoke(async () => await StopAsync());
+ if (wParam == 3) Dispatcher.BeginInvoke(RequestSnapshot);
+ return 0;
}
private void CreateTray()
@@ -343,6 +553,7 @@ private void CreateTray()
var menu = new System.Windows.Forms.ContextMenuStrip();
menu.Items.Add("Show AnimeEnhancer", null, (_, _) => { Show(); Activate(); });
menu.Items.Add("Start / Stop", null, (_, _) => Dispatcher.Invoke(() => Start_Click(this, new RoutedEventArgs())));
+ menu.Items.Add("Save comparison PNG", null, (_, _) => Dispatcher.Invoke(RequestSnapshot));
menu.Items.Add("Exit", null, async (_, _) => { await StopAsync(); _allowClose = true; Close(); System.Windows.Application.Current.Shutdown(); });
_tray.ContextMenuStrip = menu; _tray.DoubleClick += (_, _) => { Show(); Activate(); };
}
@@ -350,6 +561,6 @@ protected override async void OnClosing(CancelEventArgs e)
{
if (!_allowClose && _settings.MinimizeToTray) { e.Cancel = true; Hide(); return; }
await StopAsync(); _watcher?.Dispose(); _tray?.Dispose(); _gpu?.Dispose(); _winRtDevice?.Dispose(); _toast?.Close(); _exitControl?.Close(); _pointerBridge?.Dispose(); _overlay?.Dispose();
- var hwnd = new WindowInteropHelper(this).Handle; NativeMethods.UnregisterHotKey(hwnd, 1); NativeMethods.UnregisterHotKey(hwnd, 2); base.OnClosing(e);
+ var hwnd = new WindowInteropHelper(this).Handle; NativeMethods.UnregisterHotKey(hwnd, 1); NativeMethods.UnregisterHotKey(hwnd, 2); NativeMethods.UnregisterHotKey(hwnd, 3); base.OnClosing(e);
}
}
diff --git a/src/AnimeEnhancer.App/Native/AnimeFlow/animeflow-rife.dll b/src/AnimeEnhancer.App/Native/AnimeFlow/animeflow-rife.dll
new file mode 100644
index 0000000..99187a8
Binary files /dev/null and b/src/AnimeEnhancer.App/Native/AnimeFlow/animeflow-rife.dll differ
diff --git a/src/AnimeEnhancer.App/Native/AnimeFlow/rife-v4.6/flownet.bin b/src/AnimeEnhancer.App/Native/AnimeFlow/rife-v4.6/flownet.bin
new file mode 100644
index 0000000..70de38e
Binary files /dev/null and b/src/AnimeEnhancer.App/Native/AnimeFlow/rife-v4.6/flownet.bin differ
diff --git a/src/AnimeEnhancer.App/Native/AnimeFlow/rife-v4.6/flownet.param b/src/AnimeEnhancer.App/Native/AnimeFlow/rife-v4.6/flownet.param
new file mode 100644
index 0000000..3cb69d4
--- /dev/null
+++ b/src/AnimeEnhancer.App/Native/AnimeFlow/rife-v4.6/flownet.param
@@ -0,0 +1,217 @@
+7767517
+215 276
+Input in0 0 1 in0
+Split splitncnn_0 1 5 in0 1 2 3 4 5
+Input in1 0 1 in1
+Split splitncnn_1 1 5 in1 7 8 9 10 11
+Input in2 0 1 in2
+Split splitncnn_2 1 4 in2 13 14 15 16
+Concat cat_0 3 1 1 7 13 17
+Interp upsample_9 1 1 17 18 0=2 1=1.250000e-01 2=1.250000e-01
+Convolution convrelu_0 1 1 18 20 0=96 1=3 3=2 4=1 5=1 6=6048 9=2 -23310=1,2.000000e-01
+Convolution convrelu_1 1 1 20 21 0=192 1=3 3=2 4=1 5=1 6=165888 9=2 -23310=1,2.000000e-01
+Split splitncnn_3 1 2 21 22 23
+Convolution conv_22 1 1 23 25 0=192 1=3 4=1 5=1 6=331776
+BinaryOp add_1 2 1 25 22 26
+ReLU leakyrelu_66 1 1 26 28 0=2.000000e-01
+Split splitncnn_4 1 2 28 29 30
+Convolution conv_23 1 1 30 32 0=192 1=3 4=1 5=1 6=331776
+BinaryOp add_3 2 1 32 29 33
+ReLU leakyrelu_67 1 1 33 35 0=2.000000e-01
+Split splitncnn_5 1 2 35 36 37
+Convolution conv_24 1 1 37 39 0=192 1=3 4=1 5=1 6=331776
+BinaryOp add_5 2 1 39 36 40
+ReLU leakyrelu_68 1 1 40 42 0=2.000000e-01
+Split splitncnn_6 1 2 42 43 44
+Convolution conv_25 1 1 44 46 0=192 1=3 4=1 5=1 6=331776
+BinaryOp add_7 2 1 46 43 47
+ReLU leakyrelu_69 1 1 47 49 0=2.000000e-01
+Split splitncnn_7 1 2 49 50 51
+Convolution conv_26 1 1 51 53 0=192 1=3 4=1 5=1 6=331776
+BinaryOp add_9 2 1 53 50 54
+ReLU leakyrelu_70 1 1 54 56 0=2.000000e-01
+Split splitncnn_8 1 2 56 57 58
+Convolution conv_27 1 1 58 60 0=192 1=3 4=1 5=1 6=331776
+BinaryOp add_11 2 1 60 57 61
+ReLU leakyrelu_71 1 1 61 63 0=2.000000e-01
+Split splitncnn_9 1 2 63 64 65
+Convolution conv_28 1 1 65 67 0=192 1=3 4=1 5=1 6=331776
+BinaryOp add_13 2 1 67 64 68
+ReLU leakyrelu_72 1 1 68 70 0=2.000000e-01
+Split splitncnn_10 1 2 70 71 72
+Convolution conv_29 1 1 72 74 0=192 1=3 4=1 5=1 6=331776
+BinaryOp add_15 2 1 74 71 75
+ReLU leakyrelu_73 1 1 75 76 0=2.000000e-01
+Deconvolution deconv_60 1 1 76 77 0=24 1=4 3=2 4=1 5=1 6=73728
+PixelShuffle pixelshuffle_104 1 1 77 flow0 0=2
+Interp upsample_10 1 1 flow0 79 0=2 1=8.000000e+00 2=8.000000e+00
+Split splitncnn_11 1 2 79 80 81
+Crop slice_108 1 1 81 82 -23309=1,0 -23310=1,4 -23311=1,0
+BinaryOp mul_16 1 1 82 83 0=2 1=1 2=8.000000e+00
+Split splitncnn_12 1 4 83 84 85 86 87
+Interp upsample_11 1 1 85 88 0=2 1=2.500000e-01 2=2.500000e-01
+BinaryOp div_17 1 1 88 89 0=3 1=1 2=4.000000e+00
+Crop slice_109 1 1 87 90 -23309=1,2 -23310=1,4 -23311=1,0
+rife.Warp warp_1 2 1 11 90 91
+Crop slice_110 1 1 86 92 -23309=1,0 -23310=1,2 -23311=1,0
+rife.Warp warp_0 2 1 5 92 93
+Crop slice_111 1 1 80 94 -23309=1,4 -23310=1,5 -23311=1,0
+Split splitncnn_13 1 2 94 95 96
+Concat cat_3 4 1 93 91 14 95 97
+Interp upsample_12 1 1 97 98 0=2 1=2.500000e-01 2=2.500000e-01
+Concat cat_4 2 1 98 89 99
+Convolution convrelu_2 1 1 99 101 0=64 1=3 3=2 4=1 5=1 6=6912 9=2 -23310=1,2.000000e-01
+Convolution convrelu_3 1 1 101 102 0=128 1=3 3=2 4=1 5=1 6=73728 9=2 -23310=1,2.000000e-01
+Split splitncnn_14 1 2 102 103 104
+Convolution conv_32 1 1 104 106 0=128 1=3 4=1 5=1 6=147456
+BinaryOp add_19 2 1 106 103 107
+ReLU leakyrelu_76 1 1 107 109 0=2.000000e-01
+Split splitncnn_15 1 2 109 110 111
+Convolution conv_33 1 1 111 113 0=128 1=3 4=1 5=1 6=147456
+BinaryOp add_21 2 1 113 110 114
+ReLU leakyrelu_77 1 1 114 116 0=2.000000e-01
+Split splitncnn_16 1 2 116 117 118
+Convolution conv_34 1 1 118 120 0=128 1=3 4=1 5=1 6=147456
+BinaryOp add_23 2 1 120 117 121
+ReLU leakyrelu_78 1 1 121 123 0=2.000000e-01
+Split splitncnn_17 1 2 123 124 125
+Convolution conv_35 1 1 125 127 0=128 1=3 4=1 5=1 6=147456
+BinaryOp add_25 2 1 127 124 128
+ReLU leakyrelu_79 1 1 128 130 0=2.000000e-01
+Split splitncnn_18 1 2 130 131 132
+Convolution conv_36 1 1 132 134 0=128 1=3 4=1 5=1 6=147456
+BinaryOp add_27 2 1 134 131 135
+ReLU leakyrelu_80 1 1 135 137 0=2.000000e-01
+Split splitncnn_19 1 2 137 138 139
+Convolution conv_37 1 1 139 141 0=128 1=3 4=1 5=1 6=147456
+BinaryOp add_29 2 1 141 138 142
+ReLU leakyrelu_81 1 1 142 144 0=2.000000e-01
+Split splitncnn_20 1 2 144 145 146
+Convolution conv_38 1 1 146 148 0=128 1=3 4=1 5=1 6=147456
+BinaryOp add_31 2 1 148 145 149
+ReLU leakyrelu_82 1 1 149 151 0=2.000000e-01
+Split splitncnn_21 1 2 151 152 153
+Convolution conv_39 1 1 153 155 0=128 1=3 4=1 5=1 6=147456
+BinaryOp add_33 2 1 155 152 156
+ReLU leakyrelu_83 1 1 156 157 0=2.000000e-01
+Deconvolution deconv_61 1 1 157 158 0=24 1=4 3=2 4=1 5=1 6=49152
+PixelShuffle pixelshuffle_105 1 1 158 flow1 0=2
+Interp upsample_13 1 1 flow1 160 0=2 1=4.000000e+00 2=4.000000e+00
+Split splitncnn_22 1 2 160 161 162
+Crop slice_112 1 1 162 163 -23309=1,0 -23310=1,4 -23311=1,0
+Eltwise weighted_sum_0 2 1 84 163 164 0=1 -23301=2,1.000000e+00,4.000000e+00
+Split splitncnn_23 1 4 164 165 166 167 168
+Crop slice_113 1 1 161 169 -23309=1,4 -23310=1,5 -23311=1,0
+BinaryOp add_36 2 1 96 169 170
+Split splitncnn_24 1 2 170 171 172
+Interp upsample_14 1 1 166 173 0=2 1=5.000000e-01 2=5.000000e-01
+BinaryOp div_37 1 1 173 174 0=3 1=1 2=2.000000e+00
+Crop slice_114 1 1 168 175 -23309=1,2 -23310=1,4 -23311=1,0
+rife.Warp warp_3 2 1 10 175 176
+Crop slice_115 1 1 167 177 -23309=1,0 -23310=1,2 -23311=1,0
+rife.Warp warp_2 2 1 4 177 178
+Concat cat_7 4 1 178 176 15 171 179
+Interp upsample_15 1 1 179 180 0=2 1=5.000000e-01 2=5.000000e-01
+Concat cat_8 2 1 180 174 181
+Convolution convrelu_4 1 1 181 183 0=48 1=3 3=2 4=1 5=1 6=5184 9=2 -23310=1,2.000000e-01
+Convolution convrelu_5 1 1 183 184 0=96 1=3 3=2 4=1 5=1 6=41472 9=2 -23310=1,2.000000e-01
+Split splitncnn_25 1 2 184 185 186
+Convolution conv_42 1 1 186 188 0=96 1=3 4=1 5=1 6=82944
+BinaryOp add_39 2 1 188 185 189
+ReLU leakyrelu_86 1 1 189 191 0=2.000000e-01
+Split splitncnn_26 1 2 191 192 193
+Convolution conv_43 1 1 193 195 0=96 1=3 4=1 5=1 6=82944
+BinaryOp add_41 2 1 195 192 196
+ReLU leakyrelu_87 1 1 196 198 0=2.000000e-01
+Split splitncnn_27 1 2 198 199 200
+Convolution conv_44 1 1 200 202 0=96 1=3 4=1 5=1 6=82944
+BinaryOp add_43 2 1 202 199 203
+ReLU leakyrelu_88 1 1 203 205 0=2.000000e-01
+Split splitncnn_28 1 2 205 206 207
+Convolution conv_45 1 1 207 209 0=96 1=3 4=1 5=1 6=82944
+BinaryOp add_45 2 1 209 206 210
+ReLU leakyrelu_89 1 1 210 212 0=2.000000e-01
+Split splitncnn_29 1 2 212 213 214
+Convolution conv_46 1 1 214 216 0=96 1=3 4=1 5=1 6=82944
+BinaryOp add_47 2 1 216 213 217
+ReLU leakyrelu_90 1 1 217 219 0=2.000000e-01
+Split splitncnn_30 1 2 219 220 221
+Convolution conv_47 1 1 221 223 0=96 1=3 4=1 5=1 6=82944
+BinaryOp add_49 2 1 223 220 224
+ReLU leakyrelu_91 1 1 224 226 0=2.000000e-01
+Split splitncnn_31 1 2 226 227 228
+Convolution conv_48 1 1 228 230 0=96 1=3 4=1 5=1 6=82944
+BinaryOp add_51 2 1 230 227 231
+ReLU leakyrelu_92 1 1 231 233 0=2.000000e-01
+Split splitncnn_32 1 2 233 234 235
+Convolution conv_49 1 1 235 237 0=96 1=3 4=1 5=1 6=82944
+BinaryOp add_53 2 1 237 234 238
+ReLU leakyrelu_93 1 1 238 239 0=2.000000e-01
+Deconvolution deconv_62 1 1 239 240 0=24 1=4 3=2 4=1 5=1 6=36864
+PixelShuffle pixelshuffle_106 1 1 240 flow2 0=2
+Interp upsample_16 1 1 flow2 242 0=2 1=2.000000e+00 2=2.000000e+00
+Split splitncnn_33 1 2 242 243 244
+Crop slice_116 1 1 244 245 -23309=1,0 -23310=1,4 -23311=1,0
+Eltwise weighted_sum_1 2 1 165 245 246 0=1 -23301=2,1.000000e+00,2.000000e+00
+Split splitncnn_34 1 4 246 247 261 249 250
+Crop slice_117 1 1 243 251 -23309=1,4 -23310=1,5 -23311=1,0
+BinaryOp add_56 2 1 172 251 252
+Split splitncnn_35 1 2 252 253 254
+Crop slice_118 1 1 250 255 -23309=1,2 -23310=1,4 -23311=1,0
+rife.Warp warp_5 2 1 9 255 256
+Crop slice_119 1 1 249 257 -23309=1,0 -23310=1,2 -23311=1,0
+rife.Warp warp_4 2 1 3 257 258
+Concat cat_11 4 1 258 256 16 253 260
+Concat cat_12 2 1 260 261 262
+Convolution convrelu_6 1 1 262 264 0=32 1=3 3=2 4=1 5=1 6=3456 9=2 -23310=1,2.000000e-01
+Convolution convrelu_7 1 1 264 265 0=64 1=3 3=2 4=1 5=1 6=18432 9=2 -23310=1,2.000000e-01
+Split splitncnn_36 1 2 265 266 267
+Convolution conv_52 1 1 267 269 0=64 1=3 4=1 5=1 6=36864
+BinaryOp add_58 2 1 269 266 270
+ReLU leakyrelu_96 1 1 270 272 0=2.000000e-01
+Split splitncnn_37 1 2 272 273 274
+Convolution conv_53 1 1 274 276 0=64 1=3 4=1 5=1 6=36864
+BinaryOp add_60 2 1 276 273 277
+ReLU leakyrelu_97 1 1 277 279 0=2.000000e-01
+Split splitncnn_38 1 2 279 280 281
+Convolution conv_54 1 1 281 283 0=64 1=3 4=1 5=1 6=36864
+BinaryOp add_62 2 1 283 280 284
+ReLU leakyrelu_98 1 1 284 286 0=2.000000e-01
+Split splitncnn_39 1 2 286 287 288
+Convolution conv_55 1 1 288 290 0=64 1=3 4=1 5=1 6=36864
+BinaryOp add_64 2 1 290 287 291
+ReLU leakyrelu_99 1 1 291 293 0=2.000000e-01
+Split splitncnn_40 1 2 293 294 295
+Convolution conv_56 1 1 295 297 0=64 1=3 4=1 5=1 6=36864
+BinaryOp add_66 2 1 297 294 298
+ReLU leakyrelu_100 1 1 298 300 0=2.000000e-01
+Split splitncnn_41 1 2 300 301 302
+Convolution conv_57 1 1 302 304 0=64 1=3 4=1 5=1 6=36864
+BinaryOp add_68 2 1 304 301 305
+ReLU leakyrelu_101 1 1 305 307 0=2.000000e-01
+Split splitncnn_42 1 2 307 308 309
+Convolution conv_58 1 1 309 311 0=64 1=3 4=1 5=1 6=36864
+BinaryOp add_70 2 1 311 308 312
+ReLU leakyrelu_102 1 1 312 314 0=2.000000e-01
+Split splitncnn_43 1 2 314 315 316
+Convolution conv_59 1 1 316 318 0=64 1=3 4=1 5=1 6=36864
+BinaryOp add_72 2 1 318 315 319
+ReLU leakyrelu_103 1 1 319 320 0=2.000000e-01
+Deconvolution deconv_63 1 1 320 321 0=24 1=4 3=2 4=1 5=1 6=24576
+PixelShuffle pixelshuffle_107 1 1 321 flow3 0=2
+Split splitncnn_44 1 2 flow3 324 325
+Crop slice_120 1 1 325 326 -23309=1,0 -23310=1,4 -23311=1,0
+BinaryOp add_73 2 1 247 326 327
+Split splitncnn_45 1 2 327 328 329
+Crop slice_121 1 1 324 330 -23309=1,4 -23310=1,5 -23311=1,0
+BinaryOp add_74 2 1 254 330 331
+Sigmoid sigmoid_8 1 1 331 332
+Split splitncnn_46 1 2 332 333 334
+BinaryOp sub_75 1 1 333 335 0=7 1=1 2=1.000000e+00
+Crop slice_123 1 1 328 336 -23309=1,2 -23310=1,4 -23311=1,0
+rife.Warp warp_6 2 1 8 336 337
+BinaryOp mul_76 2 1 337 335 338 0=2
+Crop slice_122 1 1 329 339 -23309=1,0 -23310=1,2 -23311=1,0
+rife.Warp warp_7 2 1 2 339 340
+BinaryOp mul_77 2 1 340 334 341 0=2
+BinaryOp add_78 2 1 341 338 out0
diff --git a/src/AnimeEnhancer.App/Native/NVIDIA/NvOFFRUC.dll b/src/AnimeEnhancer.App/Native/NVIDIA/NvOFFRUC.dll
new file mode 100644
index 0000000..edd8de6
Binary files /dev/null and b/src/AnimeEnhancer.App/Native/NVIDIA/NvOFFRUC.dll differ
diff --git a/src/AnimeEnhancer.App/Native/NVIDIA/cudart64_110.dll b/src/AnimeEnhancer.App/Native/NVIDIA/cudart64_110.dll
new file mode 100644
index 0000000..6e7c171
Binary files /dev/null and b/src/AnimeEnhancer.App/Native/NVIDIA/cudart64_110.dll differ
diff --git a/src/AnimeEnhancer.Core/Models.cs b/src/AnimeEnhancer.Core/Models.cs
index 76a3b8b..7d51f34 100644
--- a/src/AnimeEnhancer.Core/Models.cs
+++ b/src/AnimeEnhancer.Core/Models.cs
@@ -50,6 +50,7 @@ public sealed class AppSettings
public bool Debanding { get; set; }
public bool CompressionCleanup { get; set; }
public bool LosslessScalingCompatibility { get; set; }
+ public bool RtxMotion60 { get; set; }
public Dictionary Hotkeys { get; set; } = DefaultHotkeys();
public static Dictionary DefaultHotkeys() => new()
{
diff --git a/src/AnimeEnhancer.Graphics/GpuPipeline.cs b/src/AnimeEnhancer.Graphics/GpuPipeline.cs
index cf82f43..c240930 100644
--- a/src/AnimeEnhancer.Graphics/GpuPipeline.cs
+++ b/src/AnimeEnhancer.Graphics/GpuPipeline.cs
@@ -12,6 +12,8 @@
namespace AnimeEnhancer.Graphics;
+public sealed record CapturedFrame(int Width, int Height, byte[] BgraPixels);
+
public sealed class GpuPipeline : IDisposable
{
private readonly IDXGIFactory2 _factory;
@@ -31,14 +33,24 @@ public sealed class GpuPipeline : IDisposable
private PixelSize _resourceOutputSize;
private PixelSize _outputSize;
private ID3D11Texture2D? _lastProcessed;
+ private NvidiaFruc? _fruc;
+ private PixelSize _frucSize;
+ private bool _rtxMotionRequested;
private bool _disposed;
private readonly Stopwatch _cpuTimer = new();
+ private readonly object _deviceGate = new();
+ private ID3D11Texture2D? _motionCapture, _motionStaging, _motionUpload;
+ private ID3D11RenderTargetView? _motionCaptureRtv;
+ private PixelSize _motionSize;
public ID3D11Device Device => _device;
public string AdapterName { get; }
public double LastGpuMilliseconds { get; private set; }
public IReadOnlyList ActivePasses { get; private set; } = [];
public PixelSize LastReconstructionSize { get; private set; }
+ public bool RtxMotionActive => _fruc is not null;
+ public string? LastRtxMotionError { get; private set; }
+ public long RtxMotionGeneratedFrames { get; private set; }
public GpuPipeline(string shaderRoot, string? preferredAdapter = null)
{
@@ -65,6 +77,8 @@ public GpuPipeline(string shaderRoot, string? preferredAdapter = null)
for (var i = 0; i < 4; i++) LoadPixel(shaderRoot, $"restore-s-{i}", $"Anime4K/Generated/RestoreS{i}.hlsl");
for (var i = 0; i < 5; i++) LoadPixel(shaderRoot, $"upscale-s-{i}", $"Anime4K/Generated/UpscaleS{i}.hlsl");
LoadPixel(shaderRoot, "presentation", "Presentation/Present.hlsl");
+ LoadPixel(shaderRoot, "copy", "Presentation/Copy.hlsl");
+ LoadPixel(shaderRoot, "motion-stabilize", "Presentation/MotionStabilize.hlsl");
_sampler = _device.CreateSamplerState(new SamplerDescription(Filter.MinMagMipLinear, TextureAddressMode.Clamp, TextureAddressMode.Clamp, TextureAddressMode.Clamp));
_constants = _device.CreateBuffer(new BufferDescription(48, BindFlags.ConstantBuffer, ResourceUsage.Default));
}
@@ -83,14 +97,19 @@ public static IReadOnlyList EnumerateAdapters()
private static IDXGIAdapter1 SelectAdapter(IDXGIFactory2 factory, string? preferredAdapter)
{
IDXGIAdapter1? fallback = null;
+ ulong fallbackMemory = 0;
for (uint i = 0; factory.EnumAdapters1(i, out var adapter).Success; i++)
{
var hardware = (adapter.Description1.Flags & AdapterFlags.Software) == 0;
var preferred = !string.IsNullOrWhiteSpace(preferredAdapter) && !preferredAdapter.Equals("Auto", StringComparison.OrdinalIgnoreCase);
if (!hardware) { adapter.Dispose(); continue; }
if (preferred && adapter.Description1.Description.Trim().Equals(preferredAdapter!.Trim(), StringComparison.OrdinalIgnoreCase)) { fallback?.Dispose(); return adapter; }
- if (!preferred) return adapter;
- if (fallback is null) fallback = adapter; else adapter.Dispose();
+ var memory = adapter.Description1.DedicatedVideoMemory;
+ if (fallback is null || (!preferred && memory > fallbackMemory))
+ {
+ fallback?.Dispose(); fallback = adapter; fallbackMemory = memory;
+ }
+ else adapter.Dispose();
}
return fallback ?? throw new NotSupportedException("No Direct3D 11 adapter is available.");
}
@@ -108,18 +127,62 @@ private static ReadOnlyMemory LoadBytecode(string source, string entry, st
public void Attach(nint hwnd, PixelSize output)
{
+ // A new capture session can arrive on a different free-threaded WGC
+ // worker. Never carry CUDA/FRUC state across capture sessions.
+ DisableRtxMotion();
_swapChain?.Dispose();
_outputSize = output;
var desc = new SwapChainDescription1
{
Width = (uint)output.Width, Height = (uint)output.Height, Format = Format.B8G8R8A8_UNorm,
- BufferCount = 2, BufferUsage = Usage.RenderTargetOutput, SampleDescription = SampleDescription.Default,
+ BufferCount = 3, BufferUsage = Usage.RenderTargetOutput, SampleDescription = SampleDescription.Default,
Scaling = Scaling.Stretch, SwapEffect = SwapEffect.FlipDiscard, AlphaMode = AlphaMode.Ignore,
Flags = SwapChainFlags.FrameLatencyWaitableObject
};
_swapChain = _factory.CreateSwapChainForHwnd(_device, hwnd, desc);
_factory.MakeWindowAssociation(hwnd, WindowAssociationFlags.IgnoreAltEnter);
- using var sc2 = _swapChain.QueryInterface(); sc2.MaximumFrameLatency = 1;
+ using var sc2 = _swapChain.QueryInterface(); sc2.MaximumFrameLatency = 2;
+ }
+
+ public bool ConfigureRtxMotion(bool enabled, out string? error)
+ {
+ error = null;
+ if (!enabled) { _rtxMotionRequested = false; DisableRtxMotion(); return true; }
+ if (_rtxMotionRequested) return true;
+ if (_swapChain is null || !_outputSize.IsValid) { error = "The output window is not ready."; return false; }
+ if (!AdapterName.Contains("NVIDIA", StringComparison.OrdinalIgnoreCase) &&
+ !AdapterName.Contains("GeForce", StringComparison.OrdinalIgnoreCase))
+ {
+ error = "RTX Motion requires an NVIDIA RTX GPU.";
+ return false;
+ }
+ if (!NvidiaFruc.IsRuntimePresent())
+ {
+ error = "NVIDIA Optical Flow runtime files are not available.";
+ return false;
+ }
+
+ // NvOFFRUC creates CUDA state which is used by Process(). The capture
+ // callback is free-threaded, so create that state lazily on the actual
+ // processing thread instead of the WPF activation thread.
+ _rtxMotionRequested = true;
+ LastRtxMotionError = null;
+ RtxMotionGeneratedFrames = 0;
+ return true;
+ }
+
+ private void DisableRtxMotion() { _fruc?.Dispose(); _fruc = null; _frucSize = default; }
+
+ private void EnsureRtxMotionOnProcessingThread()
+ {
+ if (_fruc is not null) return;
+ if (!_rtxMotionRequested) throw new InvalidOperationException("RTX Motion was not requested.");
+
+ // Generate motion frames at the exact reconstructed output size. A
+ // lower-resolution FRUC surface alternates soft generated frames with
+ // sharp native frames, which is perceived as sharpening flicker.
+ _frucSize = _outputSize;
+ _fruc = new NvidiaFruc(_device, _context, (uint)_frucSize.Width, (uint)_frucSize.Height);
}
public void PresentOutputTest()
@@ -149,10 +212,116 @@ public uint ReadEnhancedPixelForValidation(int x, int y)
}
}
- public void Process(ID3D11Texture2D source, PixelSize sourceSize, EnhancementPreset preset, ComparisonMode comparison, double split = .5, bool vsync = true, float saturation = 1.15f, float contrast = 1.04f, bool debanding = false, bool compressionCleanup = false)
+ public CapturedFrame CapturePresentedFrame()
+ {
+ lock (_deviceGate) return CapturePresentedFrameCore();
+ }
+
+ private CapturedFrame CapturePresentedFrameCore()
+ {
+ if (_swapChain is null) throw new InvalidOperationException("The output swap chain is not attached.");
+ using var source = _swapChain.GetBuffer(0);
+ var description = source.Description;
+ using var staging = _device.CreateTexture2D(new Texture2DDescription
+ {
+ Width = description.Width,
+ Height = description.Height,
+ MipLevels = 1,
+ ArraySize = 1,
+ Format = Format.B8G8R8A8_UNorm,
+ SampleDescription = SampleDescription.Default,
+ Usage = ResourceUsage.Staging,
+ CPUAccessFlags = CpuAccessFlags.Read
+ });
+ _context.CopyResource(staging, source);
+ _context.Map(staging, 0, MapMode.Read, Vortice.Direct3D11.MapFlags.None, out var mapped).CheckError();
+ try
+ {
+ var width = checked((int)description.Width);
+ var height = checked((int)description.Height);
+ var rowBytes = checked(width * 4);
+ var pixels = new byte[checked(rowBytes * height)];
+ for (var y = 0; y < height; y++)
+ Marshal.Copy(new IntPtr(mapped.DataPointer + y * mapped.RowPitch), pixels, y * rowBytes, rowBytes);
+ return new CapturedFrame(width, height, pixels);
+ }
+ finally { _context.Unmap(staging, 0); }
+ }
+
+ public byte[] CaptureMotionSource(ID3D11Texture2D source, PixelSize sourceSize, PixelSize motionSize)
+ {
+ lock (_deviceGate)
+ {
+ EnsureMotionResources(motionSize);
+ EnsureInputView(source);
+ Draw("copy", _inputSrv!, null, _motionCaptureRtv!, sourceSize, motionSize, 1, .5, ComparisonMode.Enhanced);
+ _context.CopyResource(_motionStaging!, _motionCapture!);
+ _context.Map(_motionStaging!, 0, MapMode.Read, Vortice.Direct3D11.MapFlags.None, out var mapped).CheckError();
+ try
+ {
+ var rowBytes = motionSize.Width * 4;
+ var pixels = new byte[rowBytes * motionSize.Height];
+ for (var y = 0; y < motionSize.Height; y++)
+ Marshal.Copy(new IntPtr(mapped.DataPointer + y * mapped.RowPitch), pixels, y * rowBytes, rowBytes);
+ return pixels;
+ }
+ finally { _context.Unmap(_motionStaging!, 0); }
+ }
+ }
+
+ public void PresentMotionFrame(byte[] bgra, PixelSize motionSize, EnhancementPreset preset, ComparisonMode comparison,
+ double split, float saturation, float contrast, bool debanding, bool compressionCleanup)
+ {
+ lock (_deviceGate)
+ {
+ EnsureMotionResources(motionSize);
+ var pin = GCHandle.Alloc(bgra, GCHandleType.Pinned);
+ try { _context.UpdateSubresource(_motionUpload!, 0, null, pin.AddrOfPinnedObject(), (uint)(motionSize.Width * 4), (uint)bgra.Length); }
+ finally { pin.Free(); }
+ ProcessCore(_motionUpload!, motionSize, preset, comparison, split, false, saturation, contrast, debanding, compressionCleanup, false, true);
+ }
+ }
+
+ private void EnsureMotionResources(PixelSize size)
+ {
+ if (_motionSize == size && _motionCapture is not null) return;
+ ReleaseMotionResources(); _motionSize = size;
+ var gpu = new Texture2DDescription
+ {
+ Width = (uint)size.Width, Height = (uint)size.Height, MipLevels = 1, ArraySize = 1,
+ Format = Format.B8G8R8A8_UNorm, SampleDescription = SampleDescription.Default,
+ Usage = ResourceUsage.Default, BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource
+ };
+ _motionCapture = _device.CreateTexture2D(gpu);
+ _motionCaptureRtv = _device.CreateRenderTargetView(_motionCapture);
+ gpu.BindFlags = BindFlags.ShaderResource;
+ _motionUpload = _device.CreateTexture2D(gpu);
+ _motionStaging = _device.CreateTexture2D(new Texture2DDescription
+ {
+ Width = (uint)size.Width, Height = (uint)size.Height, MipLevels = 1, ArraySize = 1,
+ Format = Format.B8G8R8A8_UNorm, SampleDescription = SampleDescription.Default,
+ Usage = ResourceUsage.Staging, CPUAccessFlags = CpuAccessFlags.Read
+ });
+ }
+
+ private void ReleaseMotionResources()
+ {
+ _motionCaptureRtv?.Dispose(); _motionCapture?.Dispose(); _motionStaging?.Dispose(); _motionUpload?.Dispose();
+ _motionCaptureRtv = null; _motionCapture = _motionStaging = _motionUpload = null; _motionSize = default;
+ }
+
+ public void Process(ID3D11Texture2D source, PixelSize sourceSize, EnhancementPreset preset, ComparisonMode comparison, double split = .5, bool vsync = true, float saturation = 1.15f, float contrast = 1.04f, bool debanding = false, bool compressionCleanup = false, bool rtxMotion = false)
+ {
+ lock (_deviceGate) ProcessCore(source, sourceSize, preset, comparison, split, vsync, saturation, contrast, debanding, compressionCleanup, rtxMotion, false);
+ }
+
+ private void ProcessCore(ID3D11Texture2D source, PixelSize sourceSize, EnhancementPreset preset, ComparisonMode comparison, double split, bool vsync, float saturation, float contrast, bool debanding, bool compressionCleanup, bool rtxMotion, bool motionSafe)
{
if (_swapChain is null) return;
- var logicalSize = ResolveLogicalSize(sourceSize, _outputSize);
+ var logicalSize = motionSafe
+ ? new PixelSize(Math.Min(1920, _outputSize.Width), Math.Min(_outputSize.Height,
+ Math.Max(1, (int)Math.Round(Math.Min(1920, _outputSize.Width) * (double)sourceSize.Height / sourceSize.Width))))
+ : ResolveLogicalSize(sourceSize, _outputSize);
EnsureResources(logicalSize, _outputSize);
EnsureInputView(source);
_cpuTimer.Restart();
@@ -213,23 +382,72 @@ public void Process(ID3D11Texture2D source, PixelSize sourceSize, EnhancementPre
Draw("upscale-s-2", _baseSrv!, null, _rtvA!, logicalSize, logicalSize, strength, split, comparison);
Draw("upscale-s-3", _srvA!, null, _baseRtv!, logicalSize, logicalSize, strength, split, comparison);
Draw("upscale-s-4", _baseSrv!, _srvB!, _enhancedRtv!, logicalSize, _outputSize, strength, split, comparison);
- Draw("aggressive-anime", _enhancedSrv!, null, _postRtv!, _outputSize, _outputSize, strength, split, comparison);
- if (preset is EnhancementPreset.UltraDetail or EnhancementPreset.Ultra4X)
+ if (motionSafe)
+ {
+ // RIFE output is reconstructed at the same logical resolution as
+ // the normal path, then receives a restrained temporal-safe finish.
+ Draw("aggressive-anime", _enhancedSrv!, null, _postRtv!, _outputSize, _outputSize, .82f, split, comparison);
+ }
+ else
{
- Draw("ultra-detail", _postSrv!, null, _enhancedRtv!, _outputSize, _outputSize, 1.0f, split, comparison);
- Draw("ultra-detail", _enhancedSrv!, null, _postRtv!, _outputSize, _outputSize, .62f, split, comparison);
+ Draw("aggressive-anime", _enhancedSrv!, null, _postRtv!, _outputSize, _outputSize, strength, split, comparison);
+ if (preset is EnhancementPreset.UltraDetail or EnhancementPreset.Ultra4X)
+ {
+ Draw("ultra-detail", _postSrv!, null, _enhancedRtv!, _outputSize, _outputSize, 1.0f, split, comparison);
+ Draw("ultra-detail", _enhancedSrv!, null, _postRtv!, _outputSize, _outputSize, .62f, split, comparison);
+ }
}
LastReconstructionSize = _outputSize;
- current = _postSrv!; _lastProcessed = _post;
+ current = _postSrv!;
+ _lastProcessed = _post;
}
Present:
- using var backBuffer = _swapChain.GetBuffer(0);
- using var backRtv = _device.CreateRenderTargetView(backBuffer);
- Draw("presentation", current, _inputSrv, backRtv, sourceSize, _outputSize, 1, split, comparison, saturation, contrast);
- _swapChain.Present(vsync ? 1u : 0u, PresentFlags.None).CheckError();
+ if (rtxMotion && _rtxMotionRequested)
+ {
+ try
+ {
+ EnsureRtxMotionOnProcessingThread();
+ var fruc = _fruc ?? throw new InvalidOperationException("RTX Motion initialization did not create an engine.");
+ Draw("presentation", current, _inputSrv, fruc.NextInputTarget, sourceSize, _frucSize, 1, split, comparison, saturation, contrast);
+ if (fruc.Interpolate())
+ {
+ RtxMotionGeneratedFrames++;
+ PresentFrame("motion-stabilize", fruc.Output, fruc.CurrentInput, _frucSize, _outputSize, 1, .5, ComparisonMode.Enhanced, 1);
+ PresentFrame("presentation", current, _inputSrv, sourceSize, _outputSize, 1, split, comparison, 1, saturation, contrast);
+ goto Presented;
+ }
+
+ // FRUC deliberately repeats unsafe or duplicate frames. Do not
+ // enqueue the same picture again; that creates rhythmic stalls.
+ if (fruc.HasHistory) goto Presented;
+ }
+ catch (Exception ex)
+ {
+ // Motion is optional. Never let an Optical Flow driver/runtime failure
+ // take down the proven enhancement path.
+ LastRtxMotionError = ex.Message;
+ DisableRtxMotion();
+ _rtxMotionRequested = false;
+ }
+ }
+ PresentFrame("presentation", current, _inputSrv, sourceSize, _outputSize, 1, split, comparison, vsync ? 1u : 0u, saturation, contrast);
+ Presented:
_cpuTimer.Stop(); LastGpuMilliseconds = _cpuTimer.Elapsed.TotalMilliseconds;
}
+ private void PresentFrame(string shader, ID3D11ShaderResourceView primary, ID3D11ShaderResourceView? secondary,
+ PixelSize source, PixelSize output, float strength, double split, ComparisonMode comparison, uint syncInterval,
+ float saturation = 1, float contrast = 1)
+ {
+ // Flip-model swap chains rotate buffers after every Present. Reusing an
+ // RTV from before the flip writes into an old buffer and causes cadence
+ // stalls, so buffer 0 must be reacquired for every generated frame.
+ using var backBuffer = _swapChain!.GetBuffer(0);
+ using var backRtv = _device.CreateRenderTargetView(backBuffer);
+ Draw(shader, primary, secondary, backRtv, source, output, strength, split, comparison, saturation, contrast);
+ _swapChain.Present(syncInterval, PresentFlags.None).CheckError();
+ }
+
private void Draw(string id, ID3D11ShaderResourceView primary, ID3D11ShaderResourceView? secondary, ID3D11RenderTargetView target, PixelSize source, PixelSize output, float strength, double split, ComparisonMode comparison, float saturation = 1, float contrast = 1)
{
var data = new ShaderConstants { Source = new(source.Width, source.Height), Output = new(output.Width, output.Height), Strength = strength, Split = (float)split, Comparison = (uint)comparison, Saturation = saturation, Contrast = contrast };
@@ -277,7 +495,7 @@ private void EnsureResources(PixelSize size, PixelSize output)
public void Dispose()
{
if (_disposed) return; _disposed = true; _context.ClearState(); _context.Flush();
- _inputSrv?.Dispose(); ReleaseSized(); _swapChain?.Dispose(); _constants.Dispose(); _sampler.Dispose();
+ DisableRtxMotion(); ReleaseMotionResources(); _inputSrv?.Dispose(); ReleaseSized(); _swapChain?.Dispose(); _constants.Dispose(); _sampler.Dispose();
foreach (var shader in _shaders.Values) shader.Dispose(); _vertexShader.Dispose(); _context.Dispose(); _device.Dispose(); _factory.Dispose();
}
diff --git a/src/AnimeEnhancer.Graphics/NvidiaFruc.cs b/src/AnimeEnhancer.Graphics/NvidiaFruc.cs
new file mode 100644
index 0000000..fe8185e
--- /dev/null
+++ b/src/AnimeEnhancer.Graphics/NvidiaFruc.cs
@@ -0,0 +1,307 @@
+using System.Runtime.InteropServices;
+using Vortice.Direct3D11;
+using Vortice.DXGI;
+
+namespace AnimeEnhancer.Graphics;
+
+///
+/// Thin managed bridge to NVIDIA's official Optical Flow FRUC runtime.
+/// All surfaces stay on the D3D11 GPU; no frame is copied through the CPU.
+///
+public sealed unsafe class NvidiaFruc : IDisposable
+{
+ private readonly ID3D11Device5 _device;
+ private readonly ID3D11DeviceContext4 _context;
+ private readonly ID3D11Fence _fence;
+ private readonly ID3D11Texture2D[] _inputs = new ID3D11Texture2D[2];
+ private readonly ID3D11Texture2D _output;
+ private readonly ID3D11Texture2D _upload;
+ private readonly ID3D11RenderTargetView _uploadRtv;
+ private readonly ID3D11ShaderResourceView _uploadSrv;
+ private readonly ID3D11Texture2D _display;
+ private readonly ID3D11ShaderResourceView _displaySrv;
+ private readonly nint _library;
+ private readonly CreateDelegate _create;
+ private readonly RegisterDelegate _register;
+ private readonly UnregisterDelegate _unregister;
+ private readonly ProcessDelegate _process;
+ private readonly DestroyDelegate _destroy;
+ private nint _handle;
+ private ulong _fenceValue;
+ private int _inputIndex = -1;
+ private long _frameNumber;
+ private bool _registered;
+ private bool _disposed;
+
+ public ID3D11RenderTargetView NextInputTarget
+ {
+ get
+ {
+ _inputIndex = (_inputIndex + 1) & 1;
+ return _uploadRtv;
+ }
+ }
+
+ public ID3D11ShaderResourceView Output => _displaySrv;
+ public ID3D11ShaderResourceView CurrentInput => _uploadSrv;
+ public bool HasHistory => _frameNumber > 1;
+
+ public static bool IsRuntimePresent(string? baseDirectory = null)
+ {
+ var root = baseDirectory ?? AppContext.BaseDirectory;
+ return File.Exists(Path.Combine(root, "NvOFFRUC.dll")) &&
+ File.Exists(Path.Combine(root, "cudart64_110.dll")) &&
+ File.Exists(Path.Combine(Environment.SystemDirectory, "nvofapi64.dll"));
+ }
+
+ public NvidiaFruc(ID3D11Device device, ID3D11DeviceContext context, uint width, uint height, string? baseDirectory = null)
+ {
+ if (!IsRuntimePresent(baseDirectory))
+ throw new NotSupportedException("NVIDIA Optical Flow runtime files are not available.");
+
+ _device = device.QueryInterface();
+ _context = context.QueryInterface();
+
+ _device.CreateFence(0, FenceFlags.Shared, out ID3D11Fence? fence).CheckError();
+ _fence = fence ?? throw new NotSupportedException("The NVIDIA adapter could not create a shared D3D11 fence.");
+
+ if (sizeof(CreateParams) != 160 || sizeof(FrameData) != 160 || sizeof(ProcessInParams) != 312 ||
+ sizeof(ProcessOutParams) != 304 || sizeof(RegisterParams) != 96 || sizeof(UnregisterParams) != 88)
+ throw new InvalidOperationException("NVIDIA FRUC ABI validation failed.");
+
+ var sharedDescription = new Texture2DDescription
+ {
+ Width = width,
+ Height = height,
+ MipLevels = 1,
+ ArraySize = 1,
+ Format = Format.R8G8B8A8_UNorm,
+ SampleDescription = SampleDescription.Default,
+ Usage = ResourceUsage.Default,
+ BindFlags = BindFlags.None,
+ MiscFlags = ResourceOptionFlags.Shared | ResourceOptionFlags.SharedNTHandle
+ };
+ for (var i = 0; i < _inputs.Length; i++)
+ _inputs[i] = device.CreateTexture2D(sharedDescription);
+ _output = device.CreateTexture2D(sharedDescription);
+
+ var workingDescription = sharedDescription;
+ workingDescription.MiscFlags = ResourceOptionFlags.None;
+ workingDescription.BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource;
+ _upload = device.CreateTexture2D(workingDescription);
+ _uploadRtv = device.CreateRenderTargetView(_upload);
+ _uploadSrv = device.CreateShaderResourceView(_upload);
+ _display = device.CreateTexture2D(workingDescription);
+ _displaySrv = device.CreateShaderResourceView(_display);
+
+ var dllPath = Path.Combine(baseDirectory ?? AppContext.BaseDirectory, "NvOFFRUC.dll");
+ _library = NativeLibrary.Load(dllPath);
+ _create = Load("NvOFFRUCCreate");
+ _register = Load("NvOFFRUCRegisterResource");
+ _unregister = Load("NvOFFRUCUnregisterResource");
+ _process = Load("NvOFFRUCProcess");
+ _destroy = Load("NvOFFRUCDestroy");
+
+ var createParams = new CreateParams
+ {
+ Width = width,
+ Height = height,
+ Device = _device.NativePointer,
+ ResourceType = 1,
+ SurfaceFormat = 1,
+ CudaResourceType = -1
+ };
+ nint handle;
+ Check(_create(&createParams, &handle), "create");
+ _handle = handle;
+
+ var registerParams = new RegisterParams { Fence = _fence.NativePointer, Count = 3 };
+ registerParams.Resources[0] = _output.NativePointer;
+ registerParams.Resources[1] = _inputs[0].NativePointer;
+ registerParams.Resources[2] = _inputs[1].NativePointer;
+ Check(_register(_handle, ®isterParams), "register resources");
+ _registered = true;
+ }
+
+ /// Submits the input texture most recently returned by NextInputTarget.
+ public bool Interpolate()
+ {
+ if (_inputIndex < 0) throw new InvalidOperationException("No FRUC input target was prepared.");
+
+ // NvOFFRUC and D3D11 share one fence. Follow NVIDIA's ownership cycle
+ // exactly: wait for FRUC to release the resources, write the input,
+ // signal it, let FRUC signal the generated frame, consume it, then
+ // signal that the output is free again. Omitting the final release
+ // works for a synthetic first frame but fails on a live stream.
+ _context.OMSetRenderTargets(0, [], null!);
+ _context.Wait(_fence, _fenceValue);
+ _context.CopyResource(_inputs[_inputIndex], _upload);
+ var inputReady = ++_fenceValue;
+ _context.Signal(_fence, inputReady);
+ _context.Flush();
+ var outputReady = ++_fenceValue;
+ var repeated = false;
+ var timestamp = ++_frameNumber;
+
+ var input = new ProcessInParams
+ {
+ Frame = new FrameData
+ {
+ Frame = _inputs[_inputIndex].NativePointer,
+ Timestamp = timestamp
+ },
+ Sync = new SyncValue { First = inputReady }
+ };
+ var output = new ProcessOutParams
+ {
+ Frame = new FrameData
+ {
+ Frame = _output.NativePointer,
+ Timestamp = timestamp - 0.5,
+ Repeated = (nint)(&repeated)
+ },
+ Sync = new SyncValue { First = outputReady }
+ };
+
+ Check(_process(_handle, &input, &output), "interpolate frame");
+ _context.Wait(_fence, outputReady);
+ _context.CopyResource(_display, _output);
+ _context.Signal(_fence, ++_fenceValue);
+ _context.Flush();
+ return !repeated && HasHistory;
+ }
+
+ private T Load(string name) where T : Delegate =>
+ Marshal.GetDelegateForFunctionPointer(NativeLibrary.GetExport(_library, name));
+
+ private static void Check(int status, string operation)
+ {
+ if (status != 0) throw new InvalidOperationException($"NVIDIA FRUC failed to {operation}: {StatusName(status)} (status {status}).");
+ }
+
+ private static string StatusName(int status) => status switch
+ {
+ 1 => "Optical Flow is not supported",
+ 2 => "invalid pointer",
+ 3 => "invalid parameter",
+ 4 => "invalid handle",
+ 5 => "out of system memory",
+ 6 => "out of video memory",
+ 7 => "OpenCV is unavailable",
+ 8 => "unimplemented operation",
+ 9 => "Optical Flow generation failed",
+ 10 => "duplicate resource",
+ 11 => "unregistered resource",
+ 12 => "incorrect API sequence",
+ 13 => "write failed",
+ 14 => "pipeline execution failed",
+ 15 => "synchronization write failed",
+ 16 => "internal runtime error",
+ _ => "unknown runtime error"
+ };
+
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+ try
+ {
+ if (_registered && _handle != 0)
+ {
+ var unregister = new UnregisterParams { Count = 3 };
+ unregister.Resources[0] = _output.NativePointer;
+ unregister.Resources[1] = _inputs[0].NativePointer;
+ unregister.Resources[2] = _inputs[1].NativePointer;
+ _unregister(_handle, &unregister);
+ }
+ if (_handle != 0) _destroy(_handle);
+ }
+ finally
+ {
+ _handle = 0;
+ _displaySrv.Dispose();
+ _display.Dispose();
+ _uploadSrv.Dispose();
+ _uploadRtv.Dispose();
+ _upload.Dispose();
+ _output.Dispose();
+ foreach (var texture in _inputs) texture?.Dispose();
+ _fence.Dispose();
+ _context.Dispose();
+ _device.Dispose();
+ if (_library != 0) NativeLibrary.Free(_library);
+ }
+ }
+
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ private delegate int CreateDelegate(CreateParams* parameters, nint* handle);
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ private delegate int RegisterDelegate(nint handle, RegisterParams* parameters);
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ private delegate int UnregisterDelegate(nint handle, UnregisterParams* parameters);
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ private delegate int ProcessDelegate(nint handle, ProcessInParams* input, ProcessOutParams* output);
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ private delegate int DestroyDelegate(nint handle);
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct CreateParams
+ {
+ public uint Width;
+ public uint Height;
+ public nint Device;
+ public int ResourceType;
+ public int SurfaceFormat;
+ public int CudaResourceType;
+ public fixed uint Reserved[32];
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct FrameData
+ {
+ public nint Frame;
+ public double Timestamp;
+ public nuint CudaPitch;
+ public nint Repeated;
+ public fixed uint Reserved[32];
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct SyncValue
+ {
+ public ulong First;
+ public ulong Second;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct ProcessInParams
+ {
+ public FrameData Frame;
+ public uint SkipWarp;
+ public SyncValue Sync;
+ public fixed uint Reserved[32];
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct ProcessOutParams
+ {
+ public FrameData Frame;
+ public SyncValue Sync;
+ public fixed uint Reserved[32];
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct RegisterParams
+ {
+ public fixed long Resources[10];
+ public nint Fence;
+ public uint Count;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct UnregisterParams
+ {
+ public fixed long Resources[10];
+ public uint Count;
+ }
+}
diff --git a/src/AnimeEnhancer.Graphics/RifeMotionEngine.cs b/src/AnimeEnhancer.Graphics/RifeMotionEngine.cs
new file mode 100644
index 0000000..cff51b6
--- /dev/null
+++ b/src/AnimeEnhancer.Graphics/RifeMotionEngine.cs
@@ -0,0 +1,179 @@
+using System.Collections.Concurrent;
+using System.Runtime.InteropServices;
+
+namespace AnimeEnhancer.Graphics;
+
+public sealed class RifeMotionEngine : IDisposable
+{
+ // Keep only the newest pending source frame. Interpolation needs one frame of
+ // look-ahead by design, but buffering additional source frames only adds A/V lag.
+ private readonly BlockingCollection _input = new(1);
+ private readonly ConcurrentQueue _output = new();
+ private readonly CancellationTokenSource _stop = new();
+ private readonly Task _worker;
+ private readonly int _width;
+ private readonly int _height;
+ private byte[]? _lastSubmitted;
+ private long _lastChangedTimestamp;
+ private double _changedInterval;
+ private int _fastCadenceSamples;
+ private int _passthrough;
+ private byte[]? _previous;
+ private long _previousTimestamp;
+ private double _cadenceRemainder;
+ private nint _handle;
+ private long _generatedFrames;
+
+ public string? Error { get; private set; }
+ public long GeneratedFrames => Interlocked.Read(ref _generatedFrames);
+ public int QueuedFrames => _output.Count;
+ public bool PassthroughRecommended => Volatile.Read(ref _passthrough) != 0;
+
+ public RifeMotionEngine(int width, int height, string modelDirectory)
+ {
+ _width = width;
+ _height = height;
+ _worker = Task.Factory.StartNew(() => Run(modelDirectory), _stop.Token,
+ TaskCreationOptions.LongRunning, TaskScheduler.Default);
+ }
+
+ public bool Submit(byte[] bgra, long timestamp)
+ {
+ if (bgra.Length != _width * _height * 4 || Error is not null) return PassthroughRecommended;
+ if (_lastSubmitted is not null && Difference(_lastSubmitted, bgra) < .0035) return PassthroughRecommended;
+
+ if (_lastChangedTimestamp != 0)
+ {
+ var interval = (timestamp - _lastChangedTimestamp) / (double)TimeSpan.TicksPerSecond;
+ if (interval is >= (1.0 / 120) and <= .1)
+ {
+ _changedInterval = _changedInterval == 0 ? interval : _changedInterval * .72 + interval * .28;
+ if (_changedInterval <= .0225) _fastCadenceSamples = Math.Min(12, _fastCadenceSamples + 1);
+ else _fastCadenceSamples = Math.Max(-8, _fastCadenceSamples - 2);
+
+ if (_fastCadenceSamples >= 4) Volatile.Write(ref _passthrough, 1);
+ else if (_fastCadenceSamples <= -4) Volatile.Write(ref _passthrough, 0);
+ }
+ }
+ _lastChangedTimestamp = timestamp;
+ _lastSubmitted = bgra;
+ if (PassthroughRecommended)
+ {
+ while (_input.TryTake(out _)) { }
+ ClearOutput();
+ return true;
+ }
+ while (!_input.TryAdd(new MotionInput(bgra, timestamp))) _input.TryTake(out _);
+ return false;
+ }
+
+ public bool TryGetFrame(out byte[] frame, out long timestamp)
+ {
+ if (_output.TryDequeue(out var output))
+ {
+ frame = output.Pixels;
+ timestamp = output.Timestamp;
+ return true;
+ }
+ frame = null!;
+ timestamp = 0;
+ return false;
+ }
+
+ private void Run(string modelDirectory)
+ {
+ try
+ {
+ var gpu = RifeDefaultGpu();
+ _handle = RifeCreate(gpu, modelDirectory);
+ if (_handle == 0) throw new InvalidOperationException("RIFE model could not be loaded.");
+ foreach (var input in _input.GetConsumingEnumerable(_stop.Token))
+ {
+ if (PassthroughRecommended)
+ {
+ _previous = null;
+ _cadenceRemainder = 0;
+ continue;
+ }
+ var current = input.Pixels;
+ if (_previous is null || Difference(_previous, current) > .19)
+ {
+ ClearOutput();
+ Enqueue(current, input.Timestamp);
+ _previous = current;
+ _previousTimestamp = input.Timestamp;
+ _cadenceRemainder = 0;
+ continue;
+ }
+
+ var delta = (input.Timestamp - _previousTimestamp) / (double)TimeSpan.TicksPerSecond;
+ var slots = delta is >= (1.0 / 120) and <= .1 ? delta * 60 + _cadenceRemainder : 2.5;
+ var frameCount = Math.Clamp((int)Math.Floor(slots + 1e-6), 1, 6);
+ _cadenceRemainder = Math.Clamp(slots - frameCount, 0, .999);
+ var timestampDelta = input.Timestamp - _previousTimestamp;
+ for (var i = 1; i < frameCount; i++)
+ Generate(_previous, current, i / (float)frameCount, _previousTimestamp + timestampDelta * i / frameCount);
+ Enqueue(current, input.Timestamp);
+ _previous = current;
+ _previousTimestamp = input.Timestamp;
+ }
+ }
+ catch (OperationCanceledException) { }
+ catch (Exception ex) { Error = ex.Message; }
+ }
+
+ private void Generate(byte[] first, byte[] second, float time, long timestamp)
+ {
+ var output = new byte[first.Length];
+ unsafe
+ {
+ fixed (byte* a = first, b = second, o = output)
+ {
+ var result = RifeProcess(_handle, a, b, _width, _height, _width * 4, time, o, _width * 4);
+ if (result != 0) throw new InvalidOperationException($"RIFE inference failed ({result}).");
+ }
+ }
+ Interlocked.Increment(ref _generatedFrames);
+ Enqueue(output, timestamp);
+ }
+
+ private void Enqueue(byte[] frame, long timestamp)
+ {
+ if (PassthroughRecommended) return;
+ // Timing metadata lets the presenter drop genuinely late frames instead of
+ // showing an inference burst with uneven spacing.
+ while (_output.Count >= 5) _output.TryDequeue(out _);
+ _output.Enqueue(new MotionOutput(frame, timestamp));
+ }
+
+ private void ClearOutput() { while (_output.TryDequeue(out _)) { } }
+
+ private static double Difference(byte[] a, byte[] b)
+ {
+ long total = 0;
+ var samples = 0;
+ for (var i = 0; i < a.Length; i += 64)
+ {
+ total += Math.Abs(a[i] - b[i]) + Math.Abs(a[i + 1] - b[i + 1]) + Math.Abs(a[i + 2] - b[i + 2]);
+ samples += 3;
+ }
+ return total / (samples * 255.0);
+ }
+
+ public void Dispose()
+ {
+ _input.CompleteAdding();
+ _stop.Cancel();
+ try { _worker.Wait(); } catch { }
+ if (_handle != 0) { RifeDestroy(_handle); _handle = 0; }
+ _stop.Dispose(); _input.Dispose();
+ }
+
+ [DllImport("animeflow-rife.dll", EntryPoint = "rife_default_gpu", CallingConvention = CallingConvention.Cdecl)] private static extern int RifeDefaultGpu();
+ [DllImport("animeflow-rife.dll", EntryPoint = "rife_create", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)] private static extern nint RifeCreate(int gpuId, string modelDirectory);
+ [DllImport("animeflow-rife.dll", EntryPoint = "rife_process_bgra", CallingConvention = CallingConvention.Cdecl)] private static extern unsafe int RifeProcess(nint handle, byte* first, byte* second, int width, int height, int inputStride, float timestep, byte* output, int outputStride);
+ [DllImport("animeflow-rife.dll", EntryPoint = "rife_destroy", CallingConvention = CallingConvention.Cdecl)] private static extern void RifeDestroy(nint handle);
+
+ private readonly record struct MotionInput(byte[] Pixels, long Timestamp);
+ private readonly record struct MotionOutput(byte[] Pixels, long Timestamp);
+}
diff --git a/tests/AnimeEnhancer.IntegrationTests/PlatformTests.cs b/tests/AnimeEnhancer.IntegrationTests/PlatformTests.cs
index c1ffb76..e2758bf 100644
--- a/tests/AnimeEnhancer.IntegrationTests/PlatformTests.cs
+++ b/tests/AnimeEnhancer.IntegrationTests/PlatformTests.cs
@@ -138,6 +138,35 @@ [Fact] public void CleanupSwitchesProduceMeasurablyDifferentPixels()
}
finally { DestroyWindow(output); }
}
+ [Fact] public void NvidiaOpticalFlowInterpolatesEnhancedFramesWhenAvailable()
+ {
+ if(!OperatingSystem.IsWindows()||!NvidiaFruc.IsRuntimePresent()) return;
+ var output=CreateWindowEx(0,"STATIC","AnimeEnhancerFrucValidation",unchecked((int)0x80000000),0,0,3840,2400,0,0,0,0); Assert.NotEqual(0,output);
+ try
+ {
+ var adapter=GpuPipeline.EnumerateAdapters().FirstOrDefault(x=>x.Contains("NVIDIA",StringComparison.OrdinalIgnoreCase)||x.Contains("GeForce",StringComparison.OrdinalIgnoreCase));
+ if(adapter is null) return;
+ var root=FindRoot(); using var pipeline=new GpuPipeline(Path.Combine(root,"shaders"),adapter); pipeline.Attach(output,new PixelSize(3840,2400));
+ Assert.True(pipeline.ConfigureRtxMotion(true,out var error),error);
+ using var source=pipeline.Device.CreateTexture2D(new Texture2DDescription{Width=3840,Height=2400,MipLevels=1,ArraySize=1,Format=Vortice.DXGI.Format.B8G8R8A8_UNorm,SampleDescription=Vortice.DXGI.SampleDescription.Default,Usage=ResourceUsage.Default,BindFlags=BindFlags.ShaderResource|BindFlags.RenderTarget});
+ using var sourceRtv=pipeline.Device.CreateRenderTargetView(source);
+ pipeline.Device.ImmediateContext.ClearRenderTargetView(sourceRtv,new Vortice.Mathematics.Color4(.05f,.2f,.8f,1));
+ pipeline.Process(source,new PixelSize(3840,2400),EnhancementPreset.UltraDetail,ComparisonMode.Enhanced,.5,false,1,1,false,false,true);
+ pipeline.Device.ImmediateContext.ClearRenderTargetView(sourceRtv,new Vortice.Mathematics.Color4(.8f,.1f,.15f,1));
+ pipeline.Process(source,new PixelSize(3840,2400),EnhancementPreset.UltraDetail,ComparisonMode.Enhanced,.5,false,1,1,false,false,true);
+ // Exercise the shared D3D11/CUDA fence for long enough to catch
+ // ownership bugs that only appear after the first frame pair.
+ for(var frame=0;frame<120;frame++)
+ {
+ var phase=(frame%60)/59f;
+ pipeline.Device.ImmediateContext.ClearRenderTargetView(sourceRtv,new Vortice.Mathematics.Color4(.1f+phase*.7f,.15f,.8f-phase*.6f,1));
+ pipeline.Process(source,new PixelSize(3840,2400),EnhancementPreset.UltraDetail,ComparisonMode.Enhanced,.5,false,1,1,false,false,true);
+ }
+ Assert.True(pipeline.RtxMotionActive);
+ Assert.Null(pipeline.LastRtxMotionError);
+ }
+ finally { DestroyWindow(output); }
+ }
private static string FindRoot(){var d=new DirectoryInfo(AppContext.BaseDirectory);while(d!=null&&!File.Exists(Path.Combine(d.FullName,"AnimeEnhancer.slnx")))d=d.Parent;return d?.FullName??throw new DirectoryNotFoundException();}
[DllImport("user32.dll",CharSet=CharSet.Unicode,EntryPoint="CreateWindowExW")] private static extern nint CreateWindowEx(int exStyle,string className,string windowName,int style,int x,int y,int width,int height,nint parent,nint menu,nint instance,nint param);
[DllImport("user32.dll")] private static extern bool DestroyWindow(nint hwnd);