Crafter.Graphics/examples/Decompression/main.cpp

189 lines
7.5 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;
}