ACMX 2.139.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 {
22
24 std::lock_guard<std::mutex> lock(mutex);
25 if (worker.joinable()) {
26 return true;
27 }
28 stopping = false;
29 try {
30 worker = std::thread(&SnapshotWriter::workerLoop, this);
31 } catch (const std::exception &error) {
32 std::cerr << "acmxvk: could not start snapshot worker: " << error.what() << '\n';
33 return false;
34 }
35 return true;
36 }
37
38 void SnapshotWriter::stop() noexcept {
39 {
40 std::lock_guard<std::mutex> lock(mutex);
41 if (!worker.joinable()) {
42 return;
43 }
44 stopping = true;
45 }
46 condition.notify_one();
47 worker.join();
48 }
49
51 std::lock_guard<std::mutex> lock(mutex);
53 }
54
56 {
57 std::lock_guard<std::mutex> lock(mutex);
58 jobs.push_back(std::move(job));
60 }
61 condition.notify_one();
62 }
63
64 void SnapshotWriter::savePng(const fs::path &path, std::uint8_t *rgba, int width, int height, int png_level) {
65 if (!mxvk::SavePNG_RGBA(path.string().c_str(), rgba, width, height, png_level)) {
66 throw std::runtime_error("unable to write PNG frame: " + path.string());
67 }
68 }
69
70 void SnapshotWriter::saveRaw(const fs::path &path, const std::vector<std::uint8_t> &rgba, std::uint32_t width, std::uint32_t height) {
71 if (width == 0U || height == 0U) {
72 throw std::runtime_error("invalid image dimensions for raw RGBA snapshot: " + path.string());
73 }
74
75 const std::uint64_t byte_count = static_cast<std::uint64_t>(width) * static_cast<std::uint64_t>(height) * 4U;
76 if (byte_count > rgba.size() || byte_count > static_cast<std::uint64_t>(std::numeric_limits<std::streamsize>::max())) {
77 throw std::runtime_error("invalid pixel buffer for raw RGBA snapshot: " + path.string());
78 }
79
80 std::ofstream output(path, std::ios::binary);
81 if (!output) {
82 throw std::runtime_error("unable to open raw RGBA snapshot: " + path.string());
83 }
84 output.write(reinterpret_cast<const char *>(rgba.data()), static_cast<std::streamsize>(byte_count));
85 if (!output) {
86 throw std::runtime_error("unable to write raw RGBA snapshot: " + path.string());
87 }
88 }
89
90 void SnapshotWriter::saveRaw16(const fs::path &path, const std::vector<std::uint16_t> &rgba, std::uint32_t width, std::uint32_t height) {
91 const std::uint64_t sample_count = static_cast<std::uint64_t>(width) * height * 4U;
92 const std::uint64_t byte_count = sample_count * sizeof(std::uint16_t);
93 if (width == 0U || height == 0U || sample_count > rgba.size() || byte_count > static_cast<std::uint64_t>(std::numeric_limits<std::streamsize>::max())) {
94 throw std::runtime_error("invalid pixel buffer for raw RGBA16 snapshot: " + path.string());
95 }
96 std::ofstream output(path, std::ios::binary);
97 if (!output) {
98 throw std::runtime_error("unable to open raw RGBA16 snapshot: " + path.string());
99 }
100 output.write(reinterpret_cast<const char *>(rgba.data()), static_cast<std::streamsize>(byte_count));
101 if (!output) {
102 throw std::runtime_error("unable to write raw RGBA16 snapshot: " + path.string());
103 }
104 }
105
106#ifdef ACMXVK_WITH_WEBP
107 void SnapshotWriter::saveWebP(const fs::path &path, const std::uint8_t *rgba, int width, int height) {
108 if (rgba == nullptr || width <= 0 || height <= 0 || width > std::numeric_limits<int>::max() / 4) {
109 throw std::runtime_error("invalid image dimensions for WebP snapshot: " + path.string());
110 }
111
112 std::uint8_t *encoded_pixels = nullptr;
113 const std::size_t encoded_size = WebPEncodeLosslessRGBA(rgba, width, height, width * 4, &encoded_pixels);
114 const std::unique_ptr<std::uint8_t, decltype(&WebPFree)> encoded_data(encoded_pixels, &WebPFree);
115 if (encoded_size == 0 || encoded_data == nullptr) {
116 throw std::runtime_error("unable to encode WebP snapshot: " + path.string());
117 }
118
119 std::ofstream output(path, std::ios::binary);
120 if (!output) {
121 throw std::runtime_error("unable to open WebP snapshot: " + path.string());
122 }
123 output.write(reinterpret_cast<const char *>(encoded_data.get()), static_cast<std::streamsize>(encoded_size));
124 if (!output) {
125 throw std::runtime_error("unable to write WebP snapshot: " + path.string());
126 }
127 }
128#endif
129
130#ifdef ACMXVK_WITH_TIFF
131 void SnapshotWriter::saveTiff(const fs::path &path, const std::uint8_t *rgba, int width, int height) {
132 if (rgba == nullptr || width <= 0 || height <= 0 || width > std::numeric_limits<int>::max() / 4) {
133 throw std::runtime_error("invalid image dimensions for TIFF snapshot: " + path.string());
134 }
135
136 const std::unique_ptr<TIFF, decltype(&TIFFClose)> output(TIFFOpen(path.string().c_str(), "w"), &TIFFClose);
137 if (output == nullptr) {
138 throw std::runtime_error("unable to open TIFF snapshot: " + path.string());
139 }
140
141 const std::uint16_t extra_sample = EXTRASAMPLE_UNASSALPHA;
142 const bool configured = TIFFSetField(output.get(), TIFFTAG_IMAGEWIDTH, static_cast<std::uint32_t>(width)) != 0 && TIFFSetField(output.get(), TIFFTAG_IMAGELENGTH, static_cast<std::uint32_t>(height)) != 0 && TIFFSetField(output.get(), TIFFTAG_SAMPLESPERPIXEL, 4) != 0 && TIFFSetField(output.get(), TIFFTAG_BITSPERSAMPLE, 8) != 0 && TIFFSetField(output.get(), TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT) != 0 && TIFFSetField(output.get(), TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG) != 0 && TIFFSetField(output.get(), TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB) != 0 && TIFFSetField(output.get(), TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT) != 0 && TIFFSetField(output.get(), TIFFTAG_COMPRESSION, COMPRESSION_LZW) != 0 && TIFFSetField(output.get(), TIFFTAG_ROWSPERSTRIP, TIFFDefaultStripSize(output.get(), 0)) != 0 && TIFFSetField(output.get(), TIFFTAG_EXTRASAMPLES, 1, &extra_sample) != 0 && TIFFSetField(output.get(), TIFFTAG_IMAGEDESCRIPTION, "ACMXVK processed snapshot: 8-bit RGBA TIFF") != 0;
143 if (!configured) {
144 throw std::runtime_error("unable to configure TIFF snapshot: " + path.string());
145 }
146
147 const std::size_t row_bytes = static_cast<std::size_t>(width) * 4U;
148 for (int row = 0; row < height; ++row) {
149 auto *row_pixels = const_cast<std::uint8_t *>(rgba + static_cast<std::size_t>(row) * row_bytes);
150 if (TIFFWriteScanline(output.get(), row_pixels, static_cast<std::uint32_t>(row), 0) < 0) {
151 throw std::runtime_error("unable to write TIFF snapshot: " + path.string());
152 }
153 }
154 }
155
156 void SnapshotWriter::saveTiff16(const fs::path &path, const std::uint16_t *rgba, int width, int height) {
157 if (rgba == nullptr || width <= 0 || height <= 0 || width > std::numeric_limits<int>::max() / 8) {
158 throw std::runtime_error("invalid image dimensions for 16-bit TIFF snapshot: " + path.string());
159 }
160 const std::unique_ptr<TIFF, decltype(&TIFFClose)> output(TIFFOpen(path.string().c_str(), "w"), &TIFFClose);
161 if (output == nullptr) {
162 throw std::runtime_error("unable to open TIFF snapshot: " + path.string());
163 }
164 const std::uint16_t extra_sample = EXTRASAMPLE_UNASSALPHA;
165 const bool configured = TIFFSetField(output.get(), TIFFTAG_IMAGEWIDTH, static_cast<std::uint32_t>(width)) != 0 && TIFFSetField(output.get(), TIFFTAG_IMAGELENGTH, static_cast<std::uint32_t>(height)) != 0 && TIFFSetField(output.get(), TIFFTAG_SAMPLESPERPIXEL, 4) != 0 && TIFFSetField(output.get(), TIFFTAG_BITSPERSAMPLE, 16) != 0 && TIFFSetField(output.get(), TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT) != 0 && TIFFSetField(output.get(), TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG) != 0 && TIFFSetField(output.get(), TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB) != 0 && TIFFSetField(output.get(), TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT) != 0 && TIFFSetField(output.get(), TIFFTAG_COMPRESSION, COMPRESSION_LZW) != 0 && TIFFSetField(output.get(), TIFFTAG_ROWSPERSTRIP, TIFFDefaultStripSize(output.get(), 0)) != 0 && TIFFSetField(output.get(), TIFFTAG_EXTRASAMPLES, 1, &extra_sample) != 0 && TIFFSetField(output.get(), TIFFTAG_IMAGEDESCRIPTION, "ACMXVK HDR snapshot: 16-bit RGBA") != 0;
166 if (!configured) {
167 throw std::runtime_error("unable to configure TIFF snapshot: " + path.string());
168 }
169 const std::size_t row_samples = static_cast<std::size_t>(width) * 4U;
170 for (int row = 0; row < height; ++row) {
171 auto *row_pixels = const_cast<std::uint16_t *>(rgba + static_cast<std::size_t>(row) * row_samples);
172 if (TIFFWriteScanline(output.get(), row_pixels, static_cast<std::uint32_t>(row), 0) < 0) {
173 throw std::runtime_error("unable to write TIFF snapshot: " + path.string());
174 }
175 }
176 }
177#endif
178
179 std::string_view SnapshotWriter::formatName(SnapshotFormat format) noexcept {
180 switch (format) {
182 return "WebP";
184 return "TIFF";
186 return "raw RGBA";
188 return "PNG";
189 }
190 return "snapshot";
191 }
192
194 while (true) {
195 SnapshotJob job;
196 {
197 std::unique_lock<std::mutex> lock(mutex);
198 condition.wait(lock, [&] { return stopping || !jobs.empty(); });
199 if (stopping && jobs.empty()) {
200 return;
201 }
202 job = std::move(jobs.front());
203 jobs.pop_front();
204 }
205
206 try {
207 if (job.format == SnapshotFormat::Raw) {
208 if (!job.rgba16.empty()) {
209 saveRaw16(job.path, job.rgba16, job.width, job.height);
210 } else {
211 saveRaw(job.path, job.rgba, job.width, job.height);
212 }
213 } else if (job.format == SnapshotFormat::Tiff) {
214#ifdef ACMXVK_WITH_TIFF
215 if (!job.rgba16.empty()) {
216 saveTiff16(job.path, job.rgba16.data(), static_cast<int>(job.width), static_cast<int>(job.height));
217 } else {
218 saveTiff(job.path, job.rgba.data(), static_cast<int>(job.width), static_cast<int>(job.height));
219 }
220#else
221 throw std::runtime_error("TIFF snapshot support is not compiled in");
222#endif
223 } else if (job.format == SnapshotFormat::WebP) {
224#ifdef ACMXVK_WITH_WEBP
225 saveWebP(job.path, job.rgba.data(), static_cast<int>(job.width), static_cast<int>(job.height));
226#else
227 throw std::runtime_error("WebP snapshot support is not compiled in");
228#endif
229 } else {
230 savePng(job.path, job.rgba.data(), static_cast<int>(job.width), static_cast<int>(job.height), job.png_level);
231 }
232 std::ostringstream message;
233 message << "acmxvk: took " << formatName(job.format) << " snapshot: " << job.path.string() << '\n';
234 std::cout << message.str();
235 } catch (const std::exception &error) {
236 std::ostringstream message;
237 message << "acmxvk: snapshot failed: " << error.what() << '\n';
238 std::cerr << message.str();
239 } catch (...) {
240 std::cerr << "acmxvk: snapshot failed with an unknown error\n";
241 }
242
243 std::lock_guard<std::mutex> lock(mutex);
244 if (jobs_in_flight > 0) {
246 }
247 }
248 }
249} // 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)
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 void savePng(const fs::path &path, std::uint8_t *rgba, int width, int height, int png_level)
static constexpr std::size_t QUEUE_CAPACITY
void enqueue(SnapshotJob job)
std::vector< std::uint16_t > rgba16
std::vector< std::uint8_t > rgba