Source: packages/compressed-textures/src/parsers/parseDDS.ts

import { CompressedTextureResource } from '../resources';
import { INTERNAL_FORMATS, INTERNAL_FORMAT_TO_BYTES_PER_PIXEL } from '../const';

const DDS_MAGIC_SIZE = 4;
const DDS_HEADER_SIZE = 124;
const DDS_HEADER_PF_SIZE = 32;
const DDS_HEADER_DX10_SIZE = 20;

// DDS file format magic word
const DDS_MAGIC = 0x20534444;

/**
 * DWORD offsets of the DDS file header fields (relative to file start).
 * @ignore
 */
const DDS_FIELDS = {
    SIZE: 1,
    FLAGS: 2,
    HEIGHT: 3,
    WIDTH: 4,
    MIPMAP_COUNT: 7,
    PIXEL_FORMAT: 19,
};

/**
 * DWORD offsets of the DDS PIXEL_FORMAT fields.
 * @ignore
 */
const DDS_PF_FIELDS = {
    SIZE: 0,
    FLAGS: 1,
    FOURCC: 2,
    RGB_BITCOUNT: 3,
    R_BIT_MASK: 4,
    G_BIT_MASK: 5,
    B_BIT_MASK: 6,
    A_BIT_MASK: 7
};

/**
 * DWORD offsets of the DDS_HEADER_DX10 fields.
 * @ignore
 */
const DDS_DX10_FIELDS = {
    DXGI_FORMAT: 0,
    RESOURCE_DIMENSION: 1,
    MISC_FLAG: 2,
    ARRAY_SIZE: 3,
    MISC_FLAGS2: 4
};

/**
 * @see https://docs.microsoft.com/en-us/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format
 * This is way over-blown for us! Lend us a hand, and remove the ones that aren't used (but set the remaining
 * ones to their correct value)
 * @ignore
 */
