Skip to content

Commit 4d1dc89

Browse files
YLouWashUfacebook-github-bot
authored andcommitted
{CUDA Decoding} Fix SW fallback bugs in xprsDecoderMaker, downgrade CUDA error logs, add XPRS_DISABLE_HW_DECODE
Summary: First diff in the GPU-accelerated H.265 decoding stack for projectaria-tools (see plan v11 at ~/gdrive/plans/2026-04-03-gpu-accelerated-h265-decoding-pat-v11.md and tech design at https://docs.google.com/document/d/1TMeLy0TqvAdlYo0IY3Qm4BrzE8i035z9MzYvGl_CzeM by Lou Yang). Cleans up two latent bugs in `xprsDecoderMaker` that prevent SW fallback on non-GPU machines, and reduces log spam users see when CUDA is unavailable. **Known limitation: NVDEC does not support small grayscale H.265 streams.** On Aria Gen2 recordings, the 200x200 eye-tracking and 512x512 SLAM cameras (encoded as grayscale H.265) produce `CUDA_ERROR_NOT_SUPPORTED` from `cuvidCreateDecoder` and fall back to the SW decoder. Only the 2016x1512 RGB camera stream uses the GPU path. The SW fallback machinery in this diff is what makes that graceful — without it, those streams would hard-fail. End-to-end on a 581 MB Aria Gen2 recording (RTX 5080, 580.159.03 driver), GPU vs CPU decode of the RGB stream measured 197.4 FPS vs 16.8 FPS (11.7x speedup, 300 frames). The aria_rerun_viewer load time on the same recording dropped from ~47s (CPU) to ~10s (GPU). Bug 1 (line 386 of arvr/libraries/vrs/utils/xprs/XprsDecoder.cpp): when an HW decoder's `init()` returns non-OK, the function returned `nullptr` instead of trying the next decoder in the preferred list. This kills the SW fallback path entirely. Fix: log at WARN and `continue` to the next decoder. Bug 2 (line 363 of the same file): when `xprs::enumDecoders()` returned non-OK, the function bailed out even though the function intentionally collects whatever decoders it managed to enumerate before any throw. On a non-GPU machine the HW decoder loop throws during CUDA init, but SW decoders were already collected. Fix: ignore the result code and only fail if the resulting list is empty. Log noise: also downgrades all `XR_LOGE` to `XR_LOGW` in `getNvCodecContext()` (cudaContextProvider.cpp) and the catch handler in `enumDecoders`/`enumDecodersByFormat` (xprsDecApi.cpp). On a non-GPU machine these fired every VRS file open even though callers handle the throw and fall back gracefully. Adds `XPRS_DISABLE_HW_DECODE` env var: setting it to any value forces CPU-only decoding by skipping all HW decoders during enumeration. Useful for deterministic results, working around GPU memory pressure, or comparing HW-vs-SW output. Read once into a static at first call, so the value is fixed for the process lifetime — runtime mutation has no effect. Same env var skip + same catch-handler downgrade is applied to both `enumDecoders` and `enumDecodersByFormat` (parallel functions with identical structure). Reviewed By: PiotrBrzyski Differential Revision: D103253728
1 parent 6e94fd3 commit 4d1dc89

4 files changed

Lines changed: 94 additions & 13 deletions

File tree

xprs/cudaContextProvider.cpp

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,15 @@
1414
* limitations under the License.
1515
*/
1616

17+
// DEFAULT_LOG_CHANNEL must be defined before "logging/Log.h": the OSS build's
18+
// XR_LOGE/XR_LOGW macros are gated on it (the internal logging header defines
19+
// them unconditionally, which is why this ordering bug is invisible to the Buck
20+
// build but breaks the OSS CMake build).
21+
#define DEFAULT_LOG_CHANNEL "XPRS"
22+
1723
#include "cudaContextProvider.h"
1824

1925
#include "logging/Log.h"
20-
#define DEFAULT_LOG_CHANNEL "XPRS"
2126

2227
namespace xprs {
2328

@@ -49,31 +54,35 @@ NvCodecContext NvCodecContextProvider::getNvCodecContext(const int device_num) {
4954
return nv_codec_context;
5055
}
5156

57+
// Note: all failures in this function below log at WARN, not ERROR. The only caller
58+
// (xprsDecApi.cpp::enumDecoders) catches the throw and falls back to SW
59+
// decoders. On a non-GPU machine this fires every VRS file open, so logging
60+
// ERROR-level would spam users who never asked for GPU decoding.
5261
int ret = cuda_load_functions(&nv_codec_context._cuda_functions, nullptr);
5362
if (ret < 0) {
5463
const std::string message = "Loading CUDA functions failed";
55-
XR_LOGE("{}", message.c_str());
64+
XR_LOGW("{}", message.c_str());
5665
throw std::runtime_error(message);
5766
}
5867
ret = cuvid_load_functions(&nv_codec_context._cuvid_functions, nullptr);
5968
if (ret < 0) {
6069
const std::string message = "Loading nvcuvid functions failed";
61-
XR_LOGE("{}", message.c_str());
70+
XR_LOGW("{}", message.c_str());
6271
throw std::runtime_error(message);
6372
}
6473

6574
CUresult cu_result = nv_codec_context._cuda_functions->cuInit(0);
6675
if (cu_result != CUDA_SUCCESS) {
6776
const std::string message = "cuInit failed with error code: " + std::to_string(cu_result);
68-
XR_LOGE("{}", message.c_str());
77+
XR_LOGW("{}", message.c_str());
6978
throw std::runtime_error(message);
7079
}
7180

7281
CUdevice cuda_device = 0;
7382
cu_result = nv_codec_context._cuda_functions->cuDeviceGet(&cuda_device, device_num);
7483
if (cu_result != CUDA_SUCCESS) {
7584
const std::string message = "cuDeviceGet failed with error code: " + std::to_string(cu_result);
76-
XR_LOGE("{}", message.c_str());
85+
XR_LOGW("{}", message.c_str());
7786
throw std::runtime_error(message);
7887
}
7988

@@ -82,15 +91,15 @@ NvCodecContext NvCodecContextProvider::getNvCodecContext(const int device_num) {
8291
if (cu_result != CUDA_SUCCESS) {
8392
const std::string message =
8493
"cuDeviceGetName failed with error code: " + std::to_string(cu_result);
85-
XR_LOGE("{}", message.c_str());
94+
XR_LOGW("{}", message.c_str());
8695
throw std::runtime_error(message);
8796
}
8897

8998
cu_result = nv_codec_context._cuda_functions->cuCtxCreate(
9099
&nv_codec_context._cucontext, CU_CTX_SCHED_BLOCKING_SYNC, cuda_device);
91100
if (cu_result != CUDA_SUCCESS) {
92101
const std::string message = "cuCtxCreate failed with error code: " + std::to_string(cu_result);
93-
XR_LOGE("{}", message.c_str());
102+
XR_LOGW("{}", message.c_str());
94103
throw std::runtime_error(message);
95104
}
96105

@@ -99,7 +108,7 @@ NvCodecContext NvCodecContextProvider::getNvCodecContext(const int device_num) {
99108
if (cu_result != CUDA_SUCCESS) {
100109
const std::string message =
101110
"cuCtxPopCurrent failed with error code: " + std::to_string(cu_result);
102-
XR_LOGE("{}", message.c_str());
111+
XR_LOGW("{}", message.c_str());
103112
throw std::runtime_error(message);
104113
}
105114

xprs/nvDecoder.cpp

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
* limitations under the License.
1515
*/
1616

17+
// DEFAULT_LOG_CHANNEL must be defined before <logging/Log.h>: the OSS build's
18+
// XR_LOGE/XR_LOGW/XR_LOGI macros are gated on it (the internal logging header
19+
// defines them unconditionally, which is why this ordering bug is invisible to
20+
// the Buck build but breaks the OSS CMake build).
21+
#define DEFAULT_LOG_CHANNEL "XPRS"
22+
1723
#include "nvDecoder.h"
1824
#include "Codecs.h"
1925
#include "FFmpegUtils.h"
@@ -22,11 +28,10 @@
2228

2329
#include <logging/Log.h>
2430
#include <cmath>
31+
#include <memory>
2532
#include <stdexcept>
2633
#include <string>
2734

28-
#define DEFAULT_LOG_CHANNEL "XPRS"
29-
3035
namespace xprs {
3136

3237
// Doing a device capability check via NVENC API is more generic and consistent way
@@ -167,6 +172,44 @@ int NvDecoder::HandleVideoSequence(CUVIDEOFORMAT* vdeo_format) {
167172
_decoded_image_yuv.resize(size_ofdecoded_image_yuv_format_in_bytes);
168173

169174
CUDAContextScope cuda_context_scope(_nvcodec_context);
175+
176+
// Capability pre-check (NVIDIA-recommended): NVDEC hardware does not support
177+
// every codec/chroma/bit-depth/resolution combination. Most notably it cannot
178+
// decode monochrome (4:0:0) H.265 — used by Aria's grayscale SLAM and eye
179+
// cameras — and returns CUDA_ERROR_NOT_SUPPORTED from cuvidCreateDecoder.
180+
// Querying cuvidGetDecoderCaps first lets such streams fall back to SW decode
181+
// with a single clear log line, instead of emitting alarming CUDA error logs
182+
// for an entirely expected condition. cuvidGetDecoderCaps is loaded optionally
183+
// by nv-codec-headers, so guard against a null function pointer.
184+
if (_nvcodec_context._cuvid_functions->cuvidGetDecoderCaps != nullptr) {
185+
CUVIDDECODECAPS decode_caps = {};
186+
decode_caps.eCodecType = video_decode_create_info.CodecType;
187+
decode_caps.eChromaFormat = video_decode_create_info.ChromaFormat;
188+
decode_caps.nBitDepthMinus8 = video_decode_create_info.bitDepthMinus8;
189+
// Only a *successful* caps query that reports the format unsupported (or out
190+
// of the supported size range) forces SW fallback. If the query itself fails
191+
// (transient driver/context issue), fall through to cuvidCreateDecoder and
192+
// let it surface the real error rather than permanently disabling NVDEC.
193+
if (_nvcodec_context._cuvid_functions->cuvidGetDecoderCaps(&decode_caps) == CUDA_SUCCESS) {
194+
const unsigned long width = video_decode_create_info.ulWidth;
195+
const unsigned long height = video_decode_create_info.ulHeight;
196+
const bool unsupported = !decode_caps.bIsSupported ||
197+
width < static_cast<unsigned long>(decode_caps.nMinWidth) ||
198+
height < static_cast<unsigned long>(decode_caps.nMinHeight) ||
199+
width > static_cast<unsigned long>(decode_caps.nMaxWidth) ||
200+
height > static_cast<unsigned long>(decode_caps.nMaxHeight);
201+
if (unsupported) {
202+
XR_LOGW(
203+
"NVDEC does not support this stream (chroma_format={}, bit_depth={}, {}x{}); falling back to SW decode",
204+
static_cast<int>(video_decode_create_info.ChromaFormat),
205+
static_cast<int>(video_decode_create_info.bitDepthMinus8) + 8,
206+
width,
207+
height);
208+
throw std::runtime_error("NVDEC unsupported stream format; using SW decode");
209+
}
210+
}
211+
}
212+
170213
CUDA_API_CALL(
171214
_nvcodec_context._cuvid_functions->cuvidCreateDecoder(&_decoder, &video_decode_create_info),
172215
_nvcodec_context._cuda_functions,

xprs/xprsDecApi.cpp

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "xprsUtils.h"
2020

2121
#include <algorithm>
22+
#include <cstdlib>
2223
#include <string_view>
2324

2425
#ifdef WITH_DAV1D
@@ -85,6 +86,15 @@ bool findDecoderByName(const std::string_view& name, VideoCodec& codec) {
8586
return false;
8687
}
8788

89+
// Set XPRS_DISABLE_HW_DECODE to any value to force CPU-only decoding. Useful
90+
// for deterministic results, working around GPU memory pressure, or comparing
91+
// HW-vs-SW output. Read once at first call so the value is fixed for the
92+
// process lifetime — runtime mutation of the env var has no effect.
93+
bool isHwDecodeDisabled() {
94+
static const bool disabled = std::getenv("XPRS_DISABLE_HW_DECODE") != nullptr;
95+
return disabled;
96+
}
97+
8898
} // namespace
8999

90100
///
@@ -95,10 +105,16 @@ XprsResult enumDecoders(CodecList& codecs, bool hwCapabilityCheck) {
95105

96106
codecs.clear();
97107
codecs.reserve(std::size(kPreferredDecoderImplementations));
108+
const bool hwDisabled = isHwDecodeDisabled();
98109
try {
99110
for (const auto& impl : kPreferredDecoderImplementations) {
100111
VideoCodec codec;
101112
if (findDecoderByName(impl, codec)) {
113+
if (codec.hwAccel && hwDisabled) {
114+
XR_LOGI(
115+
"Skipping HW decoder {} (XPRS_DISABLE_HW_DECODE is set)", codec.implementationName);
116+
continue;
117+
}
102118
if (codec.hwAccel && hwCapabilityCheck) {
103119
#ifdef WITH_NVCODEC
104120
const NvCodecContext nvcodecContext = NvCodecContextProvider::getNvCodecContext();
@@ -115,7 +131,10 @@ XprsResult enumDecoders(CodecList& codecs, bool hwCapabilityCheck) {
115131
}
116132
}
117133
} catch (std::exception& e) {
118-
XR_LOGE("{}", convertExceptionToError(e, result));
134+
// Downgraded from XR_LOGE: on non-GPU machines this fires every time a VRS
135+
// file is opened (CUDA init throws). Callers fall back to SW decoders that
136+
// were already collected before the throw, so this is expected, not an error.
137+
XR_LOGW("HW decoder enumeration skipped: {}", convertExceptionToError(e, result));
119138
}
120139

121140
// stable_sort so decoders with equal hwAccel keep their
@@ -137,11 +156,17 @@ enumDecodersByFormat(CodecList& codecs, VideoCodecFormat standard, bool hwCapabi
137156

138157
codecs.clear();
139158
codecs.reserve(std::size(kPreferredDecoderImplementations));
159+
const bool hwDisabled = isHwDecodeDisabled();
140160
try {
141161
for (const auto& impl : kPreferredDecoderImplementations) {
142162
VideoCodec codec;
143163
if (findDecoderByName(impl, codec)) {
144164
if (codec.format == standard) {
165+
if (codec.hwAccel && hwDisabled) {
166+
XR_LOGI(
167+
"Skipping HW decoder {} (XPRS_DISABLE_HW_DECODE is set)", codec.implementationName);
168+
continue;
169+
}
145170
if (codec.hwAccel && hwCapabilityCheck) {
146171
#ifdef WITH_NVCODEC
147172
const NvCodecContext nvcodecContext = NvCodecContextProvider::getNvCodecContext();
@@ -159,7 +184,7 @@ enumDecodersByFormat(CodecList& codecs, VideoCodecFormat standard, bool hwCapabi
159184
}
160185
}
161186
} catch (std::exception& e) {
162-
XR_LOGE("{}", convertExceptionToError(e, result));
187+
XR_LOGW("HW decoder enumeration skipped: {}", convertExceptionToError(e, result));
163188
}
164189

165190
// stable_sort so decoders with equal hwAccel keep their

xprs/xprsDecoder.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,11 @@ XprsResult CVideoDecoder::decodeFrame(Frame& frameOut, const Buffer& compressed)
279279
convertAVFrame(_pix.avFrame(), frameOut);
280280
}
281281
} catch (std::exception& e) {
282-
XR_LOGE("{}", convertExceptionToError(e, result));
282+
// WARNING, not ERROR: a decode exception here is recoverable — the error is
283+
// returned via `result` and the caller (xprsDecoderMaker) falls back to the
284+
// next decoder. The most common case is NVDEC rejecting an unsupported
285+
// stream (e.g. monochrome H.265) during HW->SW probing.
286+
XR_LOGW("{}", convertExceptionToError(e, result));
283287
}
284288

285289
return result;

0 commit comments

Comments
 (0)