@@ -71,20 +71,55 @@ static DecoderCapsCache& get_decoder_caps_cache() {
7171 return cache;
7272}
7373
74- cudaVideoSurfaceFormat get_preferred_surface_format (OutputDtype output_dtype) {
75- return output_dtype == OutputDtype::FLOAT32 ? cudaVideoSurfaceFormat_P016
76- : cudaVideoSurfaceFormat_NV12;
74+ // NVDEC's output surface formats come in a 4:2:0 and a 4:4:4 flavour, each with
75+ // an 8-bit and a 16-bit variant. We decode on the surface that respects the
76+ // source chroma, but we don't respect the source bit depth and instead try to
77+ // honor the user's requested output dtype:
78+ // - if the user wants uint8 output, we try to decode on a uint8 surface,
79+ // including for >8bit sources. It's not always supported by NVDEC, so the
80+ // caller must fallback to the >8bit surface in such case.
81+ // - similarly if the user wants float32 output, we try to decode on a >8bit
82+ // surface, including for 8bit sources. The caller must handle a similar
83+ // fallback.
84+ cudaVideoSurfaceFormat get_preferred_surface_format (
85+ cudaVideoChromaFormat chroma_format,
86+ OutputDtype output_dtype) {
87+ bool want_uint8 = output_dtype == OutputDtype::UINT8 ;
88+ if (chroma_format == cudaVideoChromaFormat_444) {
89+ return want_uint8 ? cudaVideoSurfaceFormat_YUV444
90+ : cudaVideoSurfaceFormat_YUV444_16Bit;
91+ } else {
92+ return want_uint8 ? cudaVideoSurfaceFormat_NV12
93+ : cudaVideoSurfaceFormat_P016;
94+ }
95+ }
96+
97+ NvdecSurface to_nvdec_surface (cudaVideoSurfaceFormat format) {
98+ switch (format) {
99+ case cudaVideoSurfaceFormat_P016:
100+ return NvdecSurface::P016 ;
101+ case cudaVideoSurfaceFormat_YUV444:
102+ return NvdecSurface::YUV444 ;
103+ case cudaVideoSurfaceFormat_YUV444_16Bit:
104+ return NvdecSurface::YUV444_16Bit;
105+ default :
106+ return NvdecSurface::NV12 ;
107+ }
108+ }
109+
110+ bool is_444_surface_format (cudaVideoSurfaceFormat format) {
111+ return format == cudaVideoSurfaceFormat_YUV444 ||
112+ format == cudaVideoSurfaceFormat_YUV444_16Bit;
113+ }
114+
115+ bool is_16bit_surface_format (cudaVideoSurfaceFormat format) {
116+ return format == cudaVideoSurfaceFormat_P016 ||
117+ format == cudaVideoSurfaceFormat_YUV444_16Bit;
77118}
78119
79- // Whether a frame is a CPU-fallback frame rather than a GPU NVDEC surface,
80- // inferred from its pixel format. This works today because our CPU fallback
81- // never yields NV12/P016 frames, but it's only a proxy, and it's not super
82- // robust to future changes.
83- // Note that we don't rely on decode_on_cpu_ because that field is only relevant
84- // when decoding happens, but this interface can be used in
85- // color-conversion-only mode.
86- bool is_cpu_fallback (int format) {
87- return format != AV_PIX_FMT_NV12 && !is_nvdec_16bit_surface (format);
120+ bool is_expected_pix_fmt_from_nvdec (AVPixelFormat pix_fmt) {
121+ return pix_fmt == AV_PIX_FMT_NV12 || is_nvdec_16bit_pix_fmt (pix_fmt) ||
122+ pix_fmt == AV_PIX_FMT_YUV444P || pix_fmt == AV_PIX_FMT_YUV444P16LE ;
88123}
89124
90125static bool g_cuda_nvdec = register_device_interface(
@@ -256,20 +291,40 @@ std::optional<cudaVideoSurfaceFormat> get_nvdec_surface_format(
256291 return std::nullopt ;
257292 }
258293
259- auto preferred_format = get_preferred_surface_format (output_dtype);
260- if ((caps.nOutputFormatMask >> preferred_format) & 1 ) {
294+ auto preferred_format =
295+ get_preferred_surface_format (chroma_format.value (), output_dtype);
296+
297+ auto is_supported = [&](cudaVideoSurfaceFormat format) {
298+ return ((caps.nOutputFormatMask >> format) & 1 ) != 0 ;
299+ };
300+
301+ if (is_supported (preferred_format)) {
261302 return preferred_format;
262303 }
263304
264- // P016 is typically not supported on 8-bit SDR content. In such cases, we
265- // try to fall back to NV12 if supported:
266- // NVDEC will decode to NV12, our kernel will do NV12 -> RGB producing
267- // uint8, and maybePermuteAndConvertToFloat32 will cast uint8 -> float32.
268- // For HDR content, NV12 would lose precision, so we fall back to CPU instead.
269- if (preferred_format == cudaVideoSurfaceFormat_P016 &&
270- bit_depth_minus8 == 0 &&
271- ((caps.nOutputFormatMask >> cudaVideoSurfaceFormat_NV12) & 1 )) {
272- return cudaVideoSurfaceFormat_NV12;
305+ // The preferred_format heuristic tries to take a shortcut that might cause us
306+ // to miss valid formats. We fallabck here:
307+ // if source is 8bit we can try the 8bit surface.
308+ // if surface is 8bit we can try the 16bit surface.
309+
310+ bool source_is_8_bits = bit_depth_minus8 == 0 ;
311+ if (is_16bit_surface_format (preferred_format) && source_is_8_bits) {
312+ auto narrower = preferred_format == cudaVideoSurfaceFormat_YUV444_16Bit
313+ ? cudaVideoSurfaceFormat_YUV444
314+ : cudaVideoSurfaceFormat_NV12;
315+
316+ if (is_supported (narrower)) {
317+ return narrower;
318+ }
319+ }
320+ if (!is_16bit_surface_format (preferred_format)) {
321+ auto wider = preferred_format == cudaVideoSurfaceFormat_YUV444
322+ ? cudaVideoSurfaceFormat_YUV444_16Bit
323+ : cudaVideoSurfaceFormat_P016;
324+
325+ if (is_supported (wider)) {
326+ return wider;
327+ }
273328 }
274329
275330 return std::nullopt ;
@@ -826,7 +881,7 @@ UniqueAVFrame BetaCudaDeviceInterface::convert_cuda_frame_to_av_frame(
826881 av_frame->width = width;
827882 av_frame->height = height;
828883 av_frame->format = nvdec_pix_fmt (
829- surface_format_ == cudaVideoSurfaceFormat_P016 ,
884+ to_nvdec_surface ( surface_format_) ,
830885 static_cast <int >(video_format_.bit_depth_luma_minus8 ) + 8 );
831886 av_frame->pts = disp_info.timestamp ;
832887
@@ -873,19 +928,24 @@ UniqueAVFrame BetaCudaDeviceInterface::convert_cuda_frame_to_av_frame(
873928 ? AVCOL_RANGE_JPEG
874929 : AVCOL_RANGE_MPEG ;
875930
876- // NVDEC's surface layout places the UV plane after the Y plane. For
877- // NV12/P016 the Y plane has an even number of rows (NVDEC rounds up
878- // internally), so we must use the rounded-up height for the UV offset .
931+ // NVDEC lays the chroma planes out after the Y plane, all with the same
932+ // pitch. The Y plane has an even number of rows (NVDEC rounds up internally),
933+ // so the offsets must use the rounded-up height.
879934 unsigned int even_height = round_up_to_even (height);
880- av_frame->data [0 ] = reinterpret_cast <uint8_t *>(frame_ptr);
881- av_frame->data [1 ] =
882- reinterpret_cast <uint8_t *>(frame_ptr + (pitch * even_height));
883- av_frame->data [2 ] = nullptr ;
935+ auto plane = [&](unsigned int index) {
936+ return reinterpret_cast <uint8_t *>(
937+ frame_ptr + (pitch * even_height * index));
938+ };
939+ bool is_444 = is_444_surface_format (surface_format_);
940+
941+ av_frame->data [0 ] = plane (0 );
942+ av_frame->data [1 ] = plane (1 );
943+ av_frame->data [2 ] = is_444 ? plane (2 ) : nullptr ;
884944 av_frame->data [3 ] = nullptr ;
885945 // TODO_API_BREAKDOWN_CUDA P2: Check range before cast?
886946 av_frame->linesize [0 ] = static_cast <int >(pitch);
887947 av_frame->linesize [1 ] = static_cast <int >(pitch);
888- av_frame->linesize [2 ] = 0 ;
948+ av_frame->linesize [2 ] = is_444 ? static_cast < int >(pitch) : 0 ;
889949 av_frame->linesize [3 ] = 0 ;
890950
891951 return av_frame;
@@ -912,20 +972,24 @@ void BetaCudaDeviceInterface::make_frame_standalone(UniqueAVFrame& av_frame) {
912972
913973 auto attached_data = new StandAloneFrameAttachedData ();
914974 attached_data->producer_stream = current_stream;
975+ attached_data->is_device_frame = !decoding_on_cpu_;
915976
916- if (!is_cpu_fallback (av_frame-> format ) ) {
977+ if (!decoding_on_cpu_ ) {
917978 // The amount of bytes an NV12 image takes is:
918979 // num_bytes = len(Y) + len(UV)
919980 // = num_pixels + num_pixels / 2
920981 // = num_pixels * 3 / 2
921982 //
922983 // where num_pixels = pitch * height, not num_pixels = width * height. The
923984 // pitch value also accounts for the data size (uint8 vs uint16) so this is
924- // also correct for P016.
985+ // also correct for P016. A 4:4:4 surface has two full-size chroma planes
986+ // instead of one half-height one, so it's num_pixels * 3.
925987 int64_t even_height =
926988 static_cast <int64_t >(round_up_to_even (av_frame->height ));
927989 int64_t pitch = static_cast <int64_t >(av_frame->linesize [0 ]);
928- int64_t num_bytes = pitch * even_height * 3 / 2 ;
990+ bool is_444 = is_444_surface_format (surface_format_);
991+ int64_t num_bytes =
992+ is_444 ? pitch * even_height * 3 : pitch * even_height * 3 / 2 ;
929993
930994 auto storage =
931995 torch::stable::empty ({num_bytes}, kStableUInt8 , std::nullopt , device_);
@@ -951,6 +1015,9 @@ void BetaCudaDeviceInterface::make_frame_standalone(UniqueAVFrame& av_frame) {
9511015 auto y_plane = static_cast <uint8_t *>(storage.mutable_data_ptr ());
9521016 av_frame->data [0 ] = y_plane;
9531017 av_frame->data [1 ] = y_plane + (pitch * even_height);
1018+ if (is_444) {
1019+ av_frame->data [2 ] = y_plane + (2 * pitch * even_height);
1020+ }
9541021
9551022 // TODO_API_BREAKDOWN_CUDA P2: We don't *really* need to std::move it I
9561023 // guess?
@@ -966,8 +1033,10 @@ void BetaCudaDeviceInterface::make_frame_standalone(UniqueAVFrame& av_frame) {
9661033}
9671034
9681035bool BetaCudaDeviceInterface::is_device_frame (
969- const UniqueAVFrame& av_frame) const {
970- return !is_cpu_fallback (av_frame->format );
1036+ [[maybe_unused]] const UniqueAVFrame& av_frame) const {
1037+ // Only reached through a PacketDecoder, i.e. in decoder-only mode, where
1038+ // whether we decoded on the GPU is decided once for the whole stream.
1039+ return !decoding_on_cpu_;
9711040}
9721041
9731042void BetaCudaDeviceInterface::flush () {
@@ -1139,7 +1208,21 @@ void BetaCudaDeviceInterface::convert_av_frame_to_frame_output(
11391208 FrameOutput& frame_output,
11401209 std::optional<torch::stable::Tensor> pre_allocated_output_tensor) {
11411210 CudaContextGuard context_guard (device_.index ());
1142- bool cpu_fallback = is_cpu_fallback (av_frame.format );
1211+
1212+ // In ColorConverterOnly mode the frame comes from a PacketDecoder, which
1213+ // recorded where its samples live and which stream produced them. Otherwise
1214+ // we're the interface that decoded it, and know first-hand.
1215+ const StandAloneFrameAttachedData* attached_data = nullptr ;
1216+ if (mode () == Mode::ColorConverterOnly) {
1217+ STD_TORCH_CHECK (
1218+ av_frame.opaque_ref != nullptr ,
1219+ " ColorConverter received a non-standalone frame; frames fed to a "
1220+ " standalone ColorConverter must come from a PacketDecoder." );
1221+ attached_data = reinterpret_cast <const StandAloneFrameAttachedData*>(
1222+ av_frame.opaque_ref ->data );
1223+ }
1224+ bool cpu_fallback =
1225+ attached_data ? !attached_data->is_device_frame : decoding_on_cpu_;
11431226
11441227 if (cpu_fallback) {
11451228 // When the CPU fallback happens, we'll try to run the color-conversion on
@@ -1190,23 +1273,15 @@ void BetaCudaDeviceInterface::convert_av_frame_to_frame_output(
11901273 }
11911274 const AVFrame& gpu_frame = cpu_fallback ? *transferred_frame : av_frame;
11921275
1276+ auto gpu_pix_fmt = static_cast <AVPixelFormat>(gpu_frame.format );
11931277 STD_TORCH_CHECK (
1194- gpu_frame. format == AV_PIX_FMT_NV12 ||
1195- is_nvdec_16bit_surface (gpu_frame. format ) ,
1196- " Expected NV12 or 16-bit semi-planar format frame " );
1278+ is_expected_pix_fmt_from_nvdec (gpu_pix_fmt),
1279+ " Expected a pixel format we can color-convert on the GPU, got " ,
1280+ av_get_pix_fmt_name (gpu_pix_fmt) );
11971281
1198- cudaStream_t producer_stream;
1199- if (mode () == Mode::ColorConverterOnly) {
1200- STD_TORCH_CHECK (
1201- av_frame.opaque_ref != nullptr ,
1202- " ColorConverter received a non-standalone frame; frames fed to a "
1203- " standalone ColorConverter must come from a PacketDecoder." );
1204- auto attached_data = reinterpret_cast <StandAloneFrameAttachedData*>(
1205- av_frame.opaque_ref ->data );
1206- producer_stream = attached_data->producer_stream ;
1207- } else {
1208- producer_stream = get_current_cuda_stream (device_.index ());
1209- }
1282+ cudaStream_t producer_stream = attached_data
1283+ ? attached_data->producer_stream
1284+ : get_current_cuda_stream (device_.index ());
12101285
12111286 auto convert_frame = [&](std::optional<torch::stable::Tensor> pre_alloc)
12121287 -> torch::stable::Tensor {
@@ -1266,12 +1341,9 @@ void BetaCudaDeviceInterface::apply_rotation(
12661341}
12671342
12681343OutputDtype BetaCudaDeviceInterface::get_pre_allocation_dtype (
1269- OutputDtype requested_dtype) const {
1270- if (requested_dtype == OutputDtype::FLOAT32 &&
1271- surface_format_ == cudaVideoSurfaceFormat_NV12) {
1272- return OutputDtype::UINT8 ;
1273- }
1274- return requested_dtype;
1344+ [[maybe_unused]] OutputDtype requested_dtype) const {
1345+ return is_16bit_surface_format (surface_format_) ? OutputDtype::FLOAT32
1346+ : OutputDtype::UINT8 ;
12751347}
12761348
12771349std::string BetaCudaDeviceInterface::get_details () {
0 commit comments