Skip to content

fix: destroy the ncnn GPU instance before exit instead of during DLL unload - #1500

Open
FortunaCournot wants to merge 4 commits into
k4yt3x:masterfrom
FortunaCournot:pr/fix-segfault-on-exit
Open

fix: destroy the ncnn GPU instance before exit instead of during DLL unload#1500
FortunaCournot wants to merge 4 commits into
k4yt3x:masterfrom
FortunaCournot:pr/fix-segfault-on-exit

Conversation

@FortunaCournot

Copy link
Copy Markdown

The bug

video2x always terminates with a segmentation fault — even when the run succeeds. It prints Video processed successfully, writes its complete processing summary, and only then dies:

[info] Video processed successfully
====== Video2X Processing summary ======
Total frames processed: 2309
Output written to: ...-part1.mp4
...
line 173: 32464 Segmentation fault      video2x.exe -i ... -p rife ...

A successful run and a failed one are therefore indistinguishable by exit code, so every script driving video2x has to ignore the exit code and inspect the output file instead. That is a bad contract for a CLI tool.

Cause

Nothing ever destroys ncnn's Vulkan instance on purpose. ncnn creates it lazily on first use and only tears it down from its static __ncnn_vulkan_instance_holder destructor plus an atexit() handler it registers itself.

On Windows that is too late. ncnn is a shared library hereCMakeLists.txt says "Use the pre-built shared ncnn library on Windows", and build.yml fetches ncnn-*-windows-vs2022-shared.zip — so both of those run while the loader is already unloading DLLs at process exit. Calling into another DLL at that point is documented as unsafe, and the teardown does exactly that: vkDeviceWaitIdle(), vkDestroyInstance() and glslang::FinalizeProcess().

The access violation therefore lands after main() has returned, which is also why Windows Error Reporting never logs it — it happens inside the loader teardown.

ncnn's author is aware of the hazard. gpu.cpp carries this comment next to the atexit() call:

// the global __ncnn_vulkan_instance_holder destructor will call destroy_gpu_instance() on exit
// atexit() seems to be helpful for calling it earlier    --- nihui

and his own reference tools (rife-ncnn-vulkan, realesrgan-ncnn-vulkan) call destroy_gpu_instance() explicitly from main() rather than relying on that teardown.

The fix

Do the same. libvideo2x gains video2x::release_gpu_resources(), and the CLI calls it from an RAII guard declared as the first local in main() — so it fires on every exit path, and is destroyed after the VideoProcessor: the processor's Vulkan buffers must be gone before the instance they came from is.

destroy_gpu_instance() returns immediately when no instance was ever created, so this is a no-op for the libplacebo path, for --help, and for a failed argument parse. ncnn's own atexit handler still runs afterwards and finds nothing to do.

Evidence

I built two Windows Release binaries from this workflow whose only difference is the 64-line fix, and ran both on the same clip:

RIFE (-p rife -m 2) Real-ESRGAN (-p realesrgan -s 2)
unpatched 6.4.0 139, 139, 139 139
with this fix 0, 0, 0 0

The produced videos are byte-identical (same MD5) — the fix changes the exit code and nothing else.

About the first three commits

They are not padding: the Windows job cannot currently build at all, so without them this PR would arrive with red CI and the fix could not be verified by anyone.

  1. setup-vulkan-sdk@v1.2.0 pulls actions/cache@v2, which GitHub now auto-fails → bumped to v1.2.1.
  2. The pinned Vulkan SDK 1.3.204.0 (2021) declares cmake_minimum_required below 3.5, which CMake 4 — now on windows-latest — refuses outright → bumped to 1.4.313.0.
  3. CMakeLists.txt spells out the Boost DLL as vc143; windows-latest now builds it as vc145, so install fails after everything has compiled and linked → matched by pattern instead.

Happy to split those into a separate PR if you would rather review them apart.

Caveat

Tested on Windows 11 with an RTX 5070 Ti. I have no stack trace from a debug build — the diagnosis rests on the observed behaviour, on the source, and on the control experiment above. Corrections welcome.

… actions/cache@v2)

