I've implemented a resource management class for CUDA interop using RAII to ensure exception safety. The goal is to handle the registration/unregistration and mapping/unmapping, of graphics resources (like buffers and textures) while managing their lifetime in a specific CUDA stream.
Key features & constraints
- C++20: Uses features like
std::formatandstd::source_location. - Flexible registration: Can manage any resource type by passing a registration function (e.g.,
cudaGraphicsGLRegisterBuffer,cudaGraphicsGLRegisterImage) and its arguments to a templated constructor. - Stream-aware: All operations (map/unmap) use a specific CUDA stream provided at construction to avoid blocking the main thread.
Here's the code:
include/cuda_types/resource.h
#ifndef SPACE_EXPLORER_CUDA_RESOURCE_H
#define SPACE_EXPLORER_CUDA_RESOURCE_H
#include <cuda_gl_interop.h>
#include <memory>
#include "cuda_types/fwd.h"
#include "cuda_types/stream.h"
namespace raw::cuda_types {
// The definitions of exception class and macro are in separate files in the actual code
// but i figured it would be easier to put them here
class cuda_exception : public std::exception {
private:
std::string message;
public:
explicit cuda_exception(std::string message) : message(std::move(message)) {}
[[nodiscard]] const char* what() const noexcept override {
return message.c_str();
}
};
#define CUDA_SAFE_CALL(call) \
do { \
cudaError_t error = call; \
if (error != cudaSuccess) { \
const char* msg = cudaGetErrorName(error); \
const char* msg_name = cudaGetErrorString(error); \
throw cuda_exception(std::format( \
"[Error] Function {} failed with error: {} and description: {} in file: {} on line {}", \
#call, msg, msg_name, std::source_location::current().file_name(), \
std::source_location::current().line())); \
} \
} while (0)
/**
* @class resource
* @brief Base class for managing external resources, takes in the constructor function to register the
* resource and the parameters, unmaps and unregisters the stored resource in the destructor
*/
class resource {
private:
cudaGraphicsResource_t m_resource = nullptr;
bool mapped = false;
std::shared_ptr<cuda_stream> stream;
protected:
// I heard somewhere that this down here is better than directly accessing the protected member
cudaGraphicsResource_t &get_resource();
private:
void unmap_noexcept() noexcept;
void cleanup() noexcept;
public:
resource() = default;
template<typename F, typename... Args>
requires std::invocable<F, cudaGraphicsResource_t *, Args...>
explicit resource(const F &&func, std::shared_ptr<cuda_stream> stream, Args &&...args)
: stream(stream) {
create(func, std::forward<Args &&>(args)...);
}
template<typename F, typename... Args>
void create(const F &&func, Args &&...args) {
cleanup();
CUDA_SAFE_CALL(func(&m_resource, std::forward<Args &&>(args)...));
}
void unmap();
void map();
void set_stream(std::shared_ptr<cuda_stream> stream_);
virtual ~resource();
resource &operator=(const resource &) = delete;
resource(const resource &) = delete;
resource &operator=(resource &&rhs) noexcept;
resource(resource &&rhs) noexcept;
};
} // namespace raw::cuda_types
#endif // SPACE_EXPLORER_CUDA_RESOURCE_H
src/cuda_types/resource.cpp
#include "cuda_types/resource.h"
#include <iostream>
namespace raw::cuda_types {
void resource::unmap_noexcept() noexcept {
if (mapped && m_resource) {
try {
CUDA_SAFE_CALL(
cudaGraphicsUnmapResources(1, &m_resource, stream ? stream->stream() : nullptr));
mapped = false;
} catch (const cuda_exception& e) {
std::cerr << std::format("[CRITICAL] Failed to unmap graphics resource. \n{}",
e.what());
}
}
}
void resource::cleanup() noexcept {
unmap_noexcept();
if (m_resource) {
try {
CUDA_SAFE_CALL(cudaGraphicsUnregisterResource(m_resource));
} catch (const cuda_exception& e) {
std::cerr << std::format("[CRITICAL] Failed to unregister resource. \n{}", e.what());
}
}
}
resource& resource::operator=(resource&& rhs) noexcept {
if (this == &rhs) {
return *this;
}
cleanup();
m_resource = rhs.m_resource;
mapped = rhs.mapped;
stream = std::move(rhs.stream);
rhs.m_resource = nullptr;
rhs.mapped = false;
return *this;
}
resource::resource(resource&& rhs) noexcept
: m_resource(rhs.m_resource), mapped(rhs.mapped), stream(std::move(rhs.stream)) {
rhs.m_resource = nullptr;
rhs.mapped = false;
}
cudaGraphicsResource_t& resource::get_resource() {
return m_resource;
}
void resource::unmap() {
if (mapped && m_resource) {
CUDA_SAFE_CALL(cudaGraphicsUnmapResources(1, &m_resource, stream->stream()));
mapped = false;
}
}
void resource::map() {
if (!mapped && m_resource) {
CUDA_SAFE_CALL(cudaGraphicsMapResources(1, &m_resource, stream->stream()));
mapped = true;
}
}
void resource::set_stream(std::shared_ptr<cuda_stream> stream_) {
stream = std::move(stream_);
}
resource::~resource() {
cleanup();
}
} // namespace raw::cuda_types
And it can be used like this (pseudocode)
class gl_buffer : public resource {
private:
T* data = nullptr;
public:
using resource::resource;
gl_buffer(size_t* amount_of_bytes, UI buffer_object, std::shared_ptr<cuda_stream> stream)
: resource(cudaGraphicsGLRegisterBuffer, stream, buffer_object,
cudaGraphicsRegisterFlagsWriteDiscard) {
map();
CUDA_SAFE_CALL(cudaGraphicsResourceGetMappedPointer((void**)&data, amount_of_bytes, get_resource()));
unmap();
}
[[nodiscard]] T* get_data() const {
return data;
}
~buffer() override = default;
};
My questions
- Exception Safety: Is the exception handling in the destructor and move operations correct and sufficient? Should the noexcept cleanup attempts be handled differently?
- Design Choice: The design uses a templated constructor that accepts a function pointer for resource registration. Is this a good approach for handling different types of resources (buffers vs. textures), or would a different design be better and less error-prone?
Would appreciate any help or suggestions!