// 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 MakeCubeMesh() { MeshAsset 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 MakeCheckerboard() { TextureAsset 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(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 srcMesh = MakeCubeMesh(); TextureAsset 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> v(loadedMesh.vertexCount); std::vector i(loadedMesh.indexCount); std::array, 3> outputs = { std::as_writable_bytes(std::span(v)), std::as_writable_bytes(std::span(i)), std::span{}, }; 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 p(loadedTex.sizeX * loadedTex.sizeY); std::array, 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 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; }