protonscr

stalker cop enhanced edition: videos need ID3D12VideoProcessor, not a decoder

vkd3dclosed
HansKristian-Work/vkd3d-proton#3212 · opened 2026-08-14 by dappermint · updated 2026-08-14 · 2 comments · github
Ddappermint 2026-08-14 github

S.T.A.L.K.E.R. Call of Pripyat Enhanced Edition renders its intro and menu videos as two half resolution copies with the chroma flat, an olive cast, on vkd3d-proton and on macos/d3dmetal, and never on windows. it is the same missing capability on both, and it is smaller than it looks.

the engine does not want gpu decode. it wants ID3D12VideoProcessor.

i implemented ID3D12VideoDevice on the d3dmetal side to find out what xrEngine actually does with it, and it makes exactly one call before deciding:

device QI {1f052807-0b46-4acc}   -> video device
CheckFeatureSupport feature=5 size=608

feature 5 is D3D12_FEATURE_VIDEO_PROCESS_SUPPORT. there is no CreateVideoDecoder, no decoder heap, no DecodeFrame, no compressed bitstream, ever. the engine decodes h264 itself in engine and only wants d3d12 for the colour conversion.

the failure mode is worth knowing before implementing anything: my first attempt implemented the decode side and returned E_INVALIDARG for the one feature it did not recognise. the engine asked that single question, got a failure, and fell straight back to its broken software path with no further calls. a video device that fails feature 5 behaves exactly like no video device at all, so a partial implementation that only covers decode buys nothing here.

once feature 5 answers supported, the whole rest of the negotiation is:

CreateVideoProcessor 1 input stream, out fmt=28 cs=0
  in 0 fmt=103 cs=9 src 64x64..3840x2160 dst 64x64..3840x2160
       field=0 deint=0 past=0 future=0 alpha=0 auto=0
ProcessFrames streams=1
  out tex=... sub=0 target=0,0..3840,2160
  in 0  tex=... sub=0 src=0,0..3840,2160 dst=0,0..3840,2160 orient=0 flags=0x0

which is: NV12 (fmt=103) in, R8G8B8A8_UNORM (fmt=28) out, 1:1 at source resolution, one input stream, no scaling, no filters, no deinterlace, no past or future reference frames, alpha fill opaque. the engine uploads both planes itself, sub=0 as R8 full size and sub=1 as R8G8 at half, so ProcessFrames is a plain two plane YUV to RGB blit.

i implemented exactly that as a fullscreen pass sampling both planes with the matrix from the declared colour space, and the videos then play correctly at the full 3840x2160.

note that this fixes the resolution too, not only the colour. the engine's fallback decodes at 1920x1080 into a 2048x2048 texture, which is where the two side by side copies come from; the processor path runs the real 4k.

one content caveat: the engine declares cs=9, YCBCR_FULL_G22_LEFT_P709, but the files are yuv420p(tv, bt709), studio range. obeying the declaration leaves black at 16 and the video visibly washed out, so i treat the declaration as studio by default and keep the literal behaviour behind an option. worth deciding deliberately rather than by accident.

so the ask is ID3D12VideoDevice answering VIDEO_PROCESS_SUPPORT, CreateVideoProcessor, a VIDEO_PROCESS command list and ProcessFrames doing the blit. no bitstream parsing, no DPB, no DXVA structures. happy to supply more traces if useful.

background thread with the earlier wrong theories and how they were ruled out: https://github.com/ValveSoftware/Proton/issues/8717

BBlisto91 2026-08-14 github

Hi there.
Missing video support the stalker enhanced editions is already tracked at https://github.com/HansKristian-Work/vkd3d-proton/issues/2479 and https://github.com/HansKristian-Work/vkd3d-proton/issues/2991

Ddappermint 2026-08-14 github

Thanks, closing as a duplicate of #2479, which already has the right title.

Leaving the short version here in case it is useful when that one gets picked up, since the open question there is how big the minimum is. I implemented this subset on the macos/d3dmetal side to find out, and the videos play at full resolution, so the answer is smaller than it looks:

xrEngine makes exactly three calls. CheckFeatureSupport(D3D12_FEATURE_VIDEO_PROCESS_SUPPORT), CreateVideoProcessor, ProcessFrames. There is no decoder, no decoder heap, no DecodeFrame and no compressed bitstream anywhere; it decodes h264 itself and only wants the colour conversion.

ProcessFrames is one unfiltered blit: NV12 in, R8G8B8A8_UNORM out, 1:1 at source resolution, one input stream, no scaling, no filters, no deinterlace, no past or future reference frames, alpha fill opaque. The engine uploads both planes itself, sub=0 as R8 and sub=1 as R8G8 at half size.

One trap worth knowing before anyone starts: my first attempt returned E_INVALIDARG for the one feature it did not recognise, and the engine asked that single question, got a failure, and went straight back to its broken fallback with no further calls. A video device that fails feature 5 is indistinguishable from no video device at all, so a partial implementation buys nothing until that one query answers.

Happy to put the full trace on #2479 instead if that is wanted rather than here.