enum DXGI_FORMAT
    {
    DXGI_FORMAT_UNKNOWN,
    DXGI_FORMAT_R32G32B32A32_TYPELESS,
    DXGI_FORMAT_R32G32B32A32_FLOAT,
    DXGI_FORMAT_R32G32B32A32_UINT,
    DXGI_FORMAT_R32G32B32A32_SINT,
    DXGI_FORMAT_R32G32B32_TYPELESS,
    DXGI_FORMAT_R32G32B32_FLOAT,
    DXGI_FORMAT_R32G32B32_UINT,
    DXGI_FORMAT_R32G32B32_SINT,
    DXGI_FORMAT_R16G16B16A16_TYPELESS,
    DXGI_FORMAT_R16G16B16A16_FLOAT,
    DXGI_FORMAT_R16G16B16A16_UNORM,
    DXGI_FORMAT_R16G16B16A16_UINT,
    DXGI_FORMAT_R16G16B16A16_SNORM,
    DXGI_FORMAT_R16G16B16A16_SINT,
    DXGI_FORMAT_R32G32_TYPELESS,
    DXGI_FORMAT_R32G32_FLOAT,
    DXGI_FORMAT_R32G32_UINT,
    DXGI_FORMAT_R32G32_SINT,
    DXGI_FORMAT_R32G8X24_TYPELESS,
    DXGI_FORMAT_D32_FLOAT_S8X24_UINT,
    DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS,
    DXGI_FORMAT_X32_TYPELESS_G8X24_UINT,
    DXGI_FORMAT_R10G10B10A2_TYPELESS,
    DXGI_FORMAT_R10G10B10A2_UNORM,
    DXGI_FORMAT_R10G10B10A2_UINT,
    DXGI_FORMAT_R11G11B10_FLOAT,
    DXGI_FORMAT_R8G8B8A8_TYPELESS,
    DXGI_FORMAT_R8G8B8A8_UNORM,
    DXGI_FORMAT_R8G8B8A8_UNORM_SRGB,
    DXGI_FORMAT_R8G8B8A8_UINT,
    DXGI_FORMAT_R8G8B8A8_SNORM,
    DXGI_FORMAT_R8G8B8A8_SINT,
    DXGI_FORMAT_R16G16_TYPELESS,
    DXGI_FORMAT_R16G16_FLOAT,
    DXGI_FORMAT_R16G16_UNORM,
    DXGI_FORMAT_R16G16_UINT,
    DXGI_FORMAT_R16G16_SNORM,
    DXGI_FORMAT_R16G16_SINT,
    DXGI_FORMAT_R32_TYPELESS,
    DXGI_FORMAT_D32_FLOAT,
    DXGI_FORMAT_R32_FLOAT,
    DXGI_FORMAT_R32_UINT,
    DXGI_FORMAT_R32_SINT,
    DXGI_FORMAT_R24G8_TYPELESS,
    DXGI_FORMAT_D24_UNORM_S8_UINT,
    DXGI_FORMAT_R24_UNORM_X8_TYPELESS,
    DXGI_FORMAT_X24_TYPELESS_G8_UINT,
    DXGI_FORMAT_R8G8_TYPELESS,
    DXGI_FORMAT_R8G8_UNORM,
    DXGI_FORMAT_R8G8_UINT,
    DXGI_FORMAT_R8G8_SNORM,
    DXGI_FORMAT_R8G8_SINT,
    DXGI_FORMAT_R16_TYPELESS,
    DXGI_FORMAT_R16_FLOAT,
    DXGI_FORMAT_D16_UNORM,
    DXGI_FORMAT_R16_UNORM,
    DXGI_FORMAT_R16_UINT,
    DXGI_FORMAT_R16_SNORM,
    DXGI_FORMAT_R16_SINT,
    DXGI_FORMAT_R8_TYPELESS,
    DXGI_FORMAT_R8_UNORM,
    DXGI_FORMAT_R8_UINT,
    DXGI_FORMAT_R8_SNORM,
    DXGI_FORMAT_R8_SINT,
    DXGI_FORMAT_A8_UNORM,
    DXGI_FORMAT_R1_UNORM,
    DXGI_FORMAT_R9G9B9E5_SHAREDEXP,
    DXGI_FORMAT_R8G8_B8G8_UNORM,
    DXGI_FORMAT_G8R8_G8B8_UNORM,
    DXGI_FORMAT_BC1_TYPELESS,
    DXGI_FORMAT_BC1_UNORM,
    DXGI_FORMAT_BC1_UNORM_SRGB,
    DXGI_FORMAT_BC2_TYPELESS,
    DXGI_FORMAT_BC2_UNORM,
    DXGI_FORMAT_BC2_UNORM_SRGB,
    DXGI_FORMAT_BC3_TYPELESS,
    DXGI_FORMAT_BC3_UNORM,
    DXGI_FORMAT_BC3_UNORM_SRGB,
    DXGI_FORMAT_BC4_TYPELESS,
    DXGI_FORMAT_BC4_UNORM,
    DXGI_FORMAT_BC4_SNORM,
    DXGI_FORMAT_BC5_TYPELESS,
    DXGI_FORMAT_BC5_UNORM,
    DXGI_FORMAT_BC5_SNORM,
    DXGI_FORMAT_B5G6R5_UNORM,
    DXGI_FORMAT_B5G5R5A1_UNORM,
    DXGI_FORMAT_B8G8R8A8_UNORM,
    DXGI_FORMAT_B8G8R8X8_UNORM,
    DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM,
    DXGI_FORMAT_B8G8R8A8_TYPELESS,
    DXGI_FORMAT_B8G8R8A8_UNORM_SRGB,
    DXGI_FORMAT_B8G8R8X8_TYPELESS,
    DXGI_FORMAT_B8G8R8X8_UNORM_SRGB,
    DXGI_FORMAT_BC6H_TYPELESS,
    DXGI_FORMAT_BC6H_UF16,
    DXGI_FORMAT_BC6H_SF16,
    DXGI_FORMAT_BC7_TYPELESS,
    DXGI_FORMAT_BC7_UNORM,
    DXGI_FORMAT_BC7_UNORM_SRGB,
    DXGI_FORMAT_AYUV,
    DXGI_FORMAT_Y410,
    DXGI_FORMAT_Y416,
    DXGI_FORMAT_NV12,
    DXGI_FORMAT_P010,
    DXGI_FORMAT_P016,
    DXGI_FORMAT_420_OPAQUE,
    DXGI_FORMAT_YUY2,
    DXGI_FORMAT_Y210,
    DXGI_FORMAT_Y216,
    DXGI_FORMAT_NV11,
    DXGI_FORMAT_AI44,
    DXGI_FORMAT_IA44,
    DXGI_FORMAT_P8,
    DXGI_FORMAT_A8P8,
    DXGI_FORMAT_B4G4R4A4_UNORM,
    DXGI_FORMAT_P208,
    DXGI_FORMAT_V208,
    DXGI_FORMAT_V408,
    DXGI_FORMAT_SAMPLER_FEEDBACK_MIN_MIP_OPAQUE,
    DXGI_FORMAT_SAMPLER_FEEDBACK_MIP_REGION_USED_OPAQUE,
    DXGI_FORMAT_FORCE_UINT
}

/**
 * Possible values of the field DDS_DX10_FIELDS.RESOURCE_DIMENSION
 * @ignore
 */
