ACMX 2.136.0
Dual-Backend Real-Time GPU Video Synthesis
Loading...
Searching...
No Matches
snapshot_writer.cpp
Go to the documentation of this file.
1#include "snapshot_writer.hpp"
2
3#include <mxvk/mxvk_png.hpp>
4
5#ifdef ACMXVK_WITH_WEBP
6#include <webp/encode.h>
7#endif
8#ifdef ACMXVK_WITH_TIFF
9#include <tiffio.h>
10#endif
11
12#include <fstream>
13#include <iostream>
14#include <limits>
15#include <memory>
16#include <sstream>
17#include <stdexcept>
18#include <utility>
19
20namespace acmxvk {
24
26 std::lock_guard<std::mutex> lock(mutex);
27 if (worker.joinable()) {
28 return true;
29 }
30 stopping = false;
31 try {
32 worker = std::thread(&SnapshotWriter::workerLoop, this);
33 } catch (const std::exception &error) {
34 std::cerr << "acmxvk: could not start snapshot worker: "
35 << error.what() << '\n';
36 return false;
37 }
38 return true;
39 }
40
41 void SnapshotWriter::stop() noexcept {
42 {
43 std::lock_guard<std::mutex> lock(mutex);
44 if (!worker.joinable()) {
45 return;
46 }
47 stopping = true;
48 }
49 condition.notify_one();
50 worker.join();
51 }
52
54 std::lock_guard<std::mutex> lock(mutex);
56 }
57
59 {
60 std::lock_guard<std::mutex> lock(mutex);
61 jobs.push_back(std::move(job));
63 }
64 condition.notify_one();
65 }
66
67 void SnapshotWriter::savePng(const fs::path &path, std::uint8_t *rgba,
68 int width, int height) {
69 if (!mxvk::SavePNG_RGBA(path.string().c_str(), rgba, width, height)) {
70 throw std::runtime_error("unable to write PNG frame: " +
71 path.string());
72 }
73 }
74
75 void SnapshotWriter::saveRaw(const fs::path &path,
76 const std::vector<std::uint8_t> &rgba,
77 std::uint32_t width, std::uint32_t height) {
78 if (width == 0U || height == 0U) {
79 throw std::runtime_error(
80 "invalid image dimensions for raw RGBA snapshot: " +
81 path.string());
82 }
83
84 const std::uint64_t byte_count =
85 static_cast<std::uint64_t>(width) *
86 static_cast<std::uint64_t>(height) * 4U;
87 if (byte_count > rgba.size() ||
88 byte_count > static_cast<std::uint64_t>(
89 std::numeric_limits<std::streamsize>::max())) {
90 throw std::runtime_error(
91 "invalid pixel buffer for raw RGBA snapshot: " + path.string());
92 }
93
94 std::ofstream output(path, std::ios::binary);
95 if (!output) {
96 throw std::runtime_error("unable to open raw RGBA snapshot: " +
97 path.string());
98 }
99 output.write(reinterpret_cast<const char *>(rgba.data()),
100 static_cast<std::streamsize>(byte_count));
101 if (!output) {
102 throw std::runtime_error("unable to write raw RGBA snapshot: " +
103 path.string());
104 }
105 }
106
108 const fs::path &path, const std::vector<std::uint16_t> &rgba,
109 std::uint32_t width, std::uint32_t height) {
110 const std::uint64_t sample_count =
111 static_cast<std::uint64_t>(width) * height * 4U;
112 const std::uint64_t byte_count = sample_count * sizeof(std::uint16_t);
113 if (width == 0U || height == 0U || sample_count > rgba.size() ||
114 byte_count > static_cast<std::uint64_t>(
115 std::numeric_limits<std::streamsize>::max())) {
116 throw std::runtime_error(
117 "invalid pixel buffer for raw RGBA16 snapshot: " +
118 path.string());
119 }
120 std::ofstream output(path, std::ios::binary);
121 if (!output) {
122 throw std::runtime_error("unable to open raw RGBA16 snapshot: " +
123 path.string());
124 }
125 output.write(reinterpret_cast<const char *>(rgba.data()),
126 static_cast<std::streamsize>(byte_count));
127 if (!output) {
128 throw std::runtime_error("unable to write raw RGBA16 snapshot: " +
129 path.string());
130 }
131 }
132
133#ifdef ACMXVK_WITH_WEBP
134 void SnapshotWriter::saveWebP(const fs::path &path,
135 const std::uint8_t *rgba, int width,
136 int height) {
137 if (rgba == nullptr || width <= 0 || height <= 0 ||
138 width > std::numeric_limits<int>::max() / 4) {
139 throw std::runtime_error(
140 "invalid image dimensions for WebP snapshot: " + path.string());
141 }
142
143 std::uint8_t *encoded_pixels = nullptr;
144 const std::size_t encoded_size = WebPEncodeLosslessRGBA(
145 rgba, width, height, width * 4, &encoded_pixels);
146 const std::unique_ptr<std::uint8_t, decltype(&WebPFree)> encoded_data(
147 encoded_pixels, &WebPFree);
148 if (encoded_size == 0 || encoded_data == nullptr) {
149 throw std::runtime_error("unable to encode WebP snapshot: " +
150 path.string());
151 }
152
153 std::ofstream output(path, std::ios::binary);
154 if (!output) {
155 throw std::runtime_error("unable to open WebP snapshot: " +
156 path.string());
157 }
158 output.write(reinterpret_cast<const char *>(encoded_data.get()),
159 static_cast<std::streamsize>(encoded_size));
160 if (!output) {
161 throw std::runtime_error("unable to write WebP snapshot: " +
162 path.string());
163 }
164 }
165#endif
166
167#ifdef ACMXVK_WITH_TIFF
168 void SnapshotWriter::saveTiff(const fs::path &path,
169 const std::uint8_t *rgba, int width,
170 int height) {
171 if (rgba == nullptr || width <= 0 || height <= 0 ||
172 width > std::numeric_limits<int>::max() / 4) {
173 throw std::runtime_error(
174 "invalid image dimensions for TIFF snapshot: " + path.string());
175 }
176
177 const std::unique_ptr<TIFF, decltype(&TIFFClose)> output(
178 TIFFOpen(path.string().c_str(), "w"), &TIFFClose);
179 if (output == nullptr) {
180 throw std::runtime_error("unable to open TIFF snapshot: " +
181 path.string());
182 }
183
184 const std::uint16_t extra_sample = EXTRASAMPLE_UNASSALPHA;
185 const bool configured =
186 TIFFSetField(output.get(), TIFFTAG_IMAGEWIDTH,
187 static_cast<std::uint32_t>(width)) != 0 &&
188 TIFFSetField(output.get(), TIFFTAG_IMAGELENGTH,
189 static_cast<std::uint32_t>(height)) != 0 &&
190 TIFFSetField(output.get(), TIFFTAG_SAMPLESPERPIXEL, 4) != 0 &&
191 TIFFSetField(output.get(), TIFFTAG_BITSPERSAMPLE, 8) != 0 &&
192 TIFFSetField(output.get(), TIFFTAG_ORIENTATION,
193 ORIENTATION_TOPLEFT) != 0 &&
194 TIFFSetField(output.get(), TIFFTAG_PLANARCONFIG,
195 PLANARCONFIG_CONTIG) != 0 &&
196 TIFFSetField(output.get(), TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB) !=
197 0 &&
198 TIFFSetField(output.get(), TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT) !=
199 0 &&
200 TIFFSetField(output.get(), TIFFTAG_COMPRESSION, COMPRESSION_LZW) !=
201 0 &&
202 TIFFSetField(output.get(), TIFFTAG_ROWSPERSTRIP,
203 TIFFDefaultStripSize(output.get(), 0)) != 0 &&
204 TIFFSetField(output.get(), TIFFTAG_EXTRASAMPLES, 1,
205 &extra_sample) != 0 &&
206 TIFFSetField(output.get(), TIFFTAG_IMAGEDESCRIPTION,
207 "ACMXVK processed snapshot: 8-bit RGBA TIFF") != 0;
208 if (!configured) {
209 throw std::runtime_error("unable to configure TIFF snapshot: " +
210 path.string());
211 }
212
213 const std::size_t row_bytes = static_cast<std::size_t>(width) * 4U;
214 for (int row = 0; row < height; ++row) {
215 auto *row_pixels = const_cast<std::uint8_t *>(
216 rgba + static_cast<std::size_t>(row) * row_bytes);
217 if (TIFFWriteScanline(output.get(), row_pixels,
218 static_cast<std::uint32_t>(row), 0) < 0) {
219 throw std::runtime_error("unable to write TIFF snapshot: " +
220 path.string());
221 }
222 }
223 }
224
225 void SnapshotWriter::saveTiff16(const fs::path &path,
226 const std::uint16_t *rgba, int width,
227 int height) {
228 if (rgba == nullptr || width <= 0 || height <= 0 ||
229 width > std::numeric_limits<int>::max() / 8) {
230 throw std::runtime_error(
231 "invalid image dimensions for 16-bit TIFF snapshot: " +
232 path.string());
233 }
234 const std::unique_ptr<TIFF, decltype(&TIFFClose)> output(
235 TIFFOpen(path.string().c_str(), "w"), &TIFFClose);
236 if (output == nullptr) {
237 throw std::runtime_error("unable to open TIFF snapshot: " +
238 path.string());
239 }
240 const std::uint16_t extra_sample = EXTRASAMPLE_UNASSALPHA;
241 const bool configured =
242 TIFFSetField(output.get(), TIFFTAG_IMAGEWIDTH,
243 static_cast<std::uint32_t>(width)) != 0 &&
244 TIFFSetField(output.get(), TIFFTAG_IMAGELENGTH,
245 static_cast<std::uint32_t>(height)) != 0 &&
246 TIFFSetField(output.get(), TIFFTAG_SAMPLESPERPIXEL, 4) != 0 &&
247 TIFFSetField(output.get(), TIFFTAG_BITSPERSAMPLE, 16) != 0 &&
248 TIFFSetField(output.get(), TIFFTAG_ORIENTATION,
249 ORIENTATION_TOPLEFT) != 0 &&
250 TIFFSetField(output.get(), TIFFTAG_PLANARCONFIG,
251 PLANARCONFIG_CONTIG) != 0 &&
252 TIFFSetField(output.get(), TIFFTAG_PHOTOMETRIC,
253 PHOTOMETRIC_RGB) != 0 &&
254 TIFFSetField(output.get(), TIFFTAG_SAMPLEFORMAT,
255 SAMPLEFORMAT_UINT) != 0 &&
256 TIFFSetField(output.get(), TIFFTAG_COMPRESSION, COMPRESSION_LZW) !=
257 0 &&
258 TIFFSetField(output.get(), TIFFTAG_ROWSPERSTRIP,
259 TIFFDefaultStripSize(output.get(), 0)) != 0 &&
260 TIFFSetField(output.get(), TIFFTAG_EXTRASAMPLES, 1,
261 &extra_sample) != 0 &&
262 TIFFSetField(output.get(), TIFFTAG_IMAGEDESCRIPTION,
263 "ACMXVK HDR snapshot: 16-bit RGBA") != 0;
264 if (!configured) {
265 throw std::runtime_error("unable to configure TIFF snapshot: " +
266 path.string());
267 }
268 const std::size_t row_samples = static_cast<std::size_t>(width) * 4U;
269 for (int row = 0; row < height; ++row) {
270 auto *row_pixels = const_cast<std::uint16_t *>(
271 rgba + static_cast<std::size_t>(row) * row_samples);
272 if (TIFFWriteScanline(output.get(), row_pixels,
273 static_cast<std::uint32_t>(row), 0) < 0) {
274 throw std::runtime_error("unable to write TIFF snapshot: " +
275 path.string());
276 }
277 }
278 }
279#endif
280
281 std::string_view SnapshotWriter::formatName(SnapshotFormat format) noexcept {
282 switch (format) {
284 return "WebP";
286 return "TIFF";
288 return "raw RGBA";
290 return "PNG";
291 }
292 return "snapshot";
293 }
294
296 while (true) {
297 SnapshotJob job;
298 {
299 std::unique_lock<std::mutex> lock(mutex);
300 condition.wait(lock,
301 [&] { return stopping || !jobs.empty(); });
302 if (stopping && jobs.empty()) {
303 return;
304 }
305 job = std::move(jobs.front());
306 jobs.pop_front();
307 }
308
309 try {
310 if (job.format == SnapshotFormat::Raw) {
311 if (!job.rgba16.empty()) {
312 saveRaw16(job.path, job.rgba16, job.width, job.height);
313 } else {
314 saveRaw(job.path, job.rgba, job.width, job.height);
315 }
316 } else if (job.format == SnapshotFormat::Tiff) {
317#ifdef ACMXVK_WITH_TIFF
318 if (!job.rgba16.empty()) {
319 saveTiff16(job.path, job.rgba16.data(),
320 static_cast<int>(job.width),
321 static_cast<int>(job.height));
322 } else {
323 saveTiff(job.path, job.rgba.data(),
324 static_cast<int>(job.width),
325 static_cast<int>(job.height));
326 }
327#else
328 throw std::runtime_error(
329 "TIFF snapshot support is not compiled in");
330#endif
331 } else if (job.format == SnapshotFormat::WebP) {
332#ifdef ACMXVK_WITH_WEBP
333 saveWebP(job.path, job.rgba.data(),
334 static_cast<int>(job.width),
335 static_cast<int>(job.height));
336#else
337 throw std::runtime_error(
338 "WebP snapshot support is not compiled in");
339#endif
340 } else {
341 savePng(job.path, job.rgba.data(),
342 static_cast<int>(job.width),
343 static_cast<int>(job.height));
344 }
345 std::ostringstream message;
346 message << "acmxvk: took " << formatName(job.format)
347 << " snapshot: " << job.path.string() << '\n';
348 std::cout << message.str();
349 } catch (const std::exception &error) {
350 std::ostringstream message;
351 message << "acmxvk: snapshot failed: " << error.what() << '\n';
352 std::cerr << message.str();
353 } catch (...) {
354 std::cerr
355 << "acmxvk: snapshot failed with an unknown error\n";
356 }
357
358 std::lock_guard<std::mutex> lock(mutex);
359 if (jobs_in_flight > 0) {
361 }
362 }
363 }
364} // namespace acmxvk
std::condition_variable condition
static void saveRaw16(const fs::path &path, const std::vector< std::uint16_t > &rgba, std::uint32_t width, std::uint32_t height)
static void savePng(const fs::path &path, std::uint8_t *rgba, int width, int height)
std::deque< SnapshotJob > jobs
static std::string_view formatName(SnapshotFormat format) noexcept
static void saveRaw(const fs::path &path, const std::vector< std::uint8_t > &rgba, std::uint32_t width, std::uint32_t height)
static constexpr std::size_t QUEUE_CAPACITY
void enqueue(SnapshotJob job)
std::vector< std::uint16_t > rgba16
std::vector< std::uint8_t > rgba