Skip to content

Commit 4559cb7

Browse files
authored
Expand native NVDEC decoding coverage (#1635)
1 parent 1ebfad0 commit 4559cb7

13 files changed

Lines changed: 519 additions & 172 deletions

src/torchcodec/_core/BetaCudaDeviceInterface.cpp

Lines changed: 131 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -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

90125
static 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

9681035
bool 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

9731042
void 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

12681343
OutputDtype 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

12771349
std::string BetaCudaDeviceInterface::get_details() {

src/torchcodec/_core/BetaCudaDeviceInterface.h

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ namespace facebook::torchcodec {
3636
struct StandAloneFrameAttachedData {
3737
cudaStream_t producer_stream = nullptr;
3838
torch::stable::Tensor storage;
39+
// Whether the frame's samples are on the GPU. False for the CPU-fallback
40+
// frames a PacketDecoder hands out for streams NVDEC can't decode. The
41+
// pixel format can't tell the two apart: a 4:4:4 stream yields yuv444p
42+
// either way, natively from NVDEC or from the CPU fallback.
43+
bool is_device_frame = false;
3944
};
4045

4146
class BetaCudaDeviceInterface : public DeviceInterface {
@@ -56,7 +61,7 @@ class BetaCudaDeviceInterface : public DeviceInterface {
5661
const std::optional<FrameDims>& resized_output_dims) override;
5762

5863
OutputDtype get_pre_allocation_dtype(
59-
OutputDtype requested_dtype) const override;
64+
[[maybe_unused]] OutputDtype requested_dtype) const override;
6065

6166
void convert_av_frame_to_frame_output(
6267
const AVFrame& av_frame,
@@ -103,7 +108,8 @@ class BetaCudaDeviceInterface : public DeviceInterface {
103108

104109
void make_frame_standalone(UniqueAVFrame& av_frame) override;
105110

106-
bool is_device_frame(const UniqueAVFrame& av_frame) const override;
111+
bool is_device_frame(
112+
[[maybe_unused]] const UniqueAVFrame& av_frame) const override;
107113

108114
UniqueAVFrame transfer_cpu_frame_to_gpu(
109115
const AVFrame& cpu_frame,

src/torchcodec/_core/FFMPEGCommon.cpp

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,44 +16,40 @@ extern "C" {
1616

1717
namespace facebook::torchcodec {
1818

19+
// The AVPixelFormat describing an NVDEC surface for a given source bit depth.
20+
// bit_depth is the source's, and only matters for the 16-bit containers.
21+
// FFmpeg < 6 has no P012LE. P016LE describes the same samples just as
22+
// validly: they're msb-aligned, so a 12-bit surface is a 16-bit one with
23+
// 4 zeroed low bits.
24+
AVPixelFormat nvdec_pix_fmt(NvdecSurface surface, int bit_depth) {
25+
switch (surface) {
26+
case NvdecSurface::NV12:
27+
return AV_PIX_FMT_NV12;
28+
case NvdecSurface::YUV444:
29+
return AV_PIX_FMT_YUV444P;
30+
case NvdecSurface::YUV444_16Bit:
31+
return AV_PIX_FMT_YUV444P16LE;
32+
case NvdecSurface::P016:
33+
if (bit_depth == 10) {
34+
return AV_PIX_FMT_P010LE;
35+
}
1936
#if FFMPEG_HAS_P012
20-
// takes is_p016_surface as input instead of the actual NVDEC surface type so we
21-
// don't have to include the NVDEC headers here
22-
AVPixelFormat nvdec_pix_fmt(bool is_p016_surface, int bit_depth) {
23-
if (!is_p016_surface) {
24-
return AV_PIX_FMT_NV12;
25-
}
26-
switch (bit_depth) {
27-
case 10:
28-
return AV_PIX_FMT_P010LE;
29-
case 12:
30-
return AV_PIX_FMT_P012LE;
31-
default:
37+
if (bit_depth == 12) {
38+
return AV_PIX_FMT_P012LE;
39+
}
40+
#endif
3241
return AV_PIX_FMT_P016LE;
3342
}
43+
return AV_PIX_FMT_NV12;
3444
}
35-
#else
36-
AVPixelFormat nvdec_pix_fmt(bool is_p016_surface, int bit_depth) {
37-
// TODO_API_BREAKDOWN P2: needs a comment about P012 missing and why it's
38-
// still OK to return P016LE.
39-
if (!is_p016_surface) {
40-
return AV_PIX_FMT_NV12;
41-
}
42-
return bit_depth == 10 ? AV_PIX_FMT_P010LE : AV_PIX_FMT_P016LE;
43-
}
44-
#endif // FFMPEG_HAS_P012
4545

46+
bool is_nvdec_16bit_pix_fmt(int format) {
47+
return format == AV_PIX_FMT_P010LE || format == AV_PIX_FMT_P016LE ||
4648
#if FFMPEG_HAS_P012
47-
bool is_nvdec_16bit_surface(int format) {
48-
return format == AV_PIX_FMT_P010LE || format == AV_PIX_FMT_P012LE ||
49-
format == AV_PIX_FMT_P016LE;
50-
}
51-
#else
52-
53-
bool is_nvdec_16bit_surface(int format) {
54-
return format == AV_PIX_FMT_P010LE || format == AV_PIX_FMT_P016LE;
55-
}
49+
format == AV_PIX_FMT_P012LE ||
5650
#endif
51+
false;
52+
}
5753

5854
OutputDtype resolve_output_dtype(
5955
OutputDtypeConfig output_dtype_config,

0 commit comments

Comments
 (0)