Crafter.Graphics/examples/Decompression/main.cpp

189 lines
7.6 KiB
C++
Raw Normal View History

2026-05-12 00:24:48 +02:00
// End-to-end demo of GPU asset decompression via VK_EXT_memory_decompression.
//
// Walks the full compressed-asset pipeline:
// 1. Build a procedural cube + checkerboard texture in memory.
// 2. SaveCompressed → on-disk .cmesh / .ctex (GDeflate streams).
// 3. LoadCompressed* → CompressedMeshAsset / CompressedTextureAsset.
// 4. CPU verification: blob → DecompressCPU → byte-equal to source.
// 5. GPU: Mesh::Build(compressedMesh, cmd) and ImageVulkan::Update(compressedTex, cmd, layout).
// Picks the GPU path on NVIDIA (extension supported) or the CPU fallback elsewhere.
//
// No rendering — the goal is to exercise the asset pipeline and prove the
// new APIs round-trip. Validation layers are enabled by Crafter.Graphics in
// debug builds, so any VUID violation surfaces during FinishInit().
#include "vulkan/vulkan.h"
import Crafter.Graphics;
import Crafter.Asset;
import Crafter.Math;
import std;
using namespace Crafter;
namespace fs = std::filesystem;
namespace {
// Procedural unit cube centered at origin, scaled so it's visible if the
// example is ever wired into a renderer. 8 unique positions, 36 indices.
MeshAsset<void> MakeCubeMesh() {
MeshAsset<void> mesh;
mesh.vertexes = {
{-50.f, -50.f, -50.f}, { 50.f, -50.f, -50.f},
{ 50.f, 50.f, -50.f}, {-50.f, 50.f, -50.f},
{-50.f, -50.f, 50.f}, { 50.f, -50.f, 50.f},
{ 50.f, 50.f, 50.f}, {-50.f, 50.f, 50.f},
};
mesh.indexes = {
// -Z
0, 1, 2, 0, 2, 3,
// +Z
4, 6, 5, 4, 7, 6,
// -X
0, 3, 7, 0, 7, 4,
// +X
1, 5, 6, 1, 6, 2,
// -Y
0, 4, 5, 0, 5, 1,
// +Y
3, 2, 6, 3, 6, 7,
};
return mesh;
}
struct RGBA8 { std::uint8_t r, g, b, a; };
// 256×256 checkerboard with smooth radial fade — compressible enough to make
// the GDeflate ratio interesting, structured enough that the demo is
// meaningful.
TextureAsset<RGBA8> MakeCheckerboard() {
TextureAsset<RGBA8> tex;
tex.sizeX = 256;
tex.sizeY = 256;
tex.opaque = OpaqueType::FullyOpaque;
tex.pixels.resize(tex.sizeX * tex.sizeY);
for (std::uint32_t y = 0; y < tex.sizeY; ++y) {
for (std::uint32_t x = 0; x < tex.sizeX; ++x) {
bool checker = ((x / 32) ^ (y / 32)) & 1;
float dx = float(x) - 128.0f;
float dy = float(y) - 128.0f;
float d = std::sqrt(dx * dx + dy * dy) / 180.0f;
float fade = std::clamp(1.0f - d, 0.0f, 1.0f);
std::uint8_t base = checker ? 220 : 40;
std::uint8_t lit = static_cast<std::uint8_t>(base * fade + 16);
tex.pixels[y * tex.sizeX + x] = RGBA8{lit, lit, lit, 255};
}
}
return tex;
}
double Ratio(std::size_t a, std::size_t b) {
return b == 0 ? 0.0 : 100.0 * double(a) / double(b);
}
} // namespace
int main() {
const fs::path meshPath = "cube.cmesh";
const fs::path texPath = "checker.ctex";
// ── 1. Build procedural assets ─────────────────────────────────────
MeshAsset<void> srcMesh = MakeCubeMesh();
TextureAsset<RGBA8> srcTex = MakeCheckerboard();
const std::size_t srcMeshBytes =
srcMesh.vertexes.size() * sizeof(srcMesh.vertexes[0])
+ srcMesh.indexes.size() * sizeof(srcMesh.indexes[0]);
const std::size_t srcTexBytes =
srcTex.pixels.size() * sizeof(RGBA8);
std::println("Procedural cube: {} vertices, {} indices ({} bytes)",
srcMesh.vertexes.size(), srcMesh.indexes.size(), srcMeshBytes);
std::println("Procedural checker: {}x{} RGBA8 ({} bytes)",
srcTex.sizeX, srcTex.sizeY, srcTexBytes);
// ── 2. SaveCompressed ──────────────────────────────────────────────
srcMesh.SaveCompressed(meshPath);
srcTex.SaveCompressed(texPath);
const std::size_t meshFileSize = fs::file_size(meshPath);
const std::size_t texFileSize = fs::file_size(texPath);
std::println("Saved {}: {} bytes ({:.1f}% of raw)",
meshPath.string(), meshFileSize, Ratio(meshFileSize, srcMeshBytes));
std::println("Saved {}: {} bytes ({:.1f}% of raw)",
texPath.string(), texFileSize, Ratio(texFileSize, srcTexBytes));
// ── 3. LoadCompressed ──────────────────────────────────────────────
CompressedMeshAsset loadedMesh = LoadCompressedMesh(meshPath);
CompressedTextureAsset loadedTex = LoadCompressedTexture(texPath);
if (loadedMesh.vertexCount != srcMesh.vertexes.size()
|| loadedMesh.indexCount != srcMesh.indexes.size()) {
std::println(std::cerr,"[FAIL] mesh header mismatch after LoadCompressedMesh");
return 1;
}
if (loadedTex.sizeX != srcTex.sizeX || loadedTex.sizeY != srcTex.sizeY) {
std::println(std::cerr,"[FAIL] texture header mismatch after LoadCompressedTexture");
return 1;
}
std::println("Loaded headers OK.");
// ── 4. CPU roundtrip verification ──────────────────────────────────
{
std::vector<Vector<float, 3, 3>> v(loadedMesh.vertexCount);
std::vector<std::uint32_t> i(loadedMesh.indexCount);
std::array<std::span<std::byte>, 3> outputs = {
std::as_writable_bytes(std::span(v)),
std::as_writable_bytes(std::span(i)),
std::span<std::byte>{},
};
Compression::DecompressCPU(loadedMesh.blob,
std::span(outputs).first(loadedMesh.blob.regions.size()));
if (v != srcMesh.vertexes || i != srcMesh.indexes) {
std::println(std::cerr,"[FAIL] CPU mesh decompress != source");
return 1;
}
}
{
std::vector<RGBA8> p(loadedTex.sizeX * loadedTex.sizeY);
std::array<std::span<std::byte>, 1> outputs = {
std::as_writable_bytes(std::span(p)),
};
Compression::DecompressCPU(loadedTex.blob, outputs);
if (std::memcmp(p.data(), srcTex.pixels.data(), srcTexBytes) != 0) {
std::println(std::cerr,"[FAIL] CPU texture decompress != source");
return 1;
}
}
std::println("CPU roundtrip OK (mesh + texture decode byte-equal).");
// ── 5. GPU path via Mesh::Build / ImageVulkan::Update ──────────────
Device::Initialize();
Window window(800, 600, "Decompression");
std::println("VK_EXT_memory_decompression: {}",
Device::memoryDecompressionSupported ? "AVAILABLE → GPU path" : "absent → CPU fallback");
VkCommandBuffer cmd = window.StartInit();
DescriptorHeapVulkan heap;
heap.Initialize(/*images*/ 1, /*buffers*/ 1, /*samplers*/ 0);
window.descriptorHeap = &heap;
// Mesh cubeMesh;
// cubeMesh.Build(loadedMesh, cmd);
ImageVulkan<RGBA8> checkerImage;
checkerImage.Create(
loadedTex.sizeX, loadedTex.sizeY, /*mipLevels*/ 1, cmd,
VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
checkerImage.Update(loadedTex, cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
window.FinishInit();
std::println("GPU init submit + wait completed without validation errors.");
//std::println("BLAS device address: 0x{:x}", cubeMesh.blasAddr);
// Cleanup happens via Window/Device dtors when they go out of scope.
return 0;
}