enum D3D10_RESOURCE_DIMENSION
    {
    DDS_DIMENSION_TEXTURE1D = 2,
    DDS_DIMENSION_TEXTURE2D = 3,
    DDS_DIMENSION_TEXTURE3D = 6
}

const PF_FLAGS = 1;

// PIXEL_FORMAT flags
const DDPF_ALPHA = 0x2;
const DDPF_FOURCC = 0x4;
const DDPF_RGB = 0x40;
const DDPF_YUV = 0x200;
const DDPF_LUMINANCE = 0x20000;

// Four character codes for DXTn formats
const FOURCC_DXT1 = 0x31545844;
const FOURCC_DXT3 = 0x33545844;
const FOURCC_DXT5 = 0x35545844;
const FOURCC_DX10 = 0x30315844;

// Cubemap texture flag (for DDS_DX10_FIELDS.MISC_FLAG)
const DDS_RESOURCE_MISC_TEXTURECUBE = 0x4;

/**
 * Maps `FOURCC_*` formats to internal formats (see PIXI.INTERNAL_FORMATS).
 * @ignore
 */
const FOURCC_TO_FORMAT: { [id: number]: number } = {
    [FOURCC_DXT1]: INTERNAL_FORMATS.COMPRESSED_RGBA_S3TC_DXT1_EXT,
    [FOURCC_DXT3]: INTERNAL_FORMATS.COMPRESSED_RGBA_S3TC_DXT3_EXT,
    [FOURCC_DXT5]: INTERNAL_FORMATS.COMPRESSED_RGBA_S3TC_DXT5_EXT
};

/**
 * Maps DXGI_FORMAT to types/internal-formats (see PIXI.TYPES, PIXI.INTERNAL_FORMATS)
 * @ignore
 */
const DXGI_TO_FORMAT: { [id: number]: number } = {
    // WEBGL_compressed_texture_s3tc
    [DXGI_FORMAT.DXGI_FORMAT_BC1_TYPELESS]: INTERNAL_FORMATS.COMPRESSED_RGBA_S3TC_DXT1_EXT,
    [DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM]: INTERNAL_FORMATS.COMPRESSED_RGBA_S3TC_DXT1_EXT,
    [DXGI_FORMAT.DXGI_FORMAT_BC2_TYPELESS]: INTERNAL_FORMATS.COMPRESSED_RGBA_S3TC_DXT3_EXT,
    [DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM]: INTERNAL_FORMATS.COMPRESSED_RGBA_S3TC_DXT3_EXT,
    [DXGI_FORMAT.DXGI_FORMAT_BC3_TYPELESS]: INTERNAL_FORMATS.COMPRESSED_RGBA_S3TC_DXT5_EXT,
    [DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM]: INTERNAL_FORMATS.COMPRESSED_RGBA_S3TC_DXT5_EXT,

    // WEBGL_compressed_texture_s3tc_srgb
    [DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM_SRGB]: INTERNAL_FORMATS.COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT,
    [DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM_SRGB]: INTERNAL_FORMATS.COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT,
    [DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM_SRGB]: INTERNAL_FORMATS.COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT
};

/**
 * Parses the DDS file header, generates base-textures, and puts them into the texture cache.
 * @see https://docs.microsoft.com/en-us/windows/win32/direct3ddds/dx-graphics-dds-pguide
 * @param arrayBuffer
 * @memberof PIXI
 */
