asset compression

This commit is contained in:
Jorijn van der Graaf 2026-05-11 18:37:30 +02:00
commit 30a283c1b3
57 changed files with 13237 additions and 8 deletions

View file

@ -0,0 +1,59 @@
/*
Crafter®.Asset
Copyright (C) 2026 Catcrafts®
catcrafts.net
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License version 3.0 as published by the Free Software Foundation;
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
export module Crafter.Asset:Compression;
import std;
export namespace Crafter::Compression {
// One independently-decompressable GDeflate stream inside CompressedBlob::bytes.
// Layout matches what VK_EXT_memory_decompression's VkDecompressMemoryRegionEXT
// expects, minus the device addresses (which the consumer fills in).
struct RegionMeta {
std::uint64_t srcOffset;
std::uint64_t compressedSize;
std::uint64_t decompressedSize;
};
struct CompressedBlob {
std::vector<std::byte> bytes;
std::vector<RegionMeta> regions;
std::uint64_t TotalDecompressedSize() const noexcept {
std::uint64_t sum = 0;
for (const RegionMeta& r : regions) sum += r.decompressedSize;
return sum;
}
};
// Compresses each input span as its own GDeflate tile-stream; concatenates
// them into one byte buffer with a parallel region table. Streams are
// independent and can be addressed individually by VkDecompressMemoryRegionEXT
// entries on the GPU path.
CompressedBlob CompressStreams(std::span<const std::span<const std::byte>> streams);
// CPU fallback decoder. outputs.size() must equal blob.regions.size();
// outputs[i].size() must equal blob.regions[i].decompressedSize.
void DecompressCPU(const CompressedBlob& blob, std::span<const std::span<std::byte>> outputs);
// Length-prefixed serialization of a CompressedBlob. Used by per-asset
// SaveCompressed/LoadCompressed implementations after they've written
// their own type-specific header.
void WriteBlob(std::ostream& file, const CompressedBlob& blob);
CompressedBlob ReadBlob(std::istream& file);
}

View file

@ -18,6 +18,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
export module Crafter.Asset:Mesh;
import :Compression;
import Crafter.Math;
import std;
namespace fs = std::filesystem;
@ -35,6 +36,44 @@ export namespace Crafter {
Vector<float, 2, 0> uv;
};
// GDeflate-compressed counterpart of MeshAsset<T>::Save output. Three
// regions: [vertex, index, data]. dataCount==0 leaves the data region
// empty (zero compressedSize/decompressedSize). dataStride records sizeof(T)
// at compress time so consumers can validate.
struct CompressedMeshAsset {
std::uint32_t vertexCount = 0;
std::uint32_t indexCount = 0;
std::uint32_t dataCount = 0;
std::uint32_t dataStride = 0;
Compression::CompressedBlob blob;
};
namespace MeshAssetFormat {
inline constexpr char magic[4] = {'C', 'G', 'D', 'M'};
inline constexpr std::uint32_t version = 1;
}
inline CompressedMeshAsset LoadCompressedMesh(fs::path path) {
std::ifstream file(path, std::ios::binary);
char magic[4];
file.read(magic, 4);
if (std::memcmp(magic, MeshAssetFormat::magic, 4) != 0) {
throw std::runtime_error("LoadCompressedMesh: bad magic on " + path.string());
}
std::uint32_t version = 0;
file.read(reinterpret_cast<char*>(&version), sizeof(version));
if (version != MeshAssetFormat::version) {
throw std::runtime_error("LoadCompressedMesh: unsupported version on " + path.string());
}
CompressedMeshAsset out;
file.read(reinterpret_cast<char*>(&out.vertexCount), sizeof(out.vertexCount));
file.read(reinterpret_cast<char*>(&out.indexCount), sizeof(out.indexCount));
file.read(reinterpret_cast<char*>(&out.dataCount), sizeof(out.dataCount));
file.read(reinterpret_cast<char*>(&out.dataStride), sizeof(out.dataStride));
out.blob = Compression::ReadBlob(file);
return out;
}
template <typename T>
struct MeshAsset {
std::vector<Vector<float, 3, 3>> vertexes;
@ -57,6 +96,29 @@ export namespace Crafter {
file.write(reinterpret_cast<char*>(datas.data()), dataCount * sizeof(T));
}
void SaveCompressed(fs::path path) const {
std::array<std::span<const std::byte>, 3> streams = {
std::as_bytes(std::span(vertexes)),
std::as_bytes(std::span(indexes)),
std::as_bytes(std::span(datas)),
};
Compression::CompressedBlob blob = Compression::CompressStreams(streams);
std::ofstream file(path, std::ios::binary);
file.write(MeshAssetFormat::magic, 4);
std::uint32_t version = MeshAssetFormat::version;
std::uint32_t vc = static_cast<std::uint32_t>(vertexes.size());
std::uint32_t ic = static_cast<std::uint32_t>(indexes.size());
std::uint32_t dc = static_cast<std::uint32_t>(datas.size());
std::uint32_t stride = static_cast<std::uint32_t>(sizeof(T));
file.write(reinterpret_cast<const char*>(&version), sizeof(version));
file.write(reinterpret_cast<const char*>(&vc), sizeof(vc));
file.write(reinterpret_cast<const char*>(&ic), sizeof(ic));
file.write(reinterpret_cast<const char*>(&dc), sizeof(dc));
file.write(reinterpret_cast<const char*>(&stride), sizeof(stride));
Compression::WriteBlob(file, blob);
}
static MeshAsset<T> Load(fs::path path) {
MeshAsset<T> mesh;
@ -196,6 +258,32 @@ export namespace Crafter {
file.write(reinterpret_cast<char*>(vertexes.data()), vertexCount * sizeof(Vector<float, 3, 3>));
file.write(reinterpret_cast<char*>(indexes.data()), indexCount * sizeof(std::uint32_t));
}
void SaveCompressed(fs::path path) const {
// Three regions to keep file format identical to the templated
// variant; the data region is empty (skipped on the GPU path).
std::array<std::span<const std::byte>, 3> streams = {
std::as_bytes(std::span(vertexes)),
std::as_bytes(std::span(indexes)),
std::span<const std::byte>{},
};
Compression::CompressedBlob blob = Compression::CompressStreams(streams);
std::ofstream file(path, std::ios::binary);
file.write(MeshAssetFormat::magic, 4);
std::uint32_t version = MeshAssetFormat::version;
std::uint32_t vc = static_cast<std::uint32_t>(vertexes.size());
std::uint32_t ic = static_cast<std::uint32_t>(indexes.size());
std::uint32_t dc = 0;
std::uint32_t stride = 0;
file.write(reinterpret_cast<const char*>(&version), sizeof(version));
file.write(reinterpret_cast<const char*>(&vc), sizeof(vc));
file.write(reinterpret_cast<const char*>(&ic), sizeof(ic));
file.write(reinterpret_cast<const char*>(&dc), sizeof(dc));
file.write(reinterpret_cast<const char*>(&stride), sizeof(stride));
Compression::WriteBlob(file, blob);
}
static MeshAsset<void> Load(fs::path path) {
MeshAsset<void> mesh;

View file

@ -21,6 +21,7 @@ module;
#define STB_IMAGE_IMPLEMENTATION
#include "../lib/stb_image.h"
export module Crafter.Asset:Texture;
import :Compression;
import std;
import Crafter.Math;
namespace fs = std::filesystem;
@ -38,6 +39,42 @@ export namespace Crafter {
OpaqueType opaque;
};
// GDeflate-compressed counterpart of TextureAsset<T>::Save output.
// Single-region blob: the pixel array as one stream.
struct CompressedTextureAsset {
std::uint16_t sizeX = 0;
std::uint16_t sizeY = 0;
OpaqueType opaque = OpaqueType::FullyOpaque;
std::uint32_t pixelStride = 0;
Compression::CompressedBlob blob;
};
namespace TextureAssetFormat {
inline constexpr char magic[4] = {'C', 'G', 'D', 'T'};
inline constexpr std::uint32_t version = 1;
}
inline CompressedTextureAsset LoadCompressedTexture(fs::path path) {
std::ifstream file(path, std::ios::binary);
char magic[4];
file.read(magic, 4);
if (std::memcmp(magic, TextureAssetFormat::magic, 4) != 0) {
throw std::runtime_error("LoadCompressedTexture: bad magic on " + path.string());
}
std::uint32_t version = 0;
file.read(reinterpret_cast<char*>(&version), sizeof(version));
if (version != TextureAssetFormat::version) {
throw std::runtime_error("LoadCompressedTexture: unsupported version on " + path.string());
}
CompressedTextureAsset out;
file.read(reinterpret_cast<char*>(&out.sizeX), sizeof(out.sizeX));
file.read(reinterpret_cast<char*>(&out.sizeY), sizeof(out.sizeY));
file.read(reinterpret_cast<char*>(&out.opaque), sizeof(out.opaque));
file.read(reinterpret_cast<char*>(&out.pixelStride), sizeof(out.pixelStride));
out.blob = Compression::ReadBlob(file);
return out;
}
template <typename T>
struct TextureAsset {
std::uint16_t sizeX;
@ -54,6 +91,24 @@ export namespace Crafter {
file.write(reinterpret_cast<char*>(pixels.data()), pixels.size() * sizeof(T));
}
void SaveCompressed(fs::path path) const {
std::array<std::span<const std::byte>, 1> streams = {
std::as_bytes(std::span(pixels)),
};
Compression::CompressedBlob blob = Compression::CompressStreams(streams);
std::ofstream file(path, std::ios::binary);
file.write(TextureAssetFormat::magic, 4);
std::uint32_t version = TextureAssetFormat::version;
std::uint32_t stride = static_cast<std::uint32_t>(sizeof(T));
file.write(reinterpret_cast<const char*>(&version), sizeof(version));
file.write(reinterpret_cast<const char*>(&sizeX), sizeof(sizeX));
file.write(reinterpret_cast<const char*>(&sizeY), sizeof(sizeY));
file.write(reinterpret_cast<const char*>(&opaque), sizeof(opaque));
file.write(reinterpret_cast<const char*>(&stride), sizeof(stride));
Compression::WriteBlob(file, blob);
}
static TextureAsset<T> Load(fs::path path) {
TextureAsset<T> tex;

View file

@ -19,5 +19,6 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
export module Crafter.Asset;
export import :Compression;
export import :Mesh;
export import :Texture;