@@ -76,6 +76,14 @@ cudaVideoSurfaceFormat get_preferred_surface_format(OutputDtype output_dtype) {
7676 : cudaVideoSurfaceFormat_NV12;
7777}
7878
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+ bool is_cpu_fallback (int format) {
84+ return format != AV_PIX_FMT_NV12 && format != AV_PIX_FMT_P016LE ;
85+ }
86+
7987static bool g_cuda_nvdec = register_device_interface(
8088 DeviceInterfaceKey (kStableCUDA , /* variant=*/ " default" ),
8189 [](const StableDevice& device) {
@@ -324,6 +332,17 @@ BetaCudaDeviceInterface::BetaCudaDeviceInterface(const StableDevice& device)
324332 nvcuvid_available_ = load_nvcuvid_library ();
325333}
326334
335+ BetaCudaDeviceInterface::Mode BetaCudaDeviceInterface::mode () const {
336+ if (decoding_initialized_ && color_conversion_initialized_) {
337+ return Mode::Both;
338+ } else if (decoding_initialized_) {
339+ return Mode::DecoderOnly;
340+ } else if (color_conversion_initialized_) {
341+ return Mode::ColorConverterOnly;
342+ }
343+ return Mode::Uninitialized;
344+ }
345+
327346void BetaCudaDeviceInterface::initialize_color_conversion (
328347 const VideoStreamOptions& video_stream_options,
329348 const std::vector<std::unique_ptr<Transform>>& transforms,
@@ -333,6 +352,7 @@ void BetaCudaDeviceInterface::initialize_color_conversion(
333352 cpu_fallback_->initialize_color_conversion (
334353 video_stream_options, transforms, resized_output_dims);
335354 }
355+ color_conversion_initialized_ = true ;
336356}
337357
338358void BetaCudaDeviceInterface::initialize_video_decoding (
@@ -341,6 +361,7 @@ void BetaCudaDeviceInterface::initialize_video_decoding(
341361 const VideoStreamOptions& video_stream_options) {
342362 STD_TORCH_CHECK (av_stream != nullptr , " AVStream cannot be null" );
343363 CudaContextGuard context_guard (device_.index ());
364+ decoding_initialized_ = true ;
344365 rotation_ = rotation_from_degrees (get_rotation_from_stream (av_stream));
345366 output_dtype_ = video_stream_options.output_dtype ;
346367
@@ -867,56 +888,62 @@ void BetaCudaDeviceInterface::make_frame_standalone(UniqueAVFrame& av_frame) {
867888 // StandAloneFrameAttachedData struct, which is then used by the
868889 // ColorConverter in convert_cuda_frame_to_av_frame() to perform the
869890 // color-conversion correctly.
891+ // The above is mainly relevant for GPU frames, but the CPU frames (in case of
892+ // a fallback) are still handled here for consistency.
893+ STD_TORCH_CHECK (
894+ mode () == Mode::DecoderOnly,
895+ " make_frame_standalone() is only valid in decoder-only mode: standalone "
896+ " frames are meant to be consumed by a separate ColorConverter." );
870897 CudaContextGuard context_guard (device_.index ());
871- if (!(av_frame->format == AV_PIX_FMT_P016LE ||
872- av_frame->format == AV_PIX_FMT_NV12 )) {
873- // The CPU frames are already standalone, so we don't need to do anything.
874- return ;
875- }
876-
877- // The amount of bytes an NV12 image takes is:
878- // num_bytes = len(Y) + len(UV)
879- // = num_pixels + num_pixels / 2
880- // = num_pixels * 3 / 2
881- //
882- // where num_pixels = pitch * height, not num_pixels = width * height. The
883- // pitch value also accounts for the data size (uint8 vs uint16) so this is
884- // also correct for P016.
885- int64_t even_height =
886- static_cast <int64_t >(round_up_to_even (av_frame->height ));
887- int64_t pitch = static_cast <int64_t >(av_frame->linesize [0 ]);
888- int64_t num_bytes = pitch * even_height * 3 / 2 ;
889-
890- auto storage =
891- torch::stable::empty ({num_bytes}, kStableUInt8 , std::nullopt , device_);
892-
893- // TODO_API_BREAKDOWN_CUDA P1: I suspect we don't need to wait on the nvdec
894- // stream here, because we can only arrive here from a path where the frame
895- // has already been mapped so its data is available - worth double checking.
896898 cudaStream_t current_stream = get_current_cuda_stream (device_.index ());
897- cudaError_t err = cudaMemcpyAsync (
898- storage.mutable_data_ptr (),
899- av_frame->data [0 ],
900- static_cast <size_t >(num_bytes),
901- cudaMemcpyDeviceToDevice,
902- current_stream);
903- STD_TORCH_CHECK (
904- err == cudaSuccess,
905- " Failed to copy NVDEC surface: " ,
906- cudaGetErrorString (err));
907-
908- // TODO_API_BREAKDOWN_CUDA P2: Should we unmap here? Or let the next
909- // receive_frame() call do it?
910- // unmap_previous_frame();
911-
912- auto y_plane = static_cast <uint8_t *>(storage.mutable_data_ptr ());
913- av_frame->data [0 ] = y_plane;
914- av_frame->data [1 ] = y_plane + (pitch * even_height);
915899
916900 auto attached_data = new StandAloneFrameAttachedData ();
917901 attached_data->producer_stream = current_stream;
918- // TODO_API_BREAKDOWN_CUDA P2: We don't *really* need to std::move it I guess?
919- attached_data->storage = std::move (storage);
902+
903+ if (!is_cpu_fallback (av_frame->format )) {
904+ // The amount of bytes an NV12 image takes is:
905+ // num_bytes = len(Y) + len(UV)
906+ // = num_pixels + num_pixels / 2
907+ // = num_pixels * 3 / 2
908+ //
909+ // where num_pixels = pitch * height, not num_pixels = width * height. The
910+ // pitch value also accounts for the data size (uint8 vs uint16) so this is
911+ // also correct for P016.
912+ int64_t even_height =
913+ static_cast <int64_t >(round_up_to_even (av_frame->height ));
914+ int64_t pitch = static_cast <int64_t >(av_frame->linesize [0 ]);
915+ int64_t num_bytes = pitch * even_height * 3 / 2 ;
916+
917+ auto storage =
918+ torch::stable::empty ({num_bytes}, kStableUInt8 , std::nullopt , device_);
919+
920+ // TODO_API_BREAKDOWN_CUDA P1: I suspect we don't need to wait on the nvdec
921+ // stream here, because we can only arrive here from a path where the frame
922+ // has already been mapped so its data is available - worth double checking.
923+ cudaError_t err = cudaMemcpyAsync (
924+ storage.mutable_data_ptr (),
925+ av_frame->data [0 ],
926+ static_cast <size_t >(num_bytes),
927+ cudaMemcpyDeviceToDevice,
928+ current_stream);
929+ STD_TORCH_CHECK (
930+ err == cudaSuccess,
931+ " Failed to copy NVDEC surface: " ,
932+ cudaGetErrorString (err));
933+
934+ // TODO_API_BREAKDOWN_CUDA P2: Should we unmap here? Or let the next
935+ // receive_frame() call do it?
936+ // unmap_previous_frame();
937+
938+ auto y_plane = static_cast <uint8_t *>(storage.mutable_data_ptr ());
939+ av_frame->data [0 ] = y_plane;
940+ av_frame->data [1 ] = y_plane + (pitch * even_height);
941+
942+ // TODO_API_BREAKDOWN_CUDA P2: We don't *really* need to std::move it I
943+ // guess?
944+ attached_data->storage = std::move (storage);
945+ }
946+
920947 av_frame->opaque_ref = av_buffer_create (
921948 reinterpret_cast <uint8_t *>(attached_data),
922949 sizeof (StandAloneFrameAttachedData),
@@ -1094,11 +1121,7 @@ void BetaCudaDeviceInterface::convert_av_frame_to_frame_output(
10941121 FrameOutput& frame_output,
10951122 std::optional<torch::stable::Tensor> pre_allocated_output_tensor) {
10961123 CudaContextGuard context_guard (device_.index ());
1097- // TODO_API_BREAKDOWN_CUDA P0 is that accurate and safe? Can there be a CPU
1098- // NV12 frame in our code? Should we create a helper used in the
1099- // make_standalone function too?
1100- bool cpu_fallback = av_frame.format != AV_PIX_FMT_NV12 &&
1101- av_frame.format != AV_PIX_FMT_P016LE ;
1124+ bool cpu_fallback = is_cpu_fallback (av_frame.format );
11021125
11031126 if (cpu_fallback) {
11041127 // When the CPU fallback happens, we'll try to run the color-conversion on
@@ -1139,6 +1162,10 @@ void BetaCudaDeviceInterface::convert_av_frame_to_frame_output(
11391162 AVPixelFormat target_pix_fmt = (output_dtype_ == OutputDtype::FLOAT32 )
11401163 ? AV_PIX_FMT_P016LE
11411164 : AV_PIX_FMT_NV12 ;
1165+ // TODO_API_BREAKDOWN P1: we should do this before the color-conversion,
1166+ // right? We want the PacketDecoder to return a GPU frame! This will
1167+ // probably become immediately relevant once we start outputting raw YUV
1168+ // data.
11421169 transferred_frame = transfer_cpu_frame_to_gpu (av_frame, target_pix_fmt);
11431170 }
11441171 const AVFrame& gpu_frame = cpu_fallback ? *transferred_frame : av_frame;
@@ -1148,12 +1175,12 @@ void BetaCudaDeviceInterface::convert_av_frame_to_frame_output(
11481175 gpu_frame.format == AV_PIX_FMT_P016LE ,
11491176 " Expected NV12 or P016LE format frame" );
11501177
1151- // TODO_API_BREAKDOWN P1: Cleanup how we get the attached data? Make it more
1152- // robust? Should we couple it to a flag on the interface saying "I'm
1153- // color-conversion only, I absolutely expect frames to be standalone"?
11541178 cudaStream_t producer_stream;
1155- if (av_frame.opaque_ref != nullptr &&
1156- av_frame.opaque_ref ->size == sizeof (StandAloneFrameAttachedData)) {
1179+ if (mode () == Mode::ColorConverterOnly) {
1180+ STD_TORCH_CHECK (
1181+ av_frame.opaque_ref != nullptr ,
1182+ " ColorConverter received a non-standalone frame; frames fed to a "
1183+ " standalone ColorConverter must come from a PacketDecoder." );
11571184 auto attached_data = reinterpret_cast <StandAloneFrameAttachedData*>(
11581185 av_frame.opaque_ref ->data );
11591186 producer_stream = attached_data->producer_stream ;
0 commit comments