DirectX Computational Graphics Tutorials

Mastering the Power of Graphics Programming

Loading DDS Textures

DirectDraw Surface (.dds) files are a popular format for storing texture data in real-time graphics applications, especially games. They offer several advantages, including support for various compression formats, mipmaps, and cubemaps, all within a single file. This tutorial will guide you through the process of loading DDS textures using DirectX.

Understanding the DDS File Format

A DDS file is structured into two main parts:

The most critical piece of information in the header for loading is the dwMagic value (which should be 'DDS ') and the ddspf structure, which describes the pixel format. Understanding these formats, particularly compressed formats like DXT1, DXT3, and DXT5, is crucial for efficient texture loading.

Steps for Loading a DDS Texture

  1. Open the DDS file: Use standard file I/O operations to open the DDS file in binary read mode.
  2. Read the DDS Header: Read the first 128 bytes of the file into a DDS_HEADER structure. Verify that the dwMagic member is equal to 'DDS '.
  3. Check for DDS_MAGIC: The magic number 'DDS ' (0x20534444 in little-endian) must be present at the beginning of the file.
  4. Determine Pixel Format: Examine the ddspf member of the header. This will tell you if the texture is uncompressed (e.g., RGBA8) or compressed (e.g., DXT1).
  5. Calculate Mipmap Information: If the DDSD_MIPMAPCOUNT flag is set in dwFlags, the texture contains multiple mipmap levels. You'll need to determine the size of each mipmap level based on its dimensions. The formula for calculating the size of a mipmap level is typically:
    size = max(1, width) * max(1, height) * bytes_per_pixel
    For compressed formats, bytes_per_pixel is usually fixed (e.g., 8 bytes for DXT1, 16 bytes for DXT5, per 4x4 block).
  6. Allocate GPU Memory: Create a DirectX texture resource (e.g., ID3D11Texture2D) on the GPU with the appropriate dimensions, format, and mipmap count.
  7. Upload Pixel Data: Read the pixel data from the file and upload it to the GPU resource. For multiple mipmap levels, you'll iterate through each level, read its data, and use UpdateSubresource or similar functions.
  8. Create Shader Resource View: Create an ID3D11ShaderResourceView for the texture resource, allowing it to be used in shaders.

Example Code Snippets (Conceptual C++)

This is a simplified illustration and assumes you have a DDS_HEADER structure defined and are using the DirectX 11 API.


#include <d3d11.h>
#include <fstream>
#include <vector>
#include <string>
#include <algorithm> // For std::max

// Simplified DDS_PIXELFORMAT structure (actual is more complex)
struct DDS_PIXELFORMAT {
    uint32_t dwSize;
    uint32_t dwFlags;
    uint32_t dwFourCC;
    uint32_t dwRGBBitCount;
    uint32_t dwRBitMask, dwGBitMask, dwBBitMask, dwAlphaBitMask;
};

// Simplified DDS_HEADER structure (actual is more complex)
struct DDS_HEADER {
    uint32_t dwMagic;
    uint32_t dwSize;
    uint32_t dwFlags;
    uint32_t dwHeight;
    uint32_t dwWidth;
    uint32_t dwPitchOrLinearSize;
    uint32_t dwDepth;
    uint32_t dwMipMapCount;
    uint32_t dwReserved1[11];
    DDS_PIXELFORMAT ddspf;
    uint32_t dwCaps;
    uint32_t dwCaps2;
    uint32_t dwCaps3;
    uint32_t dwCaps4;
    uint32_t dwReserved2;
};

// Function to get bytes per pixel based on format
uint32_t GetBytesPerPixel(DXGI_FORMAT format) {
    switch (format) {
        case DXGI_FORMAT_R8G8B8A8_UNORM: return 4;
        case DXGI_FORMAT_BC1_UNORM:      return 0.5f; // DXT1 is 4 bits per pixel
        case DXGI_FORMAT_BC3_UNORM:      return 1.0f; // DXT5 is 8 bits per pixel
        // Add other formats as needed
        default: return 0;
    }
}

// Function to convert DDS format to DXGI_FORMAT
DXGI_FORMAT ConvertDDSToDXGIFormat(const DDS_PIXELFORMAT& ddsf) {
    if (ddsf.dwFlags & 0x4) { // DDPF_FOURCC
        if (ddsf.dwFourCC == 0x31545844) return DXGI_FORMAT_BC1_UNORM; // DXT1
        if (ddsf.dwFourCC == 0x33545844) return DXGI_FORMAT_BC3_UNORM; // DXT5
        if (ddsf.dwFourCC == 0x32545844) return DXGI_FORMAT_BC2_UNORM; // DXT3
        // Add other FourCC conversions
    } else if (ddsf.dwFlags & 0x40) { // DDPF_RGB
        if (ddsf.dwRGBBitCount == 32 && ddsf.dwRBitMask == 0x00FF0000 && ddsf.dwGBitMask == 0x0000FF00 && ddsf.dwBBitMask == 0x000000FF && ddsf.dwAlphaBitMask == 0xFF000000) return DXGI_FORMAT_R8G8B8A8_UNORM;
        // Add other RGB conversions
    }
    return DXGI_FORMAT_UNKNOWN;
}

