GCC Code Coverage Report


ernie/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
11 / 11
100.0%
Branches:
70 / 104
67.3%
Functions:
-
-%

io/texture.h
Line Branch Exec Source
1 // SPDX-License-Identifier: BSD-3-Clause
2 /// @file texture.h
3 /// @brief Stored textures: formats, DDS and KTX2 reading, BC decoding, baking images into mipped,
4 /// block-compressed DDS files, and a disk cache of baked textures.
5 #pragma once
6
7 #include "ernie/export.h"
8
9 #include "ernie/io/image.h"
10 #include "ernie/io/raster.h"
11 #include "ernie/util/file.h"
12
13 #include "ernie/util/glm.h"
14
15 #include <cstddef>
16 #include <cstdint>
17 #include <filesystem>
18 #include <memory>
19 #include <optional>
20 #include <span>
21 #include <string_view>
22 #include <vector>
23
24 namespace ernie {
25
26 // ----------------------------------------------------------------------------------------------
27 // Formats
28
29 /// @brief A stored texture's texel layout.
30 /// Plain texels, or BC1-BC7 blocks of 4 x 4 uploaded as they are.
31 enum class TextureFormat : std::uint32_t {
32 R8,
33 Rg8,
34 Rgba8,
35 Rgba8Srgb,
36 Bgra8,
37 Bgra8Srgb,
38 Rgba16F,
39 Rgba32F,
40 /// With one-bit alpha (DXT1).
41 Bc1,
42 Bc1Srgb,
43 Bc2,
44 Bc2Srgb,
45 Bc3,
46 Bc3Srgb,
47 Bc4,
48 Bc4Snorm,
49 Bc5,
50 Bc5Snorm,
51 Bc6hUfloat,
52 Bc6hSfloat,
53 Bc7,
54 Bc7Srgb,
55 };
56
57 /// @brief What a `TextureFormat` stores.
58 struct TextureFormatInfo {
59 /// Texels along a block's side; 1 when uncompressed.
60 std::uint32_t blockSize = 1;
61 /// Bytes per block (or texel).
62 std::uint32_t blockBytes = 4;
63 /// Channels it decodes to.
64 std::uint32_t channels = 4;
65 /// Whether colour decodes from sRGB.
66 bool srgb = false;
67 /// @return Whether the format is block-compressed.
68 455 bool compressed() const {
69
3/4
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 210 times.
✓ Branch 3 taken 240 times.
455 return blockSize > 1;
70 }
71 };
72
73 /// @brief Describes a texture format.
74 /// @param format The format.
75 /// @return Its block size, bytes, channels and sRGB flag.
76 ERNIE_API TextureFormatInfo FormatInfo(TextureFormat format);
77
78 /// @brief Switches sRGB decoding on or off where a format has both variants (RGBA8, BGRA8, BC1,
79 /// BC2, BC3, BC7).
80 /// @param format The format.
81 /// @param srgb Whether to decode from sRGB.
82 /// @return The variant, or `format` unchanged.
83 ERNIE_API TextureFormat WithSrgb(TextureFormat format, bool srgb);
84
85 /// @brief Bytes of an image in whole blocks.
86 /// @param format The format.
87 /// @param width Width, in texels.
88 /// @param height Height, in texels.
89 /// @return The size in bytes.
90 ERNIE_API std::size_t ImageBytes(TextureFormat format, std::uint32_t width, std::uint32_t height);
91
92 /// @brief Mips of a full chain, down to 1 x 1.
93 /// @param width Width of mip 0, in texels.
94 /// @param height Height of mip 0, in texels.
95 /// @return The number of mips.
96 ERNIE_API std::uint32_t MipCount(std::uint32_t width, std::uint32_t height);
97
98 /// @brief A side of a mip.
99 /// @param side The side at mip 0, in texels.
100 /// @param mip The mip.
101 /// @return The side halved `mip` times, at least 1.
102 3203 constexpr std::uint32_t MipSide(std::uint32_t side, std::uint32_t mip) {
103
47/56
✓ Branch 0 taken 319 times.
✓ Branch 1 taken 14 times.
✓ Branch 2 taken 81 times.
✓ Branch 3 taken 14 times.
✓ Branch 4 taken 93 times.
✓ Branch 5 taken 2 times.
✓ Branch 6 taken 128 times.
✓ Branch 7 taken 12 times.
✓ Branch 8 taken 136 times.
✓ Branch 9 taken 4 times.
✓ Branch 10 taken 16 times.
✓ Branch 11 taken 14 times.
✓ Branch 12 taken 20 times.
✓ Branch 13 taken 10 times.
✓ Branch 14 taken 207 times.
✓ Branch 15 taken 18 times.
✓ Branch 16 taken 67 times.
✓ Branch 17 taken 6 times.
✓ Branch 18 taken 68 times.
✓ Branch 19 taken 5 times.
✓ Branch 20 taken 35 times.
✗ Branch 21 not taken.
✓ Branch 22 taken 33 times.
✓ Branch 23 taken 1 time.
✓ Branch 24 taken 2 times.
✗ Branch 25 not taken.
✓ Branch 26 taken 2 times.
✗ Branch 27 not taken.
✓ Branch 28 taken 16 times.
✗ Branch 29 not taken.
✓ Branch 30 taken 16 times.
✗ Branch 31 not taken.
✓ Branch 32 taken 146 times.
✓ Branch 33 taken 2 times.
✓ Branch 34 taken 124 times.
✓ Branch 35 taken 24 times.
✓ Branch 36 taken 214 times.
✓ Branch 37 taken 24 times.
✓ Branch 38 taken 227 times.
✓ Branch 39 taken 11 times.
✓ Branch 40 taken 13 times.
✗ Branch 41 not taken.
✓ Branch 42 taken 13 times.
✗ Branch 43 not taken.
✓ Branch 44 taken 17 times.
✗ Branch 45 not taken.
✓ Branch 46 taken 17 times.
✗ Branch 47 not taken.
✓ Branch 48 taken 48 times.
✓ Branch 49 taken 5 times.
✓ Branch 50 taken 47 times.
✓ Branch 51 taken 6 times.
✓ Branch 52 taken 450 times.
✓ Branch 53 taken 13 times.
✓ Branch 54 taken 425 times.
✓ Branch 55 taken 38 times.
3113 return side >> mip > 0 ? side >> mip : 1u;
104 }
105
106 // ----------------------------------------------------------------------------------------------
107 // Stored textures
108
109 /// @brief A texture as a DDS or KTX2 file stores it, not decoded.
110 /// Each layer holds `mips` levels, halving down from full size; a cube map's six faces are layers.
111 struct ERNIE_API TextureData {
112 /// The texel format.
113 TextureFormat format = TextureFormat::Rgba8;
114 /// Width of mip 0, in texels.
115 std::uint32_t width = 0;
116 /// Height of mip 0, in texels.
117 std::uint32_t height = 0;
118 /// Array layers.
119 std::uint32_t layers = 1;
120 /// Mips per layer.
121 std::uint32_t mips = 1;
122 /// Whether the layers are cube faces.
123 bool cube = false;
124 /// The owned texels; empty when read in place from `mapping`.
125 std::vector<std::byte> bytes;
126 /// The mapped file holding the texels; null when in `bytes`.
127 std::shared_ptr<const MappedFile> mapping;
128 /// The texels within `mapping`.
129 std::span<const std::byte> mapped;
130 /// Where each mip starts in `Data()`, at `layer * mips + mip`.
131 std::vector<std::size_t> offsets;
132
133 /// @return The texels: the mapped file's, else `bytes`.
134 240 std::span<const std::byte> Data() const {
135
16/30
<unknown function 134>:
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 4 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 4 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 4 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 1 time.
✓ Branch 16 taken 1 time.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✓ Branch 19 taken 1 time.
✓ Branch 20 taken 1 time.
✗ Branch 21 not taken.
✓ Branch 22 taken 1 time.
✗ Branch 23 not taken.
✓ Branch 24 taken 22 times.
✗ Branch 25 not taken.
<unknown function 68>:
✗ Branch 0 not taken.
✓ Branch 1 taken 37 times.
✓ Branch 2 taken 101 times.
✓ Branch 3 taken 47 times.
221 return mapping != nullptr ? mapped : std::span<const std::byte>(bytes);
136 }
137
138 /// @brief One mip of one layer.
139 /// @param layer The layer.
140 /// @param mip The mip.
141 /// @return Its bytes.
142 /// @throws std::out_of_range If the layer or mip does not exist.
143 std::span<const std::byte> Mip(std::uint32_t layer, std::uint32_t mip) const;
144 };
145
146 /// @brief Tests for a .dds or .ktx2 extension, in any case.
147 /// @param path The file.
148 /// @return Whether `LoadTextureFile` reads it.
149 ERNIE_API bool IsTextureFile(const std::filesystem::path& path);
150
151 /// @brief Reads a DDS or KTX2 file as stored, by extension.
152 /// The file is mapped and read in place, so texels load from disk only as used; supercompressed
153 /// KTX2 levels decode into memory. DDS: DXGI formats, legacy DXTn/ATIn/BC4/BC5 codes and 32-bit
154 /// RGBA or BGRA; 2D, arrays and cubes, not volumes. KTX2: the same formats, stored or zstd; Basis
155 /// ETC1S and UASTC transcode to BC5 for two channels (`toktx --normal_mode`), BC6H for HDR, else
156 /// BC7.
157 /// @param path The file.
158 /// @return The texture.
159 /// @throws std::runtime_error If the file cannot be read, is malformed or truncated, or has an
160 /// unsupported format.
161 ///
162 /// References:
163 /// - Microsoft, "DDS programming guide" (DDS_HEADER, DDS_HEADER_DXT10).
164 /// https://learn.microsoft.com/en-us/windows/win32/direct3ddds/dx-graphics-dds-pguide
165 /// - Khronos, "KTX File Format Specification", version 2.0.
166 /// https://registry.khronos.org/KTX/specs/2.0/ktxspec.v2.html
167 /// - Binomial, "Basis Universal" (the transcoder). https://github.com/BinomialLLC/basis_universal
168 ERNIE_API TextureData LoadTextureFile(const std::filesystem::path& path);
169
170 /// @brief Parses a DDS file's contents (`LoadTextureFile`).
171 /// @param file The bytes.
172 /// @return The texture, owning its texels.
173 ERNIE_API TextureData ReadDds(std::span<const std::byte> file);
174
175 /// @brief Parses a KTX2 file's contents (`LoadTextureFile`).
176 /// @param file The bytes.
177 /// @return The texture, owning its texels.
178 ERNIE_API TextureData ReadKtx2(std::span<const std::byte> file);
179
180 /// @brief Writes a DDS file with the DX10 header (`ReadDds` reads it back).
181 /// @param path The file.
182 /// @param texture Every layer's mips.
183 /// @throws std::runtime_error If the file cannot be written.
184 ERNIE_API void WriteDdsFile(const std::filesystem::path& path, const TextureData& texture);
185
186 /// @brief Decodes BC blocks for devices that cannot sample them.
187 /// BC1/BC3/BC7 become RGBA8 (sRGB kept), BC4 R8 and BC5 RG8, every layer and mip.
188 /// @param texture The texture.
189 /// @return The decoded texture; uncompressed ones unchanged.
190 /// @throws std::invalid_argument For BC2, BC6H and signed BC4/BC5.
191 ///
192 /// References:
193 /// - R. Geldreich, "bc7enc_rdo" (rgbcx's BC1 to BC5 decoders, bc7decomp's BC7), MIT or public
194 /// domain. https://github.com/richgel999/bc7enc_rdo
195 ERNIE_API TextureData DecodeBlocks(const TextureData& texture);
196
197 // ----------------------------------------------------------------------------------------------
198 // Baking
199
200 /// @brief What a texture holds, deciding how it is mipped and stored.
201 enum class TextureRole {
202 /// Colour, sRGB unless said otherwise; BC7.
203 Colour,
204 /// Tangent-space normals as n / 2 + 1/2, mipped as `MipFilter::NormalSlopes`, stored as x and
205 /// y (BC5); shaders rebuild z.
206 Normal,
207 /// Linear values such as roughness or metallic; BC7.
208 Data,
209 };
210
211 /// @brief Encodes RGBA8 images into BC blocks off the CPU.
212 /// Bakes use it for the formats it handles (`GpuBlockEncoder`), the CPU encoders for the rest.
213 class ERNIE_API BlockEncoder {
214 public:
215 virtual ~BlockEncoder() = default;
216
217 /// @param format A block format.
218 /// @return Whether this encoder handles it.
219 virtual bool Encodes(TextureFormat format) const = 0;
220
221 /// @brief Encodes an image; edges repeat the last row and column.
222 /// @param rgba RGBA8 texels, rows top to bottom.
223 /// @param width Width, in texels.
224 /// @param height Height, in texels.
225 /// @param format The block format.
226 /// @param out `ImageBytes(format, width, height)` bytes.
227 virtual void Encode(std::span<const std::uint8_t> rgba, std::uint32_t width,
228 std::uint32_t height, TextureFormat format, std::byte* out) = 0;
229 };
230
231 /// @brief How `BakeTexture` stores a texture.
232 struct BakeOptions {
233 /// What the texture holds.
234 TextureRole role = TextureRole::Colour;
235 /// The stored format; the role's by default. BC1, BC3, BC4, BC5, BC7 and RGBA8 are written;
236 /// BC6H is not.
237 std::optional<TextureFormat> format;
238 /// Whether colour is stored as sRGB rather than linear.
239 bool srgb = true;
240 /// BC7 effort on the CPU, 0 (fastest) to 4.
241 int quality = 1;
242 /// An encoder used where it can, such as on the GPU; null for the CPU's alone.
243 BlockEncoder* encoder = nullptr;
244 };
245
246 /// @brief Encodes a mip chain into a stored format.
247 /// BC blocks past a mip's edge repeat its last row and column.
248 /// @param mips Levels as `MipChain` makes them, full size first.
249 /// @param format The stored format.
250 /// @param encoding How 8-bit values are encoded.
251 /// @param quality BC7 effort, 0 to 4.
252 /// @param encoder Used where it handles `format`; else all CPU threads.
253 /// @return The texture.
254 /// @throws std::invalid_argument For an unsupported format, or mips that do not halve.
255 ERNIE_API TextureData EncodeTexture(std::span<const Image> mips, TextureFormat format,
256 ImageEncoding encoding, int quality = 1,
257 BlockEncoder* encoder = nullptr);
258
259 /// @brief Reads an 8-bit PNG or JPEG's size from its header.
260 /// @param file The image.
261 /// @return Width and height; none for other or unreadable files.
262 ERNIE_API std::optional<glm::uvec2> Rgba8ImageSize(const std::filesystem::path& file);
263
264 /// @brief Decodes an 8-bit PNG or JPEG and mips it in 8 bits.
265 /// For images too large for floats (a 21600 x 10800 Earth is 3.7 GB as floats, 1.2 GB as RGBA8
266 /// mips). Each mip is the 2 x 2 average of the last.
267 /// @param file The image.
268 /// @param srgb Whether rgb is averaged as linear light.
269 /// @return RGBA8 levels down to 1 x 1, full size first.
270 /// @throws std::runtime_error If it cannot be read or is not an 8-bit PNG or JPEG.
271 ERNIE_API std::vector<std::vector<std::uint8_t>> LoadRgba8Mips(const std::filesystem::path& file,
272 bool srgb);
273
274 /// @brief Bakes an image into a mipped DDS texture.
275 /// Mips are filtered by role, then compressed: what virtual textures need, at a quarter to an
276 /// eighth of RGBA8's memory. 8-bit PNG and JPEG bake in 8 bits; normals, EXR and 16-bit go
277 /// through floats. TIFF, PDS3 and `.mosaic` bake in bands (`RasterBake`) and return mapped from
278 /// `output`.
279 /// @param input The source image.
280 /// @param output The DDS file to write.
281 /// @param options How to store it.
282 /// @return The stored texture.
283 /// @throws std::runtime_error If the input cannot be read or the output written.
284 /// @throws std::invalid_argument As `EncodeTexture`.
285 ///
286 /// References:
287 /// - R. Geldreich, "bc7enc_rdo" (bc7enc and rgbcx). https://github.com/richgel999/bc7enc_rdo
288 ERNIE_API TextureData BakeTexture(const std::filesystem::path& input,
289 const std::filesystem::path& output,
290 const BakeOptions& options = {});
291
292 /// @brief Bakes a colour or data raster into DDS a band at a time.
293 /// For images larger than memory, JPEG's limit or one file, and to spread a bake over frames. A
294 /// worker reads 1024-row bands and mips them exactly; `Step` encodes ready work on the caller's
295 /// thread. At most about 256 MB of band mips wait. Samples are 8- or 16-bit (high byte kept); 1
296 /// channel is grey, 2 grey and alpha, 3 RGB.
297 class ERNIE_API RasterBake {
298 public:
299 /// @brief Starts a bake and writes the file's header.
300 /// @param source The raster.
301 /// @param output The DDS file.
302 /// @param options How to store it.
303 /// @throws std::invalid_argument For normal maps or samples not 8- or 16-bit.
304 /// @throws std::runtime_error If `output` cannot be written.
305 RasterBake(std::unique_ptr<GridSource> source, const std::filesystem::path& output,
306 const BakeOptions& options = {});
307 /// @brief Stops the bake; an unfinished file is removed.
308 ~RasterBake();
309 RasterBake(const RasterBake&) = delete;
310 RasterBake& operator=(const RasterBake&) = delete;
311
312 /// @return The stored texture's layout; texels are in the file.
313 const TextureData& Layout() const;
314
315 /// @brief Encodes and writes what the reader has ready.
316 /// @param texels About how many texels to encode; at least a band's row of blocks.
317 /// @param wait Whether to wait for the reader.
318 /// @return True once the file is complete.
319 /// @throws std::exception Whatever reading or writing threw.
320 bool Step(std::uint64_t texels, bool wait = false);
321
322 /// @brief Encodes everything left, waiting for the reader.
323 /// @return The stored texture's layout.
324 const TextureData& Finish();
325
326 /// @return Texels encoded so far, over every mip.
327 std::uint64_t Encoded() const;
328 /// @return Texels to encode in all.
329 std::uint64_t Total() const;
330
331 private:
332 struct State;
333 std::unique_ptr<State> state_;
334 };
335
336 /// @brief Bakes a mosaic of 8-bit PNG or JPEG tiles, as `BakeTexture`.
337 /// Each tile's top mips are encoded in turn; smaller mips come from the tiles' last mips joined.
338 /// At most two tiles are held at once. Normal maps are not supported.
339 /// @param tiles Tiles of one size, row by row, top to bottom.
340 /// @param columns Tiles per row.
341 /// @param output The DDS file.
342 /// @param options How to store it.
343 /// @return The stored layout; texels are in the file.
344 /// @throws std::invalid_argument For tiles not filling whole rows or of different sizes, or normal
345 /// maps.
346 /// @throws std::runtime_error If a tile cannot be read or the output written.
347 ERNIE_API TextureData BakeMosaic(std::span<const std::filesystem::path> tiles,
348 std::uint32_t columns, const std::filesystem::path& output,
349 const BakeOptions& options = {});
350
351 // ----------------------------------------------------------------------------------------------
352 // Cache
353
354 /// @brief Keeps prepared textures between runs as DDS files.
355 /// A texture is decoded, mipped and encoded once until its file or read settings change. Least
356 /// recently used entries go past the capacity. Any thread may read; I/O failures count as misses.
357
2/10
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
✓ Branch 3 taken 1 time.
✗ Branch 4 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
4 class ERNIE_API TextureCache {
358 public:
359 /// @brief The default folder, from `ERNIE_TEXTURE_CACHE` (empty or "0" for none) or
360 /// `<user cache>/ernie/textures`.
361 /// @return The folder, or nothing when disabled.
362 static std::optional<std::filesystem::path> DefaultDirectory();
363
364 /// @brief Opens a cache.
365 /// @param directory The folder; none disables the cache.
366 /// @param capacity The size limit in bytes; 16 GB by default (a global Blue Marble mosaic is
367 /// 5 GB of BC7).
368 explicit TextureCache(std::optional<std::filesystem::path> directory = DefaultDirectory(),
369 std::uintmax_t capacity = std::uintmax_t{16} << 30);
370
371 /// @return Whether the cache has a folder.
372 2 bool Enabled() const {
373
2/4
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 time.
2 return directory_.has_value();
374 }
375
376 /// @brief Reads a cached texture and marks it as just used.
377 /// @param key The entry's key.
378 /// @return The texture, mapped as by `LoadTextureFile`; none if absent or unreadable.
379 std::optional<TextureData> Read(std::string_view key) const;
380
381 /// @brief Caches a texture atomically, then prunes.
382 /// @param key The entry's key.
383 /// @param texture The texture.
384 void Write(std::string_view key, const TextureData& texture) const;
385
386 /// @brief A new file for a DDS written in parts (`RasterBake`), then cached by `Publish`.
387 /// @param key The entry's key.
388 /// @return The path, or none without a cache.
389 std::optional<std::filesystem::path> PartialPath(std::string_view key) const;
390
391 /// @brief Moves a complete DDS file into the cache, then prunes others.
392 /// @param partial The file from `PartialPath`.
393 /// @param key The entry's key.
394 /// @return False if it could not be moved; it is then removed.
395 bool Publish(const std::filesystem::path& partial, std::string_view key) const;
396
397 /// @brief Forgets a cached texture, if any.
398 /// @param key The entry's key.
399 void Remove(std::string_view key) const;
400
401 private:
402 std::filesystem::path EntryPath(std::string_view key) const;
403 /// Removes the least recently used past the capacity, but not `kept`.
404 void Prune(const std::filesystem::path& kept) const;
405
406 /// The folder, if enabled.
407 std::optional<std::filesystem::path> directory_;
408 /// The size limit, in bytes.
409 std::uintmax_t capacity_;
410 };
411
412 } // namespace ernie
413