TerrainGen coverage


terraingen/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
24 / 183
13.1%
Branches:
11 / 109
10.1%
Functions:
7 / 35
20.0%

gpu/cuda.cpp
Line Branch Exec Source
1 #include "terraingen/gpu/cuda.h"
2
3 #include "terraingen/util/log.h"
4
5 #if defined(_WIN32)
6 #define NOMINMAX
7 #define WIN32_LEAN_AND_MEAN
8 #include <windows.h>
9 #else
10 #include <dlfcn.h>
11 #endif
12
13 #include <algorithm>
14 #include <cstring>
15 #include <map>
16 #include <mutex>
17 #include <stdexcept>
18 #include <utility>
19
20 namespace terraingen {
21
22 namespace {
23
24 /// The part of the CUDA driver API TerrainGen calls, declared from cuda.h so the build needs no
25 /// CUDA headers; the driver ABI is stable across versions.
26 namespace cu {
27
28 using Result = int;
29 using Device = int;
30 using Context = struct Context_st*;
31 using Module = struct Module_st*;
32 using Function = struct Function_st*;
33 using Stream = struct Stream_st*;
34 using DevicePtr = unsigned long long;
35
36 constexpr Result kSuccess = 0;
37 constexpr Result kErrorUnsupportedPtxVersion = 222;
38
39 } // namespace cu
40
41 #if defined(_WIN32)
42 constexpr const char* kCudaDriver = "nvcuda.dll";
43 void* OpenLibrary(const char* name) {
44 return reinterpret_cast<void*>(LoadLibraryA(name));
45 }
46 void* Symbol(void* library, const char* name) {
47 return reinterpret_cast<void*>(GetProcAddress(static_cast<HMODULE>(library), name));
48 }
49 #else
50 constexpr const char* kCudaDriver = "libcuda.so.1";
51 15 void* OpenLibrary(const char* name) {
52 15 return dlopen(name, RTLD_NOW | RTLD_LOCAL);
53 }
54 ✗ void* Symbol(void* library, const char* name) {
55 ✗ return dlsym(library, name);
56 }
57 #endif
58
59 /// The driver entry points, loaded from the driver library.
60 struct Driver {
61 cu::Result (*init)(unsigned int) = nullptr;
62 cu::Result (*deviceGetCount)(int*) = nullptr;
63 cu::Result (*deviceGet)(cu::Device*, int) = nullptr;
64 cu::Result (*deviceGetName)(char*, int, cu::Device) = nullptr;
65 cu::Result (*primaryCtxRetain)(cu::Context*, cu::Device) = nullptr;
66 cu::Result (*primaryCtxRelease)(cu::Device) = nullptr;
67 cu::Result (*ctxSetCurrent)(cu::Context) = nullptr;
68 cu::Result (*ctxSynchronize)() = nullptr;
69 cu::Result (*moduleLoadData)(cu::Module*, const void*) = nullptr;
70 cu::Result (*moduleUnload)(cu::Module) = nullptr;
71 cu::Result (*moduleGetFunction)(cu::Function*, cu::Module, const char*) = nullptr;
72 cu::Result (*moduleGetGlobal)(cu::DevicePtr*, std::size_t*, cu::Module, const char*) = nullptr;
73 cu::Result (*memAlloc)(cu::DevicePtr*, std::size_t) = nullptr;
74 cu::Result (*memFree)(cu::DevicePtr) = nullptr;
75 cu::Result (*memcpyHtoD)(cu::DevicePtr, const void*, std::size_t) = nullptr;
76 cu::Result (*memcpyDtoH)(void*, cu::DevicePtr, std::size_t) = nullptr;
77 cu::Result (*launchKernel)(cu::Function, unsigned int, unsigned int, unsigned int, unsigned int,
78 unsigned int, unsigned int, unsigned int, cu::Stream, void**,
79 void**) = nullptr;
80 cu::Result (*getErrorString)(cu::Result, const char**) = nullptr;
81 };
82
83 /// Sets `function` to the driver symbol `name`; false when the driver lacks it.
84 ✗ template <typename Function> bool Load(void* library, const char* name, Function& function) {
85 ✗ function = reinterpret_cast<Function>(Symbol(library, name));
86 ✗ return function != nullptr;
87 }
88
89 /// Loads every entry point of `driver` from `library`; the name of the first missing one, or null.
90 ✗ const char* LoadDriver(void* library, Driver& d) {
91 const std::pair<const char*, bool> loaded[] = {
92 ✗ {"cuInit", Load(library, "cuInit", d.init)},
93 ✗ {"cuDeviceGetCount", Load(library, "cuDeviceGetCount", d.deviceGetCount)},
94 ✗ {"cuDeviceGet", Load(library, "cuDeviceGet", d.deviceGet)},
95 ✗ {"cuDeviceGetName", Load(library, "cuDeviceGetName", d.deviceGetName)},
96 ✗ {"cuDevicePrimaryCtxRetain", Load(library, "cuDevicePrimaryCtxRetain", d.primaryCtxRetain)},
97 {"cuDevicePrimaryCtxRelease_v2",
98 ✗ Load(library, "cuDevicePrimaryCtxRelease_v2", d.primaryCtxRelease)},
99 ✗ {"cuCtxSetCurrent", Load(library, "cuCtxSetCurrent", d.ctxSetCurrent)},
100 ✗ {"cuCtxSynchronize", Load(library, "cuCtxSynchronize", d.ctxSynchronize)},
101 ✗ {"cuModuleLoadData", Load(library, "cuModuleLoadData", d.moduleLoadData)},
102 ✗ {"cuModuleUnload", Load(library, "cuModuleUnload", d.moduleUnload)},
103 ✗ {"cuModuleGetFunction", Load(library, "cuModuleGetFunction", d.moduleGetFunction)},
104 ✗ {"cuModuleGetGlobal_v2", Load(library, "cuModuleGetGlobal_v2", d.moduleGetGlobal)},
105 ✗ {"cuMemAlloc_v2", Load(library, "cuMemAlloc_v2", d.memAlloc)},
106 ✗ {"cuMemFree_v2", Load(library, "cuMemFree_v2", d.memFree)},
107 ✗ {"cuMemcpyHtoD_v2", Load(library, "cuMemcpyHtoD_v2", d.memcpyHtoD)},
108 ✗ {"cuMemcpyDtoH_v2", Load(library, "cuMemcpyDtoH_v2", d.memcpyDtoH)},
109 ✗ {"cuLaunchKernel", Load(library, "cuLaunchKernel", d.launchKernel)},
110 ✗ {"cuGetErrorString", Load(library, "cuGetErrorString", d.getErrorString)},
111 ✗ };
112
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ for (const auto& [name, ok] : loaded) {
113
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (!ok) {
114 ✗ return name;
115 }
116 }
117 ✗ return nullptr;
118 }
119
120 /// Device allocations of one `Run`, freed when it goes out of scope however the run ends.
121 struct RunAllocations {
122 const Driver& driver;
123 std::vector<cu::DevicePtr> pointers;
124
125 ✗ explicit RunAllocations(const Driver& d) : driver(d) {}
126 RunAllocations(const RunAllocations&) = delete;
127 RunAllocations& operator=(const RunAllocations&) = delete;
128 ✗ ~RunAllocations() {
129
0/2
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ for (const cu::DevicePtr pointer : pointers) {
130 ✗ driver.memFree(pointer);
131 }
132 }
133 };
134
135 } // namespace
136
137 /// The driver, device, primary context and loaded modules.
138 struct CudaCompute::State {
139 Driver driver;
140 cu::Device device = 0;
141 cu::Context context = nullptr;
142 std::string name;
143 std::mutex mutex;
144 /// Modules keyed by their PTX, which lives for the whole process.
145 std::map<const char*, cu::Module> modules;
146
147 15 State() = default;
148 State(const State&) = delete;
149 State& operator=(const State&) = delete;
150
151 15 ~State() {
152
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 15 times.
15 if (context != nullptr) {
153 ✗ driver.ctxSetCurrent(context);
154
0/2
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ for (const auto& [ptx, module] : modules) {
155 ✗ driver.moduleUnload(module);
156 }
157 ✗ driver.primaryCtxRelease(device);
158 }
159 15 }
160
161 /// Throws std::runtime_error naming `what` and the driver's message unless `result` succeeded.
162 ✗ void Check(cu::Result result, const char* what) const {
163
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (result == cu::kSuccess) {
164 ✗ return;
165 }
166 ✗ const char* message = nullptr;
167
0/1
✗ Branch 1 not taken.
✗ driver.getErrorString(result, &message);
168
0/4
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ Branch 7 not taken.
✗ Branch 10 not taken.
✗ throw std::runtime_error(std::string("CUDA ") + what + " failed (" +
169
0/9
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✗ Branch 8 not taken.
✗ Branch 11 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
✗ (message != nullptr ? message : std::to_string(result)) + ")");
170 }
171
172 /// Opens the driver, the first device and its primary context.
173 15 void Open() {
174 15 void* library = OpenLibrary(kCudaDriver);
175
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 if (library == nullptr) {
176
3/3
✓ Branch 1 taken 15 times.
✓ Branch 4 taken 15 times.
✓ Branch 7 taken 15 times.
30 throw std::runtime_error(std::string("no NVIDIA driver found (") + kCudaDriver +
177
1/1
✓ Branch 2 taken 15 times.
45 "); install the NVIDIA driver to use CUDA");
178 }
179
0/3
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ if (const char* missing = LoadDriver(library, driver)) {
180
0/3
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ Branch 7 not taken.
✗ throw std::runtime_error(std::string("the NVIDIA driver lacks ") + missing +
181
0/1
✗ Branch 2 not taken.
✗ "; update the driver");
182 }
183
0/2
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ Check(driver.init(0), "initialization");
184 ✗ int count = 0;
185
0/2
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ Check(driver.deviceGetCount(&count), "device count");
186
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (count == 0) {
187
0/1
✗ Branch 2 not taken.
✗ throw std::runtime_error("no CUDA devices");
188 }
189
0/2
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ Check(driver.deviceGet(&device, 0), "device lookup");
190 ✗ char deviceName[256] = {};
191
0/2
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ Check(driver.deviceGetName(deviceName, static_cast<int>(sizeof(deviceName)), device),
192 "device name");
193
0/1
✗ Branch 1 not taken.
✗ name = deviceName;
194
0/2
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ Check(driver.primaryCtxRetain(&context, device), "context creation");
195 }
196
197 /// The module of `ptx`, loaded on first use.
198 ✗ cu::Module ModuleFor(std::span<const char> ptx) {
199
0/1
✗ Branch 2 not taken.
✗ const auto found = modules.find(ptx.data());
200
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ if (found != modules.end()) {
201 ✗ return found->second;
202 }
203 ✗ cu::Module module = nullptr;
204
0/1
✗ Branch 2 not taken.
✗ const cu::Result loaded = driver.moduleLoadData(&module, ptx.data());
205
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (loaded == cu::kErrorUnsupportedPtxVersion) {
206 throw std::runtime_error("the NVIDIA driver is older than this build's CUDA kernels; "
207
0/1
✗ Branch 2 not taken.
✗ "update the driver");
208 }
209
0/1
✗ Branch 1 not taken.
✗ Check(loaded, "module loading");
210
0/1
✗ Branch 2 not taken.
✗ return modules.emplace(ptx.data(), module).first->second;
211 }
212 };
213
214 namespace {
215
216 std::once_flag gOnce;
217 CudaCompute* gCompute = nullptr;
218 std::string gReason;
219
220 } // namespace
221
222 ✗ CudaCompute::CudaCompute(std::unique_ptr<State> state) : state_(std::move(state)) {}
223
224 ✗ CudaCompute::~CudaCompute() = default;
225
226 67 CudaCompute* CudaCompute::Get() {
227
1/1
✓ Branch 1 taken 67 times.
67 std::call_once(gOnce, [] {
228 try {
229
1/1
✓ Branch 1 taken 15 times.
15 auto state = std::make_unique<State>();
230
0/1
✗ Branch 2 not taken.
15 state->Open();
231
0/1
✗ Branch 2 not taken.
✗ const std::string name = state->name;
232
0/1
✗ Branch 1 not taken.
✗ gCompute = new CudaCompute(std::move(state));
233
0/1
✗ Branch 2 not taken.
✗ LogInfo("gpu", "CUDA compute on ", name);
234
1/2
✗ Branch 4 not taken.
✓ Branch 5 taken 15 times.
30 } catch (const std::exception& error) {
235
1/1
✓ Branch 2 taken 15 times.
15 gReason = error.what();
236
1/1
✓ Branch 2 taken 15 times.
15 LogInfo("gpu", "CUDA unavailable: ", gReason);
237 15 }
238 15 });
239 67 return gCompute;
240 }
241
242 32 std::string CudaCompute::Reason() {
243 32 Get();
244 32 return gReason;
245 }
246
247 ✗ const std::string& CudaCompute::DeviceName() const {
248 ✗ return state_->name;
249 }
250
251 ✗ void CudaCompute::Run(std::span<const char> ptx, const std::vector<CudaBuffer>& buffers,
252 std::span<const std::byte> pushConstants, std::array<std::uint32_t, 3> groups,
253 std::array<std::uint32_t, 3> groupSize) {
254 ✗ State& s = *state_;
255 ✗ const Driver& d = s.driver;
256
0/1
✗ Branch 1 not taken.
✗ const std::lock_guard lock(s.mutex);
257
0/2
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ s.Check(d.ctxSetCurrent(s.context), "context binding");
258
0/1
✗ Branch 1 not taken.
✗ const cu::Module module = s.ModuleFor(ptx);
259 ✗ cu::Function function = nullptr;
260
0/2
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ s.Check(d.moduleGetFunction(&function, module, "main_0"), "kernel lookup");
261 ✗ cu::DevicePtr globals = 0;
262 ✗ std::size_t globalsSize = 0;
263
0/2
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ s.Check(d.moduleGetGlobal(&globals, &globalsSize, module, "SLANG_globalParams"),
264 "global parameter lookup");
265 struct BufferParam {
266 cu::DevicePtr data;
267 std::uint64_t count;
268 };
269
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ if (globalsSize != buffers.size() * sizeof(BufferParam) + sizeof(cu::DevicePtr)) {
270
0/1
✗ Branch 1 not taken.
✗ throw std::runtime_error("The CUDA kernel's global parameters are not " +
271
0/3
✗ Branch 3 not taken.
✗ Branch 6 not taken.
✗ Branch 9 not taken.
✗ std::to_string(buffers.size()) + " buffers and push constants");
272 }
273 ✗ RunAllocations run(d);
274
0/1
✗ Branch 1 not taken.
✗ std::vector<std::byte> params(globalsSize);
275
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ for (std::size_t i = 0; i < buffers.size(); ++i) {
276 ✗ cu::DevicePtr pointer = 0;
277
0/2
✗ Branch 4 not taken.
✗ Branch 7 not taken.
✗ s.Check(d.memAlloc(&pointer, std::max<std::size_t>(buffers[i].bytes.size(), 4)),
278 "memory allocation");
279
0/1
✗ Branch 1 not taken.
✗ run.pointers.push_back(pointer);
280
0/2
✗ Branch 5 not taken.
✗ Branch 8 not taken.
✗ s.Check(d.memcpyHtoD(pointer, buffers[i].bytes.data(), buffers[i].bytes.size()), "upload");
281 ✗ const BufferParam param{pointer, buffers[i].count};
282 ✗ std::memcpy(params.data() + i * sizeof(BufferParam), &param, sizeof(param));
283 }
284 ✗ cu::DevicePtr push = 0;
285
0/2
✗ Branch 3 not taken.
✗ Branch 6 not taken.
✗ s.Check(d.memAlloc(&push, std::max<std::size_t>(pushConstants.size(), 4)), "memory allocation");
286
0/1
✗ Branch 1 not taken.
✗ run.pointers.push_back(push);
287
0/2
✗ Branch 3 not taken.
✗ Branch 6 not taken.
✗ s.Check(d.memcpyHtoD(push, pushConstants.data(), pushConstants.size()), "upload");
288 ✗ std::memcpy(params.data() + buffers.size() * sizeof(BufferParam), &push, sizeof(push));
289
0/2
✗ Branch 3 not taken.
✗ Branch 6 not taken.
✗ s.Check(d.memcpyHtoD(globals, params.data(), params.size()), "parameter upload");
290
0/2
✗ Branch 6 not taken.
✗ Branch 9 not taken.
✗ s.Check(d.launchKernel(function, groups[0], groups[1], groups[2], groupSize[0], groupSize[1],
291 ✗ groupSize[2], 0, nullptr, nullptr, nullptr),
292 "kernel launch");
293
0/2
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ s.Check(d.ctxSynchronize(), "kernel");
294
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ for (std::size_t i = 0; i < buffers.size(); ++i) {
295
0/2
✗ Branch 6 not taken.
✗ Branch 9 not taken.
✗ s.Check(d.memcpyDtoH(buffers[i].bytes.data(), run.pointers[i], buffers[i].bytes.size()),
296 "download");
297 }
298 }
299
300 } // namespace terraingen
301