Skip to content

vf_d3d11vpp: create own device if the VO provides none - #18301

Open
flowreen wants to merge 2 commits into
mpv-player:masterfrom
flowreen:d3d11vpp-own-device
Open

vf_d3d11vpp: create own device if the VO provides none#18301
flowreen wants to merge 2 commits into
mpv-player:masterfrom
flowreen:d3d11vpp-own-device

Conversation

@flowreen

Copy link
Copy Markdown

In short: d3d11vpp takes its D3D11 device from the video output, so with --gpu-api=vulkan it cannot initialise at all and mpv drops the video track. RTX Video HDR and RTX Video Super Resolution are therefore unreachable unless you also switch renderer. This falls back to a standalone D3D11 device when the VO has none.

Background

People enable RTX Video HDR, get nothing, and have no way to tell why. The prompt for this was someone asking exactly that: https://www.reddit.com/r/mpv/comments/1v6msec/do_i_need_to_change_config_to_enable_rtxhdr_in_mpv/

That is a single report, not evidence of wide demand, and --gpu-api=d3d11 remains the better answer on merit. But the current behaviour gives no signal at all: the filter fails to construct and the video track disappears, with nothing pointing at the video output as the cause. Either it should work or it should say why, and working turned out to be the smaller change.

Problem

[vo/gpu-next] Loading hwdec drivers for format: 'd3d11'
[vo/gpu-next] Loading hwdec driver 'd3d11va'
[user_filter_wrapper] Creating filter 'd3d11vpp' failed.
Video: no video

The lazy loader does run, but hwdec_d3d11va's init starts with

if (!ra_is_d3d11(hw->ra_ctx->ra))
    return -1;

so with a Vulkan rendering abstraction it declines immediately, no D3D11 device is ever registered in hwdec_devs, and vf_d3d11vpp_create() bails. The driver loads, refuses, and leaves the list empty, which is why the log looks like it should have worked.

Fix

The video processor does not have to run on the VO's device. When the VO provides none, create a standalone one via hwcontext_fns_d3d11, the same helper the copy hwdecs use, and advertise it with hwdec_devices_add() so the generic upload and download filters can move frames in and out of it.

This costs an upload and a download per frame, so the filter warns, and it still prefers the VO's device whenever there is one. --gpu-api=d3d11 is unaffected.

Two details that are not obvious:

  • Every instance registers its own mp_hwdec_ctx, even when sharing a device. mp_output_chain_update_filters() creates new filters before freeing the old ones, so an instance that merely borrowed a peer's device is left with nothing registered once that peer deregisters, and the next upload probe fails. Symptom was that any runtime vf change silently disabled the filter.
  • Instances share one device rather than each creating their own. An ID3D11VideoProcessorInputView cannot be created from another device's texture, so a chain of two d3d11vpp filters breaks if they disagree on a device.

Known limitations, documented in vf.rst

  • --hwdec=d3d11va still cannot be used with a non-D3D11 VO, because init_video_decoder() runs before recreate_video_filters(), so the device is registered too late for the decoder. It falls back to software decoding.
  • d3d11_create_standalone() passes a NULL DXGI adapter, so on a hybrid system the device lands on whatever enumerates first. That is the discrete GPU on my machine, but it is not guaranteed.
  • Decoders that emit a different hardware format need f_autoconvert: convert between hw formats via system memory #18300 as well. Without it, this patch alone works with --hwdec=no and with every copy-back mode (auto-copy, nvdec-copy, d3d11va-copy, all verified), but --hwdec=auto selecting nvdec fails, because its CUDA frames cannot be uploaded into D3D11.

Testing

Windows 11, RTX 5090, driver 610.74, --vo=gpu-next.

RTX Video HDR and RTX VSR both engage under --gpu-api=vulkan, output is correctly tagged rgb/bt.2020/pq/full, and libplacebo picks an HDR10_ST2084 swapchain. Pixel output matches the --gpu-api=d3d11 reference at SSIM 1.000000 and PSNR 95 dB, against PSNR 28 dB for a no-filter control.

Also covered: playlist advance, seeking, runtime vf changes, two d3d11vpp filters chained, already-HDR sources (correctly a no-op), and 60 filter create/destroy cycles with a threaded decoder (--vd-queue-enable=yes) showing no failures and no handle or memory growth against a --gpu-api=d3d11 control. Confirmed the d3d11 path still takes the VO's device and stays zero-copy.

