import { CompressedTextureResource } from '../resources';
import { INTERNAL_FORMATS, INTERNAL_FORMAT_TO_BYTES_PER_PIXEL } from '../const';
import { LoaderResource } from '@pixi/loaders';
import { registerCompressedTextures } from './registerCompressedTextures';
import type { ILoaderResource } from '@pixi/loaders';
// Set DDS files to be loaded as an ArrayBuffer
LoaderResource.setExtensionXhrType('dds', LoaderResource.XHR_RESPONSE_TYPE.BUFFER);
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
* @ignore
*/
// 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)
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
};
/**
* @class
* @memberof PIXI
* @implements PIXI.ILoaderPlugin
* @see https://docs.microsoft.com/en-us/windows/win32/direct3ddds/dx-graphics-dds-pguide
*/
export class DDSLoader
{
public static use(resource: ILoaderResource, next: (...args: any[]) => void): void
{
if (resource.extension === 'dds' && resource.data)
{
registerCompressedTextures(resource.name || resource.url, DDSLoader.parse(resource.data));
}
next();
}
/**
* Parses the DDS file header, generates base-textures, and puts them into the texture
* cache.
*/
private static parse(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(`DDSLoader cannot parse texture data with DXGI format ${dxgiFormat}`);
}
if (miscFlag === DDS_RESOURCE_MISC_TEXTURECUBE)
{
// FIXME: Anybody excited about cubemap compressed textures?
throw new Error('DDSLoader does not support cubemap textures');
}
if (resourceDimension === D3D10_RESOURCE_DIMENSION.DDS_DIMENSION_TEXTURE3D)
{
// FIXME: Anybody excited about 3D compressed textures?
throw new Error('DDSLoader 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('DDSLoader does not support uncompressed texture data.');
}
if (formatFlags & DDPF_YUV)
{
// FIXME: Does anybody need this feature?
throw new Error('DDSLoader 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('DDSLoader does not support single-channel (lumninance) texture data!');
}
if (formatFlags & DDPF_ALPHA)
{
// FIXME: I'm tired! See above =)
throw new Error('DDSLoader does not support single-channel (alpha) texture data!');
}
throw new Error('DDSLoader failed to load a texture file due to an unknown reason!');
}
}