From 50f1503f09f1cf2a27f253650670f681dd4ee3cd Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Mon, 10 Nov 2025 18:30:14 +0900 Subject: [PATCH] ADD: KTX loader completed --- docs/TextureLoading.md | 8 +- shaders/gbuffer.frag | 9 +- shaders/mesh.frag | 8 +- src/CMakeLists.txt | 36 +++---- src/core/ktx2_loader.cpp | 213 ------------------------------------- src/core/ktx2_loader.h | 33 ------ src/core/texture_cache.cpp | 162 +++++++++++++++++++++------- src/core/vk_engine.cpp | 2 +- src/scene/vk_loader.cpp | 1 + 9 files changed, 152 insertions(+), 320 deletions(-) delete mode 100644 src/core/ktx2_loader.cpp delete mode 100644 src/core/ktx2_loader.h diff --git a/docs/TextureLoading.md b/docs/TextureLoading.md index 71913f7..b7950a3 100644 --- a/docs/TextureLoading.md +++ b/docs/TextureLoading.md @@ -18,13 +18,13 @@ Data Flow - `pumpLoads(...)` looks for entries in `Unloaded` or `Evicted` state that were seen recently (`now == 0` or `now - lastUsed <= 1`) and starts at most `max_loads_per_pump` decodes per call, while enforcing a byte budget for uploads per frame. - Render passes mark used sets each frame with `markSetUsed(...)` (or specific handles via `markUsed(...)`). - Decode - - FilePath: if the path ends with `.ktx2` or a sibling exists, parse KTX2 (2D, single‑face, single‑layer, no supercompression). Otherwise, decode to RGBA8 via stb_image. + - FilePath: if the path ends with `.ktx2` or a sibling exists, we load via libktx (and transcode to BCn if needed). Otherwise, decode to RGBA8 via stb_image. - Bytes: always decode via stb_image (no sibling discovery possible). - Admission & Upload - Before upload, an expected resident size is computed (exact for KTX2 by summing level byte lengths; estimated for raster by format×area×mip‑factor). A per‑frame byte budget (`max_bytes_per_pump`) throttles uploads. - If a GPU texture budget is set, the cache evicts least‑recently‑used textures not used this frame. If it still cannot fit, the decode is deferred or dropped with backoff. - Raster: `ResourceManager::create_image(...)` stages a single region, then optionally generates mips on GPU. - - KTX2: `ResourceManager::create_image_compressed(...)` allocates an image with the file’s `VkFormat` and records one `VkBufferImageCopy` per mip level (no GPU mip gen). Immediate path transitions to `SHADER_READ_ONLY_OPTIMAL`; RG path leaves it in `TRANSFER_DST` until a sampling pass. + - KTX2: `ResourceManager::create_image_compressed(...)` allocates an image with the file’s `VkFormat` (from libktx) and records one `VkBufferImageCopy` per mip level (no GPU mip gen). Immediate path transitions to `SHADER_READ_ONLY_OPTIMAL`; the RenderGraph path transitions after copy when no mip gen. - If the device cannot sample the KTX2 format, the cache falls back to raster decode. - After upload: state → `Resident`, descriptors recorded via `watchBinding` are rewritten to the new image view with the chosen sampler and `SHADER_READ_ONLY_OPTIMAL` layout. For Bytes‑backed keys, compressed source bytes are dropped unless `keep_source_bytes` is enabled. - Eviction & Reload @@ -71,8 +71,8 @@ Implementation Notes - The decode thread downsizes large images by powers of 2 until within `Max Upload Dimension`, reducing both staging and VRAM. You can increase the cap or disable it (set to 0) from the UI. KTX2 specifics -- Supported: 2D, single‑face, single‑layer, no supercompression; pre‑transcoded BCn (including sRGB variants). -- Not supported: UASTC/BasisLZ transcoding at runtime, cube/array/multilayer. +- Supported: 2D, single‑face, single‑layer KTX2. If BasisLZ/UASTC, libktx transcodes to BCn. sRGB/UNORM is honored from the file’s DFD and can be nudged by request (albedo sRGB, MR/normal UNORM). +- Not supported: Cube/array/multilayer KTX2 (current code path assumes single layer, 2D). Limitations / Future Work - Linear‑blit capability check diff --git a/shaders/gbuffer.frag b/shaders/gbuffer.frag index 9283dc6..71faf85 100644 --- a/shaders/gbuffer.frag +++ b/shaders/gbuffer.frag @@ -22,10 +22,13 @@ void main() { float metallic = clamp(mrTex.y * materialData.metal_rough_factors.x, 0.0, 1.0); // Normal mapping: decode tangent-space normal and transform to world space - // Expect UNORM normal map (not sRGB). Flat fallback is (0.5, 0.5, 1.0). - vec3 Nm = texture(normalMap, inUV).xyz * 2.0 - 1.0; + // Expect UNORM normal map; support BC5 (RG) by reconstructing Z from XY. + vec2 enc = texture(normalMap, inUV).xy * 2.0 - 1.0; float normalScale = max(materialData.extra[0].x, 0.0); - Nm.xy *= normalScale; + enc *= normalScale; + float z2 = 1.0 - dot(enc, enc); + float nz = z2 > 0.0 ? sqrt(z2) : 0.0; + vec3 Nm = vec3(enc, nz); vec3 N = normalize(inNormal); vec3 T = normalize(inTangent.xyz); vec3 B = normalize(cross(N, T)) * inTangent.w; diff --git a/shaders/mesh.frag b/shaders/mesh.frag index f881195..af3d701 100644 --- a/shaders/mesh.frag +++ b/shaders/mesh.frag @@ -59,9 +59,13 @@ void main() float metallic = clamp(mrTex.y * materialData.metal_rough_factors.x, 0.0, 1.0); // Normal mapping path for forward/transparent pipeline - vec3 Nm = texture(normalMap, inUV).xyz * 2.0 - 1.0; + // Expect UNORM normal map; support BC5 (RG) by reconstructing Z from XY. + vec2 enc = texture(normalMap, inUV).xy * 2.0 - 1.0; float normalScale = max(materialData.extra[0].x, 0.0); - Nm.xy *= normalScale; + enc *= normalScale; + float z2 = 1.0 - dot(enc, enc); + float nz = z2 > 0.0 ? sqrt(z2) : 0.0; + vec3 Nm = vec3(enc, nz); vec3 Nn = normalize(inNormal); vec3 T = normalize(inTangent.xyz); vec3 B = normalize(cross(Nn, T)) * inTangent.w; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 055d819..0d17230 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -34,8 +34,6 @@ add_executable (vulkan_engine core/frame_resources.cpp core/texture_cache.h core/texture_cache.cpp - core/ktx2_loader.h - core/ktx2_loader.cpp core/config.h core/vk_engine.h core/vk_engine.cpp @@ -115,26 +113,16 @@ add_custom_command(TARGET vulkan_engine POST_BUILD COMMAND_EXPAND_LISTS ) -option(ENABLE_LIBKTX "Enable KTX2 loading via libktx" ON) -if (ENABLE_LIBKTX) - find_package(ktx CONFIG QUIET) - if (NOT ktx_FOUND) - find_package(ktx QUIET) - endif() - - set(_KTX_TARGET "") - if (TARGET ktx::ktx) - set(_KTX_TARGET ktx::ktx) - elseif (TARGET KTX::ktx) - set(_KTX_TARGET KTX::ktx) - elseif (TARGET ktx) - set(_KTX_TARGET ktx) - endif() - - if (_KTX_TARGET STREQUAL "") - message(STATUS "libktx not found via find_package; looking for in-tree build...") - else() - target_link_libraries(vulkan_engine PUBLIC ${_KTX_TARGET}) - target_compile_definitions(vulkan_engine PUBLIC VULKAN_ENGINE_HAS_KTX=1) - endif() +find_package(ktx CONFIG REQUIRED) +set(_KTX_TARGET "") +if (TARGET ktx::ktx) + set(_KTX_TARGET ktx::ktx) +elseif (TARGET KTX::ktx) + set(_KTX_TARGET KTX::ktx) +elseif (TARGET ktx) + set(_KTX_TARGET ktx) endif() +if (_KTX_TARGET STREQUAL "") + message(FATAL_ERROR "libktx not found; please install KTX v2 and expose its CMake package") +endif() +target_link_libraries(vulkan_engine PUBLIC ${_KTX_TARGET}) diff --git a/src/core/ktx2_loader.cpp b/src/core/ktx2_loader.cpp deleted file mode 100644 index 6acea40..0000000 --- a/src/core/ktx2_loader.cpp +++ /dev/null @@ -1,213 +0,0 @@ -#include "ktx2_loader.h" - -#include -#include -#include - -namespace { - -struct KTX2Header { - uint8_t magic[12]; - uint32_t vkFormat; - uint32_t typeSize; - uint32_t pixelWidth; - uint32_t pixelHeight; - uint32_t pixelDepth; - uint32_t layerCount; - uint32_t faceCount; - uint32_t levelCount; - uint32_t supercompressionScheme; - uint64_t dfdByteOffset; - uint32_t dfdByteLength; - uint64_t kvdByteOffset; - uint32_t kvdByteLength; - uint64_t sgdByteOffset; - uint64_t sgdByteLength; -}; - -struct KTX2LevelIndexEntry { - uint64_t byteOffset; - uint64_t byteLength; - uint64_t uncompressedByteLength; -}; - -constexpr uint8_t KTX2_MAGIC[12] = { - 0xAB,'K','T','X',' ', '2','0', 0xBB, 0x0D, 0x0A, 0x1A, 0x0A -}; - -template -static inline bool read_into(const uint8_t* base, size_t size, size_t off, T& out) -{ - if (off + sizeof(T) > size) return false; - std::memcpy(&out, base + off, sizeof(T)); - return true; -} - -} // namespace - -static inline uint32_t bc_block_bytes(VkFormat fmt) -{ - switch (fmt) - { - case VK_FORMAT_BC1_RGB_UNORM_BLOCK: - case VK_FORMAT_BC1_RGB_SRGB_BLOCK: - case VK_FORMAT_BC1_RGBA_UNORM_BLOCK: - case VK_FORMAT_BC1_RGBA_SRGB_BLOCK: - case VK_FORMAT_BC4_UNORM_BLOCK: - case VK_FORMAT_BC4_SNORM_BLOCK: - return 8u; - case VK_FORMAT_BC2_UNORM_BLOCK: - case VK_FORMAT_BC2_SRGB_BLOCK: - case VK_FORMAT_BC3_UNORM_BLOCK: - case VK_FORMAT_BC3_SRGB_BLOCK: - case VK_FORMAT_BC5_UNORM_BLOCK: - case VK_FORMAT_BC5_SNORM_BLOCK: - case VK_FORMAT_BC6H_UFLOAT_BLOCK: - case VK_FORMAT_BC6H_SFLOAT_BLOCK: - case VK_FORMAT_BC7_UNORM_BLOCK: - case VK_FORMAT_BC7_SRGB_BLOCK: - return 16u; - default: return 0u; - } -} - -bool parse_ktx2(const uint8_t* bytes, size_t size, KTX2Image& out, std::string* err) -{ - if (!bytes || size < sizeof(KTX2Header)) - { - if (err) *err = "KTX2: buffer too small"; - return false; - } - - KTX2Header hdr{}; - if (!read_into(bytes, size, 0, hdr)) - { - if (err) *err = "KTX2: failed to read header"; - return false; - } - if (std::memcmp(hdr.magic, KTX2_MAGIC, sizeof(KTX2_MAGIC)) != 0) - { - if (err) *err = "KTX2: bad magic"; - return false; - } - - if (hdr.levelCount == 0 || hdr.pixelWidth == 0 || hdr.pixelHeight == 0) - { - if (err) *err = "KTX2: invalid dimensions or levels"; - return false; - } - if (hdr.layerCount > 1 || hdr.faceCount != 1) - { - if (err) *err = "KTX2: only 2D, single-face, single-layer supported"; - return false; - } - if (hdr.supercompressionScheme != 0) - { - if (err) *err = "KTX2: supercompressed payloads not supported"; - return false; - } - if (hdr.vkFormat == 0) - { - if (err) *err = "KTX2: vkFormat undefined (expected pre-transcoded BCn)"; - return false; - } - - // Level index immediately follows header in KTX2 layout. - const size_t levelIndexSize = sizeof(KTX2LevelIndexEntry) * static_cast(hdr.levelCount); - auto align8 = [](uint64_t x) { return (x + 7ull) & ~7ull; }; - - // Per KTX2 spec, the Level Index immediately follows the fixed-size 80-byte header. - size_t levelIndexOffset = sizeof(KTX2Header); - if (levelIndexOffset + levelIndexSize > size) - { - if (err) *err = "KTX2: truncated level index"; - return false; - } - - std::vector levels(hdr.levelCount); - std::memcpy(levels.data(), bytes + levelIndexOffset, levelIndexSize); - - // Debug header/offsets when requested via env (VE_TEX_DEBUG=1) - if (const char* dbg = std::getenv("VE_TEX_DEBUG"); dbg && dbg[0] == '1') - { - fmt::println("[KTX2] hdr: fmt={}, size={}x{} levels={} dfdOff={} dfdLen={} kvdOff={} kvdLen={} sgdOff={} sgdLen={} liOff={}", - (unsigned)hdr.vkFormat, hdr.pixelWidth, hdr.pixelHeight, hdr.levelCount, - (unsigned long long)hdr.dfdByteOffset, (unsigned)hdr.dfdByteLength, - (unsigned long long)hdr.kvdByteOffset, (unsigned)hdr.kvdByteLength, - (unsigned long long)hdr.sgdByteOffset, (unsigned long long)hdr.sgdByteLength, - (unsigned long long)levelIndexOffset); - for (uint32_t i = 0; i < hdr.levelCount; ++i) - { - fmt::println("[KTX2] LI[{}]: offRel={} len={} uncomp={}", i, - (unsigned long long)levels[i].byteOffset, - (unsigned long long)levels[i].byteLength, - (unsigned long long)levels[i].uncompressedByteLength); - } - } - - // Compute dataStart per spec: after level index and any optional blocks, 8-byte aligned. - uint64_t afterIndex = align8(static_cast(levelIndexOffset + levelIndexSize)); - uint64_t dfdEnd = static_cast(hdr.dfdByteOffset) + static_cast(hdr.dfdByteLength); - uint64_t kvdEnd = static_cast(hdr.kvdByteOffset) + static_cast(hdr.kvdByteLength); - uint64_t sgdEnd = static_cast(hdr.sgdByteOffset) + static_cast(hdr.sgdByteLength); - uint64_t dataStart = align8(std::max({ afterIndex, dfdEnd, kvdEnd, sgdEnd })); - if (dataStart == 0 || dataStart > size) - { - if (err) *err = "KTX2: could not locate level data start"; - return false; - } - - out = {}; - out.format = static_cast(hdr.vkFormat); - out.width = hdr.pixelWidth; - out.height = hdr.pixelHeight; - out.mipLevels = hdr.levelCount; - out.faceCount = hdr.faceCount; - out.layerCount = hdr.layerCount; - out.supercompression = hdr.supercompressionScheme; - out.data.assign(bytes, bytes + size); // retain backing store for staging copies - out.levels.resize(hdr.levelCount); - // Map entries to mip levels: assign largest byteLength to mip 0, next to mip 1, etc. - std::vector order(hdr.levelCount); - for (uint32_t i = 0; i < hdr.levelCount; ++i) order[i] = i; - std::sort(order.begin(), order.end(), [&](uint32_t a, uint32_t b){ return levels[a].byteLength > levels[b].byteLength; }); - - out.levels.resize(hdr.levelCount); - const uint32_t blockBytes = bc_block_bytes(static_cast(hdr.vkFormat)); - for (uint32_t mip = 0; mip < hdr.levelCount; ++mip) - { - const auto &li = levels[ order[mip] ]; - const uint32_t w = std::max(1u, hdr.pixelWidth >> mip); - const uint32_t h = std::max(1u, hdr.pixelHeight >> mip); - - if (blockBytes) - { - uint64_t bx = (w + 3u) / 4u; - uint64_t by = (h + 3u) / 4u; - uint64_t expected = bx * by * blockBytes; - if (li.byteLength < expected) - { - if (err) - { - char buf[256]; - snprintf(buf, sizeof(buf), - "KTX2: level length smaller than expected footprint (mip=%u fmt=%u w=%u h=%u blocks=%llux%llu blockBytes=%u expected=%llu got=%llu)", - mip, (unsigned)hdr.vkFormat, w, h, - (unsigned long long)bx, (unsigned long long)by, - blockBytes, (unsigned long long)expected, (unsigned long long)li.byteLength); - *err = buf; - } - return false; - } - } - - uint64_t absOff = dataStart + li.byteOffset; - if (absOff + li.byteLength > size) - { - if (err) *err = "KTX2: level range out of bounds"; - return false; - } - out.levels[mip] = KTX2LevelInfo{ absOff, li.byteLength, w, h }; - } - return true; -} diff --git a/src/core/ktx2_loader.h b/src/core/ktx2_loader.h deleted file mode 100644 index 1834598..0000000 --- a/src/core/ktx2_loader.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -// Minimal KTX2 reader for 2D textures with pre-transcoded BCn payloads. -// Supports: faceCount==1, layerCount==1, supercompression==0 (none). -// Extracts vkFormat, base width/height, mip count, and per-level byte ranges. - -struct KTX2LevelInfo { - uint64_t offset{0}; - uint64_t length{0}; - uint32_t width{0}; - uint32_t height{0}; -}; - -struct KTX2Image { - VkFormat format{VK_FORMAT_UNDEFINED}; - uint32_t width{0}; - uint32_t height{0}; - uint32_t mipLevels{0}; - uint32_t faceCount{0}; - uint32_t layerCount{0}; - uint32_t supercompression{0}; - std::vector data; // full file payload to back staging buffer copies - std::vector levels; // level 0..mipLevels-1 -}; - -// Parse from memory. Returns true on success; on failure, 'err' (if provided) describes the issue. -bool parse_ktx2(const uint8_t* bytes, size_t size, KTX2Image& out, std::string* err = nullptr); - diff --git a/src/core/texture_cache.cpp b/src/core/texture_cache.cpp index 29706a1..b4422dd 100644 --- a/src/core/texture_cache.cpp +++ b/src/core/texture_cache.cpp @@ -6,7 +6,8 @@ #include #include #include "stb_image.h" -#include "ktx2_loader.h" +#include +#include #include #include "vk_device.h" #include @@ -181,6 +182,40 @@ static inline size_t bytes_per_texel(VkFormat fmt) } } +static inline VkFormat to_srgb_variant(VkFormat fmt) +{ + switch (fmt) + { + case VK_FORMAT_BC1_RGB_UNORM_BLOCK: return VK_FORMAT_BC1_RGB_SRGB_BLOCK; + case VK_FORMAT_BC1_RGBA_UNORM_BLOCK: return VK_FORMAT_BC1_RGBA_SRGB_BLOCK; + case VK_FORMAT_BC2_UNORM_BLOCK: return VK_FORMAT_BC2_SRGB_BLOCK; + case VK_FORMAT_BC3_UNORM_BLOCK: return VK_FORMAT_BC3_SRGB_BLOCK; + case VK_FORMAT_BC7_UNORM_BLOCK: return VK_FORMAT_BC7_SRGB_BLOCK; + case VK_FORMAT_R8G8B8A8_UNORM: return VK_FORMAT_R8G8B8A8_SRGB; + case VK_FORMAT_B8G8R8A8_UNORM: return VK_FORMAT_B8G8R8A8_SRGB; + case VK_FORMAT_R8_UNORM: return VK_FORMAT_R8_SRGB; + case VK_FORMAT_R8G8_UNORM: return VK_FORMAT_R8G8_SRGB; + default: return fmt; + } +} + +static inline VkFormat to_unorm_variant(VkFormat fmt) +{ + switch (fmt) + { + case VK_FORMAT_BC1_RGB_SRGB_BLOCK: return VK_FORMAT_BC1_RGB_UNORM_BLOCK; + case VK_FORMAT_BC1_RGBA_SRGB_BLOCK: return VK_FORMAT_BC1_RGBA_UNORM_BLOCK; + case VK_FORMAT_BC2_SRGB_BLOCK: return VK_FORMAT_BC2_UNORM_BLOCK; + case VK_FORMAT_BC3_SRGB_BLOCK: return VK_FORMAT_BC3_UNORM_BLOCK; + case VK_FORMAT_BC7_SRGB_BLOCK: return VK_FORMAT_BC7_UNORM_BLOCK; + case VK_FORMAT_R8G8B8A8_SRGB: return VK_FORMAT_R8G8B8A8_UNORM; + case VK_FORMAT_B8G8R8A8_SRGB: return VK_FORMAT_B8G8R8A8_UNORM; + case VK_FORMAT_R8_SRGB: return VK_FORMAT_R8_UNORM; + case VK_FORMAT_R8G8_SRGB: return VK_FORMAT_R8G8_UNORM; + default: return fmt; + } +} + static inline float mip_factor_for_levels(uint32_t levels) { if (levels <= 1) return 1.0f; @@ -402,50 +437,85 @@ void TextureCache::worker_loop() if (hasKTX2) { attemptedKTX2 = true; - // Read file fmt::println("[TextureCache] KTX2 candidate for '{}' → '{}'", rq.path, ktxPath.string()); - std::ifstream ifs(ktxPath, std::ios::binary); - if (ifs) + ktxTexture2* ktex = nullptr; + ktxResult kres = ktxTexture2_CreateFromNamedFile(ktxPath.string().c_str(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &ktex); + if (kres != KTX_SUCCESS || !ktex) { - std::vector fileBytes(std::istreambuf_iterator(ifs), {}); - fmt::println("[TextureCache] KTX2 read {} bytes", fileBytes.size()); - KTX2Image ktx{}; - std::string err; - if (parse_ktx2(fileBytes.data(), fileBytes.size(), ktx, &err)) - { - fmt::println("[TextureCache] KTX2 parsed: format={}, {}x{}, mips={}, faces={}, layers={}, supercompression={}", - string_VkFormat(static_cast(ktx.format)), ktx.width, ktx.height, - ktx.mipLevels, ktx.faceCount, ktx.layerCount, ktx.supercompression); - size_t sum = 0; for (const auto &lv: ktx.levels) sum += static_cast(lv.length); - fmt::println("[TextureCache] KTX2 levels: {} totalBytes={}", ktx.levels.size(), sum); - for (size_t li = 0; li < ktx.levels.size(); ++li) - { - fmt::println(" L{}: off={}, len={}, extent={}x{}", li, ktx.levels[li].offset, - ktx.levels[li].length, - std::max(1u, ktx.width >> li), - std::max(1u, ktx.height >> li)); - } - out.isKTX2 = true; - out.ktxFormat = ktx.format; - out.ktxMipLevels = ktx.mipLevels; - out.ktx.bytes = std::move(ktx.data); - out.ktx.levels.reserve(ktx.levels.size()); - for (const auto &lv : ktx.levels) - { - out.ktx.levels.push_back({lv.offset, lv.length, lv.width, lv.height}); - } - out.width = static_cast(ktx.width); - out.height = static_cast(ktx.height); - } - else - { - fmt::println("[TextureCache] parse_ktx2 failed for '{}' ({} bytes): {}", - ktxPath.string(), fileBytes.size(), err); - } + fmt::println("[TextureCache] libktx open failed for '{}': {}", ktxPath.string(), ktxErrorString(kres)); } else { - fmt::println("[TextureCache] Failed to open KTX2 file '{}'", ktxPath.string()); + if (ktxTexture2_NeedsTranscoding(ktex)) + { + ktx_transcode_fmt_e target = (rq.key.channels == TextureKey::ChannelsHint::RG) ? KTX_TTF_BC5_RG : KTX_TTF_BC7_RGBA; + kres = ktxTexture2_TranscodeBasis(ktex, target, 0); + if (kres != KTX_SUCCESS) + { + fmt::println("[TextureCache] libktx transcode failed for '{}': {}", ktxPath.string(), ktxErrorString(kres)); + ktxTexture_Destroy(ktxTexture(ktex)); + ktex = nullptr; + } + } + if (ktex) + { + VkFormat vkfmt = static_cast(ktex->vkFormat); + uint32_t mipLevels = ktex->numLevels; + uint32_t baseW = ktex->baseWidth; + uint32_t baseH = ktex->baseHeight; + ktx_size_t totalSize = ktxTexture_GetDataSize(ktxTexture(ktex)); + const uint8_t* dataPtr = reinterpret_cast(ktxTexture_GetData(ktxTexture(ktex))); + + switch (vkfmt) + { + case VK_FORMAT_BC1_RGB_UNORM_BLOCK: + case VK_FORMAT_BC1_RGB_SRGB_BLOCK: + case VK_FORMAT_BC1_RGBA_UNORM_BLOCK: + case VK_FORMAT_BC1_RGBA_SRGB_BLOCK: + case VK_FORMAT_BC2_UNORM_BLOCK: + case VK_FORMAT_BC2_SRGB_BLOCK: + case VK_FORMAT_BC3_UNORM_BLOCK: + case VK_FORMAT_BC3_SRGB_BLOCK: + case VK_FORMAT_BC4_UNORM_BLOCK: + case VK_FORMAT_BC4_SNORM_BLOCK: + case VK_FORMAT_BC5_UNORM_BLOCK: + case VK_FORMAT_BC5_SNORM_BLOCK: + case VK_FORMAT_BC6H_UFLOAT_BLOCK: + case VK_FORMAT_BC6H_SFLOAT_BLOCK: + case VK_FORMAT_BC7_UNORM_BLOCK: + case VK_FORMAT_BC7_SRGB_BLOCK: + break; + default: + fmt::println("[TextureCache] libktx returned non-BC format {} — skipping KTX2", string_VkFormat(vkfmt)); + ktxTexture_Destroy(ktxTexture(ktex)); + ktex = nullptr; + break; + } + + if (ktex) + { + out.isKTX2 = true; + out.ktxFormat = vkfmt; + out.ktxMipLevels = mipLevels; + out.ktx.bytes.assign(dataPtr, dataPtr + totalSize); + out.ktx.levels.clear(); + out.ktx.levels.reserve(mipLevels); + for (uint32_t mip = 0; mip < mipLevels; ++mip) + { + ktx_size_t off = 0, len = 0; + ktxTexture_GetImageOffset(ktxTexture(ktex), mip, 0, 0, &off); + ktxTexture_GetImageSize(ktxTexture(ktex), mip, &len); + uint32_t w = std::max(1u, baseW >> mip); + uint32_t h = std::max(1u, baseH >> mip); + out.ktx.levels.push_back({ static_cast(off), static_cast(len), w, h }); + } + out.width = static_cast(baseW); + out.height = static_cast(baseH); + fmt::println("[TextureCache] libktx parsed: format={}, {}x{}, mips={}, dataSize={}", + string_VkFormat(vkfmt), baseW, baseH, mipLevels, (unsigned long long)totalSize); + ktxTexture_Destroy(ktxTexture(ktex)); + } + } } } else if (p.extension() == ".ktx2") @@ -541,6 +611,14 @@ size_t TextureCache::drain_ready_uploads(ResourceManager &rm, size_t budgetBytes if (res.isKTX2) { fmt = res.ktxFormat; + // Nudge format to sRGB/UNORM variant based on request to avoid gamma mistakes + VkFormat reqFmt = e.key.srgb ? to_srgb_variant(fmt) : to_unorm_variant(fmt); + if (reqFmt != fmt) + { + fmt = reqFmt; + fmt::println("[TextureCache] Overriding KTX2 format to {} based on request (original {})", + string_VkFormat(fmt), string_VkFormat(res.ktxFormat)); + } desiredLevels = res.ktxMipLevels; for (const auto &lv : res.ktx.levels) expectedBytes += static_cast(lv.length); } @@ -801,6 +879,10 @@ void TextureCache::debug_snapshot(std::vector &outRows, DebugStats &ou { row.name = std::string(" (") + std::to_string(e.bytes.size()) + ")"; } + if (e.state == EntryState::Resident && e.image.image) + { + row.name += std::string(" [") + string_VkFormat(e.image.imageFormat) + "]"; + } row.bytes = e.sizeBytes; row.lastUsed = e.lastUsedFrame; row.state = static_cast(e.state); diff --git a/src/core/vk_engine.cpp b/src/core/vk_engine.cpp index 4e49e3d..7ecc810 100644 --- a/src/core/vk_engine.cpp +++ b/src/core/vk_engine.cpp @@ -613,7 +613,7 @@ void VulkanEngine::init() auto imguiPass = std::make_unique(); _renderPassManager->setImGuiPass(std::move(imguiPass)); - const std::string structurePath = _assetManager->modelPath("seoul_high/scene.gltf"); + const std::string structurePath = _assetManager->modelPath("mirage2000/scene.gltf"); const auto structureFile = _assetManager->loadGLTF(structurePath); assert(structureFile.has_value()); diff --git a/src/scene/vk_loader.cpp b/src/scene/vk_loader.cpp index 4563927..2cbf178 100644 --- a/src/scene/vk_loader.cpp +++ b/src/scene/vk_loader.cpp @@ -412,6 +412,7 @@ std::optional > loadGltf(VulkanEngine *engine, std:: const bool hasSampler = tex.samplerIndex.has_value(); const VkSampler sampler = hasSampler ? file.samplers[tex.samplerIndex.value()] : engine->_samplerManager->defaultLinear(); auto key = buildTextureKey(imgIndex, false); + key.channels = TextureCache::TextureKey::ChannelsHint::RG; // prefer BC5 for normals if (key.hash != 0) { hNorm = cache->request(key, sampler);