From a0c92af5db9ea36c45275c78f2c8ce1639024c88 Mon Sep 17 00:00:00 2001 From: atdunbg Date: Wed, 23 Sep 2026 14:21:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20DDS=20=E7=BA=B9=E7=90=86=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E5=99=A8=EF=BC=88BC=20=E5=8E=8B=E7=BC=A9=E7=9B=B4?= =?UTF-8?q?=E4=BC=A0=20+=20mip=20=E9=93=BE=EF=BC=8C=E5=8F=82=E8=80=83=20Ho?= =?UTF-8?q?rizon=20=E5=AE=9E=E7=8E=B0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Assets::Dds 解析器:中性 DdsFormat 枚举(Assets 层禁 Vulkan 头铁律), 支持 legacy fourcc(DXT1/3/5/ATI1/2)+ DX10 头(BC1..BC7/RGBA8/BGRA8) + 未压缩 32bpp + 2D mip 链;数据不解码,直接上传由硬件采样解码 - Texture:按扩展名分发(.dds → InitFromDds);渲染侧负责格式映射与 sRGB 语义(颜色图走 SRGB 变体,BC4/5/6H 数据图保持线性);view/sampler 覆盖完整 mip 链;逐 mip copyBufferToImage(紧凑排布) - Model::GetOrLoadTexture:FBX/DCC 路径反斜杠归一化(否则 Linux 全挂) - DescriptorManager 池 1000 → 4096(SunTemple 1056 材质集打爆过; 材质去重属材质系统任务) - main.cpp 临时挂 SunTemple 场景验证:48 张 DDS 全部加载(2048²、12 mips), 1056 mesh 渲染正常,validation 零警告 --- App/main.cpp | 25 +- Engine/include/Engine/Assets/DdsLoader.h | 55 +++++ Engine/include/Engine/ECS/Components.h | 6 +- .../include/Engine/Vulkan/DescriptorManager.h | 5 +- Engine/include/Engine/Vulkan/Texture.h | 12 +- Engine/src/Assets/DdsLoader.cpp | 170 +++++++++++++ Engine/src/Renderer/Model.cpp | 7 +- Engine/src/Vulkan/Texture.cpp | 229 ++++++++++++++++-- 8 files changed, 469 insertions(+), 40 deletions(-) create mode 100644 Engine/include/Engine/Assets/DdsLoader.h create mode 100644 Engine/src/Assets/DdsLoader.cpp diff --git a/App/main.cpp b/App/main.cpp index a159d8c..be5bdf4 100644 --- a/App/main.cpp +++ b/App/main.cpp @@ -47,7 +47,8 @@ protected: EnableImGui(); // 模型资产(AssetManager 负责加载/缓存/生命周期,返回句柄) - const auto backpack = GetAssets().LoadModel("assets/models/backpack/backpack.obj"); + // SunTemple:FBX + DDS 压缩贴图(DdsLoader 验证场景) + const auto temple = GetAssets().LoadModel("assets/models/SunTemple/SunTemple.fbx"); // 地面:程序化纯色平面(半边长 15,中心 y=-2) const auto ground = GetAssets().CreatePlane(15.0f, 140, 140, 145); @@ -64,23 +65,13 @@ protected: camera.primary = true; camera.camera.SetPosition(glm::vec3(0.0f, 0.0f, 7.0f)); - // 背包实体 × 3(悬浮在地面上方) - auto size = 1; - for (int i = 0; i < size; ++i) + // 神庙实体 × 1( FBX 单位较大,缩放到场景尺度) { - for (int j = 0; j < size; j++) - { - auto e = scene.CreateEntity("Backpack" + std::to_string(i)); - - e.AddComponent() - .SetPosition(glm::vec3((i - size * 0.5) * 1.5f, 0.0f, (j - size * 0.5) * 1.5f)) - .SetScale(glm::vec3(0.5f)) - .Compose(); - - e.AddComponent(MeshRendererComponent{backpack}); - - m_Spinners.push_back(e); - } + auto e = scene.CreateEntity("SunTemple"); + e.AddComponent() + .SetScale(glm::vec3(0.05f)) + .Compose(); + e.AddComponent(MeshRendererComponent{temple}); } // 灯光实体(第一个灯 = 阴影光源) diff --git a/Engine/include/Engine/Assets/DdsLoader.h b/Engine/include/Engine/Assets/DdsLoader.h new file mode 100644 index 0000000..54890ce --- /dev/null +++ b/Engine/include/Engine/Assets/DdsLoader.h @@ -0,0 +1,55 @@ +// +// Created by atdunbg on 2026/9/23. +// +// DDS 文件解析(最小实现,不解码压缩块)。 +// +// 设计:GPU 原生支持 BC 压缩格式——DDS 的压缩数据**直接上传**为 +// BC1/BC2/BC3/BC4/BC5/BC6H/BC7 纹理由硬件采样解码,不做 CPU 解码。 +// 输出中性 DdsFormat(本层禁 Vulkan 头),vk::Format 映射与 sRGB +// 语义由调用方(Texture,渲染侧)决定。 +// +// 支持: +// - legacy fourcc:DXT1/DXT3/DXT5/ATI1/BC4U/ATI2/BC5U +// - DX10 扩展头:BC1..BC7、R8G8B8A8、B8G8R8A8 +// - 未压缩 32bpp(DDPF_RGB | DDPF_ALPHAPIXELS) +// - 2D + mip 链(cubemap 面标志暂不支持,报错) +// +// 输出:Mips[r] = 第 r 级完整数据(BC 按块行紧凑排布: +// bytes = ceil(w/4)*ceil(h/4)*blockBytes)。 +// + +#ifndef HELLOVULKAN_DDSLOADER_H +#define HELLOVULKAN_DDSLOADER_H + +#include + +#include +#include + +namespace Engine::Assets::Dds { + + // 中性格式(Assets 层不感知 Vulkan;映射在渲染侧 Texture) + enum class DdsFormat { + BC1, BC2, BC3, BC4, BC5, BC6H, BC7, + RGBA8, + BGRA8, // 按字节序上传后视作 RGBA(近似,触发警告) + }; + + struct DdsData { + u32 Width = 0; + u32 Height = 0; + u32 MipLevels = 1; + DdsFormat Format = DdsFormat::RGBA8; + std::vector> Mips; // 每级完整数据(紧凑排布) + bool BlockCompressed = false; // BC 格式(rowPitch 按块计算) + }; + + // 解析 DDS 文件;失败返回 false 并写 errMsg + bool Load(const std::string& path, DdsData& out, std::string& errMsg); + + // 指定 mip 级的单行字节跨度(BC = 块行;RGBA8 = w*4) + u32 RowPitch(const DdsData& data, u32 mipLevel); + +} // namespace Engine::Assets::Dds + +#endif //HELLOVULKAN_DDSLOADER_H diff --git a/Engine/include/Engine/ECS/Components.h b/Engine/include/Engine/ECS/Components.h index 346ccc6..ebf5837 100644 --- a/Engine/include/Engine/ECS/Components.h +++ b/Engine/include/Engine/ECS/Components.h @@ -50,12 +50,12 @@ namespace Engine::ECS { // 分量 → world 矩阵(T * R * S) TransformComponent& Compose() { - glm::mat4 t = glm::translate(glm::mat4(1.0f), position); - glm::mat4 r = + const glm::mat4 t = glm::translate(glm::mat4(1.0f), position); + const glm::mat4 r = glm::rotate(glm::mat4(1.0f), glm::radians(rotation.y), glm::vec3(0, 1, 0)) * glm::rotate(glm::mat4(1.0f), glm::radians(rotation.x), glm::vec3(1, 0, 0)) * glm::rotate(glm::mat4(1.0f), glm::radians(rotation.z), glm::vec3(0, 0, 1)); - glm::mat4 s = glm::scale(glm::mat4(1.0f), scale); + const glm::mat4 s = glm::scale(glm::mat4(1.0f), scale); world = t * r * s; return *this; } diff --git a/Engine/include/Engine/Vulkan/DescriptorManager.h b/Engine/include/Engine/Vulkan/DescriptorManager.h index d94e6df..605104a 100644 --- a/Engine/include/Engine/Vulkan/DescriptorManager.h +++ b/Engine/include/Engine/Vulkan/DescriptorManager.h @@ -56,7 +56,10 @@ namespace Engine { Device& m_Device; vk::DescriptorPool m_Pool; - static constexpr u32 kMaxSets = 1000; + // 描述符池容量:需 ≥ 场景全部材质集合数 + 每帧集合。 + // SunTemple(984 mesh 材质)把 1000 打爆过;材质去重(同贴图共享 + // Material)属于材质系统任务,在那之前留足余量 + static constexpr u32 kMaxSets = 4096; }; } // namespace Engine diff --git a/Engine/include/Engine/Vulkan/Texture.h b/Engine/include/Engine/Vulkan/Texture.h index 590d532..db928cd 100644 --- a/Engine/include/Engine/Vulkan/Texture.h +++ b/Engine/include/Engine/Vulkan/Texture.h @@ -21,7 +21,7 @@ namespace Engine { class Texture { public: - // 从磁盘图片加载 + // 从磁盘图片加载(按扩展名分发:.dds → DdsLoader,其它 → stbi) Texture(Device& device, MemoryAllocator& allocator, const std::string& path); // 程序化纹理:纯色(1x1),用于地面/调试等不依赖磁盘资产的场景 @@ -38,6 +38,8 @@ namespace Engine { vk::ImageView GetView() const { return m_View; } u32 GetWidth() const { return m_Width; } u32 GetHeight() const { return m_Height; } + vk::Format GetFormat() const { return m_Format; } + u32 GetMipLevels() const { return m_MipLevels; } vk::Sampler GetSampler() const { return m_Sampler; } @@ -46,13 +48,19 @@ namespace Engine { Texture(Device& device, MemoryAllocator& allocator, ProceduralTag, const std::array& rgba); - // 公共路径:staging → device local image → view/sampler + // 公共路径:staging → device local image → view/sampler(RGBA8 单 mip) void InitFromPixels(const void* pixels, u32 width, u32 height); + // DDS 路径:压缩数据直传(BC 硬件采样解码)+ 文件自带 mip 链 + void InitFromDds(const std::string& path); + Device& m_Device; vk::Image m_Image; vk::ImageView m_View; + vk::Format m_Format = vk::Format::eUndefined; + u32 m_MipLevels = 1; + vk::Sampler m_Sampler; diff --git a/Engine/src/Assets/DdsLoader.cpp b/Engine/src/Assets/DdsLoader.cpp new file mode 100644 index 0000000..14f624c --- /dev/null +++ b/Engine/src/Assets/DdsLoader.cpp @@ -0,0 +1,170 @@ +// +// Created by atdunbg on 2026/9/23. +// + +#include +#include + +#include +#include +#include + +namespace Engine::Assets::Dds +{ + namespace + { + constexpr u32 kMagic = 0x20534444; // "DDS " + + // 头部偏移(dwSpfFourCC @ 84) + constexpr size_t kOffsetHeight = 12; + constexpr size_t kOffsetWidth = 16; + constexpr size_t kOffsetMips = 28; + constexpr size_t kOffsetFourCC = 84; + constexpr size_t kHeaderSize = 128; + constexpr size_t kDx10Size = 20; + + u32 ReadU32(const u8* p) { return *reinterpret_cast(p); } + + // legacy fourcc → 中性格式 + bool MapFourCC(u32 fourcc, DdsFormat& out) + { + switch (fourcc) { + case 0x31545844: out = DdsFormat::BC1; return true; // "DXT1" + case 0x33545844: out = DdsFormat::BC2; return true; // "DXT3" + case 0x35545844: out = DdsFormat::BC3; return true; // "DXT5" + case 0x31495441: out = DdsFormat::BC4; return true; // "ATI1" + case 0x55344342: out = DdsFormat::BC4; return true; // "BC4U" + case 0x32495441: out = DdsFormat::BC5; return true; // "ATI2" + case 0x55354342: out = DdsFormat::BC5; return true; // "BC5U" + default: return false; + } + } + + // DX10 DXGI 值 → 中性格式 + bool MapDxgi(u32 dxgi, DdsFormat& out) + { + switch (dxgi) { + case 70: case 71: out = DdsFormat::BC1; return true; // BC1 + case 73: case 74: out = DdsFormat::BC2; return true; // BC2 + case 77: case 78: out = DdsFormat::BC3; return true; // BC3 + case 79: case 80: out = DdsFormat::BC4; return true; // BC4 + case 82: case 83: out = DdsFormat::BC5; return true; // BC5 + case 94: case 95: out = DdsFormat::BC6H; return true; // BC6H + case 97: case 98: out = DdsFormat::BC7; return true; // BC7 + case 28: case 29: out = DdsFormat::RGBA8; return true; // R8G8B8A8 + case 87: case 91: out = DdsFormat::BGRA8; return true; // B8G8R8A8(近似) + default: return false; + } + } + + u32 BytesPerBlock(DdsFormat format) + { + switch (format) { + case DdsFormat::BC1: + case DdsFormat::BC4: + return 8; + case DdsFormat::BC2: + case DdsFormat::BC3: + case DdsFormat::BC5: + case DdsFormat::BC6H: + case DdsFormat::BC7: + return 16; + default: + return 4; // RGBA8/BGRA8 系 + } + } + + bool IsBlockCompressed(DdsFormat format) + { + return format != DdsFormat::RGBA8 && format != DdsFormat::BGRA8; + } + } + + bool Load(const std::string& path, DdsData& out, std::string& errMsg) + { + std::ifstream f(path, std::ios::binary); + if (!f) { + errMsg = "无法打开文件"; + return false; + } + + u8 header[kHeaderSize]; + f.read(reinterpret_cast(header), kHeaderSize); + if (!f || ReadU32(header) != kMagic) { + errMsg = "DDS 魔数不符"; + return false; + } + + out.Width = ReadU32(header + kOffsetWidth); + out.Height = ReadU32(header + kOffsetHeight); + const u32 mips = ReadU32(header + kOffsetMips); + out.MipLevels = mips ? mips : 1; + + const u32 fourcc = ReadU32(header + kOffsetFourCC); + size_t dataOffset = kHeaderSize; + + if (fourcc == 0x30313044) { // "DX10" + u8 dx10[kDx10Size]; + f.read(reinterpret_cast(dx10), kDx10Size); + if (!f) { + errMsg = "DX10 头不完整"; + return false; + } + const u32 dxgi = ReadU32(dx10); + if (!MapDxgi(dxgi, out.Format)) { + errMsg = "DX10 DXGI 格式不支持"; + return false; + } + dataOffset += kDx10Size; + } else if (!MapFourCC(fourcc, out.Format)) { + // 未压缩路径:仅支持 32bpp RGBA(DDPF_ALPHAPIXELS(0x1) | DDPF_RGB(0x40)) + const u32 ddspfFlags = ReadU32(header + 80); + if (ddspfFlags & 0x41) { + out.Format = DdsFormat::BGRA8; // 常见 32bpp DDS 是 B8G8R8A8 字节序 + ENGINE_WARN("DDS: 未压缩 32bpp 按 BGRA8 上传 '{}'", path); + } else { + errMsg = "fourcc 不支持"; + return false; + } + } + + out.BlockCompressed = IsBlockCompressed(out.Format); + f.seekg(dataOffset); + + // 数据量校验 + 逐级读取(紧凑排布) + const u32 bpb = BytesPerBlock(out.Format); + const u32 cols = out.BlockCompressed ? 4 : 1; + out.Mips.resize(out.MipLevels); + + u32 w = out.Width, h = out.Height; + for (u32 mip = 0; mip < out.MipLevels; ++mip) { + const u32 blocksW = std::max(1u, (w + cols - 1) / cols); + const u32 blocksH = std::max(1u, (h + cols - 1) / cols); + const size_t bytes = size_t(blocksW) * blocksH * bpb; + out.Mips[mip].resize(bytes); + f.read(reinterpret_cast(out.Mips[mip].data()), + std::streamsize(bytes)); + if (!f) { + errMsg = "mip 数据不完整"; + return false; + } + w = std::max(1u, w / 2); + h = std::max(1u, h / 2); + } + return true; + } + + u32 RowPitch(const DdsData& data, u32 mipLevel) + { + u32 w = data.Width; + for (u32 m = 0; m < mipLevel; ++m) { + w = std::max(1u, w / 2); + } + const u32 bpb = BytesPerBlock(data.Format); + if (data.BlockCompressed) { + return std::max(1u, (w + 3) / 4) * bpb; + } + return w * bpb; + } + +} // namespace Engine::Assets::Dds diff --git a/Engine/src/Renderer/Model.cpp b/Engine/src/Renderer/Model.cpp index f9227ef..4a6b753 100644 --- a/Engine/src/Renderer/Model.cpp +++ b/Engine/src/Renderer/Model.cpp @@ -15,6 +15,7 @@ #include #include +#include namespace Engine { @@ -110,8 +111,12 @@ void Model::ProcessNode(aiNode* node, const aiScene* scene) } } -Texture* Model::GetOrLoadTexture(const std::string& path) +Texture* Model::GetOrLoadTexture(const std::string& rawPath) { + // FBX/DCC 导出的路径常带反斜杠(Windows 语义),统一成正斜杠 + std::string path = rawPath; + std::replace(path.begin(), path.end(), '\\', '/'); + auto it = m_TextureCache.find(path); if (it != m_TextureCache.end()) { return it->second.get(); diff --git a/Engine/src/Vulkan/Texture.cpp b/Engine/src/Vulkan/Texture.cpp index b7ce63f..b4f9791 100644 --- a/Engine/src/Vulkan/Texture.cpp +++ b/Engine/src/Vulkan/Texture.cpp @@ -5,10 +5,13 @@ #include #include #include +#include #include #include +#include +#include #include #include "Engine/Core/Stats.h" @@ -18,21 +21,31 @@ namespace Engine Texture::Texture(Device& device, MemoryAllocator& allocator, const std::string& path) : m_Device(device) , m_Allocator(allocator) { - // 1. CPU 加载图片 - int width = 0, height = 0, channels = 0; - stbi_uc* pixels = stbi_load(path.c_str(), &width, &height, &channels, STBI_rgb_alpha); + // 按扩展名分发:DDS(BC 压缩直传 + mip 链)vs stbi(无压缩单 mip) + const std::string ext = path.size() >= 4 + ? path.substr(path.size() - 4) : ""; + std::string lower; + for (char c : ext) lower += static_cast(std::tolower(c)); - if (!pixels) - { - ENGINE_ERROR("Failed to load texture: {}", path); - return; + if (lower == ".dds") { + InitFromDds(path); + } else { + // 1. CPU 加载图片 + int width = 0, height = 0, channels = 0; + stbi_uc* pixels = stbi_load(path.c_str(), &width, &height, &channels, + STBI_rgb_alpha); + + if (!pixels) { + ENGINE_ERROR("Failed to load texture: {}", path); + return; + } + + InitFromPixels(pixels, static_cast(width), static_cast(height)); + + stbi_image_free(pixels); + + ENGINE_INFO("Texture loaded: {} ({}x{})", path, m_Width, m_Height); } - - InitFromPixels(pixels, static_cast(width), static_cast(height)); - - stbi_image_free(pixels); - - ENGINE_INFO("Texture loaded: {} ({}x{})", path, m_Width, m_Height); } Texture::Texture(Device& device, MemoryAllocator& allocator, ProceduralTag, @@ -53,6 +66,8 @@ namespace Engine { m_Width = width; m_Height = height; + m_Format = vk::Format::eR8G8B8A8Srgb; + m_MipLevels = 1; vk::DeviceSize imageSize = static_cast(width) * static_cast(height) * 4; // RGBA8 @@ -87,10 +102,10 @@ namespace Engine vk::ImageViewCreateInfo viewInfo; viewInfo.setImage(m_Image) .setViewType(vk::ImageViewType::e2D) - .setFormat(vk::Format::eR8G8B8A8Srgb) + .setFormat(m_Format) .setSubresourceRange({ vk::ImageAspectFlagBits::eColor, - 0, 1, 0, 1 + 0, m_MipLevels, 0, 1 }); m_View = m_Device.GetDevice().createImageView(viewInfo); @@ -107,7 +122,9 @@ namespace Engine .setBorderColor(vk::BorderColor::eIntOpaqueBlack) .setUnnormalizedCoordinates(VK_FALSE) .setCompareEnable(VK_FALSE) - .setMipmapMode(vk::SamplerMipmapMode::eLinear); + .setMipmapMode(vk::SamplerMipmapMode::eLinear) + .setMinLod(0.0f) + .setMaxLod(static_cast(m_MipLevels)); m_Sampler = m_Device.GetDevice().createSampler(samplerInfo); @@ -197,6 +214,186 @@ namespace Engine STATS.AddTexture(m_TextureBytes); } + // DDS → Vulkan 格式映射(渲染侧职责:sRGB 语义按颜色贴图默认开启; + // BC4/BC5/BC6H 是数据图,保持线性) + vk::Format ToVkFormat(Assets::Dds::DdsFormat f) + { + using DF = Assets::Dds::DdsFormat; + switch (f) { + case DF::BC1: return vk::Format::eBc1RgbaSrgbBlock; + case DF::BC2: return vk::Format::eBc2SrgbBlock; + case DF::BC3: return vk::Format::eBc3SrgbBlock; + case DF::BC4: return vk::Format::eBc4UnormBlock; + case DF::BC5: return vk::Format::eBc5UnormBlock; + case DF::BC6H: return vk::Format::eBc6HUfloatBlock; + case DF::BC7: return vk::Format::eBc7SrgbBlock; + case DF::RGBA8: return vk::Format::eR8G8B8A8Srgb; + case DF::BGRA8: return vk::Format::eB8G8R8A8Srgb; + } + return vk::Format::eUndefined; + } + + u32 MipDim(u32 v, u32 level) + { + return std::max(1u, v >> level); + } + + void Texture::InitFromDds(const std::string& path) + { + Assets::Dds::DdsData data; + std::string err; + if (!Assets::Dds::Load(path, data, err)) { + ENGINE_ERROR("DDS: {} ({})", err, path); + return; + } + + m_Width = data.Width; + m_Height = data.Height; + m_MipLevels = data.MipLevels; + m_Format = ToVkFormat(data.Format); + + ENGINE_INFO("DDS loaded: {} ({}x{}, {} mips, {})", path, + m_Width, m_Height, m_MipLevels, vk::to_string(m_Format)); + + // 1. staging:各 mip 紧凑排布,记偏移 + vk::DeviceSize total = 0; + for (const auto& mip : data.Mips) { + total += mip.size(); + } + + auto stagingAlloc = m_Allocator.CreateBuffer( + total, + vk::BufferUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eHostVisible | + vk::MemoryPropertyFlagBits::eHostCoherent); + + std::vector offsets(m_MipLevels); + { + vk::DeviceSize off = 0; + for (u32 mip = 0; mip < m_MipLevels; ++mip) { + offsets[mip] = off; + std::memcpy(stagingAlloc.MappedPtr + off, + data.Mips[mip].data(), data.Mips[mip].size()); + off += data.Mips[mip].size(); + } + } + + // 2. device local image(带完整 mip 链) + vk::ImageCreateInfo imageInfo; + imageInfo.setImageType(vk::ImageType::e2D) + .setFormat(m_Format) + .setExtent({m_Width, m_Height, 1}) + .setMipLevels(m_MipLevels) + .setArrayLayers(1) + .setSamples(vk::SampleCountFlagBits::e1) + .setTiling(vk::ImageTiling::eOptimal) + .setUsage(vk::ImageUsageFlagBits::eTransferDst | + vk::ImageUsageFlagBits::eSampled) + .setSharingMode(vk::SharingMode::eExclusive) + .setInitialLayout(vk::ImageLayout::eUndefined); + + m_ImageAllocation = m_Allocator.CreateImage(imageInfo, + vk::MemoryPropertyFlagBits::eDeviceLocal); + m_Image = m_ImageAllocation.Image; + + // 3. view / sampler(覆盖整个 mip 链) + m_View = m_Device.GetDevice().createImageView( + vk::ImageViewCreateInfo{} + .setImage(m_Image) + .setViewType(vk::ImageViewType::e2D) + .setFormat(m_Format) + .setSubresourceRange({vk::ImageAspectFlagBits::eColor, + 0, m_MipLevels, 0, 1})); + + m_Sampler = m_Device.GetDevice().createSampler( + vk::SamplerCreateInfo{} + .setMagFilter(vk::Filter::eLinear) + .setMinFilter(vk::Filter::eLinear) + .setMipmapMode(vk::SamplerMipmapMode::eLinear) + .setAddressModeU(vk::SamplerAddressMode::eRepeat) + .setAddressModeV(vk::SamplerAddressMode::eRepeat) + .setAddressModeW(vk::SamplerAddressMode::eRepeat) + .setAnisotropyEnable(VK_FALSE) + .setBorderColor(vk::BorderColor::eIntOpaqueBlack) + .setUnnormalizedCoordinates(VK_FALSE) + .setCompareEnable(VK_FALSE) + .setMinLod(0.0f) + .setMaxLod(static_cast(m_MipLevels))); + + // 4. 一次性命令缓冲:整图 → TransferDst,逐 mip 拷贝 → ShaderRead + vk::CommandPoolCreateInfo poolInfo; + poolInfo.setQueueFamilyIndex(m_Device.GetGraphicsFamily()) + .setFlags(vk::CommandPoolCreateFlagBits::eTransient); + vk::CommandPool pool = m_Device.GetDevice().createCommandPool(poolInfo); + + vk::CommandBufferAllocateInfo allocInfo; + allocInfo.setCommandPool(pool) + .setLevel(vk::CommandBufferLevel::ePrimary) + .setCommandBufferCount(1); + vk::CommandBuffer cmd = + m_Device.GetDevice().allocateCommandBuffers(allocInfo)[0]; + + cmd.begin(vk::CommandBufferBeginInfo{ + vk::CommandBufferUsageFlagBits::eOneTimeSubmit}); + + vk::ImageMemoryBarrier2 toTransfer; + toTransfer.setSrcStageMask(vk::PipelineStageFlagBits2::eNone) + .setSrcAccessMask(vk::AccessFlagBits2::eNone) + .setDstStageMask(vk::PipelineStageFlagBits2::eTransfer) + .setDstAccessMask(vk::AccessFlagBits2::eTransferWrite) + .setOldLayout(vk::ImageLayout::eUndefined) + .setNewLayout(vk::ImageLayout::eTransferDstOptimal) + .setImage(m_Image) + .setSubresourceRange({vk::ImageAspectFlagBits::eColor, + 0, m_MipLevels, 0, 1}); + cmd.pipelineBarrier2(vk::DependencyInfo{}.setImageMemoryBarriers(toTransfer)); + + // BC 数据按块紧凑排布 → bufferRowLength/Height 留 0(紧凑) + for (u32 mip = 0; mip < m_MipLevels; ++mip) { + vk::BufferImageCopy region; + region.setBufferOffset(offsets[mip]) + .setBufferRowLength(0) + .setBufferImageHeight(0) + .setImageSubresource({vk::ImageAspectFlagBits::eColor, + mip, 0, 1}) + .setImageOffset({0, 0, 0}) + .setImageExtent({MipDim(m_Width, mip), + MipDim(m_Height, mip), 1}); + cmd.copyBufferToImage(stagingAlloc.Buffer, m_Image, + vk::ImageLayout::eTransferDstOptimal, region); + } + + vk::ImageMemoryBarrier2 toShader; + toShader.setSrcStageMask(vk::PipelineStageFlagBits2::eTransfer) + .setSrcAccessMask(vk::AccessFlagBits2::eTransferWrite) + .setDstStageMask(vk::PipelineStageFlagBits2::eFragmentShader) + .setDstAccessMask(vk::AccessFlagBits2::eShaderRead) + .setOldLayout(vk::ImageLayout::eTransferDstOptimal) + .setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal) + .setImage(m_Image) + .setSubresourceRange({vk::ImageAspectFlagBits::eColor, + 0, m_MipLevels, 0, 1}); + cmd.pipelineBarrier2(vk::DependencyInfo{}.setImageMemoryBarriers(toShader)); + + cmd.end(); + + // 5. 提交并等待,清理 staging + vk::Fence fence = m_Device.GetDevice().createFence({}); + m_Device.GetGraphicsQueue().submit( + vk::SubmitInfo{}.setCommandBuffers(cmd), fence); + if (m_Device.GetDevice().waitForFences(fence, VK_TRUE, UINT64_MAX) != + vk::Result::eSuccess) { + ENGINE_ERROR("DDS upload: waitForFences failed"); + } + + m_Device.GetDevice().destroyFence(fence); + m_Device.GetDevice().destroyCommandPool(pool); + m_Allocator.Destroy(stagingAlloc); + + m_TextureBytes = total; + STATS.AddTexture(m_TextureBytes); + } + Texture::~Texture() { const auto& vkDevice = m_Device.GetDevice();