ADD: KTX loader completed
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
#include "ktx2_loader.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
#include <fmt/core.h>
|
||||
|
||||
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 <typename T>
|
||||
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<size_t>(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<KTX2LevelIndexEntry> 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<uint64_t>(levelIndexOffset + levelIndexSize));
|
||||
uint64_t dfdEnd = static_cast<uint64_t>(hdr.dfdByteOffset) + static_cast<uint64_t>(hdr.dfdByteLength);
|
||||
uint64_t kvdEnd = static_cast<uint64_t>(hdr.kvdByteOffset) + static_cast<uint64_t>(hdr.kvdByteLength);
|
||||
uint64_t sgdEnd = static_cast<uint64_t>(hdr.sgdByteOffset) + static_cast<uint64_t>(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<VkFormat>(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<uint32_t> 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<VkFormat>(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;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
// 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<uint8_t> data; // full file payload to back staging buffer copies
|
||||
std::vector<KTX2LevelInfo> 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);
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
#include <core/config.h>
|
||||
#include <algorithm>
|
||||
#include "stb_image.h"
|
||||
#include "ktx2_loader.h"
|
||||
#include <ktx.h>
|
||||
#include <ktxvulkan.h>
|
||||
#include <algorithm>
|
||||
#include "vk_device.h"
|
||||
#include <cstring>
|
||||
@@ -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<uint8_t> fileBytes(std::istreambuf_iterator<char>(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<VkFormat>(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<size_t>(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<int>(ktx.width);
|
||||
out.height = static_cast<int>(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<VkFormat>(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<const uint8_t*>(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<uint64_t>(off), static_cast<uint64_t>(len), w, h });
|
||||
}
|
||||
out.width = static_cast<int>(baseW);
|
||||
out.height = static_cast<int>(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<size_t>(lv.length);
|
||||
}
|
||||
@@ -801,6 +879,10 @@ void TextureCache::debug_snapshot(std::vector<DebugRow> &outRows, DebugStats &ou
|
||||
{
|
||||
row.name = std::string("<bytes> (") + 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<uint8_t>(e.state);
|
||||
|
||||
@@ -613,7 +613,7 @@ void VulkanEngine::init()
|
||||
auto imguiPass = std::make_unique<ImGuiPass>();
|
||||
_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());
|
||||
|
||||
@@ -412,6 +412,7 @@ std::optional<std::shared_ptr<LoadedGLTF> > 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);
|
||||
|
||||
Reference in New Issue
Block a user