export function parseDDS(arrayBuffer: ArrayBuffer): CompressedTextureResource[]
{
    const data = new Uint32Array(arrayBuffer);
    const magicWord = data[0];

    if (magicWord !== DDS_MAGIC)
    {
        throw new Error('Invalid DDS file magic word');
    }

    const header = new Uint32Array(arrayBuffer, 0, DDS_HEADER_SIZE / Uint32Array.BYTES_PER_ELEMENT);

    // DDS header fields
    const height = header[DDS_FIELDS.HEIGHT];
    const width = header[DDS_FIELDS.WIDTH];
    const mipmapCount = header[DDS_FIELDS.MIPMAP_COUNT];

    // PIXEL_FORMAT fields
    const pixelFormat = new Uint32Array(
        arrayBuffer,
        DDS_FIELDS.PIXEL_FORMAT * Uint32Array.BYTES_PER_ELEMENT,
        DDS_HEADER_PF_SIZE / Uint32Array.BYTES_PER_ELEMENT);
    const formatFlags = pixelFormat[PF_FLAGS];

    // File contains compressed texture(s)
    if (formatFlags & DDPF_FOURCC)
    {
        const fourCC = pixelFormat[DDS_PF_FIELDS.FOURCC];

        // File contains one DXTn compressed texture
        if (fourCC !== FOURCC_DX10)
        {
            const internalFormat = FOURCC_TO_FORMAT[fourCC];

            const dataOffset = DDS_MAGIC_SIZE + DDS_HEADER_SIZE;
            const texData = new Uint8Array(arrayBuffer, dataOffset);

            const resource = new CompressedTextureResource(texData, {
                format: internalFormat,
                width,
                height,
                levels: mipmapCount // CompressedTextureResource will separate the levelBuffers for us!
            });

            return [resource];
        }

        // FOURCC_DX10 indicates there is a 20-byte DDS_HEADER_DX10 after DDS_HEADER
        const dx10Offset = DDS_MAGIC_SIZE + DDS_HEADER_SIZE;
        const dx10Header = new Uint32Array(
            data.buffer,
            dx10Offset,
            DDS_HEADER_DX10_SIZE / Uint32Array.BYTES_PER_ELEMENT);
        const dxgiFormat = dx10Header[DDS_DX10_FIELDS.DXGI_FORMAT];
        const resourceDimension = dx10Header[DDS_DX10_FIELDS.RESOURCE_DIMENSION];
        const miscFlag = dx10Header[DDS_DX10_FIELDS.MISC_FLAG];
        const arraySize = dx10Header[DDS_DX10_FIELDS.ARRAY_SIZE];

        // Map dxgiFormat to PIXI.INTERNAL_FORMATS
        const internalFormat = DXGI_TO_FORMAT[dxgiFormat];

        if (internalFormat === undefined)
        {
            throw new Error(`DDSParser cannot parse texture data with DXGI format ${dxgiFormat}`);
        }
        if (miscFlag === DDS_RESOURCE_MISC_TEXTURECUBE)
        {
            // FIXME: Anybody excited about cubemap compressed textures?
            throw new Error('DDSParser does not support cubemap textures');
        }
        if (resourceDimension === D3D10_RESOURCE_DIMENSION.DDS_DIMENSION_TEXTURE3D)
        {
            // FIXME: Anybody excited about 3D compressed textures?
            throw new Error('DDSParser does not supported 3D texture data');
        }

        // Uint8Array buffers of image data, including all mipmap levels in each image
        const imageBuffers = new Array<Uint8Array>();
        const dataOffset = DDS_MAGIC_SIZE
                + DDS_HEADER_SIZE
                + DDS_HEADER_DX10_SIZE;

        if (arraySize === 1)
        {
            // No need bothering with the imageSize calculation!
            imageBuffers.push(new Uint8Array(arrayBuffer, dataOffset));
        }
        else
        {
            // Calculate imageSize for each texture, and then locate each image's texture data

            const pixelSize = INTERNAL_FORMAT_TO_BYTES_PER_PIXEL[internalFormat];
            let imageSize = 0;
            let levelWidth = width;
            let levelHeight = height;

            for (let i = 0; i < mipmapCount; i++)
            {
                const alignedLevelWidth = Math.max(1, (levelWidth + 3) & ~3);
                const alignedLevelHeight = Math.max(1, (levelHeight + 3) & ~3);

                const levelSize = alignedLevelWidth * alignedLevelHeight * pixelSize;

                imageSize += levelSize;

                levelWidth = levelWidth >>> 1;
                levelHeight = levelHeight >>> 1;
            }

            let imageOffset = dataOffset;

            // NOTE: Cubemaps have 6-images per texture (but they aren't supported so ^_^)
            for (let i = 0; i < arraySize; i++)
            {
                imageBuffers.push(new Uint8Array(arrayBuffer, imageOffset, imageSize));
                imageOffset += imageSize;
            }
        }

        // Uint8Array -> CompressedTextureResource, and we're done!
        return imageBuffers.map((buffer) => new CompressedTextureResource(buffer, {
            format: internalFormat,
            width,
            height,
            levels: mipmapCount
        }));
    }
    if (formatFlags & DDPF_RGB)
    {
        // FIXME: We might want to allow uncompressed *.dds files?
        throw new Error('DDSParser does not support uncompressed texture data.');
    }
    if (formatFlags & DDPF_YUV)
    {
        // FIXME: Does anybody need this feature?
        throw new Error('DDSParser does not supported YUV uncompressed texture data.');
    }
    if (formatFlags & DDPF_LUMINANCE)
    {
        // FIXME: Microsoft says older DDS filers use this feature! Probably not worth the effort!
        throw new Error('DDSParser does not support single-channel (lumninance) texture data!');
    }
    if (formatFlags & DDPF_ALPHA)
    {
        // FIXME: I'm tired! See above =)
        throw new Error('DDSParser does not support single-channel (alpha) texture data!');
    }

    throw new Error('DDSParser failed to load a texture file due to an unknown reason!');
}