/** * @file src/nvenc/nvenc_cuda.cpp * @brief Native CUDA NVENC encoder used by the experimental Linux backend. */ #if defined(__linux__) && defined(SUNSHINE_BUILD_CUDA) #include "nvenc_cuda.h" #include #include #include "nvenc_api.h" #include "src/logging.h" namespace nvenc { namespace { constexpr auto nvenc_library_name = "libnvidia-encode.so.1"; class cuda_context_guard_t { public: explicit cuda_context_guard_t(nvenc_cuda &encoder): encoder(encoder), active(encoder.push_context()) { } ~cuda_context_guard_t() { if (active) { encoder.pop_context(); } } explicit operator bool() const noexcept { return active; } private: nvenc_cuda &encoder; bool active; }; } // namespace nvenc_cuda::nvenc_cuda(CUcontext cuda_context, CudaFunctions *cuda_functions, CUstream conversion_stream): nvenc_base(NV_ENC_DEVICE_TYPE_CUDA), cuda_context(cuda_context), cuda_functions(cuda_functions), conversion_stream(conversion_stream) { device = cuda_context; } nvenc_cuda::~nvenc_cuda() { if (encoder) { destroy_encoder(); } if (cuda_input_buffer) { cuda_context_guard_t context_guard {*this}; if (context_guard && !registered_input_buffer && !encoder) { release_input_buffer(); } else { BOOST_LOG(error) << "NvEnc: leaking the CUDA input surface because native resource teardown did not complete safely"; } } if (library_handle && !encoder) { dlclose(library_handle); library_handle = nullptr; } else if (library_handle) { BOOST_LOG(error) << "NvEnc: leaving the native library loaded because the encoder could not be destroyed"; } } CUdeviceptr nvenc_cuda::input_buffer() const noexcept { return cuda_input_buffer; } std::size_t nvenc_cuda::input_pitch() const noexcept { return cuda_input_pitch; } void nvenc_cuda::enable_io_streams() { cuda_context_guard_t context_guard {*this}; if (!context_guard || !encoder || !nvenc->nvEncSetIOCudaStreams) { return; } if (nvenc_failed(nvenc->nvEncSetIOCudaStreams( encoder, &conversion_stream, &conversion_stream ))) { BOOST_LOG(warning) << "NvEnc: couldn't associate the CUDA conversion stream; using a CPU synchronization fallback: " << last_nvenc_error_string; return; } io_streams_enabled = true; BOOST_LOG(info) << "NvEnc: CUDA input/output stream synchronization enabled"; } bool nvenc_cuda::prepare_to_destroy() { // Conversion may have queued a copy before a later plane failed, without // ever submitting the frame to NVENC. Destroying the encoder alone does // not wait for that copy. Keep the complete device quarantined if CUDA // cannot confirm completion, so its input buffer cannot be freed in use. if (conversion_stream) { cuda_context_guard_t context_guard {*this}; if (!context_guard || cuda_functions->cuStreamSynchronize(conversion_stream) != CUDA_SUCCESS) { BOOST_LOG(error) << "NvEnc: couldn't finish CUDA conversion before teardown"; return false; } } return !encoder || destroy_encoder(); } bool nvenc_cuda::push_context() { if (!cuda_context || !cuda_functions) { return false; } if (cuda_functions->cuCtxPushCurrent(cuda_context) != CUDA_SUCCESS) { BOOST_LOG(error) << "NvEnc: couldn't make the CUDA context current"; return false; } return true; } bool nvenc_cuda::enter_context() { return push_context(); } void nvenc_cuda::leave_context() { pop_context(); } bool nvenc_cuda::preserve_encoder_on_destroy_failure() const noexcept { return true; } void nvenc_cuda::pop_context() { CUcontext popped_context {}; if (cuda_functions->cuCtxPopCurrent(&popped_context) != CUDA_SUCCESS) { BOOST_LOG(error) << "NvEnc: couldn't restore the previous CUDA context"; } } bool nvenc_cuda::init_library(uint32_t api_version) { if (library_handle && nvenc && function_list_api_version == api_version) { return true; } if (!library_handle) { library_handle = dlopen(nvenc_library_name, RTLD_LAZY | RTLD_LOCAL); } if (!library_handle) { BOOST_LOG(error) << "NvEnc: couldn't load " << nvenc_library_name << ": " << dlerror(); return false; } if (!max_driver_api_version) { auto get_max_version = reinterpret_cast( dlsym(library_handle, "NvEncodeAPIGetMaxSupportedVersion") ); if (!get_max_version) { BOOST_LOG(error) << "NvEnc: NvEncodeAPIGetMaxSupportedVersion() is missing from " << nvenc_library_name; return false; } uint32_t packed_driver_version = 0; if (get_max_version(&packed_driver_version) != NV_ENC_SUCCESS) { BOOST_LOG(error) << "NvEnc: NvEncodeAPIGetMaxSupportedVersion() failed"; return false; } max_driver_api_version = api::driver_max_to_api_version(packed_driver_version); BOOST_LOG(info) << "NvEnc: driver supports up to API " << api::version_string(max_driver_api_version) << ", compiled SDK " << NVENCAPI_MAJOR_VERSION << '.' << NVENCAPI_MINOR_VERSION; } if (!api::driver_supports_api_version(max_driver_api_version, api_version)) { last_nvenc_status = NV_ENC_ERR_INVALID_VERSION; last_nvenc_error_string = "driver max API " + api::version_string(max_driver_api_version); return false; } auto create_instance = reinterpret_cast( dlsym(library_handle, "NvEncodeAPICreateInstance") ); if (!create_instance) { BOOST_LOG(error) << "NvEnc: NvEncodeAPICreateInstance() is missing from " << nvenc_library_name; return false; } auto new_nvenc = std::make_unique(); new_nvenc->version = api::function_list_version(api_version); if (nvenc_failed(create_instance(new_nvenc.get()))) { if (last_nvenc_status == NV_ENC_ERR_INVALID_VERSION) { BOOST_LOG(debug) << "NvEnc: NvEncodeAPICreateInstance() rejected API " << api::version_string(api_version); } else { BOOST_LOG(error) << "NvEnc: NvEncodeAPICreateInstance() failed: " << last_nvenc_error_string; } return false; } nvenc = std::move(new_nvenc); function_list_api_version = api_version; return true; } bool nvenc_cuda::create_and_register_input_buffer() { cuda_context_guard_t context_guard {*this}; if (!context_guard) { return false; } std::size_t bytes_per_sample; switch (encoder_params.buffer_format) { case NV_ENC_BUFFER_FORMAT_NV12: bytes_per_sample = 1; break; case NV_ENC_BUFFER_FORMAT_YUV420_10BIT: bytes_per_sample = 2; break; default: BOOST_LOG(error) << "NvEnc: native CUDA input format is not supported: " << static_cast(encoder_params.buffer_format); return false; } if (!cuda_input_buffer) { const auto allocation_height = encoder_params.height + (encoder_params.height / 2); if (cuda_functions->cuMemAllocPitch( &cuda_input_buffer, &cuda_input_pitch, static_cast(encoder_params.width) * bytes_per_sample, allocation_height, 16 ) != CUDA_SUCCESS) { BOOST_LOG(error) << "NvEnc: couldn't allocate the CUDA input surface"; cuda_input_buffer = {}; cuda_input_pitch = {}; return false; } } NV_ENC_REGISTER_RESOURCE register_resource = {api::register_resource_version(selected_api_version)}; register_resource.resourceType = NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR; register_resource.width = encoder_params.width; register_resource.height = encoder_params.height; register_resource.pitch = static_cast(cuda_input_pitch); register_resource.resourceToRegister = reinterpret_cast(cuda_input_buffer); register_resource.bufferFormat = encoder_params.buffer_format; register_resource.bufferUsage = NV_ENC_INPUT_IMAGE; if (nvenc_failed(nvenc->nvEncRegisterResource(encoder, ®ister_resource))) { BOOST_LOG(error) << "NvEnc: NvEncRegisterResource(CUDA) failed: " << last_nvenc_error_string; return false; } registered_input_buffer = register_resource.registeredResource; return true; } bool nvenc_cuda::synchronize_input_buffer() { if (io_streams_enabled) { return true; } cuda_context_guard_t context_guard {*this}; if (!context_guard) { return false; } if (cuda_functions->cuStreamSynchronize(conversion_stream) != CUDA_SUCCESS) { BOOST_LOG(error) << "NvEnc: CUDA color-conversion stream synchronization failed"; return false; } return true; } void nvenc_cuda::release_input_buffer() { if (!cuda_input_buffer || !cuda_functions) { return; } if (cuda_functions->cuMemFree(cuda_input_buffer) != CUDA_SUCCESS) { BOOST_LOG(error) << "NvEnc: couldn't free the CUDA input surface"; } cuda_input_buffer = {}; cuda_input_pitch = {}; } } // namespace nvenc #endif