GCC Code Coverage Report


ernie/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
826 / 927
89.1%
Branches:
689 / 1149
60.0%
Functions:
63 / 66
95.5%

io/texture.cpp
Line Branch Exec Source
1 // SPDX-License-Identifier: BSD-3-Clause
2 /// @file texture.cpp
3 /// @brief Texture formats, DDS and KTX2 reading and writing, Basis transcoding, BC encoding and
4 /// decoding, whole, banded and mosaic bakes, and the texture cache.
5 #include "ernie/io/texture.h"
6
7 #include "ernie/util/bytes.h"
8 #include "ernie/util/disk_cache.h"
9 #include "ernie/util/parallel.h"
10 #include "ernie/util/profiler.h"
11 #include "ernie/util/text.h"
12
13 #include <basisu_transcoder.h>
14 #include <bc7decomp.h>
15 #include <bc7enc.h>
16 #include <rgbcx.h>
17 #include <stb_image.h>
18 #include <zstd.h>
19
20 #include <algorithm>
21 #include <array>
22 #include <atomic>
23 #include <bit>
24 #include <chrono>
25 #include <climits>
26 #include <cmath>
27 #include <condition_variable>
28 #include <cstring>
29 #include <deque>
30 #include <exception>
31 #include <fstream>
32 #include <mutex>
33 #include <numeric>
34 #include <random>
35 #include <stdexcept>
36 #include <string>
37 #include <thread>
38 #include <utility>
39
40 namespace ernie {
41
42 namespace fs = std::filesystem;
43
44 namespace {
45
46 using F = TextureFormat;
47
48 // ----------------------------------------------------------------------------------------------
49 // Formats
50
51 /// A format's layout and the codes DDS (DXGI) and KTX2 (VkFormat) name it by.
52 struct FormatRow {
53 TextureFormat format;
54 TextureFormatInfo info;
55 std::uint32_t dxgi;
56 std::uint32_t vk;
57 };
58
59 /// Every format, in `TextureFormat`'s order.
60 constexpr std::array<FormatRow, 22> kFormats{{
61 {F::R8, {1, 1, 1, false}, 61, 9},
62 {F::Rg8, {1, 2, 2, false}, 49, 16},
63 {F::Rgba8, {1, 4, 4, false}, 28, 37},
64 {F::Rgba8Srgb, {1, 4, 4, true}, 29, 43},
65 {F::Bgra8, {1, 4, 4, false}, 87, 44},
66 {F::Bgra8Srgb, {1, 4, 4, true}, 91, 50},
67 {F::Rgba16F, {1, 8, 4, false}, 10, 97},
68 {F::Rgba32F, {1, 16, 4, false}, 2, 109},
69 {F::Bc1, {4, 8, 4, false}, 71, 133},
70 {F::Bc1Srgb, {4, 8, 4, true}, 72, 134},
71 {F::Bc2, {4, 16, 4, false}, 74, 135},
72 {F::Bc2Srgb, {4, 16, 4, true}, 75, 136},
73 {F::Bc3, {4, 16, 4, false}, 77, 137},
74 {F::Bc3Srgb, {4, 16, 4, true}, 78, 138},
75 {F::Bc4, {4, 8, 1, false}, 80, 139},
76 {F::Bc4Snorm, {4, 8, 1, false}, 81, 140},
77 {F::Bc5, {4, 16, 2, false}, 83, 141},
78 {F::Bc5Snorm, {4, 16, 2, false}, 84, 142},
79 {F::Bc6hUfloat, {4, 16, 3, false}, 95, 143},
80 {F::Bc6hSfloat, {4, 16, 3, false}, 96, 144},
81 {F::Bc7, {4, 16, 4, false}, 98, 145},
82 {F::Bc7Srgb, {4, 16, 4, true}, 99, 146},
83 }};
84
85 constexpr bool InFormatOrder() {
86 for (std::size_t i = 0; i < kFormats.size(); ++i) {
87 if (static_cast<std::size_t>(kFormats[i].format) != i) {
88 return false;
89 }
90 }
91 return true;
92 }
93 static_assert(InFormatOrder(), "kFormats is indexed by TextureFormat");
94
95 1848 const FormatRow& Row(TextureFormat format) {
96 1848 const auto index = static_cast<std::size_t>(format);
97
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1848 times.
1848 if (index >= kFormats.size()) {
98
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ throw std::invalid_argument("Unknown texture format");
99 }
100 1848 return kFormats[index];
101 }
102
103 55 template <typename Matches> std::optional<TextureFormat> FindFormat(const Matches& matches) {
104 55 const auto row = std::find_if(kFormats.begin(), kFormats.end(), matches);
105
3/4
std::optional<ernie::TextureFormat> ernie::(anonymous namespace)::FindFormat<ernie::(anonymous namespace)::FromDxgi(unsigned int)::{lambda(ernie::(anonymous namespace)::FormatRow const&)#1}>(ernie::(anonymous namespace)::FromDxgi(unsigned int)::{lambda(ernie::(anonymous namespace)::FormatRow const&)#1} const&):
✗ Branch 0 not taken.
✓ Branch 1 taken 44 times.
std::optional<ernie::TextureFormat> ernie::(anonymous namespace)::FindFormat<ernie::(anonymous namespace)::FromVkFormat(unsigned int)::{lambda(ernie::(anonymous namespace)::FormatRow const&)#1}>(ernie::(anonymous namespace)::FromVkFormat(unsigned int)::{lambda(ernie::(anonymous namespace)::FormatRow const&)#1} const&):
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 10 times.
55 return row == kFormats.end() ? std::nullopt : std::optional(row->format);
106 }
107
108 44 std::optional<TextureFormat> FromDxgi(std::uint32_t dxgi) {
109
11/14
✓ Branch 0 taken 133 times.
✓ Branch 1 taken 7 times.
✓ Branch 2 taken 128 times.
✓ Branch 3 taken 5 times.
✓ Branch 4 taken 120 times.
✓ Branch 5 taken 8 times.
✓ Branch 6 taken 108 times.
✓ Branch 7 taken 12 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 11 times.
✓ Branch 11 taken 1 time.
✓ Branch 12 taken 11 times.
✗ Branch 13 not taken.
163 return FindFormat([&](const FormatRow& row) { return row.dxgi == dxgi; });
110 }
111
112 11 std::optional<TextureFormat> FromVkFormat(std::uint32_t vk) {
113 // BC1_RGB (131, 132) reads as BC1_RGBA: they differ only in the alpha of blocks in
114 // three-colour mode.
115
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 11 times.
11 if (vk == 131 || vk == 132) {
116 ✗ vk += 2;
117 }
118
10/14
✓ Branch 0 taken 26 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 26 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 19 times.
✓ Branch 5 taken 7 times.
✓ Branch 6 taken 19 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 2 times.
✓ Branch 11 taken 1 time.
✓ Branch 12 taken 1 time.
✓ Branch 13 taken 1 time.
32 return FindFormat([&](const FormatRow& row) { return row.vk == vk; });
119 }
120
121 // ----------------------------------------------------------------------------------------------
122 // Layout
123
124 // Bounds that keep every size below in range, far past what devices sample whole (virtual
125 // textures page larger ones: global mosaics run to 86400 texels across).
126 constexpr std::uint32_t kMaxSide = 1u << 20;
127 constexpr std::uint32_t kMaxLayers = 2048;
128
129 /// A texture of `format`, `width` x `height` with `layers` layers and `mips` mips, its bytes still
130 /// to fill (with `allocate`, else left to a mapped file): checks the shape and lays out `offsets`
131 /// layer by layer.
132 104 TextureData Shape(TextureFormat format, std::uint32_t width, std::uint32_t height,
133 std::uint32_t layers, std::uint32_t mips, bool allocate = true) {
134
3/4
✓ Branch 0 taken 103 times.
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✓ Branch 3 taken 103 times.
104 if (width == 0 || height == 0 || width > kMaxSide || height > kMaxSide) {
135
2/4
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 5 taken 1 time.
✗ Branch 6 not taken.
3 throw std::runtime_error("Texture has a size of " + std::to_string(width) + " x " +
136
1/2
✓ Branch 3 taken 1 time.
✗ Branch 4 not taken.
3 std::to_string(height));
137 }
138
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 103 times.
103 if (layers == 0 || layers > kMaxLayers) {
139
0/4
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ throw std::runtime_error("Texture has " + std::to_string(layers) + " layers");
140 }
141
3/4
✓ Branch 0 taken 103 times.
✗ Branch 1 not taken.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 102 times.
103 if (mips == 0 || mips > MipCount(width, height)) {
142
2/4
✓ Branch 3 taken 1 time.
✗ Branch 4 not taken.
✓ Branch 6 taken 1 time.
✗ Branch 7 not taken.
3 throw std::runtime_error("Texture has " + std::to_string(mips) + " mips");
143 }
144 102 TextureData data;
145 102 data.format = format;
146 102 data.width = width;
147 102 data.height = height;
148 102 data.layers = layers;
149 102 data.mips = mips;
150 102 std::size_t total = 0;
151
2/2
✓ Branch 0 taken 114 times.
✓ Branch 1 taken 102 times.
216 for (std::uint32_t layer = 0; layer < layers; ++layer) {
152
2/2
✓ Branch 0 taken 463 times.
✓ Branch 1 taken 114 times.
577 for (std::uint32_t mip = 0; mip < mips; ++mip) {
153
1/2
✓ Branch 1 taken 463 times.
✗ Branch 2 not taken.
463 data.offsets.push_back(total);
154
5/6
✓ Branch 0 taken 450 times.
✓ Branch 1 taken 13 times.
✓ Branch 2 taken 425 times.
✓ Branch 3 taken 38 times.
✓ Branch 5 taken 463 times.
✗ Branch 6 not taken.
1338 total += ImageBytes(format, MipSide(width, mip), MipSide(height, mip));
155 }
156 }
157
2/2
✓ Branch 0 taken 46 times.
✓ Branch 1 taken 56 times.
102 if (allocate) {
158
1/2
✓ Branch 1 taken 46 times.
✗ Branch 2 not taken.
46 data.bytes.resize(total);
159 }
160 102 return data;
161 ✗ }
162
163 /// Bytes of every layer's mips of `texture`, as `Shape` laid them out.
164 53 std::size_t ShapeBytes(const TextureData& texture) {
165 53 const std::uint32_t last = texture.mips - 1;
166 53 return texture.offsets.back() +
167
4/4
✓ Branch 0 taken 48 times.
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 47 times.
✓ Branch 3 taken 6 times.
148 ImageBytes(texture.format, MipSide(texture.width, last), MipSide(texture.height, last));
168 }
169
170 /// A single-layer texture of a format the bakes write, laid out as `Shape`.
171 /// @throws std::invalid_argument for a format it does not write.
172 31 TextureData Allocate(TextureFormat format, std::uint32_t width, std::uint32_t height,
173 std::uint32_t mips, bool allocate = true) {
174 31 constexpr std::array kWritten{F::Rgba8, F::Rgba8Srgb, F::Bc1, F::Bc1Srgb, F::Bc3,
175 F::Bc3Srgb, F::Bc4, F::Bc5, F::Bc7, F::Bc7Srgb};
176
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 30 times.
31 if (std::find(kWritten.begin(), kWritten.end(), format) == kWritten.end()) {
177
1/2
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
1 throw std::invalid_argument("Textures are not encoded to this format");
178 }
179 30 return Shape(format, width, height, 1, mips, allocate);
180 }
181
182 // ----------------------------------------------------------------------------------------------
183 // DDS and KTX2 reading
184
185 /// The header fields of both formats: little-endian 32-bit words.
186 889 std::uint32_t U32(std::span<const std::byte> file, std::size_t at) {
187 889 return ReadLittleEndian<std::uint32_t>(file, at);
188 }
189
190 /// The format of a DDS file without a DX10 header, from its pixel format (`DDS_PIXELFORMAT` at
191 /// `at`).
192 13 TextureFormat LegacyDdsFormat(std::span<const std::byte> file, std::size_t at) {
193 13 constexpr std::uint32_t kFourCcFlag = 0x4;
194 13 constexpr std::uint32_t kRgbFlag = 0x40;
195 13 const std::uint32_t flags = U32(file, at + 4);
196
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 3 times.
13 if ((flags & kFourCcFlag) != 0) {
197 10 const std::uint32_t code = U32(file, at + 8);
198
4/10
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 time.
✓ Branch 9 taken 7 times.
10 switch (code) {
199 case FourCc("DXT1"):
200 return F::Bc1;
201 ✗ case FourCc("DXT2"):
202 ✗ case FourCc("DXT3"):
203 ✗ return F::Bc2;
204 1 case FourCc("DXT4"):
205 1 case FourCc("DXT5"):
206 1 return F::Bc3;
207 ✗ case FourCc("ATI1"):
208 ✗ case FourCc("BC4U"):
209 ✗ return F::Bc4;
210 ✗ case FourCc("BC4S"):
211 ✗ return F::Bc4Snorm;
212 1 case FourCc("ATI2"):
213 1 case FourCc("BC5U"):
214 1 return F::Bc5;
215 ✗ case FourCc("BC5S"):
216 ✗ return F::Bc5Snorm;
217 ✗ case 113:
218 ✗ return F::Rgba16F; // A16B16G16R16F
219 ✗ case 116:
220 ✗ return F::Rgba32F; // A32B32G32R32F
221 1 default:
222
1/2
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
2 throw std::runtime_error("DDS file has an unsupported FourCC code " +
223
1/2
✓ Branch 3 taken 1 time.
✗ Branch 4 not taken.
3 std::to_string(code));
224 }
225 }
226 3 const std::uint32_t bits = U32(file, at + 12);
227 3 const std::uint32_t red = U32(file, at + 16);
228 3 const std::uint32_t green = U32(file, at + 20);
229 3 const std::uint32_t blue = U32(file, at + 24);
230
3/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1 time.
3 if ((flags & kRgbFlag) != 0 && bits == 32 && green == 0xff00u) {
231
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
2 if (red == 0xffu && blue == 0xff0000u) {
232 return F::Rgba8;
233 }
234
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
1 if (red == 0xff0000u && blue == 0xffu) {
235 return F::Bgra8;
236 }
237 }
238
1/2
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
1 throw std::runtime_error("DDS file has an unsupported uncompressed layout");
239 }
240
241 /// A DDS file's texture: copied into `bytes` with `copy`, else read from `file` in place
242 /// (`mapped`, the offsets into it).
243 61 TextureData ParseDds(std::span<const std::byte> file, bool copy) {
244 // "DDS ", then the 124-byte DDS_HEADER, its pixel format at 76.
245 61 constexpr std::size_t kHeader = 4;
246 61 constexpr std::size_t kPixelFormat = kHeader + 72;
247
3/4
✓ Branch 1 taken 59 times.
✓ Branch 2 taken 2 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 59 times.
61 if (U32(file, 0) != FourCc("DDS ") || U32(file, kHeader) != 124) {
248
1/2
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 throw std::runtime_error("Not a DDS file");
249 }
250 59 constexpr std::uint32_t kMipCountFlag = 0x20000;
251 59 constexpr std::uint32_t kDepthFlag = 0x800000;
252 59 constexpr std::uint32_t kCubeMap = 0x200;
253 59 const std::uint32_t flags = U32(file, kHeader + 4);
254 59 const std::uint32_t height = U32(file, kHeader + 8);
255 59 const std::uint32_t width = U32(file, kHeader + 12);
256 59 const std::uint32_t depth = U32(file, kHeader + 20);
257 59 const std::uint32_t mipCount = U32(file, kHeader + 24);
258 59 const std::uint32_t caps2 = U32(file, kHeader + 108);
259
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 58 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
58 if ((flags & kDepthFlag) != 0 && depth > 1) {
260
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ throw std::runtime_error("DDS volume textures are not read");
261 }
262
3/4
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 38 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 20 times.
58 const std::uint32_t mips = (flags & kMipCountFlag) != 0 && mipCount > 0 ? mipCount : 1;
263
264 58 std::size_t data = kHeader + 124;
265 58 TextureFormat format;
266 58 std::uint32_t layers = 1;
267 58 bool cube = (caps2 & kCubeMap) != 0;
268
2/2
✓ Branch 1 taken 45 times.
✓ Branch 2 taken 13 times.
58 if (U32(file, kPixelFormat + 8) == FourCc("DX10")) {
269 // DDS_HEADER_DXT10: format, dimension, misc flags, array size.
270 45 const std::uint32_t dxgi = U32(file, data);
271 45 const std::uint32_t dimension = U32(file, data + 4);
272 45 constexpr std::uint32_t kTexture2d = 3;
273 45 constexpr std::uint32_t kCubeFlag = 0x4;
274
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 44 times.
45 if (dimension != kTexture2d) {
275
1/2
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
2 throw std::runtime_error("DDS file is not a 2D texture (dimension " +
276
1/2
✓ Branch 3 taken 1 time.
✗ Branch 4 not taken.
4 std::to_string(dimension) + ")");
277 }
278 44 const std::optional<TextureFormat> known = FromDxgi(dxgi);
279
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 44 times.
44 if (!known) {
280
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ throw std::runtime_error("DDS file has an unsupported DXGI format " +
281
0/2
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ std::to_string(dxgi));
282 }
283 44 format = *known;
284 44 cube = (U32(file, data + 8) & kCubeFlag) != 0;
285
2/2
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 43 times.
44 layers = std::max(1u, U32(file, data + 12));
286 44 data += 20;
287 } else {
288 13 format = LegacyDdsFormat(file, kPixelFormat);
289 }
290
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 53 times.
55 if (cube) {
291
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (layers > kMaxLayers / 6) {
292
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ throw std::runtime_error("DDS file has too many cube maps");
293 }
294 2 layers *= 6;
295 }
296 55 TextureData texture = Shape(format, width, height, layers, mips, copy);
297 53 texture.cube = cube;
298 // Each layer's mips follow the one before, as `TextureData` lays them out.
299
1/2
✓ Branch 1 taken 53 times.
✗ Branch 2 not taken.
53 const std::size_t total = ShapeBytes(texture);
300
3/4
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52 times.
✓ Branch 3 taken 1 time.
53 if (file.size() < data || file.size() - data < total) {
301
1/2
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
1 throw std::runtime_error("DDS file is truncated");
302 }
303
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 44 times.
52 if (copy) {
304 8 std::memcpy(texture.bytes.data(), file.data() + data, total);
305 } else {
306
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 44 times.
44 texture.mapped = file.subspan(data, total);
307 }
308 52 return texture;
309 1 }
310
311 /// A KTX2 file of Basis Universal's ETC1S or UASTC formats transcoded to BC blocks: two-channel
312 /// data (normal maps' x and y) to BC5, HDR to BC6H, the rest to BC7, sRGB as the file's transfer
313 /// function says.
314 6 TextureData TranscodeBasis(std::span<const std::byte> file) {
315 6 ERNIE_ZONE("Transcode Basis");
316 6 static std::once_flag initialised;
317
1/2
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
6 std::call_once(initialised, basist::basisu_transcoder_init);
318
1/2
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
6 basist::ktx2_transcoder ktx2;
319
3/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 1 time.
12 if (file.size() > UINT32_MAX ||
320
3/6
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 3 taken 6 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✓ Branch 6 taken 5 times.
11 !ktx2.init(file.data(), static_cast<std::uint32_t>(file.size())) ||
321
1/2
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
5 !ktx2.start_transcoding()) {
322
1/2
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
1 throw std::runtime_error("KTX2 file holds no Basis Universal data it can transcode");
323 }
324 5 TextureFormat format = F::Bc7;
325 5 basist::transcoder_texture_format target = basist::transcoder_texture_format::cTFBC7_RGBA;
326 // BC5 takes x from red and y from alpha unless told otherwise.
327 5 int first = -1;
328 5 int second = -1;
329
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 3 times.
5 const auto channel0 = ktx2.get_dfd_channel_id0();
330
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 3 times.
5 const bool twoChannels =
331
4/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 1 time.
5 ktx2.is_etc1s() ? channel0 == basist::KTX2_DF_CHANNEL_ETC1S_RRR &&
332
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
1 ktx2.get_dfd_channel_id1() == basist::KTX2_DF_CHANNEL_ETC1S_GGG
333 3 : channel0 == basist::KTX2_DF_CHANNEL_UASTC_RRRG ||
334 5 channel0 == basist::KTX2_DF_CHANNEL_UASTC_RG;
335
2/4
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
✗ Branch 4 not taken.
5 if (ktx2.is_hdr()) {
336 format = F::Bc6hUfloat;
337 target = basist::transcoder_texture_format::cTFBC6H;
338
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 3 times.
5 } else if (twoChannels) {
339 2 format = F::Bc5;
340 2 target = basist::transcoder_texture_format::cTFBC5_RG;
341
3/4
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 time.
2 if (!ktx2.is_etc1s() && channel0 == basist::KTX2_DF_CHANNEL_UASTC_RG) {
342 ✗ first = 0;
343 ✗ second = 1;
344 }
345
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 } else if (ktx2.get_dfd_transfer_func() == basist::KTX2_KHR_DF_TRANSFER_SRGB) {
346 ✗ format = F::Bc7Srgb;
347 }
348
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 const std::uint32_t faces = ktx2.get_faces();
349
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 const std::uint32_t layers = std::max(1u, ktx2.get_layers());
350
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 2 times.
5 const std::uint32_t levels = std::max(1u, ktx2.get_levels());
351
2/4
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
5 if (layers > kMaxLayers / std::max(1u, faces)) {
352
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ throw std::runtime_error("KTX2 file has too many layers");
353 }
354 5 TextureData texture =
355
1/2
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
5 Shape(format, ktx2.get_width(), ktx2.get_height(), layers * faces, levels);
356 5 texture.cube = faces == 6;
357
2/2
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 5 times.
22 for (std::uint32_t level = 0; level < levels; ++level) {
358
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 const std::uint32_t width = MipSide(texture.width, level);
359
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 const std::uint32_t height = MipSide(texture.height, level);
360 17 const auto blocks = static_cast<std::uint32_t>(((width + 3) / 4) * ((height + 3) / 4));
361
2/2
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 17 times.
34 for (std::uint32_t layer = 0; layer < layers; ++layer) {
362
2/2
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 17 times.
34 for (std::uint32_t face = 0; face < faces; ++face) {
363 17 const std::size_t slot = (std::size_t{layer} * faces + face) * levels + level;
364
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 17 times.
17 if (!ktx2.transcode_image_level(level, layer, face,
365
1/2
✓ Branch 1 taken 17 times.
✗ Branch 2 not taken.
17 texture.bytes.data() + texture.offsets[slot],
366 blocks, target, 0, 0, 0, first, second)) {
367
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ throw std::runtime_error("KTX2 file's level " + std::to_string(level) +
368
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ " does not transcode");
369 }
370 }
371 }
372 }
373 5 return texture;
374 7 }
375
376 /// A KTX2 file's texture: its levels as `ParseDds` reads, in place without `copy` unless they are
377 /// supercompressed (decoded into `bytes`).
378 19 TextureData ParseKtx2(std::span<const std::byte> file, bool copy) {
379 19 constexpr std::array<std::uint8_t, 12> kIdentifier{0xAB, 0x4B, 0x54, 0x58, 0x20, 0x32,
380 0x30, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A};
381
3/4
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 18 times.
19 if (file.size() < 80 || std::memcmp(file.data(), kIdentifier.data(), 12) != 0) {
382
1/2
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
1 throw std::runtime_error("Not a KTX2 file");
383 }
384 18 const std::uint32_t vkFormat = U32(file, 12);
385 18 const std::uint32_t width = U32(file, 20);
386 18 const std::uint32_t height = U32(file, 24);
387 18 const std::uint32_t depth = U32(file, 28);
388 18 const std::uint32_t layerCount = U32(file, 32);
389 18 const std::uint32_t faces = U32(file, 36);
390
2/2
✓ Branch 1 taken 7 times.
✓ Branch 2 taken 11 times.
18 const std::uint32_t levels = std::max(1u, U32(file, 40));
391 18 const std::uint32_t supercompression = U32(file, 44);
392
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 18 times.
18 if (depth > 1) {
393
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ throw std::runtime_error("KTX2 volume textures are not read");
394 }
395
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 18 times.
18 if (faces != 1 && faces != 6) {
396
0/4
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ throw std::runtime_error("KTX2 file has " + std::to_string(faces) + " faces");
397 }
398 // Basis Universal's formats (ETC1S, often BasisLZ-supercompressed, and UASTC) have no
399 // VkFormat.
400
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 12 times.
18 if (vkFormat == 0) {
401 6 return TranscodeBasis(file);
402 }
403 12 constexpr std::uint32_t kZstd = 2;
404
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 11 times.
12 if (supercompression != 0 && supercompression != kZstd) {
405
1/2
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
2 throw std::runtime_error("KTX2 file uses supercompression scheme " +
406 2 std::to_string(supercompression) +
407
1/2
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
3 ", which is not read (only zstd)");
408 }
409 11 const std::optional<TextureFormat> format = FromVkFormat(vkFormat);
410
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 10 times.
11 if (!format) {
411
1/2
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
2 throw std::runtime_error("KTX2 file has an unsupported VkFormat " +
412
1/2
✓ Branch 3 taken 1 time.
✗ Branch 4 not taken.
3 std::to_string(vkFormat));
413 }
414
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 9 times.
10 const std::uint32_t arrays = std::max(1u, layerCount);
415
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (arrays > kMaxLayers / faces) {
416
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ throw std::runtime_error("KTX2 file has too many layers");
417 }
418 // Stored levels are read in place: each image is contiguous in the file, the offsets pointing
419 // at it.
420 10 const bool inPlace = !copy && supercompression == 0;
421 10 TextureData texture = Shape(*format, width, height, arrays * faces, levels, !inPlace);
422 10 texture.cube = faces == 6;
423
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 8 times.
10 if (inPlace) {
424 2 texture.mapped = file;
425 }
426 // The level index follows the 80-byte header: each level's offset, stored length and
427 // uncompressed length, level 0 the largest. A level holds every layer's faces in turn.
428 10 std::vector<std::byte> decompressed;
429
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 6 times.
19 for (std::uint32_t level = 0; level < levels; ++level) {
430 13 const std::size_t entry = 80 + std::size_t{level} * 24;
431
1/2
✓ Branch 1 taken 13 times.
✗ Branch 2 not taken.
13 const auto offset = ReadLittleEndian<std::uint64_t>(file, entry);
432
1/2
✓ Branch 1 taken 13 times.
✗ Branch 2 not taken.
13 const auto length = ReadLittleEndian<std::uint64_t>(file, entry + 8);
433
1/2
✓ Branch 1 taken 13 times.
✗ Branch 2 not taken.
13 const auto uncompressed = ReadLittleEndian<std::uint64_t>(file, entry + 16);
434
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 const std::size_t image =
435
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 3 taken 13 times.
✗ Branch 4 not taken.
26 ImageBytes(*format, MipSide(width, level), MipSide(height, level));
436 13 const std::size_t expected = image * texture.layers;
437
5/6
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 10 times.
✓ Branch 5 taken 2 times.
25 if (offset > file.size() || file.size() - offset < length ||
438
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 8 times.
12 (supercompression == 0 ? length : uncompressed) != expected) {
439
2/4
✓ Branch 3 taken 3 times.
✗ Branch 4 not taken.
✓ Branch 6 taken 3 times.
✗ Branch 7 not taken.
6 throw std::runtime_error("KTX2 file has a malformed level " + std::to_string(level));
440 }
441
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 7 times.
10 if (inPlace) {
442
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
6 for (std::uint32_t layer = 0; layer < texture.layers; ++layer) {
443 3 texture.offsets[std::size_t{layer} * levels + level] =
444 3 static_cast<std::size_t>(offset) + std::size_t{layer} * image;
445 }
446 3 continue;
447 3 }
448 7 const std::byte* source = file.data() + offset;
449
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 4 times.
7 if (supercompression == kZstd) {
450
1/2
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
3 decompressed.resize(expected);
451
1/2
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
3 const std::size_t size = ZSTD_decompress(decompressed.data(), expected, source,
452 static_cast<std::size_t>(length));
453
4/6
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 1 time.
✓ Branch 5 taken 2 times.
✗ Branch 6 not taken.
3 if (ZSTD_isError(size) != 0 || size != expected) {
454
1/2
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
2 throw std::runtime_error("KTX2 file's level " + std::to_string(level) +
455
1/2
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
3 " does not decompress");
456 }
457 2 source = decompressed.data();
458 }
459
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 6 times.
13 for (std::uint32_t layer = 0; layer < texture.layers; ++layer) {
460 7 std::memcpy(texture.bytes.data() + texture.offsets[std::size_t{layer} * levels + level],
461 7 source + std::size_t{layer} * image, image);
462 }
463 }
464 6 return texture;
465 14 }
466
467 // ----------------------------------------------------------------------------------------------
468 // DDS writing
469
470 /// Bytes before a DDS file's texels as `WriteDdsHeader` writes them.
471 constexpr std::size_t kDdsHeaderBytes = (32 + 5) * 4;
472
473 /// Writes the header of a DDS file of `texture`, with the DX10 header.
474 47 void WriteDdsHeader(std::ostream& file, const TextureData& texture) {
475 47 static_assert(std::endian::native == std::endian::little,
476 "DDS headers are written as host words");
477 // "DDS ", DDS_HEADER (31 words after the magic), DDS_HEADER_DXT10.
478 47 std::array<std::uint32_t, 32 + 5> header{};
479 47 header[0] = FourCc("DDS ");
480 47 header[1] = 124;
481 // Caps, height, width, pixel format, linear size and mip count.
482
2/2
✓ Branch 0 taken 25 times.
✓ Branch 1 taken 22 times.
47 header[2] = 0x1 | 0x2 | 0x4 | 0x1000 | 0x80000 | (texture.mips > 1 ? 0x20000u : 0u);
483 47 header[3] = texture.height;
484 47 header[4] = texture.width;
485 94 header[5] =
486 47 static_cast<std::uint32_t>(ImageBytes(texture.format, texture.width, texture.height));
487 47 header[7] = texture.mips;
488 47 header[19] = 32;
489 47 header[20] = 0x4; // FourCC
490 47 header[21] = FourCc("DX10");
491 // DDSCAPS_TEXTURE, and COMPLEX and MIPMAP with mips.
492
2/2
✓ Branch 0 taken 25 times.
✓ Branch 1 taken 22 times.
47 header[27] = 0x1000 | (texture.mips > 1 ? 0x400008u : 0u);
493
1/2
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
47 header[28] = texture.cube ? 0xfe00u : 0u;
494 47 header[32] = Row(texture.format).dxgi;
495 47 header[33] = 3; // A 2D texture.
496
1/2
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
47 header[34] = texture.cube ? 0x4u : 0u;
497
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 47 times.
47 header[35] = texture.cube ? texture.layers / 6 : texture.layers;
498 47 static_assert(sizeof(header) == kDdsHeaderBytes);
499 47 file.write(reinterpret_cast<const char*>(header.data()), sizeof(header));
500 47 }
501
502 // ----------------------------------------------------------------------------------------------
503 // Encoding
504
505 /// Encodes one 4 x 4 block of RGBA8 `pixels` into `block`.
506 162908 void EncodeBlock(TextureFormat format, const std::uint8_t* pixels, std::byte* block,
507 const bc7enc_compress_block_params& bc7) {
508 // rgbcx's effort for BC1 and BC3, of 0 to 18.
509 162908 constexpr std::uint32_t kLevel = 10;
510
4/6
✓ Branch 0 taken 162196 times.
✓ Branch 1 taken 524 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 94 times.
✓ Branch 4 taken 94 times.
✗ Branch 5 not taken.
162908 switch (format) {
511 162196 case F::Bc7:
512 162196 case F::Bc7Srgb:
513 162196 bc7enc_compress_block(block, pixels, &bc7);
514 162196 return;
515 524 case F::Bc1:
516 524 case F::Bc1Srgb:
517 524 rgbcx::encode_bc1(kLevel, block, pixels, true, false);
518 524 return;
519 ✗ case F::Bc3:
520 ✗ case F::Bc3Srgb:
521 ✗ rgbcx::encode_bc3(kLevel, block, pixels);
522 ✗ return;
523 94 case F::Bc4:
524 94 rgbcx::encode_bc4(block, pixels, 4);
525 94 return;
526 94 case F::Bc5:
527 94 rgbcx::encode_bc5(block, pixels, 0, 1, 4);
528 94 return;
529 ✗ default:
530
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ throw std::invalid_argument("Textures are not encoded to this format");
531 }
532 }
533
534 /// `rgba` (`width` x `height` RGBA8) encoded into BC `format` blocks at `out` on the CPU, rows of
535 /// blocks on every thread.
536 240 void EncodeBlocks(TextureFormat format, std::span<const std::uint8_t> rgba, std::uint32_t width,
537 std::uint32_t height, std::byte* out, const bc7enc_compress_block_params& bc7) {
538 240 ERNIE_ZONE("Encode blocks on the CPU");
539
1/2
✓ Branch 1 taken 240 times.
✗ Branch 2 not taken.
240 const std::uint32_t blockBytes = FormatInfo(format).blockBytes;
540 240 const std::uint32_t across = (width + 3) / 4;
541
1/2
✓ Branch 1 taken 240 times.
✗ Branch 2 not taken.
240 ParallelLoop((height + 3) / 4, [&](std::size_t blockRow) {
542 3533 const auto row = static_cast<std::uint32_t>(blockRow);
543 3533 std::array<std::uint8_t, 64> pixels{};
544
2/2
✓ Branch 0 taken 162908 times.
✓ Branch 1 taken 3533 times.
166441 for (std::uint32_t column = 0; column < across; ++column) {
545 // Texels past the edge repeat the last row and column.
546
2/2
✓ Branch 0 taken 651632 times.
✓ Branch 1 taken 162908 times.
814540 for (std::uint32_t y = 0; y < 4; ++y) {
547
2/2
✓ Branch 0 taken 718 times.
✓ Branch 1 taken 650914 times.
651632 const std::uint32_t sy = std::min(row * 4 + y, height - 1);
548
2/2
✓ Branch 0 taken 2606528 times.
✓ Branch 1 taken 651632 times.
3258160 for (std::uint32_t x = 0; x < 4; ++x) {
549
2/2
✓ Branch 0 taken 12632 times.
✓ Branch 1 taken 2593896 times.
2606528 const std::uint32_t sx = std::min(column * 4 + x, width - 1);
550 2606528 std::memcpy(pixels.data() + 4 * (y * 4 + x),
551 2606528 rgba.data() + 4 * (std::size_t{sy} * width + sx), 4);
552 }
553 }
554 162908 EncodeBlock(format, pixels.data(),
555 162908 out + (std::size_t{row} * across + column) * blockBytes, bc7);
556 }
557 3533 });
558 240 }
559
560 /// BC7's settings for `format` at effort `quality`, the encoders initialised.
561 29 bc7enc_compress_block_params Bc7Params(TextureFormat format, int quality) {
562 29 static std::once_flag initialised;
563 29 std::call_once(initialised, [] {
564 bc7enc_compress_block_init();
565 rgbcx::init();
566 });
567 29 bc7enc_compress_block_params bc7{};
568 29 bc7enc_compress_block_params_init(&bc7);
569
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 27 times.
29 if (format == F::Bc7) {
570 2 bc7enc_compress_block_params_init_linear_weights(&bc7);
571 }
572
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
29 bc7.m_uber_level = static_cast<std::uint32_t>(std::clamp(quality, 0, BC7ENC_MAX_UBER_LEVEL));
573 29 return bc7;
574 }
575
576 /// `rgba` (`width` x `height` RGBA8) stored as `format` at `out`: copied when uncompressed, else
577 /// by `encoder` where it handles the format, else on the CPU.
578 450 void EncodeRgba8(TextureFormat format, std::span<const std::uint8_t> rgba, std::uint32_t width,
579 std::uint32_t height, std::byte* out, const bc7enc_compress_block_params& bc7,
580 BlockEncoder* encoder) {
581
2/2
✓ Branch 1 taken 210 times.
✓ Branch 2 taken 240 times.
450 if (!FormatInfo(format).compressed()) {
582 210 std::memcpy(out, rgba.data(), rgba.size());
583
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 240 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
240 } else if (encoder != nullptr && encoder->Encodes(format)) {
584 ✗ encoder->Encode(rgba, width, height, format, out);
585 } else {
586 240 EncodeBlocks(format, rgba, width, height, out, bc7);
587 }
588 450 }
589
590 /// The next mip of RGBA8 `rgba` (`width` x `height`): each texel the average of the 2 x 2 above
591 /// it, clamped at odd edges, as `MipChain`'s `MipFilter::Average`; with `srgb` its rgb averaged as
592 /// linear values.
593 245 std::vector<std::uint8_t> HalveRgba8(std::span<const std::uint8_t> rgba, std::uint32_t width,
594 std::uint32_t height, bool srgb) {
595 245 ERNIE_ZONE("Halve RGBA8");
596 8 static const std::array<float, 256> kLinear = [] {
597 8 std::array<float, 256> table{};
598
2/2
✓ Branch 0 taken 2048 times.
✓ Branch 1 taken 8 times.
2056 for (std::size_t i = 0; i < table.size(); ++i) {
599 2048 table[i] = SrgbToLinear(static_cast<float>(i) / 255.0f);
600 }
601 8 return table;
602
4/6
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 237 times.
✓ Branch 3 taken 8 times.
✗ Branch 4 not taken.
✓ Branch 6 taken 8 times.
✗ Branch 7 not taken.
245 }();
603 // Linear back to sRGB bytes by table rather than a power a texel, the most of the halving's
604 // time: 2^14 steps of linear, finer than half a byte of sRGB even near black, where its slope
605 // is 12.92.
606 245 constexpr std::size_t kSteps = 1u << 14;
607 8 static const std::array<std::uint8_t, kSteps + 1> kSrgb = [] {
608 8 std::array<std::uint8_t, kSteps + 1> table{};
609
2/2
✓ Branch 0 taken 131080 times.
✓ Branch 1 taken 8 times.
131088 for (std::size_t i = 0; i < table.size(); ++i) {
610 131080 table[i] = static_cast<std::uint8_t>(
611 131080 std::lround(LinearToSrgb(static_cast<float>(i) / kSteps) * 255.0f));
612 }
613 8 return table;
614
4/6
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 237 times.
✓ Branch 3 taken 8 times.
✗ Branch 4 not taken.
✓ Branch 6 taken 8 times.
✗ Branch 7 not taken.
245 }();
615
2/2
✓ Branch 0 taken 221 times.
✓ Branch 1 taken 24 times.
245 const std::uint32_t w = MipSide(width, 1);
616
2/2
✓ Branch 0 taken 234 times.
✓ Branch 1 taken 11 times.
245 const std::uint32_t h = MipSide(height, 1);
617
1/2
✓ Branch 1 taken 245 times.
✗ Branch 2 not taken.
245 std::vector<std::uint8_t> half(std::size_t{w} * h * 4);
618
1/2
✓ Branch 1 taken 245 times.
✗ Branch 2 not taken.
245 ParallelLoop(h, [&](std::size_t line) {
619 15184 const auto y = static_cast<std::uint32_t>(line);
620
2/2
✓ Branch 0 taken 1591358 times.
✓ Branch 1 taken 15184 times.
1606542 for (std::uint32_t x = 0; x < w; ++x) {
621
2/2
✓ Branch 0 taken 6365432 times.
✓ Branch 1 taken 1591358 times.
7956790 for (std::uint32_t c = 0; c < 4; ++c) {
622 float sum = 0.0f;
623
2/2
✓ Branch 0 taken 12730864 times.
✓ Branch 1 taken 6365432 times.
19096296 for (std::uint32_t dy = 0; dy < 2; ++dy) {
624
2/2
✓ Branch 0 taken 25461728 times.
✓ Branch 1 taken 12730864 times.
38192592 for (std::uint32_t dx = 0; dx < 2; ++dx) {
625
2/2
✓ Branch 0 taken 336 times.
✓ Branch 1 taken 25461392 times.
25461728 const std::uint32_t sx = std::min(2 * x + dx, width - 1);
626
2/2
✓ Branch 0 taken 184 times.
✓ Branch 1 taken 25461544 times.
25461728 const std::uint32_t sy = std::min(2 * y + dy, height - 1);
627
2/2
✓ Branch 0 taken 25419008 times.
✓ Branch 1 taken 42720 times.
25461728 const std::uint8_t value = rgba[(std::size_t{sy} * width + sx) * 4 + c];
628
4/4
✓ Branch 0 taken 25419008 times.
✓ Branch 1 taken 42720 times.
✓ Branch 2 taken 19064256 times.
✓ Branch 3 taken 6354752 times.
25461728 sum += srgb && c < 3 ? kLinear[value] : static_cast<float>(value) / 255.0f;
629 }
630 }
631
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6365432 times.
6365432 const float mean = std::clamp(sum / 4.0f, 0.0f, 1.0f);
632 6365432 half[(std::size_t{y} * w + x) * 4 + c] =
633
4/4
✓ Branch 0 taken 6354752 times.
✓ Branch 1 taken 10680 times.
✓ Branch 2 taken 4766064 times.
✓ Branch 3 taken 1588688 times.
6365432 srgb && c < 3 ? kSrgb[static_cast<std::size_t>(std::lround(mean * kSteps))]
634 1599368 : static_cast<std::uint8_t>(std::lround(mean * 255.0f));
635 }
636 }
637 15184 });
638 245 return half;
639 245 }
640
641 /// An 8-bit PNG or JPEG decoded as RGBA8, in stb's buffer.
642
0/6
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ struct Rgba8Image {
643 std::unique_ptr<stbi_uc, decltype(&stbi_image_free)> texels{nullptr, &stbi_image_free};
644 std::uint32_t width = 0;
645 std::uint32_t height = 0;
646
647 12 std::span<const std::uint8_t> Texels() const {
648 12 return {texels.get(), std::size_t{width} * height * 4};
649 }
650 };
651
652 12 Rgba8Image DecodeRgba8(const fs::path& file) {
653 12 ERNIE_ZONE("Decode image");
654
1/2
✓ Branch 1 taken 12 times.
✗ Branch 2 not taken.
12 const std::string name = file.string();
655 12 int width = 0;
656 12 int height = 0;
657 12 int channels = 0;
658
1/2
✓ Branch 1 taken 12 times.
✗ Branch 2 not taken.
12 Rgba8Image image;
659
2/4
✓ Branch 1 taken 12 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 12 times.
12 image.texels.reset(stbi_load(name.c_str(), &width, &height, &channels, 4));
660
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 12 times.
12 if (image.texels == nullptr) {
661
0/8
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✗ throw std::runtime_error("Cannot read " + name + ": " + stbi_failure_reason());
662 }
663 12 image.width = static_cast<std::uint32_t>(width);
664 12 image.height = static_cast<std::uint32_t>(height);
665 24 return image;
666 12 }
667
668 /// The format and sRGB encoding a bake stores in.
669 23 TextureFormat BakeFormat(const BakeOptions& options, bool srgb) {
670
2/2
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 1 time.
23 const TextureFormat role = options.role == TextureRole::Normal ? F::Bc5 : F::Bc7;
671
2/2
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 6 times.
40 return WithSrgb(options.format.value_or(role), srgb);
672 }
673
674 23 bool BakesSrgb(const BakeOptions& options) {
675
4/4
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 19 times.
23 return options.role == TextureRole::Colour && options.srgb;
676 }
677
678 } // namespace
679
680 // ----------------------------------------------------------------------------------------------
681 // Formats
682
683 1801 TextureFormatInfo FormatInfo(TextureFormat format) {
684 1801 return Row(format).info;
685 }
686
687 26 TextureFormat WithSrgb(TextureFormat format, bool srgb) {
688 26 constexpr std::array<std::array<F, 2>, 6> kPairs{{
689 {F::Rgba8, F::Rgba8Srgb},
690 {F::Bgra8, F::Bgra8Srgb},
691 {F::Bc1, F::Bc1Srgb},
692 {F::Bc2, F::Bc2Srgb},
693 {F::Bc3, F::Bc3Srgb},
694 {F::Bc7, F::Bc7Srgb},
695 }};
696
2/2
✓ Branch 0 taken 108 times.
✓ Branch 1 taken 3 times.
111 for (const auto& pair : kPairs) {
697
4/4
✓ Branch 0 taken 86 times.
✓ Branch 1 taken 22 times.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 85 times.
108 if (format == pair[0] || format == pair[1]) {
698
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 20 times.
26 return pair[srgb ? 1 : 0];
699 }
700 }
701 return format;
702 }
703
704 1084 std::size_t ImageBytes(TextureFormat format, std::uint32_t width, std::uint32_t height) {
705 1084 const TextureFormatInfo info = FormatInfo(format);
706 1084 const std::size_t across = (width + info.blockSize - 1) / info.blockSize;
707 1084 const std::size_t down = (height + info.blockSize - 1) / info.blockSize;
708 1084 return across * down * info.blockBytes;
709 }
710
711 127 std::uint32_t MipCount(std::uint32_t width, std::uint32_t height) {
712
5/6
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 107 times.
✓ Branch 2 taken 127 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 125 times.
✓ Branch 5 taken 2 times.
274 return std::max(1u, static_cast<std::uint32_t>(std::bit_width(std::max(width, height))));
713 }
714
715 // ----------------------------------------------------------------------------------------------
716 // Stored textures
717
718 149 std::span<const std::byte> TextureData::Mip(std::uint32_t layer, std::uint32_t mip) const {
719
3/4
✓ Branch 0 taken 149 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 148 times.
149 if (layer >= layers || mip >= mips) {
720
2/4
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 5 taken 1 time.
✗ Branch 6 not taken.
3 throw std::out_of_range("Texture has no layer " + std::to_string(layer) + " mip " +
721
1/2
✓ Branch 3 taken 1 time.
✗ Branch 4 not taken.
3 std::to_string(mip));
722 }
723
2/2
✓ Branch 0 taken 101 times.
✓ Branch 1 taken 47 times.
148 const std::size_t at = offsets[std::size_t{layer} * mips + mip];
724
7/8
✓ Branch 0 taken 101 times.
✓ Branch 1 taken 47 times.
✓ Branch 2 taken 146 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 124 times.
✓ Branch 5 taken 24 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 148 times.
566 return Data().subspan(at, ImageBytes(format, MipSide(width, mip), MipSide(height, mip)));
725 }
726
727 57 bool IsTextureFile(const fs::path& path) {
728
1/2
✓ Branch 2 taken 57 times.
✗ Branch 3 not taken.
171 const std::string extension = ToLower(path.extension().string());
729
4/4
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 47 times.
✓ Branch 2 taken 8 times.
✓ Branch 3 taken 2 times.
65 return extension == ".dds" || extension == ".ktx2";
730 57 }
731
732 53 TextureData LoadTextureFile(const fs::path& path) {
733 53 ERNIE_ZONE("Load texture file");
734
2/4
✓ Branch 1 taken 53 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 53 times.
53 if (!IsTextureFile(path)) {
735
0/6
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ throw std::runtime_error(path.string() + " is neither a .dds nor a .ktx2 file");
736 }
737 // Mapped, so texels are read from disk only as they are used: a virtual texture's tiles in
738 // view, not the whole file.
739
2/2
✓ Branch 1 taken 52 times.
✓ Branch 2 taken 1 time.
53 auto mapping = std::make_shared<const MappedFile>(path);
740 52 try {
741
2/4
✓ Branch 1 taken 52 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 52 times.
✗ Branch 5 not taken.
158 TextureData texture = ToLower(path.extension().string()) == ".dds"
742
2/2
✓ Branch 0 taken 45 times.
✓ Branch 1 taken 7 times.
52 ? ParseDds(mapping->Bytes(), false)
743
3/4
✓ Branch 1 taken 44 times.
✓ Branch 2 taken 1 time.
✓ Branch 4 taken 7 times.
✗ Branch 5 not taken.
52 : ParseKtx2(mapping->Bytes(), false);
744
2/2
✓ Branch 0 taken 46 times.
✓ Branch 1 taken 5 times.
51 if (!texture.mapped.empty()) {
745 46 texture.mapping = std::move(mapping);
746 }
747 102 return texture;
748
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
52 } catch (const std::runtime_error& error) {
749
4/8
✓ Branch 3 taken 1 time.
✗ Branch 4 not taken.
✓ Branch 6 taken 1 time.
✗ Branch 7 not taken.
✓ Branch 9 taken 1 time.
✗ Branch 10 not taken.
✓ Branch 12 taken 1 time.
✗ Branch 13 not taken.
4 throw std::runtime_error(path.string() + ": " + error.what());
750 1 }
751 53 }
752
753 16 TextureData ReadDds(std::span<const std::byte> file) {
754 16 return ParseDds(file, true);
755 }
756
757 12 TextureData ReadKtx2(std::span<const std::byte> file) {
758 12 return ParseKtx2(file, true);
759 }
760
761 37 void WriteDdsFile(const fs::path& path, const TextureData& texture) {
762 37 ERNIE_ZONE("Write DDS");
763
1/2
✓ Branch 1 taken 37 times.
✗ Branch 2 not taken.
37 std::ofstream file(path, std::ios::binary);
764
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 37 times.
37 if (!file) {
765
0/6
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ throw std::runtime_error("Cannot write " + path.string());
766 }
767
1/2
✓ Branch 1 taken 37 times.
✗ Branch 2 not taken.
37 WriteDdsHeader(file, texture);
768 // Layer after layer, each layer's mips in turn: `TextureData`'s order.
769
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 37 times.
37 const std::span<const std::byte> data = texture.Data();
770
1/2
✓ Branch 1 taken 37 times.
✗ Branch 2 not taken.
37 file.write(reinterpret_cast<const char*>(data.data()),
771
1/2
✓ Branch 1 taken 37 times.
✗ Branch 2 not taken.
37 static_cast<std::streamsize>(data.size()));
772
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 37 times.
37 if (!file) {
773
0/6
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ throw std::runtime_error("Cannot write " + path.string());
774 }
775 37 }
776
777 5 TextureData DecodeBlocks(const TextureData& texture) {
778 5 const TextureFormatInfo info = FormatInfo(texture.format);
779
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (!info.compressed()) {
780 ✗ return texture;
781 }
782 5 ERNIE_ZONE("Decode BC blocks");
783 5 TextureFormat decoded = F::Rgba8;
784
5/5
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 1 time.
5 switch (texture.format) {
785 case F::Bc1:
786 case F::Bc3:
787 case F::Bc7:
788 break;
789 1 case F::Bc1Srgb:
790 1 case F::Bc3Srgb:
791 1 case F::Bc7Srgb:
792 1 decoded = F::Rgba8Srgb;
793 1 break;
794 1 case F::Bc4:
795 1 decoded = F::R8;
796 1 break;
797 1 case F::Bc5:
798 1 decoded = F::Rg8;
799 1 break;
800 1 default:
801
1/2
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
1 throw std::invalid_argument("BC2, BC6H and signed BC4 and BC5 are not decoded");
802 }
803
1/2
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
4 const std::uint32_t channels = FormatInfo(decoded).channels;
804
805
1/2
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
8 TextureData out = Shape(decoded, texture.width, texture.height, texture.layers, texture.mips);
806 4 out.cube = texture.cube;
807
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 4 times.
8 for (std::uint32_t layer = 0; layer < texture.layers; ++layer) {
808
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 4 times.
20 for (std::uint32_t mip = 0; mip < texture.mips; ++mip) {
809
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 const std::uint32_t w = MipSide(texture.width, mip);
810
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 const std::uint32_t h = MipSide(texture.height, mip);
811
1/2
✓ Branch 1 taken 16 times.
✗ Branch 2 not taken.
16 const std::span<const std::byte> blocks = texture.Mip(layer, mip);
812 16 std::byte* texels = out.bytes.data() + out.offsets[std::size_t{layer} * out.mips + mip];
813 16 const std::uint32_t across = (w + 3) / 4;
814
2/2
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 16 times.
36 for (std::uint32_t by = 0; by < (h + 3) / 4; ++by) {
815
2/2
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 20 times.
48 for (std::uint32_t bx = 0; bx < across; ++bx) {
816
4/5
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 7 times.
✓ Branch 4 taken 7 times.
28 const std::byte* block =
817 28 blocks.data() + (std::size_t{by} * across + bx) * info.blockBytes;
818 // The block's 4 x 4 texels as RGBA8.
819 28 std::array<std::uint8_t, 64> rgba{};
820
4/5
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 7 times.
✓ Branch 4 taken 7 times.
28 switch (texture.format) {
821 7 case F::Bc1:
822 7 case F::Bc1Srgb:
823
1/2
✓ Branch 1 taken 7 times.
✗ Branch 2 not taken.
7 rgbcx::unpack_bc1(block, rgba.data());
824 break;
825 ✗ case F::Bc3:
826 ✗ case F::Bc3Srgb:
827
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ rgbcx::unpack_bc3(block, rgba.data());
828 break;
829 7 case F::Bc4:
830
1/2
✓ Branch 1 taken 7 times.
✗ Branch 2 not taken.
7 rgbcx::unpack_bc4(block, rgba.data());
831 break;
832 7 case F::Bc5:
833
1/2
✓ Branch 1 taken 7 times.
✗ Branch 2 not taken.
7 rgbcx::unpack_bc5(block, rgba.data());
834 break;
835 7 default:
836
1/2
✓ Branch 1 taken 7 times.
✗ Branch 2 not taken.
7 bc7decomp::unpack_bc7(
837
1/2
✓ Branch 1 taken 7 times.
✗ Branch 2 not taken.
7 block, reinterpret_cast<bc7decomp::color_rgba*>(rgba.data()));
838 break;
839 }
840
2/2
✓ Branch 0 taken 448 times.
✓ Branch 1 taken 28 times.
476 for (std::uint32_t i = 0; i < 16; ++i) {
841 448 const std::uint32_t x = bx * 4 + i % 4;
842 448 const std::uint32_t y = by * 4 + i / 4;
843
2/2
✓ Branch 0 taken 340 times.
✓ Branch 1 taken 108 times.
448 if (x < w && y < h) {
844 340 std::memcpy(texels + (std::size_t{y} * w + x) * channels,
845 340 rgba.data() + std::size_t{i} * 4, channels);
846 }
847 }
848 }
849 }
850 }
851 }
852 4 return out;
853 5 }
854
855 // ----------------------------------------------------------------------------------------------
856 // Baking
857
858 15 std::optional<glm::uvec2> Rgba8ImageSize(const fs::path& file) {
859
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
15 if (!IsPngOrJpeg(file)) {
860 ✗ return std::nullopt;
861 }
862 15 const std::string name = file.string();
863 15 int width = 0;
864 15 int height = 0;
865 15 int channels = 0;
866
4/6
✓ Branch 1 taken 15 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 13 times.
28 if (stbi_info(name.c_str(), &width, &height, &channels) == 0 ||
867
1/2
✓ Branch 1 taken 13 times.
✗ Branch 2 not taken.
13 stbi_is_16_bit(name.c_str()) != 0) {
868 2 return std::nullopt;
869 }
870 13 return glm::uvec2(static_cast<std::uint32_t>(width), static_cast<std::uint32_t>(height));
871 15 }
872
873 1 std::vector<std::vector<std::uint8_t>> LoadRgba8Mips(const fs::path& file, bool srgb) {
874 1 ERNIE_ZONE("Load RGBA8 mips");
875
2/4
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
1 if (!Rgba8ImageSize(file)) {
876
0/6
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ throw std::runtime_error(file.string() + " is not an 8-bit PNG or JPEG");
877 }
878
1/2
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
1 Rgba8Image image = DecodeRgba8(file);
879 1 std::uint32_t w = image.width;
880 1 std::uint32_t h = image.height;
881 1 std::vector<std::vector<std::uint8_t>> mips;
882
1/2
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
1 mips.emplace_back(image.Texels().begin(), image.Texels().end());
883
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1 image.texels.reset();
884 3 while (w > 1 || h > 1) {
885
1/2
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
4 mips.push_back(HalveRgba8(mips.back(), w, h, srgb));
886
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 w = MipSide(w, 1);
887
3/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1 time.
5 h = MipSide(h, 1);
888 }
889
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
2 return mips;
890 1 }
891
892 10 TextureData EncodeTexture(std::span<const Image> mips, TextureFormat format, ImageEncoding encoding,
893 int quality, BlockEncoder* encoder) {
894
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (mips.empty()) {
895
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ throw std::invalid_argument("A texture needs a mip");
896 }
897 10 TextureData texture =
898 10 Allocate(format, mips[0].width, mips[0].height, static_cast<std::uint32_t>(mips.size()));
899
2/2
✓ Branch 0 taken 35 times.
✓ Branch 1 taken 8 times.
43 for (std::uint32_t mip = 0; mip < texture.mips; ++mip) {
900
3/4
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 34 times.
✓ Branch 3 taken 1 time.
70 if (mips[mip].width != MipSide(texture.width, mip) ||
901
3/4
✓ Branch 0 taken 33 times.
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✓ Branch 3 taken 34 times.
67 mips[mip].height != MipSide(texture.height, mip)) {
902
1/2
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
2 throw std::invalid_argument("Mip " + std::to_string(mip) +
903
1/2
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
3 " does not halve the one before");
904 }
905 }
906
1/2
✓ Branch 1 taken 8 times.
✗ Branch 2 not taken.
8 const bc7enc_compress_block_params bc7 = Bc7Params(format, quality);
907
2/2
✓ Branch 0 taken 33 times.
✓ Branch 1 taken 8 times.
41 for (std::uint32_t mip = 0; mip < texture.mips; ++mip) {
908
1/2
✓ Branch 1 taken 33 times.
✗ Branch 2 not taken.
66 EncodeRgba8(format, ToRgba8(mips[mip], encoding), mips[mip].width, mips[mip].height,
909
1/2
✓ Branch 1 taken 33 times.
✗ Branch 2 not taken.
33 texture.bytes.data() + texture.offsets[mip], bc7, encoder);
910 }
911 8 return texture;
912 1 }
913
914 13 TextureData BakeTexture(const fs::path& input, const fs::path& output, const BakeOptions& options) {
915 13 ERNIE_ZONE("Bake texture");
916 13 const bool srgb = BakesSrgb(options);
917
1/2
✓ Branch 1 taken 13 times.
✗ Branch 2 not taken.
13 const TextureFormat format = BakeFormat(options, srgb);
918 // Rasters read in parts bake a band at a time.
919
2/4
✓ Branch 1 taken 13 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
39 const std::string extension = ToLower(input.extension().string());
920
4/8
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 13 times.
✗ Branch 7 not taken.
13 if (extension == ".tif" || extension == ".tiff" || extension == ".img" || extension == ".lbl" ||
921
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
13 extension == ".mosaic") {
922
0/6
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ RasterBake(OpenRaster(input), output, options).Finish();
923
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ return LoadTextureFile(output);
924 }
925 // 8-bit PNG and JPEG colour and data stay 8-bit, each mip made from the last and dropped once
926 // encoded: 4 bytes a texel of the full size rather than float's 16 for the whole chain, which
927 // a global mosaic needs. Normal maps (their slopes) and EXR and 16-bit images go through
928 // floats.
929
5/6
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 1 time.
✓ Branch 3 taken 12 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 time.
✓ Branch 6 taken 11 times.
13 if (options.role != TextureRole::Normal && Rgba8ImageSize(input)) {
930
1/2
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
11 Rgba8Image image = DecodeRgba8(input);
931 11 std::uint32_t w = image.width;
932 11 std::uint32_t h = image.height;
933
2/4
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 11 times.
✗ Branch 5 not taken.
11 TextureData texture = Allocate(format, w, h, MipCount(w, h));
934
1/2
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
11 const bc7enc_compress_block_params bc7 = Bc7Params(format, options.quality);
935 11 std::vector<std::uint8_t> halved;
936 11 std::span<const std::uint8_t> rgba = image.Texels();
937 11 for (std::uint32_t mip = 0; mip < texture.mips; ++mip) {
938 84 EncodeRgba8(format, rgba, w, h, texture.bytes.data() + texture.offsets[mip], bc7,
939
1/2
✓ Branch 1 taken 84 times.
✗ Branch 2 not taken.
84 options.encoder);
940
2/2
✓ Branch 0 taken 73 times.
✓ Branch 1 taken 11 times.
84 if (mip + 1 < texture.mips) {
941
1/2
✓ Branch 1 taken 73 times.
✗ Branch 2 not taken.
146 halved = HalveRgba8(rgba, w, h, srgb);
942
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 62 times.
73 rgba = halved;
943 // Mip 0 is read from the decoder's buffer, freed once halved.
944
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 62 times.
73 image.texels.reset();
945
2/2
✓ Branch 0 taken 67 times.
✓ Branch 1 taken 6 times.
73 w = MipSide(w, 1);
946
4/4
✓ Branch 0 taken 68 times.
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 84 times.
✓ Branch 3 taken 11 times.
168 h = MipSide(h, 1);
947 }
948 }
949
1/2
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
11 WriteDdsFile(output, texture);
950 11 return texture;
951
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
11 }
952
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
2 const ImageEncoding encoding = srgb ? ImageEncoding::Srgb : ImageEncoding::Linear;
953 2 const std::vector<Image> mips = MipChain(
954
2/2
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
2 LoadImageFile(input, encoding),
955
3/4
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
✓ Branch 3 taken 1 time.
✗ Branch 4 not taken.
3 options.role == TextureRole::Normal ? MipFilter::NormalSlopes : MipFilter::Average);
956 // Normals' mips hold n / 2 + 1/2 in rgb, as the stored-texture decode reads x and y; BC5 keeps
957 // those two.
958
1/2
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
1 TextureData texture = EncodeTexture(mips, format, encoding, options.quality, options.encoder);
959
1/2
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
1 WriteDdsFile(output, texture);
960 1 return texture;
961 14 }
962
963 namespace {
964
965 /// Rows of a band read at once.
966 constexpr std::uint32_t kBandRows = 1024;
967 /// Band mips waiting to be encoded, past which reading waits.
968 constexpr std::size_t kQueuedBytes = std::size_t{256} << 20;
969
970 /// The texels of mip `mip` from a span [`start`, `start` + `count`) of a side of `side` texels:
971 /// the span's start there and how many. Spans start at multiples of 2^mip, so a mip's spans tile
972 /// it.
973 376 std::pair<std::uint32_t, std::uint32_t> MipSpan(std::uint32_t side, std::uint32_t start,
974 std::uint32_t count, std::uint32_t mip) {
975 376 const std::uint32_t first = start >> mip;
976
2/2
✓ Branch 0 taken 233 times.
✓ Branch 1 taken 143 times.
376 const std::uint32_t end = start + count == side ? MipSide(side, mip) : (start + count) >> mip;
977
2/2
✓ Branch 0 taken 372 times.
✓ Branch 1 taken 4 times.
376 return {first, end > first ? end - first : 0};
978 }
979
980 /// `pixels` samples of `source`'s at `samples` as RGBA8 at `out`.
981 31 void SamplesToRgba8(const GridSource& source, const std::byte* samples, std::size_t pixels,
982 std::uint8_t* out) {
983 31 const std::uint32_t channels = source.Channels();
984 31 const bool wide = source.Type() == GridSampleType::UInt16;
985
2/2
✓ Branch 0 taken 3604448 times.
✓ Branch 1 taken 31 times.
3604479 for (std::size_t i = 0; i < pixels; ++i) {
986 3604448 std::array<std::uint8_t, 4> c{0, 0, 0, 255};
987
3/4
✗ Branch 0 not taken.
✓ Branch 1 taken 18022240 times.
✓ Branch 2 taken 14417792 times.
✓ Branch 3 taken 3604448 times.
18022240 for (std::uint32_t k = 0; k < std::min(channels, 4u); ++k) {
988 14417792 const std::size_t sample = i * channels + k;
989 14417792 c[k] = wide
990
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 14417792 times.
14417792 ? static_cast<std::uint8_t>(LoadAs<std::uint16_t>(samples + sample * 2) >> 8)
991 14417792 : static_cast<std::uint8_t>(samples[sample]);
992 }
993
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3604448 times.
3604448 if (channels <= 2) {
994 // Grey, and alpha.
995
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ c = {c[0], c[0], c[0], channels == 2 ? c[1] : c[3]};
996 }
997 3604448 std::memcpy(out + i * 4, c.data(), 4);
998 }
999 31 }
1000
1001 /// The rows and columns of `rgba` (`width` wide) up to `w` x `h`.
1002 ✗ std::vector<std::uint8_t> Cropped(std::vector<std::uint8_t> rgba, std::uint32_t width,
1003 std::uint32_t w, std::uint32_t h) {
1004
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (w == width) {
1005 ✗ rgba.resize(std::size_t{w} * h * 4);
1006 ✗ return rgba;
1007 }
1008 ✗ std::vector<std::uint8_t> cropped(std::size_t{w} * h * 4);
1009
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ for (std::uint32_t y = 0; y < h; ++y) {
1010 ✗ std::memcpy(cropped.data() + std::size_t{y} * w * 4,
1011 ✗ rgba.data() + std::size_t{y} * width * 4, std::size_t{w} * 4);
1012 }
1013 ✗ return cropped;
1014 ✗ }
1015
1016 } // namespace
1017
1018 struct RasterBake::State {
1019 /// A rectangle of a mip to encode: `w` x `h` texels at (`x`, `y`) of mip `mip`, RGBA8.
1020 527 struct Piece {
1021 std::uint32_t mip = 0;
1022 std::uint32_t x = 0;
1023 std::uint32_t y = 0;
1024 std::uint32_t w = 0;
1025 std::uint32_t h = 0;
1026 std::vector<std::uint8_t> rgba;
1027 };
1028
1029 std::unique_ptr<GridSource> source;
1030 fs::path output;
1031 BakeOptions options;
1032 bool srgb = true;
1033 TextureData layout;
1034 TextureFormatInfo info;
1035 bc7enc_compress_block_params bc7{};
1036 /// Mips made band by band; the rest are joined.
1037 std::uint32_t banded = 0;
1038 std::fstream file;
1039 std::uint64_t total = 0;
1040 std::atomic<std::uint64_t> encoded = 0;
1041
1042 std::mutex mutex;
1043 std::condition_variable changed;
1044 std::deque<Piece> pieces;
1045 std::size_t queued = 0; ///< Bytes of `pieces`.
1046 bool read = false; ///< Every piece queued.
1047 bool stop = false;
1048 std::exception_ptr failure;
1049
1050 /// The piece being encoded, and its rows done.
1051 std::optional<Piece> current;
1052 std::uint32_t row = 0;
1053 bool done = false;
1054 std::thread reader; ///< Joined by ~RasterBake.
1055
1056 /// Queues `piece`, once there is room.
1057 179 void Queue(Piece piece) {
1058 179 std::unique_lock lock(mutex);
1059
6/8
✓ Branch 1 taken 179 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 178 times.
✓ Branch 4 taken 1 time.
✓ Branch 5 taken 107 times.
✓ Branch 6 taken 71 times.
✓ Branch 7 taken 107 times.
✗ Branch 8 not taken.
358 changed.wait(lock, [&] { return stop || pieces.empty() || queued < kQueuedBytes; });
1060
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 178 times.
179 if (stop) {
1061
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1 return;
1062 }
1063
1/2
✓ Branch 1 taken 178 times.
✗ Branch 2 not taken.
178 queued += piece.rgba.size();
1064
1/2
✓ Branch 1 taken 178 times.
✗ Branch 2 not taken.
178 pieces.push_back(std::move(piece));
1065
1/2
✓ Branch 1 taken 178 times.
✗ Branch 2 not taken.
178 changed.notify_all();
1066 179 }
1067
1068 210 bool Stopping() {
1069 210 const std::scoped_lock lock(mutex);
1070 210 return stop;
1071 210 }
1072
1073 /// The reading thread: bands' mips, then the joined ones.
1074 10 void Read() {
1075 10 try {
1076
1/2
✓ Branch 1 taken 10 times.
✗ Branch 2 not taken.
10 ReadBands();
1077 ✗ } catch (...) {
1078
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ const std::scoped_lock lock(mutex);
1079
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ failure = std::current_exception();
1080 ✗ }
1081 10 const std::scoped_lock lock(mutex);
1082 10 read = true;
1083 10 changed.notify_all();
1084 10 }
1085
1086 10 void ReadBands() {
1087 10 ERNIE_ZONE("Read raster bands");
1088 10 const std::uint32_t width = layout.width;
1089 10 const std::uint32_t height = layout.height;
1090
2/4
✓ Branch 1 taken 10 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 10 times.
✗ Branch 5 not taken.
10 const std::size_t pixel = std::size_t{source->Channels()} * source->SampleBytes();
1091 10 const std::uint32_t joinedWidth = MipSpan(width, 0, width, banded).second;
1092 10 const std::uint32_t joinedHeight = MipSpan(height, 0, height, banded).second;
1093 10 std::vector<std::uint8_t> joined;
1094
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 1 time.
10 if (banded < layout.mips) {
1095
1/2
✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
9 joined.resize(std::size_t{joinedWidth} * joinedHeight * 4);
1096 }
1097 10 std::vector<std::byte> samples;
1098
3/4
✓ Branch 1 taken 10 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 22 times.
✓ Branch 4 taken 9 times.
31 for (const GridRect& part : source->Parts()) {
1099
2/2
✓ Branch 0 taken 32 times.
✓ Branch 1 taken 21 times.
53 for (std::uint32_t y = part.y; y < part.y + part.height; y += kBandRows) {
1100
3/4
✓ Branch 1 taken 32 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 31 times.
32 if (Stopping()) {
1101 1 return;
1102 }
1103
2/2
✓ Branch 0 taken 21 times.
✓ Branch 1 taken 10 times.
31 const std::uint32_t rows = std::min(kBandRows, part.y + part.height - y);
1104 31 std::uint32_t w = part.width;
1105 31 std::uint32_t h = rows;
1106
1/2
✓ Branch 1 taken 31 times.
✗ Branch 2 not taken.
31 samples.resize(std::size_t{w} * h * pixel);
1107
1/2
✓ Branch 1 taken 31 times.
✗ Branch 2 not taken.
31 std::vector<std::uint8_t> rgba(std::size_t{w} * h * 4);
1108 31 {
1109
1/2
✓ Branch 1 taken 31 times.
✗ Branch 2 not taken.
31 ERNIE_ZONE("Read band");
1110
1/2
✓ Branch 1 taken 31 times.
✗ Branch 2 not taken.
31 source->Read(part.x, y, w, h, samples);
1111
1/2
✓ Branch 1 taken 31 times.
✗ Branch 2 not taken.
31 SamplesToRgba8(*source, samples.data(), std::size_t{w} * h, rgba.data());
1112 31 }
1113 31 for (std::uint32_t mip = 0;; ++mip) {
1114
1/2
✓ Branch 0 taken 178 times.
✗ Branch 1 not taken.
178 const auto [x0, across] = MipSpan(width, part.x, part.width, mip);
1115 178 const auto [y0, down] = MipSpan(height, y, rows, mip);
1116
3/4
✓ Branch 0 taken 178 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 174 times.
✓ Branch 3 taken 4 times.
178 if (across == 0 || down == 0) {
1117 break;
1118 }
1119 // Halving keeps a texel of a side of one, which the whole image's mip may not
1120 // have here.
1121
2/4
✓ Branch 0 taken 174 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 174 times.
174 if (across < w || down < h) {
1122
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ rgba = Cropped(std::move(rgba), w, across, down);
1123 ✗ w = across;
1124 ✗ h = down;
1125 }
1126
2/2
✓ Branch 0 taken 25 times.
✓ Branch 1 taken 149 times.
174 if (mip == banded) {
1127
2/2
✓ Branch 0 taken 230 times.
✓ Branch 1 taken 25 times.
255 for (std::uint32_t r = 0; r < h; ++r) {
1128 230 std::memcpy(joined.data() +
1129 230 (std::size_t{y0 + r} * joinedWidth + x0) * 4,
1130 230 rgba.data() + std::size_t{r} * w * 4, std::size_t{w} * 4);
1131 }
1132 break;
1133 }
1134 149 const bool last = mip + 1 == layout.mips;
1135 149 std::vector<std::uint8_t> half =
1136
3/4
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 148 times.
✓ Branch 3 taken 148 times.
✗ Branch 4 not taken.
149 last ? std::vector<std::uint8_t>{} : HalveRgba8(rgba, w, h, srgb);
1137
1/2
✓ Branch 1 taken 149 times.
✗ Branch 2 not taken.
149 Queue(Piece{mip, x0, y0, w, h, std::move(rgba)});
1138
5/6
✓ Branch 0 taken 148 times.
✓ Branch 1 taken 1 time.
✓ Branch 3 taken 148 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 time.
✓ Branch 6 taken 147 times.
149 if (last || Stopping()) {
1139 break;
1140 }
1141 147 rgba = std::move(half);
1142
2/2
✓ Branch 0 taken 135 times.
✓ Branch 1 taken 12 times.
147 w = MipSide(w, 1);
1143
2/2
✓ Branch 0 taken 143 times.
✓ Branch 1 taken 4 times.
290 h = MipSide(h, 1);
1144 149 }
1145 31 }
1146 1 }
1147
1/2
✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
9 source->Release();
1148 9 std::uint32_t w = joinedWidth;
1149 9 std::uint32_t h = joinedHeight;
1150
2/2
✓ Branch 0 taken 30 times.
✓ Branch 1 taken 9 times.
39 for (std::uint32_t mip = banded; mip < layout.mips; ++mip) {
1151
2/4
✓ Branch 1 taken 30 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 30 times.
30 if (Stopping()) {
1152 ✗ return;
1153 }
1154
2/2
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 8 times.
30 std::vector<std::uint8_t> half = mip + 1 < layout.mips ? HalveRgba8(joined, w, h, srgb)
1155
1/2
✓ Branch 1 taken 22 times.
✗ Branch 2 not taken.
30 : std::vector<std::uint8_t>{};
1156
1/2
✓ Branch 1 taken 30 times.
✗ Branch 2 not taken.
30 Queue(Piece{mip, 0, 0, w, h, std::move(joined)});
1157 30 joined = std::move(half);
1158
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 14 times.
30 w = MipSide(w, 1);
1159
2/2
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 10 times.
50 h = MipSide(h, 1);
1160 30 }
1161 10 }
1162 };
1163
1164 11 RasterBake::RasterBake(std::unique_ptr<GridSource> source, const fs::path& output,
1165 11 const BakeOptions& options)
1166 11 : state_(std::make_unique<State>()) {
1167
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 10 times.
11 State& s = *state_;
1168
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 10 times.
11 if (options.role == TextureRole::Normal) {
1169
1/2
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
1 throw std::invalid_argument("Normal maps are not baked by bands");
1170 }
1171
2/8
✓ Branch 1 taken 10 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 10 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
10 if (source->Type() != GridSampleType::UInt8 && source->Type() != GridSampleType::UInt16) {
1172
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ throw std::invalid_argument("Textures bake from 8- or 16-bit samples");
1173 }
1174 10 s.source = std::move(source);
1175
1/2
✓ Branch 1 taken 10 times.
✗ Branch 2 not taken.
10 s.output = output;
1176 10 s.options = options;
1177 10 s.srgb = BakesSrgb(options);
1178
1/2
✓ Branch 1 taken 10 times.
✗ Branch 2 not taken.
10 const TextureFormat format = BakeFormat(options, s.srgb);
1179
1/2
✓ Branch 1 taken 10 times.
✗ Branch 2 not taken.
10 const std::uint32_t width = s.source->Width();
1180
1/2
✓ Branch 1 taken 10 times.
✗ Branch 2 not taken.
10 const std::uint32_t height = s.source->Height();
1181
1/2
✓ Branch 1 taken 10 times.
✗ Branch 2 not taken.
10 const std::uint32_t mips = MipCount(width, height);
1182
1/2
✓ Branch 1 taken 10 times.
✗ Branch 2 not taken.
10 s.layout = Allocate(format, width, height, mips, false);
1183
1/2
✓ Branch 1 taken 10 times.
✗ Branch 2 not taken.
10 s.info = FormatInfo(format);
1184
1/2
✓ Branch 1 taken 10 times.
✗ Branch 2 not taken.
10 s.bc7 = Bc7Params(format, options.quality);
1185
2/2
✓ Branch 0 taken 95 times.
✓ Branch 1 taken 10 times.
105 for (std::uint32_t mip = 0; mip < mips; ++mip) {
1186
4/4
✓ Branch 0 taken 81 times.
✓ Branch 1 taken 14 times.
✓ Branch 2 taken 93 times.
✓ Branch 3 taken 2 times.
269 s.total += std::uint64_t{MipSide(width, mip)} * MipSide(height, mip);
1187 }
1188 // Bands start at multiples of `alignment` texels: their mips are made band by band while their
1189 // starts there are of whole blocks and halve exactly into the next.
1190
1/2
✓ Branch 1 taken 10 times.
✗ Branch 2 not taken.
10 const std::vector<GridRect> parts = s.source->Parts();
1191 10 std::uint32_t alignment = kBandRows;
1192
2/2
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 10 times.
32 for (const GridRect& part : parts) {
1193 22 alignment = std::gcd(alignment, std::gcd(part.x, part.y));
1194
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 14 times.
22 if (part.x + part.width != width) {
1195 8 alignment = std::gcd(alignment, part.width);
1196 }
1197
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 14 times.
22 if (part.y + part.height != height) {
1198 8 alignment = std::gcd(alignment, part.height);
1199 }
1200 }
1201
4/4
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 5 times.
10 const bool whole = parts.size() == 1 && height <= kBandRows;
1202 1 s.banded = whole ? mips : 0;
1203
3/4
✓ Branch 0 taken 64 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 64 times.
✗ Branch 3 not taken.
65 while (!whole && s.banded < mips &&
1204
4/4
✓ Branch 0 taken 29 times.
✓ Branch 1 taken 35 times.
✓ Branch 2 taken 9 times.
✓ Branch 3 taken 55 times.
93 alignment % (std::max(s.info.blockSize, 2u) << s.banded) == 0) {
1205 55 ++s.banded;
1206 }
1207
1/2
✓ Branch 1 taken 10 times.
✗ Branch 2 not taken.
10 s.file.open(output, std::ios::binary | std::ios::out | std::ios::trunc);
1208
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (!s.file) {
1209
0/6
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ throw std::runtime_error("Cannot write " + output.string());
1210 }
1211
1/2
✓ Branch 1 taken 10 times.
✗ Branch 2 not taken.
10 WriteDdsHeader(s.file, s.layout);
1212
1/2
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
20 s.reader = std::thread([&s] { s.Read(); });
1213 11 }
1214
1215 10 RasterBake::~RasterBake() {
1216 10 State& s = *state_;
1217 10 {
1218 10 const std::scoped_lock lock(s.mutex);
1219 10 s.stop = true;
1220 10 s.changed.notify_all();
1221 10 }
1222
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if (s.reader.joinable()) {
1223 10 s.reader.join();
1224 }
1225
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 9 times.
10 if (!s.done) {
1226 1 s.file.close();
1227 1 std::error_code error;
1228 1 fs::remove(s.output, error);
1229 }
1230 10 }
1231
1232 5 const TextureData& RasterBake::Layout() const {
1233 5 return state_->layout;
1234 }
1235
1236 5 std::uint64_t RasterBake::Encoded() const {
1237 5 return state_->encoded;
1238 }
1239
1240 9 std::uint64_t RasterBake::Total() const {
1241 9 return state_->total;
1242 }
1243
1244 5 const TextureData& RasterBake::Finish() {
1245
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
5 while (!Step(~std::uint64_t{0}, true)) {
1246 }
1247 5 return Layout();
1248 }
1249
1250 92 bool RasterBake::Step(std::uint64_t texels, bool wait) {
1251 92 ERNIE_ZONE("Raster bake step");
1252 92 State& s = *state_;
1253 92 std::uint64_t spent = 0;
1254 92 const std::uint32_t side = s.info.blockSize;
1255 92 std::vector<std::byte> blocks;
1256
1/2
✓ Branch 0 taken 425 times.
✗ Branch 1 not taken.
425 while (!s.done) {
1257
2/2
✓ Branch 0 taken 179 times.
✓ Branch 1 taken 246 times.
425 if (!s.current) {
1258
1/2
✓ Branch 1 taken 179 times.
✗ Branch 2 not taken.
179 std::unique_lock lock(s.mutex);
1259
1/2
✓ Branch 0 taken 179 times.
✗ Branch 1 not taken.
179 if (wait) {
1260
5/6
✓ Branch 0 taken 66 times.
✓ Branch 1 taken 170 times.
✓ Branch 2 taken 57 times.
✓ Branch 3 taken 9 times.
✓ Branch 5 taken 179 times.
✗ Branch 6 not taken.
415 s.changed.wait(lock, [&] { return !s.pieces.empty() || s.read; });
1261 }
1262
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 179 times.
179 if (s.failure) {
1263
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ std::rethrow_exception(s.failure);
1264 }
1265
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 170 times.
179 if (s.pieces.empty()) {
1266
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (!s.read) {
1267
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ return false;
1268 }
1269
1/2
✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
9 lock.unlock();
1270
1/2
✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
9 s.file.close();
1271
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (!s.file) {
1272
0/6
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ throw std::runtime_error("Cannot write " + s.output.string());
1273 }
1274 9 s.done = true;
1275
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 break;
1276 }
1277 170 s.current = std::move(s.pieces.front());
1278 170 s.pieces.pop_front();
1279 170 s.queued -= s.current->rgba.size();
1280 170 s.row = 0;
1281
1/2
✓ Branch 1 taken 170 times.
✗ Branch 2 not taken.
170 s.changed.notify_all();
1282 179 }
1283
2/2
✓ Branch 0 taken 333 times.
✓ Branch 1 taken 83 times.
416 if (spent >= texels) {
1284 return false;
1285 }
1286 // Whole rows of blocks, about the texels left.
1287
2/2
✓ Branch 0 taken 173 times.
✓ Branch 1 taken 160 times.
333 const State::Piece& piece = *s.current;
1288 333 const std::uint64_t left = texels - spent;
1289 333 const auto fit = static_cast<std::uint32_t>(
1290
2/2
✓ Branch 0 taken 173 times.
✓ Branch 1 taken 160 times.
333 std::min<std::uint64_t>(piece.h, left / piece.w / side * side));
1291
4/4
✓ Branch 0 taken 97 times.
✓ Branch 1 taken 236 times.
✓ Branch 2 taken 164 times.
✓ Branch 3 taken 169 times.
430 const std::uint32_t rows = std::min(piece.h - s.row, std::max(fit, side));
1292 333 const std::span<const std::uint8_t> rgba(
1293
1/2
✓ Branch 1 taken 333 times.
✗ Branch 2 not taken.
333 piece.rgba.data() + std::size_t{s.row} * piece.w * 4, std::size_t{rows} * piece.w * 4);
1294
2/4
✓ Branch 1 taken 333 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 333 times.
✗ Branch 5 not taken.
333 blocks.resize(ImageBytes(s.layout.format, piece.w, rows));
1295 333 {
1296
1/2
✓ Branch 1 taken 333 times.
✗ Branch 2 not taken.
333 ERNIE_ZONE("Encode band");
1297
1/2
✓ Branch 1 taken 333 times.
✗ Branch 2 not taken.
333 EncodeRgba8(s.layout.format, rgba, piece.w, rows, blocks.data(), s.bc7,
1298 s.options.encoder);
1299 333 }
1300 // Each row of blocks in its place in the mip.
1301
2/2
✓ Branch 0 taken 319 times.
✓ Branch 1 taken 14 times.
333 const std::size_t mipAcross = (MipSide(s.layout.width, piece.mip) + side - 1) / side;
1302 333 const std::size_t rowBytes = (piece.w + side - 1) / side * s.info.blockBytes;
1303
2/2
✓ Branch 0 taken 11203 times.
✓ Branch 1 taken 333 times.
11536 for (std::uint32_t y = 0; y < (rows + side - 1) / side; ++y) {
1304 11203 const std::size_t at =
1305
1/2
✓ Branch 1 taken 11203 times.
✗ Branch 2 not taken.
11203 kDdsHeaderBytes + s.layout.offsets[piece.mip] +
1306 11203 ((std::size_t{piece.y + s.row} / side + y) * mipAcross + piece.x / side) *
1307 11203 s.info.blockBytes;
1308
1/2
✓ Branch 1 taken 11203 times.
✗ Branch 2 not taken.
11203 s.file.seekp(static_cast<std::streamoff>(at));
1309
1/2
✓ Branch 1 taken 11203 times.
✗ Branch 2 not taken.
11203 s.file.write(reinterpret_cast<const char*>(blocks.data() + y * rowBytes),
1310 static_cast<std::streamsize>(rowBytes));
1311 }
1312
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 333 times.
333 if (!s.file) {
1313
0/6
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ throw std::runtime_error("Cannot write " + s.output.string());
1314 }
1315 333 spent += std::uint64_t{piece.w} * rows;
1316
2/2
✓ Branch 0 taken 169 times.
✓ Branch 1 taken 164 times.
333 s.encoded += std::uint64_t{piece.w} * rows;
1317 333 s.row += rows;
1318
2/2
✓ Branch 0 taken 169 times.
✓ Branch 1 taken 164 times.
333 if (s.row >= piece.h) {
1319 169 s.current.reset();
1320 }
1321 }
1322 return true;
1323 92 }
1324
1325 7 TextureData BakeMosaic(std::span<const fs::path> tiles, std::uint32_t columns,
1326 const fs::path& output, const BakeOptions& options) {
1327 7 ERNIE_ZONE("Bake mosaic");
1328
4/6
✓ Branch 1 taken 7 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 4 times.
✓ Branch 5 taken 3 times.
✓ Branch 7 taken 4 times.
✗ Branch 8 not taken.
11 return RasterBake(std::make_unique<MosaicRaster>(
1329
1/2
✓ Branch 1 taken 7 times.
✗ Branch 2 not taken.
14 std::vector<fs::path>(tiles.begin(), tiles.end()), columns),
1330 output, options)
1331
2/4
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
8 .Finish();
1332 7 }
1333
1334 // ----------------------------------------------------------------------------------------------
1335 // Cache
1336
1337 namespace {
1338
1339 /// Bumped when what the cache stores, or how it keys it, changes.
1340 constexpr std::string_view kCacheFormat = "ernie-texture-1";
1341
1342 } // namespace
1343
1344 ✗ std::optional<fs::path> TextureCache::DefaultDirectory() {
1345 ✗ return CacheDirectory("ERNIE_TEXTURE_CACHE", "textures");
1346 }
1347
1348 2 TextureCache::TextureCache(std::optional<fs::path> directory, std::uintmax_t capacity)
1349 2 : directory_(std::move(directory)), capacity_(capacity) {}
1350
1351 17 fs::path TextureCache::EntryPath(std::string_view key) const {
1352 17 CacheKey hash;
1353 17 hash.Add(kCacheFormat);
1354 17 hash.Add(key);
1355
2/4
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 17 times.
✗ Branch 6 not taken.
51 return *directory_ / (hash.Hex() + ".dds");
1356 }
1357
1358 10 std::optional<TextureData> TextureCache::Read(std::string_view key) const {
1359
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 9 times.
10 if (!directory_) {
1360 1 return std::nullopt;
1361 }
1362 9 ERNIE_ZONE("Texture cache read");
1363
1/2
✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
9 const fs::path path = EntryPath(key);
1364 9 std::error_code error;
1365
2/2
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 4 times.
9 if (!fs::exists(path, error)) {
1366 5 return std::nullopt;
1367 }
1368 4 try {
1369
1/2
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
4 TextureData texture = LoadTextureFile(path);
1370 // Its modification time is its last use, for `Prune`.
1371 4 fs::last_write_time(path, fs::file_time_type::clock::now(), error);
1372 4 return texture;
1373
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
4 } catch (const std::exception&) {
1374 ✗ return std::nullopt;
1375 ✗ }
1376 9 }
1377
1378 4 void TextureCache::Write(std::string_view key, const TextureData& texture) const {
1379 4 const std::optional<fs::path> partial = PartialPath(key);
1380
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 time.
4 if (!partial) {
1381 return;
1382 }
1383
1/2
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
3 ERNIE_ZONE("Texture cache write");
1384 3 try {
1385
1/2
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
3 WriteDdsFile(*partial, texture);
1386
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ } catch (const std::exception&) {
1387 ✗ std::error_code error;
1388 ✗ fs::remove(*partial, error);
1389 ✗ return;
1390 ✗ }
1391
1/2
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
3 Publish(*partial, key);
1392
1/2
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
7 }
1393
1394 6 std::optional<fs::path> TextureCache::PartialPath(std::string_view key) const {
1395
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 4 times.
6 if (!directory_) {
1396 2 return std::nullopt;
1397 }
1398 4 std::error_code error;
1399 4 fs::create_directories(*directory_, error);
1400 // Written beside its entry, then renamed over it, so that no reader (in this process or
1401 // another) sees a part of it.
1402 4 fs::path partial = EntryPath(key);
1403
3/6
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
✓ Branch 5 taken 4 times.
✗ Branch 6 not taken.
✓ Branch 8 taken 4 times.
✗ Branch 9 not taken.
12 partial += ".part" + std::to_string(std::random_device{}());
1404 4 return partial;
1405 4 }
1406
1407 4 bool TextureCache::Publish(const fs::path& partial, std::string_view key) const {
1408
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 std::error_code error;
1409
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (!directory_) {
1410 ✗ fs::remove(partial, error);
1411 ✗ return false;
1412 }
1413 4 const fs::path path = EntryPath(key);
1414 4 fs::rename(partial, path, error);
1415
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (error) {
1416 ✗ fs::remove(partial, error);
1417 ✗ return false;
1418 }
1419
1/2
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
4 Prune(path);
1420 return true;
1421 4 }
1422
1423 ✗ void TextureCache::Remove(std::string_view key) const {
1424
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (directory_) {
1425 ✗ std::error_code error;
1426 ✗ fs::remove(EntryPath(key), error);
1427 }
1428 ✗ }
1429
1430 4 void TextureCache::Prune(const fs::path& kept) const {
1431 4 ERNIE_ZONE("Texture cache prune");
1432
0/2
✗ Branch 22 not taken.
✗ Branch 23 not taken.
51 struct Entry {
1433 fs::path path;
1434 fs::file_time_type used;
1435 std::uintmax_t bytes;
1436 };
1437 4 std::vector<Entry> entries;
1438 4 std::uintmax_t total = 0;
1439 4 std::error_code error;
1440 4 const fs::file_time_type now = fs::file_time_type::clock::now();
1441
8/14
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 4 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 4 times.
✗ Branch 8 not taken.
✓ Branch 11 taken 9 times.
✗ Branch 12 not taken.
✓ Branch 13 taken 9 times.
✓ Branch 14 taken 4 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 4 times.
29 for (const fs::directory_entry& file : fs::directory_iterator(*directory_, error)) {
1442 9 const fs::file_time_type used = file.last_write_time(error);
1443
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (error) {
1444 ✗ continue;
1445 }
1446 // Parts a day old are of writes that never finished.
1447
2/4
✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 9 times.
27 if (file.path().extension().string().starts_with(".part")) {
1448
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ if (now - used > std::chrono::hours(24)) {
1449 ✗ fs::remove(file.path(), error);
1450 }
1451 ✗ continue;
1452 }
1453
3/8
✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 9 times.
✗ Branch 5 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 9 times.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
36 if (file.path().extension() != ".dds") {
1454 ✗ continue;
1455 }
1456
1/2
✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
9 Entry entry{file.path(), used, file.file_size(error)};
1457
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (error) {
1458 ✗ continue;
1459 }
1460 9 total += entry.bytes;
1461
1/2
✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
9 entries.push_back(std::move(entry));
1462
2/6
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
13 }
1463
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 if (total <= capacity_) {
1464 2 return;
1465 }
1466 2 std::sort(entries.begin(), entries.end(),
1467
4/24
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✓ Branch 5 taken 3 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
✓ Branch 22 taken 1 time.
✓ Branch 23 taken 3 times.
8 [](const Entry& a, const Entry& b) { return a.used < b.used; });
1468
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1 time.
6 for (const Entry& entry : entries) {
1469
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1 time.
5 if (total <= capacity_) {
1470 break;
1471 }
1472
3/4
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 1 time.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
4 if (entry.path != kept && fs::remove(entry.path, error)) {
1473 3 total -= entry.bytes;
1474 }
1475 }
1476 4 }
1477
1478 } // namespace ernie
1479