TerrainGen coverage


terraingen/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
4 / 10
40.0%
Branches:
1 / 3
33.3%
Functions:
2 / 6
33.3%

gpu/metal.cpp
Line Branch Exec Source
1 #include "terraingen/gpu/metal.h"
2
3 // On Apple platforms CMake compiles this file as Objective-C++ (with ARC) against the system Metal
4 // framework; elsewhere it is C++ and only the stub below, which reports Metal unavailable, builds.
5 #if defined(__APPLE__)
6
7 #include "terraingen/util/log.h"
8
9 #import <Foundation/Foundation.h>
10 #import <Metal/Metal.h>
11
12 #include <algorithm>
13 #include <cstring>
14 #include <map>
15 #include <mutex>
16 #include <stdexcept>
17 #include <utility>
18
19 namespace terraingen {
20
21 /// The device, its command queue and the pipelines of compiled sources. Compiled with ARC, so the
22 /// Objective-C objects are released with the state.
23 struct MetalCompute::State {
24 id<MTLDevice> device = nil;
25 id<MTLCommandQueue> queue = nil;
26 std::string name;
27 std::mutex mutex;
28 /// Pipelines keyed by their source, which lives for the whole process.
29 std::map<const char*, id<MTLComputePipelineState>> pipelines;
30
31 /// Opens the system's default device and a command queue.
32 void Open() {
33 device = MTLCreateSystemDefaultDevice();
34 if (device == nil) {
35 throw std::runtime_error("no Metal device");
36 }
37 queue = [device newCommandQueue];
38 if (queue == nil) {
39 throw std::runtime_error("Metal command queue creation failed");
40 }
41 name = [[device name] UTF8String];
42 }
43
44 /// The pipeline of `source`, compiled on first use.
45 id<MTLComputePipelineState> PipelineFor(std::span<const char> source) {
46 const auto found = pipelines.find(source.data());
47 if (found != pipelines.end()) {
48 return found->second;
49 }
50 NSString* text = [[NSString alloc] initWithBytes:source.data()
51 length:source.size()
52 encoding:NSUTF8StringEncoding];
53 NSError* error = nil;
54 id<MTLLibrary> library = [device newLibraryWithSource:text options:nil error:&error];
55 if (library == nil) {
56 throw std::runtime_error(std::string("Metal kernel compilation failed: ") +
57 [[error localizedDescription] UTF8String]);
58 }
59 id<MTLFunction> function = [library newFunctionWithName:@"main_0"];
60 if (function == nil) {
61 throw std::runtime_error("The Metal kernel has no main_0");
62 }
63 id<MTLComputePipelineState> pipeline =
64 [device newComputePipelineStateWithFunction:function error:&error];
65 if (pipeline == nil) {
66 throw std::runtime_error(std::string("Metal pipeline creation failed: ") +
67 [[error localizedDescription] UTF8String]);
68 }
69 return pipelines.emplace(source.data(), pipeline).first->second;
70 }
71 };
72
73 namespace {
74
75 std::once_flag gOnce;
76 MetalCompute* gCompute = nullptr;
77 std::string gReason;
78
79 } // namespace
80
81 MetalCompute::MetalCompute(std::unique_ptr<State> state) : state_(std::move(state)) {}
82
83 MetalCompute::~MetalCompute() = default;
84
85 MetalCompute* MetalCompute::Get() {
86 std::call_once(gOnce, [] {
87 try {
88 auto state = std::make_unique<State>();
89 state->Open();
90 const std::string name = state->name;
91 gCompute = new MetalCompute(std::move(state));
92 LogInfo("gpu", "Metal compute on ", name);
93 } catch (const std::exception& error) {
94 gReason = error.what();
95 LogInfo("gpu", "Metal unavailable: ", gReason);
96 }
97 });
98 return gCompute;
99 }
100
101 std::string MetalCompute::Reason() {
102 Get();
103 return gReason;
104 }
105
106 const std::string& MetalCompute::DeviceName() const {
107 return state_->name;
108 }
109
110 void MetalCompute::Run(std::span<const char> source,
111 const std::vector<std::span<std::byte>>& buffers,
112 std::span<const std::byte> pushConstants,
113 std::array<std::uint32_t, 3> groups,
114 std::array<std::uint32_t, 3> groupSize) {
115 State& s = *state_;
116 const std::lock_guard lock(s.mutex);
117 @autoreleasepool {
118 id<MTLComputePipelineState> pipeline = s.PipelineFor(source);
119 std::vector<id<MTLBuffer>> deviceBuffers;
120 for (const std::span<std::byte> buffer : buffers) {
121 id<MTLBuffer> deviceBuffer =
122 [s.device newBufferWithBytes:buffer.data()
123 length:std::max<std::size_t>(buffer.size(), 4)
124 options:MTLResourceStorageModeShared];
125 if (deviceBuffer == nil) {
126 throw std::runtime_error("Metal buffer allocation failed");
127 }
128 deviceBuffers.push_back(deviceBuffer);
129 }
130 id<MTLCommandBuffer> command = [s.queue commandBuffer];
131 id<MTLComputeCommandEncoder> encoder = [command computeCommandEncoder];
132 [encoder setComputePipelineState:pipeline];
133 for (std::size_t i = 0; i < deviceBuffers.size(); ++i) {
134 [encoder setBuffer:deviceBuffers[i] offset:0 atIndex:i];
135 }
136 [encoder setBytes:pushConstants.data()
137 length:pushConstants.size()
138 atIndex:deviceBuffers.size()];
139 [encoder dispatchThreadgroups:MTLSizeMake(groups[0], groups[1], groups[2])
140 threadsPerThreadgroup:MTLSizeMake(groupSize[0], groupSize[1], groupSize[2])];
141 [encoder endEncoding];
142 [command commit];
143 [command waitUntilCompleted];
144 if ([command status] != MTLCommandBufferStatusCompleted) {
145 throw std::runtime_error("The Metal kernel failed");
146 }
147 for (std::size_t i = 0; i < buffers.size(); ++i) {
148 std::memcpy(buffers[i].data(), [deviceBuffers[i] contents], buffers[i].size());
149 }
150 }
151 }
152
153 } // namespace terraingen
154
155 #else
156
157 #include <stdexcept>
158 #include <utility>
159
160 namespace terraingen {
161
162 struct MetalCompute::State {};
163
164 ✗ MetalCompute::MetalCompute(std::unique_ptr<State> state) : state_(std::move(state)) {}
165
166 ✗ MetalCompute::~MetalCompute() = default;
167
168 1 MetalCompute* MetalCompute::Get() {
169 1 return nullptr;
170 }
171
172 32 std::string MetalCompute::Reason() {
173
1/1
✓ Branch 1 taken 32 times.
64 return "Metal runs only on Apple platforms";
174 }
175
176 ✗ const std::string& MetalCompute::DeviceName() const {
177
0/1
✗ Branch 2 not taken.
✗ throw std::logic_error("Metal is unavailable on this platform");
178 }
179
180 ✗ void MetalCompute::Run(std::span<const char> /*source*/,
181 const std::vector<std::span<std::byte>>& /*buffers*/,
182 std::span<const std::byte> /*pushConstants*/,
183 std::array<std::uint32_t, 3> /*groups*/,
184 std::array<std::uint32_t, 3> /*groupSize*/) {
185
0/1
✗ Branch 2 not taken.
✗ throw std::logic_error("Metal is unavailable on this platform");
186 }
187
188 } // namespace terraingen
189
190 #endif
191