Cost of the fallback, over 600 frames at 1080p: about +6.5 ms/frame CPU versus +3.5 ms/frame on the native d3d11 path, and roughly double the steady-state memory, 640 MB against 346 MB.

Not tested: non-NVIDIA hardware, so the Intel VSR path is unexercised, and hybrid systems where the integrated GPU enumerates first.

I do not know this codebase's conventions well, so if you would prefer a different approach, a narrower fix, or this split differently across commits, say so and I will rework it.

cc @kasper93

AI disclosure

Disclosure: this patch was written with AI assistance (Claude Code), which diagnosed the bug, wrote the change, and ran the testing described above on my machine. I have reviewed it and understand what it changes and why, I take responsibility for it, and review responses will be written by me. video/filter/vf_d3d11vpp.c is LGPLv2.1+ per its header; I submit the change under that licence.

@philipl

philipl commented Jul 26, 2026

Copy link
Copy Markdown
Member

The proper solution here is device derivation, which you need anyway to do proper interop.

@flowreen

flowreen commented Jul 26, 2026

Copy link
Copy Markdown
Author

Good idea, so here is what it looks like implemented. ff_hwcontext_type_d3d11va has no device_derive at all upstream, which is why av_hwdevice_ctx_create_derived() to D3D11VA returns ENOSYS for every caller, so I wrote one:

flowreen/FFmpeg@dd56c90

It reads deviceLUID from VkPhysicalDeviceIDProperties, matches it against the AdapterLuid of each DXGI adapter, and then delegates to the existing d3d11va_device_create() with that adapter index, so the option, debug and multithread handling is not duplicated. It handles both Vulkan and CUDA sources, the CUDA one taking its LUID from cuDeviceGetLuid(), which ffnvcodec loads optionally so an older driver gets ENOSYS rather than an error. I plan to send it to ffmpeg-devel, but would rather hear any objections first.

With that in place this filter derives from the VO's device instead of creating a standalone one. Based on Claude investigation, that turns out to fix a real cross-GPU bug rather than just being tidier. Forcing the VO onto the iGPU with --gpu-api=vulkan --vulkan-device=<intel uuid>, and logging which adapter the filter's device actually sits on:

with derivation:     8086:7d67 (Intel(R) Graphics)        <- same GPU as the VO
without derivation:  10de:2c58 (NVIDIA RTX 5090 Laptop)   <- a different GPU

So as it stands d3d11vpp runs the video processor on the discrete GPU while the VO renders on the integrated one. It happens to pick the right adapter whenever the discrete GPU enumerates first, which is why I did not catch this when I opened the PR.

To be clear on the scope: this only changes which GPU the filter runs on. The copies are unchanged, frames still go through system memory. Being on the same adapter is a prerequisite for frame sharing, not the sharing itself.

The standalone path stays as a fallback, since derivation needs a vulkan source to take the LUID from. Under --gpu-api=opengl there is none and the filter still works.

Tested with 2x and 3x d3d11vpp chains and five runtime vf changes on both --gpu-api=vulkan and --gpu-api=d3d11, and the fallback against a stock FFmpeg with no device_derive, so this PR does not need the lavu change to build or run.

The derivation is a separate commit on top, so dropping it leaves the original standalone-device patch intact.

The open question is sequencing, and that one is yours: would you rather the FFmpeg side land first and this wait on it, or keep the standalone fallback as the committed behaviour and switch over once it does?

flowreen added 2 commits July 26, 2026 16:27
The filter took its D3D11 device from the video output, so it failed to
initialize on any other output, most notably --gpu-api=vulkan, where
mpv then dropped the video track entirely. RTX Video HDR and RTX Video
Super Resolution were unreachable outside of --gpu-api=d3d11.

The video processor does not need to run on the VO's device. Fall back
to a standalone device from hwcontext_fns_d3d11 and advertise it with
hwdec_devices_add(), which lets the generic upload and download filters
move frames in and out of it. That costs an upload and a download per
frame, so the filter warns, and it still prefers the VO's device when
there is one.

Every instance registers its own mp_hwdec_ctx even when it shares
another instance's device. Filters are created before the old ones are
destroyed, so an instance that merely borrowed a peer's device is left
with nothing registered once the peer deregisters, and the next upload
probe fails. Sharing the device instead of creating one per instance
keeps a chain of several d3d11vpp filters working, because an input
view cannot be created from another device's texture.
@flowreen
flowreen force-pushed the d3d11vpp-own-device branch from cd25cad to 4d17121 Compare July 26, 2026 14:29
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.

2 participants