HRESULT LoadDDS(const std::string& filename, ID3D11Device* pDevice, ID3D11ShaderResourceView** ppSRV) {
    std::ifstream file(filename, std::ios::binary);
    if (!file.is_open()) {
        return E_FAIL; // File not found
    }

    DDS_HEADER header;
    file.read(reinterpret_cast(&header), sizeof(DDS_HEADER));

    // Basic validation
    if (header.dwMagic != 0x20534444) { // 'DDS '
        return E_FAIL; // Not a DDS file
    }

    if (header.dwSize != sizeof(DDS_HEADER)) {
        // Handle older DDS_HEADER versions if necessary, or return error
        return E_FAIL;
    }

    DXGI_FORMAT format = ConvertDDSToDXGIFormat(header.ddspf);
    if (format == DXGI_FORMAT_UNKNOWN) {
        return E_FAIL; // Unsupported format
    }

    D3D11_TEXTURE2D_DESC texDesc = {};
    texDesc.Width = header.dwWidth;
    texDesc.Height = header.dwHeight;
    texDesc.MipLevels = (header.dwFlags & 0x1) ? header.dwMipMapCount : 1; // DDSD_MIPMAPCOUNT
    texDesc.ArraySize = 1; // For simplicity, assuming single texture
    texDesc.Format = format;
    texDesc.Usage = D3D11_USAGE_DEFAULT;
    texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
    texDesc.CPUAccessFlags = 0;
    texDesc.MiscFlags = 0;
    texDesc.SampleDesc.Count = 1;
    texDesc.SampleDesc.Quality = 0;

    // Adjust for compressed formats: size is calculated per 4x4 block
    uint32_t blockSize = (format == DXGI_FORMAT_BC1_UNORM || format == DXGI_FORMAT_BC3_UNORM || format == DXGI_FORMAT_BC2_UNORM) ? 16 : 0; // 16 bytes per 4x4 block for DXT
    uint32_t bytesPerPixel = (blockSize > 0) ? 0 : GetBytesPerPixel(format); // 0 for compressed

    std::vector<uint8_t> textureData;
    uint32_t currentOffset = sizeof(DDS_HEADER);
    uint32_t width = header.dwWidth;
    uint32_t height = header.dwHeight;

    for (uint32_t i = 0; i < texDesc.MipLevels; ++i) {
        uint32_t mipWidth = std::max(1u, width >> i);
        uint32_t mipHeight = std::max(1u, height >> i);

        uint32_t subresourceSize;
        if (blockSize > 0) {
            // Compressed format calculation
            subresourceSize = ((mipWidth + 3) / 4) * ((mipHeight + 3) / 4) * blockSize;
        } else {
            // Uncompressed format calculation
            subresourceSize = mipWidth * mipHeight * bytesPerPixel;
        }

        textureData.resize(textureData.size() + subresourceSize);
        file.seekg(currentOffset);
        file.read(reinterpret_cast(&textureData[currentOffset - sizeof(DDS_HEADER)]), subresourceSize);
        currentOffset += subresourceSize;
    }

    ID3D11Texture2D* pTexture = nullptr;
    HRESULT hr = pDevice->CreateTexture2D(&texDesc, nullptr, &pTexture);
    if (FAILED(hr)) {
        return hr;
    }

    // Upload data to the texture
    // For multiple mipmaps, you'd need to iterate and use UpdateSubresource for each level
    // This simplified example assumes data is packed contiguously for the first mipmap if texDesc.MipLevels == 1
    D3D11_SUBRESOURCE_DATA srd;
    srd.pSysMem = textureData.data();
    srd.SysMemPitch = (blockSize > 0) ? 0 : (width * bytesPerPixel); // Pitch is tricky for compressed
    srd.SysMemSlicePitch = (blockSize > 0) ? 0 : (width * height * bytesPerPixel);

    // Recreate texture with data
    pDevice->CreateTexture2D(&texDesc, &srd, &pTexture);

    // If texture has mipmaps, we need to update each subresource
    if (texDesc.MipLevels > 1) {
        uint32_t mipDataOffset = 0;
        for (uint32_t i = 0; i < texDesc.MipLevels; ++i) {
            uint32_t mipWidth = std::max(1u, header.dwWidth >> i);
            uint32_t mipHeight = std::max(1u, header.dwHeight >> i);
            uint32_t subresourceSize;
            if (blockSize > 0) {
                 subresourceSize = ((mipWidth + 3) / 4) * ((mipHeight + 3) / 4) * blockSize;
            } else {
                 subresourceSize = mipWidth * mipHeight * bytesPerPixel;
            }

            D3D11_SUBRESOURCE_DATA srd_mip;
            srd_mip.pSysMem = textureData.data() + mipDataOffset;
            srd_mip.SysMemPitch = (blockSize > 0) ? 0 : (mipWidth * bytesPerPixel);
            srd_mip.SysMemSlicePitch = (blockSize > 0) ? 0 : (mipWidth * mipHeight * bytesPerPixel);

            pDevice->UpdateSubresource(pTexture, i, nullptr, srd_mip.pSysMem, srd_mip.SysMemPitch, srd_mip.SysMemSlicePitch);
            mipDataOffset += subresourceSize;
        }
    } else {
        // For single level texture
        pDevice->UpdateSubresource(pTexture, 0, nullptr, textureData.data(), srd.SysMemPitch, srd.SysMemSlicePitch);
    }


    D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
    srvDesc.Format = texDesc.Format;
    srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
    srvDesc.Texture2D.MostDetailedMip = 0;
    srvDesc.Texture2D.MipLevels = texDesc.MipLevels;

    hr = pDevice->CreateShaderResourceView(pTexture, &srvDesc, ppSRV);

    pTexture->Release(); // Release the texture, we only need the SRV

    return hr;
}
            

Important Considerations:

Loading DDS textures efficiently is a key skill for any DirectX developer working with 3D assets. By understanding the file format and the DirectX API, you can integrate high-quality textures into your applications.