2026-04-06 23:04:01 -04:00
|
|
|
#include "dds_io.hpp"
|
|
|
|
|
|
2026-06-06 10:12:44 -04:00
|
|
|
#include <algorithm>
|
2026-04-06 23:04:01 -04:00
|
|
|
#include <cstring>
|
|
|
|
|
#include <filesystem>
|
|
|
|
|
#include <fstream>
|
2026-04-20 20:33:14 -06:00
|
|
|
|
|
|
|
|
#include "texture.hpp"
|
2026-04-06 23:04:01 -04:00
|
|
|
|
|
|
|
|
namespace aurora::gfx::dds {
|
|
|
|
|
struct ParsedDDSLayout {
|
|
|
|
|
wgpu::TextureFormat format = wgpu::TextureFormat::Undefined;
|
|
|
|
|
size_t dataOffset = 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct DDSPixelFormat {
|
2026-04-20 20:33:14 -06:00
|
|
|
uint32_t size;
|
|
|
|
|
uint32_t flags;
|
|
|
|
|
uint32_t fourCC;
|
|
|
|
|
uint32_t rgbBitCount;
|
|
|
|
|
uint32_t rBitMask;
|
|
|
|
|
uint32_t gBitMask;
|
|
|
|
|
uint32_t bBitMask;
|
|
|
|
|
uint32_t aBitMask;
|
2026-04-06 23:04:01 -04:00
|
|
|
};
|
|
|
|
|
static_assert(sizeof(DDSPixelFormat) == 32);
|
|
|
|
|
|
|
|
|
|
struct DDSHeader {
|
2026-04-20 20:33:14 -06:00
|
|
|
uint32_t size;
|
|
|
|
|
uint32_t flags;
|
|
|
|
|
uint32_t height;
|
|
|
|
|
uint32_t width;
|
|
|
|
|
uint32_t pitchOrLinearSize;
|
|
|
|
|
uint32_t depth;
|
|
|
|
|
uint32_t mipMapCount;
|
|
|
|
|
uint32_t reserved1[11];
|
2026-04-06 23:04:01 -04:00
|
|
|
DDSPixelFormat ddspf;
|
2026-04-20 20:33:14 -06:00
|
|
|
uint32_t caps;
|
|
|
|
|
uint32_t caps2;
|
|
|
|
|
uint32_t caps3;
|
|
|
|
|
uint32_t caps4;
|
|
|
|
|
uint32_t reserved2;
|
2026-04-06 23:04:01 -04:00
|
|
|
};
|
|
|
|
|
static_assert(sizeof(DDSHeader) == 124);
|
|
|
|
|
|
|
|
|
|
struct DDSHeaderDX10 {
|
2026-04-20 20:33:14 -06:00
|
|
|
uint32_t dxgiFormat;
|
|
|
|
|
uint32_t resourceDimension;
|
|
|
|
|
uint32_t miscFlag;
|
|
|
|
|
uint32_t arraySize;
|
|
|
|
|
uint32_t miscFlags2;
|
2026-04-06 23:04:01 -04:00
|
|
|
};
|
|
|
|
|
static_assert(sizeof(DDSHeaderDX10) == 20);
|
|
|
|
|
|
2026-04-20 20:33:14 -06:00
|
|
|
constexpr uint32_t kDDSMagic = 0x20534444; // "DDS"
|
2026-06-06 10:12:44 -04:00
|
|
|
constexpr uint32_t kDDSDMipmapCount = 0x00020000;
|
|
|
|
|
constexpr uint32_t kDDSCapsComplex = 0x00000008;
|
|
|
|
|
constexpr uint32_t kDDSCapsMipmap = 0x00400000;
|
|
|
|
|
constexpr uint32_t kDDSCaps2Cubemap = 0x00000200;
|
|
|
|
|
constexpr uint32_t kDDSCaps2Volume = 0x00200000;
|
2026-04-06 23:04:01 -04:00
|
|
|
|
|
|
|
|
bool ensure_directory(const std::filesystem::path& dir) noexcept {
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
std::filesystem::create_directories(dir, ec);
|
|
|
|
|
return !ec;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 20:33:14 -06:00
|
|
|
bool write_binary_file(const std::filesystem::path& path, ArrayRef<uint8_t> data) noexcept {
|
2026-04-06 23:04:01 -04:00
|
|
|
if (!ensure_directory(path.parent_path())) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
|
|
|
|
if (!out) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 20:33:14 -06:00
|
|
|
out.write(reinterpret_cast<const char*>(data.data()), data.size());
|
2026-04-06 23:04:01 -04:00
|
|
|
return static_cast<bool>(out);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 20:33:14 -06:00
|
|
|
std::optional<ByteBuffer> read_binary_file(const std::filesystem::path& path) noexcept {
|
2026-04-06 23:04:01 -04:00
|
|
|
std::ifstream file(path, std::ios::binary | std::ios::ate);
|
|
|
|
|
if (!file) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto size = static_cast<size_t>(file.tellg());
|
|
|
|
|
if (size == 0) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 20:33:14 -06:00
|
|
|
ByteBuffer bytes{size};
|
2026-04-06 23:04:01 -04:00
|
|
|
file.seekg(0, std::ios::beg);
|
|
|
|
|
file.read(reinterpret_cast<char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
|
|
|
|
|
if (!file) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return bytes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool validate_dds_header(const DDSHeader& header) noexcept {
|
|
|
|
|
if (header.size != sizeof(DDSHeader) || header.ddspf.size != sizeof(DDSPixelFormat)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (header.width == 0 || header.height == 0) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-06-06 10:12:44 -04:00
|
|
|
if ((header.caps2 & (kDDSCaps2Cubemap | kDDSCaps2Volume)) != 0) { // Unsupported
|
2026-04-06 23:04:01 -04:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-06 10:12:44 -04:00
|
|
|
uint32_t max_mip_count(uint32_t width, uint32_t height) noexcept {
|
|
|
|
|
uint32_t count = 1;
|
|
|
|
|
while (width > 1 || height > 1) {
|
|
|
|
|
width = std::max(width >> 1, 1u);
|
|
|
|
|
height = std::max(height >> 1, 1u);
|
|
|
|
|
++count;
|
|
|
|
|
}
|
|
|
|
|
return count;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::optional<uint32_t> resolve_mip_count(const DDSHeader& header) noexcept {
|
|
|
|
|
if (header.mipMapCount <= 1) {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const bool hasMipFlags = (header.flags & kDDSDMipmapCount) != 0 && (header.caps & kDDSCapsComplex) != 0 &&
|
|
|
|
|
(header.caps & kDDSCapsMipmap) != 0;
|
|
|
|
|
if (!hasMipFlags || header.mipMapCount > max_mip_count(header.width, header.height)) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return header.mipMapCount;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 20:33:14 -06:00
|
|
|
std::optional<wgpu::TextureFormat> resolve_dx10_format(uint32_t dxgiFormat) noexcept {
|
2026-04-06 23:04:01 -04:00
|
|
|
switch (dxgiFormat) {
|
|
|
|
|
case 28:
|
|
|
|
|
return wgpu::TextureFormat::RGBA8Unorm;
|
|
|
|
|
case 87:
|
|
|
|
|
return wgpu::TextureFormat::BGRA8Unorm;
|
|
|
|
|
case 71:
|
|
|
|
|
return wgpu::TextureFormat::BC1RGBAUnorm;
|
|
|
|
|
case 77:
|
|
|
|
|
return wgpu::TextureFormat::BC3RGBAUnorm;
|
|
|
|
|
case 83:
|
|
|
|
|
return wgpu::TextureFormat::BC5RGUnorm;
|
|
|
|
|
case 98:
|
|
|
|
|
return wgpu::TextureFormat::BC7RGBAUnorm;
|
2026-06-06 07:40:41 -06:00
|
|
|
case 134:
|
|
|
|
|
return wgpu::TextureFormat::ASTC4x4Unorm;
|
|
|
|
|
case 135:
|
|
|
|
|
return wgpu::TextureFormat::ASTC4x4UnormSrgb;
|
|
|
|
|
case 138:
|
|
|
|
|
return wgpu::TextureFormat::ASTC5x4Unorm;
|
|
|
|
|
case 139:
|
|
|
|
|
return wgpu::TextureFormat::ASTC5x4UnormSrgb;
|
|
|
|
|
case 142:
|
|
|
|
|
return wgpu::TextureFormat::ASTC5x5Unorm;
|
|
|
|
|
case 143:
|
|
|
|
|
return wgpu::TextureFormat::ASTC5x5UnormSrgb;
|
|
|
|
|
case 146:
|
|
|
|
|
return wgpu::TextureFormat::ASTC6x5Unorm;
|
|
|
|
|
case 147:
|
|
|
|
|
return wgpu::TextureFormat::ASTC6x5UnormSrgb;
|
|
|
|
|
case 150:
|
|
|
|
|
return wgpu::TextureFormat::ASTC6x6Unorm;
|
|
|
|
|
case 151:
|
|
|
|
|
return wgpu::TextureFormat::ASTC6x6UnormSrgb;
|
|
|
|
|
case 154:
|
|
|
|
|
return wgpu::TextureFormat::ASTC8x5Unorm;
|
|
|
|
|
case 155:
|
|
|
|
|
return wgpu::TextureFormat::ASTC8x5UnormSrgb;
|
|
|
|
|
case 158:
|
|
|
|
|
return wgpu::TextureFormat::ASTC8x6Unorm;
|
|
|
|
|
case 159:
|
|
|
|
|
return wgpu::TextureFormat::ASTC8x6UnormSrgb;
|
|
|
|
|
case 162:
|
|
|
|
|
return wgpu::TextureFormat::ASTC8x8Unorm;
|
|
|
|
|
case 163:
|
|
|
|
|
return wgpu::TextureFormat::ASTC8x8UnormSrgb;
|
|
|
|
|
case 166:
|
|
|
|
|
return wgpu::TextureFormat::ASTC10x5Unorm;
|
|
|
|
|
case 167:
|
|
|
|
|
return wgpu::TextureFormat::ASTC10x5UnormSrgb;
|
|
|
|
|
case 170:
|
|
|
|
|
return wgpu::TextureFormat::ASTC10x6Unorm;
|
|
|
|
|
case 171:
|
|
|
|
|
return wgpu::TextureFormat::ASTC10x6UnormSrgb;
|
|
|
|
|
case 174:
|
|
|
|
|
return wgpu::TextureFormat::ASTC10x8Unorm;
|
|
|
|
|
case 175:
|
|
|
|
|
return wgpu::TextureFormat::ASTC10x8UnormSrgb;
|
|
|
|
|
case 178:
|
|
|
|
|
return wgpu::TextureFormat::ASTC10x10Unorm;
|
|
|
|
|
case 179:
|
|
|
|
|
return wgpu::TextureFormat::ASTC10x10UnormSrgb;
|
|
|
|
|
case 182:
|
|
|
|
|
return wgpu::TextureFormat::ASTC12x10Unorm;
|
|
|
|
|
case 183:
|
|
|
|
|
return wgpu::TextureFormat::ASTC12x10UnormSrgb;
|
|
|
|
|
case 186:
|
|
|
|
|
return wgpu::TextureFormat::ASTC12x12Unorm;
|
|
|
|
|
case 187:
|
|
|
|
|
return wgpu::TextureFormat::ASTC12x12UnormSrgb;
|
2026-04-06 23:04:01 -04:00
|
|
|
default:
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 20:33:14 -06:00
|
|
|
std::optional<ParsedDDSLayout> resolve_dds_layout(ArrayRef<uint8_t> bytes, const DDSHeader& header) noexcept {
|
|
|
|
|
ParsedDDSLayout out{.dataOffset = sizeof(uint32_t) + sizeof(DDSHeader)};
|
2026-04-06 23:04:01 -04:00
|
|
|
|
|
|
|
|
if ((header.ddspf.flags & 0x00000004) != 0) { // DDS has FourCC
|
|
|
|
|
switch (header.ddspf.fourCC) {
|
|
|
|
|
case 0x31545844:
|
|
|
|
|
out.format = wgpu::TextureFormat::BC1RGBAUnorm;
|
|
|
|
|
return out;
|
|
|
|
|
case 0x35545844:
|
|
|
|
|
out.format = wgpu::TextureFormat::BC3RGBAUnorm;
|
|
|
|
|
return out;
|
|
|
|
|
case 0x32495441:
|
|
|
|
|
out.format = wgpu::TextureFormat::BC5RGUnorm;
|
|
|
|
|
return out;
|
|
|
|
|
case 0x30315844: {
|
|
|
|
|
if (bytes.size() < out.dataOffset + sizeof(DDSHeaderDX10)) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
const auto* dx10 = reinterpret_cast<const DDSHeaderDX10*>(bytes.data() + out.dataOffset);
|
|
|
|
|
out.dataOffset += sizeof(DDSHeaderDX10);
|
|
|
|
|
if (dx10->resourceDimension != 3 || dx10->arraySize != 1) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
const auto format = resolve_dx10_format(dx10->dxgiFormat);
|
|
|
|
|
if (!format.has_value()) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
out.format = *format;
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ((header.ddspf.flags & 0x00000040) != 0 && header.ddspf.rgbBitCount == 32) {
|
2026-04-20 20:33:14 -06:00
|
|
|
if (header.ddspf.rBitMask == 0x000000FF && header.ddspf.gBitMask == 0x0000FF00 &&
|
|
|
|
|
header.ddspf.bBitMask == 0x00FF0000 && header.ddspf.aBitMask == 0xFF000000) {
|
2026-04-06 23:04:01 -04:00
|
|
|
out.format = wgpu::TextureFormat::RGBA8Unorm;
|
|
|
|
|
return out;
|
|
|
|
|
}
|
2026-04-20 20:33:14 -06:00
|
|
|
if (header.ddspf.rBitMask == 0x00FF0000 && header.ddspf.gBitMask == 0x0000FF00 &&
|
|
|
|
|
header.ddspf.bBitMask == 0x000000FF && header.ddspf.aBitMask == 0xFF000000) {
|
2026-04-06 23:04:01 -04:00
|
|
|
out.format = wgpu::TextureFormat::BGRA8Unorm;
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 20:33:14 -06:00
|
|
|
std::optional<ConvertedTexture> parse_dds_bytes(ArrayRef<uint8_t> bytes) noexcept {
|
|
|
|
|
if (bytes.size() < sizeof(uint32_t) + sizeof(DDSHeader)) {
|
2026-04-06 23:04:01 -04:00
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 20:33:14 -06:00
|
|
|
const uint32_t magic = *reinterpret_cast<const uint32_t*>(bytes.data());
|
2026-04-06 23:04:01 -04:00
|
|
|
if (magic != kDDSMagic) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 20:33:14 -06:00
|
|
|
const auto* header = reinterpret_cast<const DDSHeader*>(bytes.data() + sizeof(uint32_t));
|
2026-04-06 23:04:01 -04:00
|
|
|
if (!validate_dds_header(*header)) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto parsedLayout = resolve_dds_layout(bytes, *header);
|
|
|
|
|
if (!parsedLayout.has_value()) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-06 10:12:44 -04:00
|
|
|
const auto mipCount = resolve_mip_count(*header);
|
|
|
|
|
if (!mipCount.has_value()) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto expectedSize = calc_texture_size(parsedLayout->format, header->width, header->height, *mipCount);
|
2026-04-20 20:33:14 -06:00
|
|
|
if (expectedSize == 0 || parsedLayout->dataOffset + expectedSize > bytes.size()) {
|
2026-04-06 23:04:01 -04:00
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 20:33:14 -06:00
|
|
|
ByteBuffer data{expectedSize};
|
|
|
|
|
std::memcpy(data.data(), bytes.data() + parsedLayout->dataOffset, expectedSize);
|
|
|
|
|
return ConvertedTexture{
|
|
|
|
|
.format = parsedLayout->format,
|
|
|
|
|
.width = header->width,
|
|
|
|
|
.height = header->height,
|
2026-06-06 10:12:44 -04:00
|
|
|
.mips = *mipCount,
|
2026-04-20 20:33:14 -06:00
|
|
|
.data = std::move(data),
|
|
|
|
|
};
|
2026-04-06 23:04:01 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 20:33:14 -06:00
|
|
|
std::optional<ConvertedTexture> load_dds_file(const std::filesystem::path& path) noexcept {
|
2026-04-06 23:04:01 -04:00
|
|
|
const auto bytes = read_binary_file(path);
|
|
|
|
|
if (!bytes.has_value()) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
return parse_dds_bytes(*bytes);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 20:33:14 -06:00
|
|
|
ByteBuffer encode_rgba8_dds(uint32_t width, uint32_t height, ArrayRef<uint8_t> pixels) {
|
2026-04-06 23:04:01 -04:00
|
|
|
DDSHeader header{};
|
|
|
|
|
header.size = sizeof(DDSHeader);
|
|
|
|
|
header.flags = 0x1 | 0x2 | 0x4 | 0x1000 | 0x8;
|
|
|
|
|
header.height = height;
|
|
|
|
|
header.width = width;
|
|
|
|
|
header.pitchOrLinearSize = width * 4;
|
|
|
|
|
header.mipMapCount = 1;
|
|
|
|
|
header.ddspf = {
|
|
|
|
|
.size = sizeof(DDSPixelFormat),
|
|
|
|
|
.flags = 0x00000040 | 0x00000001,
|
|
|
|
|
.fourCC = 0,
|
|
|
|
|
.rgbBitCount = 32,
|
|
|
|
|
.rBitMask = 0x000000FF,
|
|
|
|
|
.gBitMask = 0x0000FF00,
|
|
|
|
|
.bBitMask = 0x00FF0000,
|
|
|
|
|
.aBitMask = 0xFF000000,
|
|
|
|
|
};
|
|
|
|
|
header.caps = 0x00001000;
|
|
|
|
|
|
2026-04-20 20:33:14 -06:00
|
|
|
ByteBuffer bytes{sizeof(uint32_t) + sizeof(DDSHeader) + pixels.size()};
|
2026-04-06 23:04:01 -04:00
|
|
|
std::memcpy(bytes.data(), &kDDSMagic, sizeof(kDDSMagic));
|
2026-04-20 20:33:14 -06:00
|
|
|
std::memcpy(bytes.data() + sizeof(uint32_t), &header, sizeof(header));
|
|
|
|
|
std::memcpy(bytes.data() + sizeof(uint32_t) + sizeof(DDSHeader), pixels.data(), pixels.size());
|
2026-04-06 23:04:01 -04:00
|
|
|
return bytes;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 20:33:14 -06:00
|
|
|
bool write_rgba8_dds(const std::filesystem::path& path, uint32_t width, uint32_t height,
|
|
|
|
|
ArrayRef<uint8_t> pixels) noexcept {
|
|
|
|
|
return write_binary_file(path, encode_rgba8_dds(width, height, pixels));
|
2026-04-06 23:04:01 -04:00
|
|
|
}
|
|
|
|
|
} // namespace aurora::gfx::dds
|