@@ -967,13 +967,15 @@ UniqueAVFrame BetaCudaDeviceInterface::convert_cuda_frame_to_av_frame(
967967 ? AVCOL_RANGE_JPEG
968968 : AVCOL_RANGE_MPEG ;
969969
970- // NVDEC lays the chroma planes out after the Y plane, all with the same
971- // pitch. The Y plane has an even number of rows (NVDEC rounds up internally),
972- // so the offsets must use the rounded-up height.
973- unsigned int even_height = round_up_to_even (height);
970+ // NVDEC stacks the planes in a single allocation, all with the same pitch,
971+ // and it rounds the Y plane's row count up to even. So consecutive planes
972+ // start plane_stride bytes apart, which is more than pitch * height for an
973+ // odd-height frame. NVIDIA's own NvDecoder addresses the chroma plane the
974+ // same way: dpSrcFrame + srcPitch * ((surface_height + 1) & ~1).
975+ unsigned int num_luma_plane_rows = round_up_to_even (height);
976+ unsigned int plane_stride = pitch * num_luma_plane_rows;
974977 auto plane = [&](unsigned int index) {
975- return reinterpret_cast <uint8_t *>(
976- frame_ptr + (pitch * even_height * index));
978+ return reinterpret_cast <uint8_t *>(frame_ptr + (plane_stride * index));
977979 };
978980 bool is_444 = is_444_surface_format (surface_format_);
979981
@@ -1065,16 +1067,17 @@ torch::stable::Tensor BetaCudaDeviceInterface::copy_nvdec_surface(
10651067 // = num_pixels + num_pixels / 2
10661068 // = num_pixels * 3 / 2
10671069 //
1068- // where num_pixels = pitch * height, not num_pixels = width * height. The
1069- // pitch value also accounts for the data size (uint8 vs uint16) so this is
1070- // also correct for P016. A 4:4:4 surface has two full-size chroma planes
1071- // instead of one half-height one, so it's num_pixels * 3.
1072- int64_t even_height =
1070+ // where num_pixels = pitch * num_luma_plane_rows, not width * height: the
1071+ // pitch accounts for both the row padding and the data size (uint8 vs
1072+ // uint16), and NVDEC rounds the Y plane's row count up to even. A 4:4:4
1073+ // surface has two full-size chroma planes instead of one half-height one, so
1074+ // it's num_pixels * 3.
1075+ int64_t num_luma_plane_rows =
10731076 static_cast <int64_t >(round_up_to_even (av_frame->height ));
10741077 int64_t pitch = static_cast <int64_t >(av_frame->linesize [0 ]);
10751078 bool is_444 = is_444_surface_format (surface_format_);
1076- int64_t num_bytes =
1077- is_444 ? pitch * even_height * 3 : pitch * even_height * 3 / 2 ;
1079+ int64_t num_bytes = is_444 ? pitch * num_luma_plane_rows * 3
1080+ : pitch * num_luma_plane_rows * 3 / 2 ;
10781081
10791082 auto storage =
10801083 torch::stable::empty ({num_bytes}, kStableUInt8 , std::nullopt , device_);
@@ -1103,10 +1106,11 @@ torch::stable::Tensor BetaCudaDeviceInterface::copy_nvdec_surface(
11031106 record_surface_read (current_stream);
11041107
11051108 auto y_plane = static_cast <uint8_t *>(storage.mutable_data_ptr ());
1109+ int64_t plane_stride = pitch * num_luma_plane_rows;
11061110 av_frame->data [0 ] = y_plane;
1107- av_frame->data [1 ] = y_plane + (pitch * even_height) ;
1111+ av_frame->data [1 ] = y_plane + plane_stride ;
11081112 if (is_444) {
1109- av_frame->data [2 ] = y_plane + (2 * pitch * even_height );
1113+ av_frame->data [2 ] = y_plane + (2 * plane_stride );
11101114 }
11111115
11121116 return storage;
@@ -1183,40 +1187,44 @@ GpuFrameAndStorage BetaCudaDeviceInterface::upload_cpu_frame_to_gpu(
11831187 int num_planes = semi_planar_420 ? 2 : 3 ;
11841188 int bytes_per_sample = want_16bit ? 2 : 1 ;
11851189
1186- // The 4:2:0 kernel works on 2x2 blocks and skips any trailing odd row or
1187- // column, so for those targets we round the frame up to even dimensions and
1188- // let the color conversion crop the result back. Nothing about the pixel
1189- // format itself requires this: FFmpeg is happy with odd-sized 4:2:0 frames.
1190- // The 4:4:4 kernel is per-pixel, so those are uploaded at their exact size.
1191- // TODO_API_BREAKDOW P1: Wait errrr does that mean we don't need this crop
1192- // dance anymore?? Should check!!!
1190+ // The 4:2:0 kernel works on 2x2 blocks and never writes a trailing odd row or
1191+ // column, so its input planes must be even-sized. We allocate the buffer with
1192+ // even dimensions but keep the frame's real width and height, exactly like
1193+ // the even-sized surfaces NVDEC hands us for odd-sized videos: the pad
1194+ // row/column is only read as part of a boundary block, and the color
1195+ // conversion crops it away. Nothing about the pixel format itself requires
1196+ // this: FFmpeg is happy with odd-sized 4:2:0 frames. The 4:4:4 kernel is
1197+ // per-pixel, so those need no padding.
11931198 int width = cpu_frame.width ;
11941199 int height = cpu_frame.height ;
1195- int target_width = semi_planar_420 ? round_up_to_even (width) : width;
1196- int target_height = semi_planar_420 ? round_up_to_even (height) : height;
1200+ int padded_width = semi_planar_420 ? round_up_to_even (width) : width;
1201+ int padded_height = semi_planar_420 ? round_up_to_even (height) : height;
11971202
11981203 UniqueAVFrame intermediate_cpu_frame (av_frame_alloc ());
11991204 STD_TORCH_CHECK (
12001205 intermediate_cpu_frame != nullptr ,
12011206 " Failed to allocate intermediate CPU frame" );
12021207
12031208 intermediate_cpu_frame->format = target_pix_fmt;
1204- intermediate_cpu_frame->width = target_width ;
1205- intermediate_cpu_frame->height = target_height ;
1209+ intermediate_cpu_frame->width = padded_width ;
1210+ intermediate_cpu_frame->height = padded_height ;
12061211
12071212 int ret = av_frame_get_buffer (intermediate_cpu_frame.get (), 0 );
12081213 STD_TORCH_CHECK (
12091214 ret >= 0 ,
12101215 " Failed to allocate intermediate CPU frame buffer: " ,
12111216 get_ffmpeg_error_string_from_error_code (ret));
12121217
1218+ // Source and destination dimensions are the same: this is a pixel format
1219+ // conversion, not a rescale. sws_scale() writes into the even-sized buffer
1220+ // allocated above but only fills the real width and height.
12131221 SwsConfig sws_config (
12141222 width,
12151223 height,
12161224 static_cast <AVPixelFormat>(cpu_frame.format ),
12171225 cpu_frame.colorspace ,
1218- target_width ,
1219- target_height ,
1226+ width ,
1227+ height ,
12201228 target_pix_fmt);
12211229
12221230 if (!sws_context_ || prev_sws_config_ != sws_config) {
@@ -1233,16 +1241,16 @@ GpuFrameAndStorage BetaCudaDeviceInterface::upload_cpu_frame_to_gpu(
12331241 intermediate_cpu_frame->data ,
12341242 intermediate_cpu_frame->linesize );
12351243 STD_TORCH_CHECK (
1236- converted_height == target_height ,
1244+ converted_height == height ,
12371245 " sws_scale failed for the CPU-fallback upload conversion" );
12381246
12391247 // The chroma plane of a semi-planar 4:2:0 frame carries interleaved UV pairs,
12401248 // so it's as wide as the luma plane but half as tall.
1241- int row_bytes = target_width * bytes_per_sample;
1249+ int row_bytes = padded_width * bytes_per_sample;
12421250 int plane_heights[3 ] = {
1243- target_height ,
1244- semi_planar_420 ? target_height / 2 : target_height ,
1245- target_height };
1251+ padded_height ,
1252+ semi_planar_420 ? padded_height / 2 : padded_height ,
1253+ padded_height };
12461254
12471255 int64_t plane_offsets[3 ] = {0 , 0 , 0 };
12481256 int64_t total_bytes = 0 ;
@@ -1260,8 +1268,8 @@ GpuFrameAndStorage BetaCudaDeviceInterface::upload_cpu_frame_to_gpu(
12601268 STD_TORCH_CHECK (gpu_frame != nullptr , " Failed to allocate GPU AVFrame" );
12611269
12621270 gpu_frame->format = target_pix_fmt;
1263- gpu_frame->width = target_width ;
1264- gpu_frame->height = target_height ;
1271+ gpu_frame->width = width ;
1272+ gpu_frame->height = height ;
12651273
12661274 // One copy per plane: av_frame_get_buffer() allocates each plane with its own
12671275 // alignment padding, so they are neither contiguous with each other nor
@@ -1317,10 +1325,6 @@ void BetaCudaDeviceInterface::convert_av_frame_to_frame_output(
13171325 CudaContextGuard context_guard (device_.index ());
13181326 cudaStream_t current_stream = get_current_cuda_stream (device_.index ());
13191327
1320- // Capture original dimensions before upload_cpu_frame_to_gpu() may round them
1321- // up to even.
1322- FrameDims original_dims (av_frame.height , av_frame.width );
1323-
13241328 // We may need to upload a frame here in case of the CPU fallback. This is
13251329 // only needed in Both() mode i.e. with the SingleStreamDecoder. The reason we
13261330 // do it here and not just after decoding is because the `decode_av_frame()`
@@ -1349,6 +1353,11 @@ void BetaCudaDeviceInterface::convert_av_frame_to_frame_output(
13491353 }
13501354 const AVFrame& gpu_frame = needs_upload ? *uploaded.av_frame : av_frame;
13511355
1356+ // Both NVDEC surfaces and uploaded CPU frames may be backed by even-sized
1357+ // buffers while describing an odd-sized frame; the color conversion crops the
1358+ // padding away.
1359+ FrameDims output_dims (gpu_frame.height , gpu_frame.width );
1360+
13521361 auto gpu_pix_fmt = static_cast <AVPixelFormat>(gpu_frame.format );
13531362 STD_TORCH_CHECK (
13541363 is_expected_pix_fmt_from_nvdec (gpu_pix_fmt),
@@ -1382,14 +1391,14 @@ void BetaCudaDeviceInterface::convert_av_frame_to_frame_output(
13821391 device_,
13831392 producer_stream,
13841393 pre_alloc,
1385- original_dims ,
1394+ output_dims ,
13861395 static_cast <AVPixelFormat>(gpu_frame.format ),
13871396 cached_color_matrix_);
13881397 };
13891398
13901399 if (rotation_ == Rotation::NONE ) {
13911400 validate_pre_allocated_tensor_shape (
1392- pre_allocated_output_tensor, original_dims );
1401+ pre_allocated_output_tensor, output_dims );
13931402 frame_output.data = convert_frame (pre_allocated_output_tensor);
13941403 } else {
13951404 // preAllocatedOutputTensor has post-rotation dimensions, but the
0 commit comments