189 lines
7.6 KiB
C++
189 lines
7.6 KiB
C++
// 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;
|
||
}
|