Skip to content

Commit 797d188

Browse files
committed
Support raw frame as output instead of RGB
1 parent e3c66fb commit 797d188

12 files changed

Lines changed: 470 additions & 20 deletions

src/torchcodec/_core/BetaCudaDeviceInterface.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -843,6 +843,11 @@ void BetaCudaDeviceInterface::make_frame_self_contained(
843843
info->storage = std::move(storage);
844844
}
845845

846+
bool BetaCudaDeviceInterface::is_device_frame(
847+
const UniqueAVFrame& av_frame) const {
848+
return get_nvdec_frame_info(av_frame) != nullptr;
849+
}
850+
846851
void BetaCudaDeviceInterface::unmap_previous_frame() {
847852
if (previously_mapped_frame_ == 0) {
848853
return;

src/torchcodec/_core/BetaCudaDeviceInterface.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ class BetaCudaDeviceInterface : public DeviceInterface {
8787

8888
void make_frame_self_contained(UniqueAVFrame& av_frame) override;
8989

90+
bool is_device_frame(const UniqueAVFrame& av_frame) const override;
91+
9092
OutputDtype get_pre_allocation_dtype(
9193
OutputDtype requested_dtype) const override;
9294

src/torchcodec/_core/DeviceInterface.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,15 @@ class DeviceInterface {
8383
virtual void make_frame_self_contained(
8484
[[maybe_unused]] UniqueAVFrame& av_frame) {}
8585

86+
// Whether av_frame's pixel data lives on this interface's device rather than
87+
// in host memory. This is a property of the frame, not of the interface: a
88+
// hardware interface that had to fall back to CPU decoding hands out frames
89+
// in host memory.
90+
virtual bool is_device_frame(
91+
[[maybe_unused]] const UniqueAVFrame& av_frame) const {
92+
return false;
93+
}
94+
8695
// Initialize the device with parameters specific to audio decoding. There is
8796
// a default empty implementation.
8897
virtual void initialize_audio(

src/torchcodec/_core/PacketDecoder.cpp

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
#include "StreamOptions.h"
1212
#include "Transform.h"
1313

14+
extern "C" {
15+
#include <libavutil/pixdesc.h>
16+
}
17+
1418
namespace facebook::torchcodec {
1519

1620
// TODO_API_BREAKDOWN: we should make sure the block APIs can dispatch to
@@ -64,7 +68,8 @@ PacketDecoder::PacketDecoder(
6468
const Demuxer& demuxer,
6569
const StableDevice& device,
6670
std::string_view device_variant,
67-
std::optional<int> ffmpeg_thread_count) {
71+
std::optional<int> ffmpeg_thread_count,
72+
OutputDtype output_dtype) {
6873
device_interface_ = create_device_interface(device, device_variant);
6974
STD_TORCH_CHECK(
7075
device_interface_ != nullptr,
@@ -77,8 +82,12 @@ PacketDecoder::PacketDecoder(
7782
stream, av_codec, device_interface_.get(), ffmpeg_thread_count);
7883
device_interface_->initialize(codec_context_);
7984

85+
const AVPixFmtDescriptor* desc = av_pix_fmt_desc_get(codec_context_->pix_fmt);
86+
STD_TORCH_CHECK(desc != nullptr, "Unknown pixel format on stream");
87+
bit_depth_ = desc->comp[0].depth;
88+
8089
VideoStreamOptions options;
81-
options.output_dtype = OutputDtype::UINT8; // dtype not exposed yet
90+
options.output_dtype = output_dtype;
8291
options.device = device;
8392

8493
// Interfaces that implement their own decoding path (NVDEC) do all of their

src/torchcodec/_core/PacketDecoder.h

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ class FORCE_PUBLIC_VISIBILITY PacketDecoder {
3434
const Demuxer& demuxer,
3535
const StableDevice& device = StableDevice(kStableCPU),
3636
std::string_view device_variant = "default",
37-
std::optional<int> ffmpeg_thread_count = std::nullopt);
37+
std::optional<int> ffmpeg_thread_count = std::nullopt,
38+
OutputDtype output_dtype = OutputDtype::UINT8);
3839

3940
// Feed one packet to the decoder. Borrows `packet` (does not take ownership).
4041
int send_packet(AVPacket* packet);
@@ -44,15 +45,29 @@ class FORCE_PUBLIC_VISIBILITY PacketDecoder {
4445
// if more input is needed, AVERROR_EOF at end, or a negative error code.
4546
int receive_frame(UniqueAVFrame& av_frame);
4647

48+
// Whether the frame's pixel data is on this decoder's device or in host
49+
// memory. A CUDA decoder yields host frames for streams NVDEC can't handle.
50+
bool is_device_frame(const UniqueAVFrame& av_frame) const {
51+
return device_interface_->is_device_frame(av_frame);
52+
}
53+
4754
// The stream time base, used to convert frame pts/duration to seconds.
4855
AVRational time_base() const {
4956
return time_base_;
5057
}
5158

59+
// Significant bits per sample of the decoded frames. This is a property of
60+
// the stream, not of the frames: NVDEC decodes 10-bit content into 16-bit
61+
// P016 surfaces, whose pixel format alone would claim 16 bits.
62+
int bit_depth() const {
63+
return bit_depth_;
64+
}
65+
5266
private:
5367
std::unique_ptr<DeviceInterface> device_interface_;
5468
SharedAVCodecContext codec_context_;
5569
AVRational time_base_ = {};
70+
int bit_depth_ = 8;
5671
};
5772

5873
} // namespace facebook::torchcodec

src/torchcodec/_core/_ffmpeg_op_names.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@
3535
"_blocks_packet_decoder_send_packet",
3636
"_blocks_packet_decoder_send_eof",
3737
"_blocks_packet_decoder_receive_frame",
38+
"_blocks_packet_decoder_bit_depth",
3839
"_blocks_create_color_converter",
3940
"_blocks_convert_frame",
41+
"_blocks_frame_to_planes",
4042
"_test_frame_pts_equality",
4143
"_get_container_json_metadata",
4244
"_get_key_frame_indices",

src/torchcodec/_core/_ffmpeg_ops.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,14 @@ def add_video_stream(
108108
_blocks_packet_decoder_receive_frame = (
109109
torch.ops.torchcodec_ns._blocks_packet_decoder_receive_frame.default
110110
)
111+
_blocks_packet_decoder_bit_depth = (
112+
torch.ops.torchcodec_ns._blocks_packet_decoder_bit_depth.default
113+
)
111114
_blocks_create_color_converter = (
112115
torch.ops.torchcodec_ns._blocks_create_color_converter.default
113116
)
114117
_blocks_convert_frame = torch.ops.torchcodec_ns._blocks_convert_frame.default
118+
_blocks_frame_to_planes = torch.ops.torchcodec_ns._blocks_frame_to_planes.default
115119

116120
_test_frame_pts_equality = torch.ops.torchcodec_ns._test_frame_pts_equality.default
117121
_get_container_json_metadata = (

src/torchcodec/_core/custom_ops.cpp

Lines changed: 175 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,18 @@ STABLE_TORCH_LIBRARY_FRAGMENT(torchcodec_ns, m) {
7777
"_blocks_create_demuxer(str filename, int? stream_index=None) -> Tensor");
7878
m.def("_blocks_demuxer_next_packet(Tensor(a!) demuxer) -> (Tensor, bool)");
7979
m.def(
80-
"_blocks_create_packet_decoder(Tensor demuxer, *, int? num_threads=None, str device=\"cpu\", str device_variant=\"default\") -> Tensor");
80+
"_blocks_create_packet_decoder(Tensor demuxer, *, int? num_threads=None, str device=\"cpu\", str device_variant=\"default\", str output_dtype=\"uint8\") -> Tensor");
8181
m.def(
8282
"_blocks_packet_decoder_send_packet(Tensor(a!) decoder, Tensor packet) -> int");
8383
m.def("_blocks_packet_decoder_send_eof(Tensor(a!) decoder) -> int");
8484
m.def(
85-
"_blocks_packet_decoder_receive_frame(Tensor(a!) decoder) -> (Tensor, int, float, float)");
85+
"_blocks_packet_decoder_receive_frame(Tensor(a!) decoder) -> (Tensor, int, float, float, bool)");
86+
m.def("_blocks_packet_decoder_bit_depth(Tensor decoder) -> int");
8687
m.def(
8788
"_blocks_create_color_converter(str device=\"cpu\", str device_variant=\"default\") -> Tensor");
8889
m.def("_blocks_convert_frame(Tensor(a!) converter, Tensor frame) -> Tensor");
90+
m.def(
91+
"_blocks_frame_to_planes(Tensor frame, str device) -> (Tensor, Tensor, Tensor, Tensor, str)");
8992
m.def("_get_key_frame_indices(Tensor(a!) decoder) -> Tensor");
9093
m.def("get_json_metadata(Tensor(a!) decoder) -> str");
9194
m.def("get_container_json_metadata(Tensor(a!) decoder) -> str");
@@ -861,18 +864,33 @@ torch::stable::Tensor _blocks_create_packet_decoder(
861864
torch::stable::Tensor& demuxer,
862865
std::optional<int64_t> num_threads,
863866
std::string device,
864-
std::string device_variant) {
867+
std::string device_variant,
868+
std::string output_dtype) {
865869
Demuxer* demuxer_ptr = unwrap_tensor_to_pointer<Demuxer>(demuxer);
866870
validate_device_interface(device, device_variant);
867871
std::optional<int> thread_count;
868872
if (num_threads.has_value()) {
869873
thread_count = static_cast<int>(num_threads.value());
870874
}
875+
STD_TORCH_CHECK(
876+
output_dtype == "uint8" || output_dtype == "float32",
877+
"Invalid output_dtype=",
878+
output_dtype,
879+
". Supported values are 'uint8' and 'float32'.");
871880
auto decoder = std::make_unique<PacketDecoder>(
872-
*demuxer_ptr, StableDevice(device), device_variant, thread_count);
881+
*demuxer_ptr,
882+
StableDevice(device),
883+
device_variant,
884+
thread_count,
885+
output_dtype == "float32" ? OutputDtype::FLOAT32 : OutputDtype::UINT8);
873886
return wrap_pointer_to_tensor<PacketDecoder>(std::move(decoder));
874887
}
875888

889+
int64_t _blocks_packet_decoder_bit_depth(torch::stable::Tensor& decoder) {
890+
PacketDecoder* decoder_ptr = unwrap_tensor_to_pointer<PacketDecoder>(decoder);
891+
return static_cast<int64_t>(decoder_ptr->bit_depth());
892+
}
893+
876894
int64_t _blocks_packet_decoder_send_packet(
877895
torch::stable::Tensor& decoder,
878896
torch::stable::Tensor& packet) {
@@ -886,13 +904,15 @@ int64_t _blocks_packet_decoder_send_eof(torch::stable::Tensor& decoder) {
886904
return static_cast<int64_t>(decoder_ptr->send_eof());
887905
}
888906

889-
// (frame_handle, status, pts_seconds, duration_seconds). status == 0 means a
890-
// frame was produced; nonzero (EAGAIN/EOF) means no frame (dummy handle,
891-
// zeros). pts/duration are stamped here (the decoder knows the stream time
892-
// base) so the ColorConverter doesn't need to be bound to a stream. Native
893-
// scalars avoid per-frame .item() overhead.
907+
// (frame_handle, status, pts_seconds, duration_seconds, is_device_frame).
908+
// status == 0 means a frame was produced; nonzero (EAGAIN/EOF) means no frame
909+
// (dummy handle, zeros). pts/duration are stamped here (the decoder knows the
910+
// stream time base) so the ColorConverter doesn't need to be bound to a stream.
911+
// is_device_frame says whether the samples are in device or host memory, which
912+
// isn't implied by the decoder's device: a CUDA decoder yields host frames for
913+
// streams NVDEC can't handle. Native scalars avoid per-frame .item() overhead.
894914
using OpsReceiveFrameOutput =
895-
std::tuple<torch::stable::Tensor, int64_t, double, double>;
915+
std::tuple<torch::stable::Tensor, int64_t, double, double, bool>;
896916

897917
OpsReceiveFrameOutput _blocks_packet_decoder_receive_frame(
898918
torch::stable::Tensor& decoder) {
@@ -905,17 +925,20 @@ OpsReceiveFrameOutput _blocks_packet_decoder_receive_frame(
905925
torch::stable::full({1}, 0, kStableInt64),
906926
static_cast<int64_t>(status),
907927
0.0,
908-
0.0);
928+
0.0,
929+
false);
909930
}
910931
AVRational time_base = decoder_ptr->time_base();
911932
double pts_seconds = pts_to_seconds(get_pts_or_dts(av_frame), time_base);
912933
double duration_seconds = pts_to_seconds(get_duration(av_frame), time_base);
934+
bool is_device_frame = decoder_ptr->is_device_frame(av_frame);
913935
AVFrame* raw_frame = av_frame.release();
914936
return std::make_tuple(
915937
wrap_frame_pointer_to_tensor(raw_frame),
916938
static_cast<int64_t>(0),
917939
pts_seconds,
918-
duration_seconds);
940+
duration_seconds,
941+
is_device_frame);
919942
}
920943

921944
torch::stable::Tensor _blocks_create_color_converter(
@@ -927,6 +950,142 @@ torch::stable::Tensor _blocks_create_color_converter(
927950
return wrap_pointer_to_tensor<ColorConverter>(std::move(converter));
928951
}
929952

953+
// Zero-copy access to a decoded frame's own samples, whatever its pixel format:
954+
// planar (yuv420p), semi-planar (nv12, p016), packed (yuyv422, bgra), RGB
955+
// (gbrp), grayscale, with or without alpha. Every layout FFmpeg describes as
956+
// whole bytes per sample is just a strided view over the frame's memory, which
957+
// is what AVPixFmtDescriptor's per-component plane/offset/step encodes.
958+
//
959+
// Returns 4 component views (unused ones are empty) plus a JSON description
960+
// naming the components that are real.
961+
using OpsPlanesOutput = std::tuple<
962+
torch::stable::Tensor,
963+
torch::stable::Tensor,
964+
torch::stable::Tensor,
965+
torch::stable::Tensor,
966+
std::string>;
967+
968+
// The name of component `index` in a frame of format `desc`.
969+
const char* component_name(const AVPixFmtDescriptor* desc, int index) {
970+
bool is_rgb = (desc->flags & AV_PIX_FMT_FLAG_RGB) != 0;
971+
bool is_alpha = (desc->flags & AV_PIX_FMT_FLAG_ALPHA) != 0 &&
972+
index == desc->nb_components - 1;
973+
if (is_alpha) {
974+
return "A";
975+
}
976+
static const char* rgb_names[] = {"R", "G", "B"};
977+
static const char* yuv_names[] = {"Y", "U", "V"};
978+
return is_rgb ? rgb_names[index] : yuv_names[index];
979+
}
980+
981+
OpsPlanesOutput _blocks_frame_to_planes(
982+
torch::stable::Tensor& frame,
983+
std::string device) {
984+
AVFrame* av_frame = unwrap_tensor_to_frame(frame);
985+
auto pix_fmt = static_cast<AVPixelFormat>(av_frame->format);
986+
const AVPixFmtDescriptor* desc = av_pix_fmt_desc_get(pix_fmt);
987+
STD_TORCH_CHECK(desc != nullptr, "Unknown pixel format on decoded frame");
988+
const char* pix_fmt_name = av_get_pix_fmt_name(pix_fmt);
989+
std::string fmt_name = pix_fmt_name ? pix_fmt_name : "unknown";
990+
991+
// Sub-byte and palettised layouts can't be addressed by a tensor stride, and
992+
// there's no way to hand them over without unpacking them into a copy.
993+
STD_TORCH_CHECK(
994+
!(desc->flags & AV_PIX_FMT_FLAG_BITSTREAM),
995+
"Cannot expose ",
996+
fmt_name,
997+
" without a copy: its samples are not byte-aligned.");
998+
STD_TORCH_CHECK(
999+
!(desc->flags & AV_PIX_FMT_FLAG_PAL),
1000+
"Cannot expose ",
1001+
fmt_name,
1002+
" without a copy: it is palettised.");
1003+
STD_TORCH_CHECK(
1004+
!(desc->flags & AV_PIX_FMT_FLAG_FLOAT),
1005+
"Cannot expose ",
1006+
fmt_name,
1007+
": floating point pixel formats are not supported yet.");
1008+
STD_TORCH_CHECK(
1009+
desc->nb_components >= 1 && desc->nb_components <= 4,
1010+
fmt_name,
1011+
" has ",
1012+
static_cast<int>(desc->nb_components),
1013+
" components, expected between 1 and 4.");
1014+
1015+
StableDevice tensor_device(device);
1016+
std::vector<torch::stable::Tensor> views;
1017+
std::stringstream component_names;
1018+
1019+
for (int c = 0; c < desc->nb_components; ++c) {
1020+
const AVComponentDescriptor& comp = desc->comp[c];
1021+
STD_TORCH_CHECK(
1022+
comp.shift == 0 && comp.depth <= 16,
1023+
"Cannot expose component ",
1024+
c,
1025+
" of ",
1026+
fmt_name,
1027+
" without a copy: its samples don't start on a byte boundary.");
1028+
1029+
int64_t bytes_per_sample = (comp.depth > 8) ? 2 : 1;
1030+
int64_t linesize = av_frame->linesize[comp.plane];
1031+
STD_TORCH_CHECK(
1032+
linesize > 0,
1033+
"Cannot expose ",
1034+
fmt_name,
1035+
": the frame has a negative line size (it is stored bottom-up), "
1036+
"which a tensor stride cannot express.");
1037+
STD_TORCH_CHECK(
1038+
comp.step % bytes_per_sample == 0 && linesize % bytes_per_sample == 0,
1039+
"Cannot expose component ",
1040+
c,
1041+
" of ",
1042+
fmt_name,
1043+
": its byte layout isn't a whole number of samples.");
1044+
1045+
// Only the chroma components are subsampled; luma and alpha are full size.
1046+
bool is_chroma = !(desc->flags & AV_PIX_FMT_FLAG_RGB) && (c == 1 || c == 2);
1047+
int64_t height = is_chroma
1048+
? AV_CEIL_RSHIFT(av_frame->height, desc->log2_chroma_h)
1049+
: av_frame->height;
1050+
int64_t width = is_chroma
1051+
? AV_CEIL_RSHIFT(av_frame->width, desc->log2_chroma_w)
1052+
: av_frame->width;
1053+
1054+
// The view keeps the frame handle alive, so the planes stay valid even if
1055+
// the caller drops the frame they came from. Copying the handle tensor is
1056+
// just a refcount bump; the AVFrame is freed when the last view goes.
1057+
auto keep_frame_alive = frame;
1058+
int64_t sizes[] = {height, width};
1059+
int64_t strides[] = {
1060+
linesize / bytes_per_sample, comp.step / bytes_per_sample};
1061+
views.push_back(torch::stable::from_blob(
1062+
av_frame->data[comp.plane] + comp.offset,
1063+
{sizes, 2},
1064+
{strides, 2},
1065+
tensor_device,
1066+
(comp.depth > 8) ? kStableUInt16 : kStableUInt8,
1067+
[keep_frame_alive](void*) {}));
1068+
1069+
component_names << (c == 0 ? "\"" : ", \"") << component_name(desc, c)
1070+
<< "\"";
1071+
}
1072+
1073+
std::stringstream metadata;
1074+
metadata << "{\"pix_fmt\": \"" << fmt_name << "\", \"components\": ["
1075+
<< component_names.str() << "], \"container_bit_depth\": "
1076+
<< static_cast<int>(desc->comp[0].depth)
1077+
<< ", \"colorspace\": " << static_cast<int>(av_frame->colorspace)
1078+
<< ", \"color_range\": " << static_cast<int>(av_frame->color_range)
1079+
<< ", \"is_rgb\": "
1080+
<< ((desc->flags & AV_PIX_FMT_FLAG_RGB) ? "true" : "false") << "}";
1081+
1082+
// Absent components come back as empty tensors; `components` above says how
1083+
// many are real.
1084+
views.resize(4, torch::stable::empty({0}, kStableUInt8));
1085+
return std::make_tuple(
1086+
views[0], views[1], views[2], views[3], metadata.str());
1087+
}
1088+
9301089
torch::stable::Tensor _blocks_convert_frame(
9311090
torch::stable::Tensor& converter,
9321091
torch::stable::Tensor& frame) {
@@ -1503,7 +1662,11 @@ STABLE_TORCH_LIBRARY_IMPL(torchcodec_ns, CPU, m) {
15031662
m.impl(
15041663
"_blocks_packet_decoder_receive_frame",
15051664
TORCH_BOX(&_blocks_packet_decoder_receive_frame));
1665+
m.impl(
1666+
"_blocks_packet_decoder_bit_depth",
1667+
TORCH_BOX(&_blocks_packet_decoder_bit_depth));
15061668
m.impl("_blocks_convert_frame", TORCH_BOX(&_blocks_convert_frame));
1669+
m.impl("_blocks_frame_to_planes", TORCH_BOX(&_blocks_frame_to_planes));
15071670
m.impl("_test_frame_pts_equality", TORCH_BOX(&_test_frame_pts_equality));
15081671
m.impl(
15091672
"scan_all_streams_to_update_metadata",

0 commit comments

Comments
 (0)