CONTROL BRANCH - upstream 6.4.0 with no source change, only the CI fix that is
needed for the Windows job to start at all. Used to prove the segfault exists in
a binary built by this very workflow, before claiming a patch fixes it.
setup-vulkan-sdk builds the Vulkan components from source. The pinned 1.3.204.0
is from 2021 and its CMakeLists declare cmake_minimum_required below 3.5, which
CMake 4 - now on windows-latest - refuses outright. The Windows job therefore
cannot even configure.
…olset

The install step spelled out boost_program_options-vc143-mt-gd-x64-1_86.dll.
windows-latest now ships Visual Studio 18, so Boost builds as vc180 and the
install fails with "file INSTALL cannot find ...-vc143-...dll" - after
everything has already compiled and linked. install(DIRECTORY ... FILES_MATCHING)
is evaluated at install time, so the glob sees the DLL that does not exist yet
when CMake configures.
…unload

video2x always terminates with a segmentation fault, even when the run
succeeds. It prints "Video processed successfully", writes its complete
processing summary, and only then dies. A successful run and a failed one are
therefore indistinguishable by exit code, and every script driving video2x has
to ignore the exit code and inspect the output file instead.

Cause: nothing ever destroys ncnn's Vulkan instance on purpose. ncnn creates it
lazily on first use and only tears it down from its static
__ncnn_vulkan_instance_holder destructor plus an atexit() handler it registers
itself. On Windows ncnn is a SHARED library here (CMakeLists.txt: "Use the
pre-built shared ncnn library on Windows"; build.yml fetches
ncnn-*-windows-vs2022-shared.zip), so both of those run while the loader is
already unloading DLLs at process exit. Calling into another DLL at that point
is unsafe, and the teardown does exactly that: vkDeviceWaitIdle(),
vkDestroyInstance() and glslang::FinalizeProcess(). The access violation lands
after main() has returned, which is also why Windows Error Reporting never logs
it.

ncnn's author knows the hazard - gpu.cpp says "atexit() seems to be helpful for
calling it earlier --- nihui" - and his reference tools (rife-ncnn-vulkan,
realesrgan-ncnn-vulkan) call destroy_gpu_instance() explicitly from main()
rather than relying on that teardown.

Do the same. libvideo2x gains video2x::release_gpu_resources(), and the CLI
calls it from an RAII guard declared as the first local in main(), so it fires
on every exit path and is destroyed after the VideoProcessor - the processor's
Vulkan buffers must be gone before the instance they came from is.
destroy_gpu_instance() returns immediately when no instance was ever created, so
this is a no-op for the libplacebo path, for --help, and for a failed argument
parse; ncnn's own atexit handler still runs afterwards and finds nothing to do.
FortunaCournot added a commit to FortunaCournot/video2x that referenced this pull request Jul 14, 2026
The VR we are installer needs a Video2X whose exit code means something. Upstream
segfaults on exit on every run, and has not released the fix
(k4yt3x#1500). Until it does, build and publish the Windows binary here.

Replaces upstream's release.yml: that one fires on every tag and then dies in its
container job on GHCR secrets this fork does not have.

Packages the FLAT layout of the official archive - video2x.exe, the DLLs and
models/ at the root - because Video2X looks for its models next to the executable
and the installer looks for the exe at the root of what it unpacks.

Ships LICENSE, NOTICE and MODIFICATIONS.md inside the archive. Upstream's zip
ships no licence text at all; AGPL 5(a) and 6 still apply. The release notes name
the Corresponding Source of every component, including the bundled FFmpeg DLLs,
which are a GPL build.
FortunaCournot added a commit to FortunaCournot/video2x that referenced this pull request Jul 14, 2026
The VR we are installer needs a Video2X whose exit code means something. Upstream
segfaults on exit on every run, and has not released the fix
(k4yt3x#1500). Until it does, build and publish the Windows binary here.

Replaces upstream's release.yml: that one fires on every tag and then dies in its
container job on GHCR secrets this fork does not have.

Packages the FLAT layout of the official archive - video2x.exe, the DLLs and
models/ at the root - because Video2X looks for its models next to the executable
and the installer looks for the exe at the root of what it unpacks.

Ships LICENSE, NOTICE and MODIFICATIONS.md inside the archive. Upstream's zip
ships no licence text at all; AGPL 5(a) and 6 still apply. The release notes name
the Corresponding Source of every component, including the bundled FFmpeg DLLs,
which are a GPL build.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant