lovr/src/modules/data/image.c

1207 lines
43 KiB
C

#include "data/image.h"
#include "data/blob.h"
#include "util.h"
#include "lib/stb/stb_image.h"
#include <stdlib.h>
#include <string.h>
enum {
IMAGE_SRGB = (1 << 0),
IMAGE_PREMULTIPLIED = (1 << 1),
IMAGE_CUBEMAP = (1 << 2)
};
typedef struct {
void* data;
size_t size;
size_t stride;
} Mipmap;
struct Image {
uint32_t ref;
uint32_t flags;
uint32_t width;
uint32_t height;
uint32_t format;
uint32_t layers;
uint32_t levels;
struct Blob* blob;
Mipmap mipmaps[1];
};
static size_t measure(uint32_t w, uint32_t h, TextureFormat format) {
switch (format) {
case FORMAT_R8: return w * h * 1;
case FORMAT_RG8: return w * h * 2;
case FORMAT_RGBA8: return w * h * 4;
case FORMAT_R16: return w * h * 2;
case FORMAT_RG16: return w * h * 4;
case FORMAT_RGBA16: return w * h * 8;
case FORMAT_R16F: return w * h * 2;
case FORMAT_RG16F: return w * h * 4;
case FORMAT_RGBA16F: return w * h * 8;
case FORMAT_R32F: return w * h * 4;
case FORMAT_RG32F: return w * h * 8;
case FORMAT_RGBA32F: return w * h * 16;
case FORMAT_RGB565: return w * h * 2;
case FORMAT_RGB5A1: return w * h * 2;
case FORMAT_RGB10A2: return w * h * 4;
case FORMAT_RG11B10F: return w * h * 4;
case FORMAT_D16: return w * h * 2;
case FORMAT_D24: return w * h * 4;
case FORMAT_D32F: return w * h * 4;
case FORMAT_D24S8: return w * h * 4;
case FORMAT_D32FS8: return w * h * 5;
case FORMAT_BC1: return ((w + 3) / 4) * ((h + 3) / 4) * 8;
case FORMAT_BC2: return ((w + 3) / 4) * ((h + 3) / 4) * 16;
case FORMAT_BC3: return ((w + 3) / 4) * ((h + 3) / 4) * 16;
case FORMAT_BC4U: return ((w + 3) / 4) * ((h + 3) / 4) * 8;
case FORMAT_BC4S: return ((w + 3) / 4) * ((h + 3) / 4) * 8;
case FORMAT_BC5U: return ((w + 3) / 4) * ((h + 3) / 4) * 16;
case FORMAT_BC5S: return ((w + 3) / 4) * ((h + 3) / 4) * 16;
case FORMAT_BC6UF: return ((w + 3) / 4) * ((h + 3) / 4) * 16;
case FORMAT_BC6SF: return ((w + 3) / 4) * ((h + 3) / 4) * 16;
case FORMAT_BC7: return ((w + 3) / 4) * ((h + 3) / 4) * 16;
case FORMAT_ASTC_4x4: return ((w + 3) / 4) * ((h + 3) / 4) * 16;
case FORMAT_ASTC_5x4: return ((w + 4) / 5) * ((h + 3) / 4) * 16;
case FORMAT_ASTC_5x5: return ((w + 4) / 5) * ((h + 4) / 5) * 16;
case FORMAT_ASTC_6x5: return ((w + 5) / 6) * ((h + 4) / 5) * 16;
case FORMAT_ASTC_6x6: return ((w + 5) / 6) * ((h + 5) / 6) * 16;
case FORMAT_ASTC_8x5: return ((w + 7) / 8) * ((h + 4) / 5) * 16;
case FORMAT_ASTC_8x6: return ((w + 7) / 8) * ((h + 5) / 6) * 16;
case FORMAT_ASTC_8x8: return ((w + 7) / 8) * ((h + 7) / 8) * 16;
case FORMAT_ASTC_10x5: return ((w + 9) / 10) * ((h + 4) / 5) * 16;
case FORMAT_ASTC_10x6: return ((w + 9) / 10) * ((h + 5) / 6) * 16;
case FORMAT_ASTC_10x8: return ((w + 9) / 10) * ((h + 7) / 8) * 16;
case FORMAT_ASTC_10x10: return ((w + 9) / 10) * ((h + 9) / 10) * 16;
case FORMAT_ASTC_12x10: return ((w + 11) / 12) * ((h + 9) / 10) * 16;
case FORMAT_ASTC_12x12: return ((w + 11) / 12) * ((h + 11) / 12) * 16;
default: lovrUnreachable();
}
}
Image* lovrImageCreateRaw(uint32_t width, uint32_t height, TextureFormat format, bool srgb) {
lovrCheck(width > 0 && height > 0, "Image dimensions must be positive");
lovrCheck(format < FORMAT_BC1, "Blank images cannot be compressed");
size_t size = measure(width, height, format);
void* data = lovrMalloc(size);
Image* image = lovrCalloc(sizeof(Image));
image->ref = 1;
image->flags = srgb ? IMAGE_SRGB : 0;
image->width = width;
image->height = height;
image->format = format;
image->layers = 1;
image->levels = 1;
image->blob = lovrBlobCreate(data, size, "Image");
image->mipmaps[0] = (Mipmap) { data, size, 0 };
return image;
}
static Image* loadDDS(Blob* blob);
static Image* loadASTC(Blob* blob);
static Image* loadKTX1(Blob* blob);
static Image* loadKTX2(Blob* blob);
static Image* loadSTB(Blob* blob);
Image* lovrImageCreateFromFile(Blob* blob) {
Image* image = NULL;
if ((image = loadDDS(blob)) != NULL) return image;
if ((image = loadASTC(blob)) != NULL) return image;
if ((image = loadKTX1(blob)) != NULL) return image;
if ((image = loadKTX2(blob)) != NULL) return image;
if ((image = loadSTB(blob)) != NULL) return image;
lovrThrow("Could not load image from '%s': Image file format not recognized", blob->name);
return NULL;
}
void lovrImageDestroy(void* ref) {
Image* image = ref;
lovrRelease(image->blob, lovrBlobDestroy);
lovrFree(image);
}
bool lovrImageIsSRGB(Image* image) {
return image->flags & IMAGE_SRGB;
}
bool lovrImageIsPremultiplied(Image* image) {
return image->flags & IMAGE_PREMULTIPLIED;
}
bool lovrImageIsCube(Image* image) {
return image->flags & IMAGE_CUBEMAP;
}
bool lovrImageIsDepth(Image* image) {
switch (image->format) {
case FORMAT_D16:
case FORMAT_D24:
case FORMAT_D32F:
case FORMAT_D24S8:
case FORMAT_D32FS8:
return true;
default:
return false;
}
}
bool lovrImageIsCompressed(Image* image) {
return image->format >= FORMAT_BC1;
}
Blob* lovrImageGetBlob(Image* image) {
return image->blob;
}
uint32_t lovrImageGetWidth(Image* image, uint32_t level) {
return MAX(image->width >> level, 1);
}
uint32_t lovrImageGetHeight(Image* image, uint32_t level) {
return MAX(image->height >> level, 1);
}
uint32_t lovrImageGetLayerCount(Image* image) {
return image->layers;
}
uint32_t lovrImageGetLevelCount(Image* image) {
return image->levels;
}
TextureFormat lovrImageGetFormat(Image* image) {
return image->format;
}
size_t lovrImageGetLayerSize(Image* image, uint32_t level) {
if (level >= image->levels) return 0;
return image->mipmaps[level].size;
}
void* lovrImageGetLayerData(Image* image, uint32_t level, uint32_t layer) {
if (layer >= image->layers || level >= image->levels) return NULL;
return (uint8_t*) image->mipmaps[level].data + layer * image->mipmaps[level].stride;
}
typedef union { void* raw; uint8_t* u8; uint16_t* u16; float* f32; } ImagePointer;
static void getPixelR8(ImagePointer src, float* dst) { for (uint32_t i = 0; i < 1; i++) dst[i] = src.u8[i] / 255.f; }
static void getPixelRG8(ImagePointer src, float* dst) { for (uint32_t i = 0; i < 2; i++) dst[i] = src.u8[i] / 255.f; }
static void getPixelRGBA8(ImagePointer src, float* dst) { for (uint32_t i = 0; i < 4; i++) dst[i] = src.u8[i] / 255.f; }
static void getPixelR16(ImagePointer src, float* dst) { for (uint32_t i = 0; i < 1; i++) dst[i] = src.u16[i] / 65535.f; }
static void getPixelRG16(ImagePointer src, float* dst) { for (uint32_t i = 0; i < 2; i++) dst[i] = src.u16[i] / 65535.f; }
static void getPixelRGBA16(ImagePointer src, float* dst) { for (uint32_t i = 0; i < 4; i++) dst[i] = src.u16[i] / 65535.f; }
static void getPixelR32F(ImagePointer src, float* dst) { for (uint32_t i = 0; i < 1; i++) dst[i] = src.f32[i]; }
static void getPixelRG32F(ImagePointer src, float* dst) { for (uint32_t i = 0; i < 2; i++) dst[i] = src.f32[i]; }
static void getPixelRGBA32F(ImagePointer src, float* dst) { for (uint32_t i = 0; i < 4; i++) dst[i] = src.f32[i]; }
static void setPixelR8(float* src, ImagePointer dst) { for (uint32_t i = 0; i < 1; i++) dst.u8[i] = (uint8_t) (src[i] * 255.f + .5f); }
static void setPixelRG8(float* src, ImagePointer dst) { for (uint32_t i = 0; i < 2; i++) dst.u8[i] = (uint8_t) (src[i] * 255.f + .5f); }
static void setPixelRGBA8(float* src, ImagePointer dst) { for (uint32_t i = 0; i < 4; i++) dst.u8[i] = (uint8_t) (src[i] * 255.f + .5f); }
static void setPixelR16(float* src, ImagePointer dst) { for (uint32_t i = 0; i < 1; i++) dst.u16[i] = (uint16_t) (src[i] * 65535.f + .5f); }
static void setPixelRG16(float* src, ImagePointer dst) { for (uint32_t i = 0; i < 2; i++) dst.u16[i] = (uint16_t) (src[i] * 65535.f + .5f); }
static void setPixelRGBA16(float* src, ImagePointer dst) { for (uint32_t i = 0; i < 4; i++) dst.u16[i] = (uint16_t) (src[i] * 65535.f + .5f); }
static void setPixelR32F(float* src, ImagePointer dst) { for (uint32_t i = 0; i < 1; i++) dst.f32[i] = src[i]; }
static void setPixelRG32F(float* src, ImagePointer dst) { for (uint32_t i = 0; i < 2; i++) dst.f32[i] = src[i]; }
static void setPixelRGBA32F(float* src, ImagePointer dst) { for (uint32_t i = 0; i < 4; i++) dst.f32[i] = src[i]; }
void lovrImageGetPixel(Image* image, uint32_t x, uint32_t y, float pixel[4]) {
lovrCheck(!lovrImageIsCompressed(image), "Unable to access individual pixels of a compressed image");
lovrCheck(x < image->width && y < image->height, "Pixel coordinates must be within Image bounds");
size_t offset = measure(y * image->width + x, 1, image->format);
ImagePointer p = { .u8 = (uint8_t*) image->mipmaps[0].data + offset };
switch (image->format) {
case FORMAT_R8: getPixelR8(p, pixel); return;
case FORMAT_RG8: getPixelRG8(p, pixel); return;
case FORMAT_RGBA8: getPixelRGBA8(p, pixel); return;
case FORMAT_R16: getPixelR16(p, pixel); return;
case FORMAT_RG16: getPixelRG16(p, pixel); return;
case FORMAT_RGBA16: getPixelRGBA16(p, pixel); return;
case FORMAT_R32F: getPixelR32F(p, pixel); return;
case FORMAT_RG32F: getPixelRG32F(p, pixel); return;
case FORMAT_RGBA32F: getPixelRGBA32F(p, pixel); return;
default: lovrThrow("Unsupported format for Image:getPixel");
}
}
void lovrImageSetPixel(Image* image, uint32_t x, uint32_t y, float pixel[4]) {
lovrCheck(!lovrImageIsCompressed(image), "Unable to access individual pixels of a compressed image");
lovrCheck(x < image->width && y < image->height, "Pixel coordinates must be within Image bounds");
size_t offset = measure(y * image->width + x, 1, image->format);
ImagePointer p = { .u8 = (uint8_t*) image->mipmaps[0].data + offset };
switch (image->format) {
case FORMAT_R8: setPixelR8(pixel, p); break;
case FORMAT_RG8: setPixelRG8(pixel, p); break;
case FORMAT_RGBA8: setPixelRGBA8(pixel, p); break;
case FORMAT_R16: setPixelR16(pixel, p); break;
case FORMAT_RG16: setPixelRG16(pixel, p); break;
case FORMAT_RGBA16: setPixelRGBA16(pixel, p); break;
case FORMAT_R32F: setPixelR32F(pixel, p); break;
case FORMAT_RG32F: setPixelRG32F(pixel, p); break;
case FORMAT_RGBA32F: setPixelRGBA32F(pixel, p); break;
default: lovrThrow("Unsupported format for Image:setPixel");
}
}
void lovrImageMapPixel(Image* image, uint32_t x0, uint32_t y0, uint32_t w, uint32_t h, MapPixelCallback* callback, void* userdata) {
lovrCheck(!lovrImageIsCompressed(image), "Unable to access individual pixels of a compressed image");
lovrCheck(x0 + w <= image->width, "Pixel rectangle must be within Image bounds");
lovrCheck(y0 + h <= image->height, "Pixel rectangle must be within Image bounds");
void (*getPixel)(ImagePointer src, float* dst);
void (*setPixel)(float* src, ImagePointer dst);
switch (image->format) {
case FORMAT_R8: getPixel = getPixelR8, setPixel = setPixelR8; break;
case FORMAT_RG8: getPixel = getPixelRG8, setPixel = setPixelRG8; break;
case FORMAT_RGBA8: getPixel = getPixelRGBA8, setPixel = setPixelRGBA8; break;
case FORMAT_R16: getPixel = getPixelR16, setPixel = setPixelR16; break;
case FORMAT_RG16: getPixel = getPixelRG16, setPixel = setPixelRG16; break;
case FORMAT_RGBA16: getPixel = getPixelRGBA16, setPixel = setPixelRGBA16; break;
case FORMAT_R32F: getPixel = getPixelR32F, setPixel = setPixelR32F; break;
case FORMAT_RG32F: getPixel = getPixelRG32F, setPixel = setPixelRG32F; break;
case FORMAT_RGBA32F: getPixel = getPixelRGBA32F, setPixel = setPixelRGBA32F; break;
}
float pixel[4] = { 0.f, 0.f, 0.f, 1.f };
uint32_t width = image->width;
char* data = image->mipmaps[0].data;
size_t stride = measure(1, 1, image->format);
for (uint32_t y = y0; y < y0 + h; y++) {
ImagePointer p = { .u8 = (uint8_t*) data + (y * width + x0) * stride };
for (uint32_t x = x0; x < x0 + w; x++) {
getPixel(p, pixel);
callback(userdata, x, y, pixel);
setPixel(pixel, p);
p.u8 += stride;
}
}
}
void lovrImageCopy(Image* src, Image* dst, uint32_t srcOffset[2], uint32_t dstOffset[2], uint32_t extent[2]) {
lovrCheck(src->format == dst->format, "To copy between Images, their formats must match");
lovrCheck(!lovrImageIsCompressed(src), "Compressed Images cannot be copied");
lovrCheck(dstOffset[0] + extent[0] <= dst->width, "Image copy region extends past the destination image width");
lovrCheck(dstOffset[1] + extent[1] <= dst->height, "Image copy region extends past the destination image height");
lovrCheck(srcOffset[0] + extent[0] <= src->width, "Image copy region extends past the source image width");
lovrCheck(srcOffset[1] + extent[1] <= src->height, "Image copy region extends past the source image height");
size_t pixelSize = measure(1, 1, src->format);
uint8_t* p = (uint8_t*) lovrImageGetLayerData(src, 0, 0) + (srcOffset[1] * src->width + srcOffset[0]) * pixelSize;
uint8_t* q = (uint8_t*) lovrImageGetLayerData(dst, 0, 0) + (dstOffset[1] * dst->width + dstOffset[0]) * pixelSize;
for (uint32_t y = 0; y < extent[1]; y++) {
memcpy(q, p, extent[0] * pixelSize);
p += src->width * pixelSize;
q += dst->width * pixelSize;
}
}
static uint32_t crc_lookup[256];
static bool crc_ready = false;
static void crc_init(void) {
if (!crc_ready) {
crc_ready = true;
for (uint32_t i = 0; i < 256; i++) {
uint32_t x = i;
for (uint32_t b = 0; b < 8; b++) {
if (x & 1) {
x = 0xedb88320L ^ (x >> 1);
} else {
x >>= 1;
}
crc_lookup[i] = x;
}
}
}
}
static uint32_t crc32(uint8_t* data, size_t length) {
uint32_t c = 0xffffffff;
for (size_t i = 0; i < length; i++) c = crc_lookup[(c ^ data[i]) & 0xff] ^ (c >> 8);
return c ^ 0xffffffff;
}
Blob* lovrImageEncode(Image* image) {
lovrCheck(image->format == FORMAT_RGBA8, "Currently, only images with the rgba8 format can be encoded");
uint32_t w = image->width;
uint32_t h = image->height;
uint8_t* pixels = (uint8_t*) image->blob->data;
uint32_t stride = (int) (w * 4);
// The world's worst png encoder
// Encoding uses one unfiltered IDAT chunk, each row is an uncompressed huffman block
// The total IDAT chunk data size is
// - 2 bytes for the zlib header
// - 6 bytes per block (5 bytes for the huffman block header + 1 byte scanline filter prefix)
// - n bytes for the image data itself (width * height * 4)
// - 4 bytes for the adler32 checksum
uint8_t signature[] = { 137, 80, 78, 71, 13, 10, 26, 10 };
uint8_t header[13] = {
w >> 24, w >> 16, w >> 8, w >> 0,
h >> 24, h >> 16, h >> 8, h >> 0,
8, 6, 0, 0, 0
};
size_t rowSize = w * 4;
size_t imageSize = rowSize * h;
size_t blockSize = rowSize + 1;
size_t idatSize = 2 + (h * (5 + 1)) + imageSize + 4;
size_t size = sizeof(signature);
size += 4 + strlen("IHDR") + sizeof(header) + 4;
size += 4 + strlen("IDAT") + idatSize + 4;
size += 4 + strlen("IEND") + 4;
uint8_t* data = lovrMalloc(size);
crc_init();
uint32_t crc;
// Signature
memcpy(data, signature, sizeof(signature));
data += sizeof(signature);
// IHDR
memcpy(data, (uint8_t[4]) { 0, 0, 0, sizeof(header) }, 4);
memcpy(data + 4, "IHDR", 4);
memcpy(data + 8, header, sizeof(header));
crc = crc32(data + 4, 4 + sizeof(header));
memcpy(data + 8 + sizeof(header), (uint8_t[4]) { crc >> 24, crc >> 16, crc >> 8, crc >> 0 }, 4);
data += 8 + sizeof(header) + 4;
// IDAT
memcpy(data, (uint8_t[4]) { idatSize >> 24 & 0xff, idatSize >> 16 & 0xff, idatSize >> 8 & 0xff, idatSize >> 0 & 0xff }, 4);
memcpy(data + 4, "IDAT", 4);
{
uint8_t* p = data + 8;
size_t length = imageSize;
// adler32 counters
uint64_t s1 = 1, s2 = 0;
// zlib header
*p++ = (7 << 4) + (8 << 0);
*p++ = 1;
while (length >= rowSize) {
// 1 indicates the final block
*p++ = (length == rowSize);
// Write length and negated length
memcpy(p + 0, &(uint16_t) { blockSize & 0xffff }, 2);
memcpy(p + 2, &(uint16_t) { ~blockSize & 0xffff }, 2);
p += 4;
// Write the filter method (0) and the row data
*p++ = 0x00;
memcpy(p, pixels, rowSize);
// Update adler32
s1 += 0;
s2 += s1;
for (size_t i = 0; i < rowSize; i++) {
s1 = (s1 + pixels[i]);
s2 = (s2 + s1);
}
s1 %= 65521;
s2 %= 65521;
// Update cursors
p += rowSize;
pixels += stride;
length -= rowSize;
}
// Write adler32 checksum
memcpy(p, (uint8_t[4]) { s2 >> 8, s2 >> 0, s1 >> 8, s1 >> 0 }, 4);
}
crc = crc32(data + 4, idatSize + 4);
memcpy(data + 8 + idatSize, (uint8_t[4]) { crc >> 24, crc >> 16, crc >> 8, crc }, 4);
data += 8 + idatSize + 4;
// IEND
memcpy(data, (uint8_t[4]) { 0 }, 4);
memcpy(data + 4, "IEND", 4);
crc = crc32(data + 4, 4);
memcpy(data + 8, (uint8_t[4]) { crc >> 24, crc >> 16, crc >> 8, crc >> 0 }, 4);
data += 8 + 4;
return lovrBlobCreate(data - size, size, "Encoded Image");
}
static Image* loadDDS(Blob* blob) {
enum { DDPF_FOURCC = 0x4, DDPF_RGB = 0x40 };
enum { DDSD_DEPTH = 0x800000 };
enum { DDS_RESOURCE_MISC_TEXTURECUBE = 0x4 };
enum { DDS_ALPHA_MODE_PREMULTIPLIED = 0x2 };
enum {
D3D10_RESOURCE_DIMENSION_UNKNOWN,
D3D10_RESOURCE_DIMENSION_BUFFER,
D3D10_RESOURCE_DIMENSION_TEXTURE1D,
D3D10_RESOURCE_DIMENSION_TEXTURE2D,
D3D10_RESOURCE_DIMENSION_TEXTURE3D
};
enum {
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
};
typedef struct DDSHeader {
uint32_t size;
uint32_t flags;
uint32_t height;
uint32_t width;
uint32_t pitchOrLinearSize;
uint32_t depth;
uint32_t mipmapCount;
uint32_t reserved[11];
struct {
uint32_t size;
uint32_t flags;
uint32_t fourCC;
uint32_t rgbBitCount;
uint32_t rMask;
uint32_t gMask;
uint32_t bMask;
uint32_t aMask;
} format;
uint32_t caps[4];
uint32_t reserved2;
} DDSHeader;
typedef struct DDSHeader10 {
uint32_t dxgiFormat;
uint32_t resourceDimension;
uint32_t miscFlag;
uint32_t arraySize;
uint32_t miscFlags2;
} DDSHeader10;
if (blob->size < 4 + sizeof(DDSHeader)) return NULL;
// Magic
char* data = blob->data;
size_t length = blob->size;
uint32_t magic;
memcpy(&magic, data, 4);
if (magic != 0x20534444) return false;
length -= 4;
data += 4;
// Header
DDSHeader* header = (DDSHeader*) data;
if (length < sizeof(DDSHeader)) return NULL;
if (header->size != sizeof(DDSHeader) || header->format.size != sizeof(header->format)) return NULL;
length -= sizeof(DDSHeader);
data += sizeof(DDSHeader);
TextureFormat format = ~0u;
uint32_t layers = 1;
uint32_t flags = 0;
// Header10
if ((header->format.flags & DDPF_FOURCC) && !memcmp(&header->format.fourCC, "DX10", 4)) {
if (length < sizeof(DDSHeader10)) return NULL;
DDSHeader10* header10 = (DDSHeader10*) data;
length -= sizeof(DDSHeader10);
data += sizeof(DDSHeader10);
switch (header10->dxgiFormat) {
case DXGI_FORMAT_R32G32B32A32_TYPELESS:
case DXGI_FORMAT_R32G32B32A32_FLOAT:
format = FORMAT_RGBA32F;
break;
case DXGI_FORMAT_R16G16B16A16_TYPELESS:
case DXGI_FORMAT_R16G16B16A16_FLOAT:
format = FORMAT_RGBA16F;
break;
case DXGI_FORMAT_R16G16B16A16_UNORM:
format = FORMAT_RGBA16;
break;
case DXGI_FORMAT_R32G32_TYPELESS:
case DXGI_FORMAT_R32G32_FLOAT:
format = FORMAT_RG32F;
break;
case DXGI_FORMAT_D32_FLOAT_S8X24_UINT:
format = FORMAT_D32FS8;
break;
case DXGI_FORMAT_R10G10B10A2_TYPELESS:
case DXGI_FORMAT_R10G10B10A2_UNORM:
format = FORMAT_RGB10A2;
break;
case DXGI_FORMAT_R11G11B10_FLOAT:
format = FORMAT_RG11B10F;
break;
case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
flags |= IMAGE_SRGB; /* fallthrough */
case DXGI_FORMAT_R8G8B8A8_TYPELESS:
case DXGI_FORMAT_R8G8B8A8_UNORM:
format = FORMAT_RGBA8;
break;
case DXGI_FORMAT_R16G16_TYPELESS:
case DXGI_FORMAT_R16G16_FLOAT:
format = FORMAT_RG16F;
break;
case DXGI_FORMAT_R16G16_UNORM:
format = FORMAT_RG16;
break;
case DXGI_FORMAT_D32_FLOAT:
format = FORMAT_D32F;
break;
case DXGI_FORMAT_R32_FLOAT:
format = FORMAT_R32F;
break;
case DXGI_FORMAT_D24_UNORM_S8_UINT:
format = FORMAT_D24S8;
break;
case DXGI_FORMAT_R8G8_TYPELESS:
case DXGI_FORMAT_R8G8_UNORM:
format = FORMAT_RG8;
break;
case DXGI_FORMAT_R16_TYPELESS:
case DXGI_FORMAT_R16_FLOAT:
format = FORMAT_R16F;
break;
case DXGI_FORMAT_D16_UNORM:
format = FORMAT_D16;
break;
case DXGI_FORMAT_R16_UNORM:
format = FORMAT_R16;
break;
case DXGI_FORMAT_R8_TYPELESS:
case DXGI_FORMAT_R8_UNORM:
format = FORMAT_R8;
break;
case DXGI_FORMAT_BC1_UNORM_SRGB:
flags |= IMAGE_SRGB; /* fallthrough */
case DXGI_FORMAT_BC1_TYPELESS:
case DXGI_FORMAT_BC1_UNORM:
format = FORMAT_BC1;
break;
case DXGI_FORMAT_BC2_UNORM_SRGB:
flags |= IMAGE_SRGB; /* fallthrough */
case DXGI_FORMAT_BC2_TYPELESS:
case DXGI_FORMAT_BC2_UNORM:
format = FORMAT_BC2;
break;
case DXGI_FORMAT_BC3_UNORM_SRGB:
flags |= IMAGE_SRGB; /* fallthrough */
case DXGI_FORMAT_BC3_TYPELESS:
case DXGI_FORMAT_BC3_UNORM:
format = FORMAT_BC3;
break;
case DXGI_FORMAT_BC4_TYPELESS:
case DXGI_FORMAT_BC4_UNORM:
format = FORMAT_BC4U;
break;
case DXGI_FORMAT_BC4_SNORM:
format = FORMAT_BC4S;
break;
case DXGI_FORMAT_BC5_TYPELESS:
case DXGI_FORMAT_BC5_UNORM:
format = FORMAT_BC5U;
break;
case DXGI_FORMAT_BC5_SNORM:
format = FORMAT_BC5S;
break;
case DXGI_FORMAT_B5G6R5_UNORM:
format = FORMAT_RGB565;
break;
case DXGI_FORMAT_B5G5R5A1_UNORM:
format = FORMAT_RGB5A1;
break;
case DXGI_FORMAT_BC6H_UF16:
format = FORMAT_BC6SF;
break;
case DXGI_FORMAT_BC6H_TYPELESS:
case DXGI_FORMAT_BC6H_SF16:
format = FORMAT_BC6UF;
break;
case DXGI_FORMAT_BC7_UNORM_SRGB:
flags |= IMAGE_SRGB; /* fallthrough */
case DXGI_FORMAT_BC7_TYPELESS:
case DXGI_FORMAT_BC7_UNORM:
format = FORMAT_BC7;
break;
default: lovrThrow("DDS file uses an unsupported DXGI format (%d)", header10->dxgiFormat);
}
lovrCheck(header10->resourceDimension != D3D10_RESOURCE_DIMENSION_TEXTURE3D, "Loading 3D DDS images is not supported");
layers = header10->arraySize;
if (header10->miscFlag & DDS_RESOURCE_MISC_TEXTURECUBE) {
flags |= IMAGE_CUBEMAP;
}
if (header10->miscFlags2 & DDS_ALPHA_MODE_PREMULTIPLIED) {
flags |= IMAGE_PREMULTIPLIED;
}
} else if (header->format.flags & DDPF_FOURCC) {
if (!memcmp(&header->format.fourCC, "DXT1", 4)) format = FORMAT_BC1;
else if (!memcmp(&header->format.fourCC, "DXT2", 4)) format = FORMAT_BC2, flags |= IMAGE_PREMULTIPLIED;
else if (!memcmp(&header->format.fourCC, "DXT3", 4)) format = FORMAT_BC2;
else if (!memcmp(&header->format.fourCC, "DXT4", 4)) format = FORMAT_BC3, flags |= IMAGE_PREMULTIPLIED;
else if (!memcmp(&header->format.fourCC, "DXT5", 4)) format = FORMAT_BC3;
else if (!memcmp(&header->format.fourCC, "BC4U", 4)) format = FORMAT_BC4U;
else if (!memcmp(&header->format.fourCC, "ATI1", 4)) format = FORMAT_BC4U;
else if (!memcmp(&header->format.fourCC, "BC4S", 4)) format = FORMAT_BC4S;
else if (!memcmp(&header->format.fourCC, "ATI2", 4)) format = FORMAT_BC5U;
else if (!memcmp(&header->format.fourCC, "BC5S", 4)) format = FORMAT_BC5S;
else if (header->format.fourCC == 0x6f) format = FORMAT_R16F;
else if (header->format.fourCC == 0x70) format = FORMAT_RG16F;
else if (header->format.fourCC == 0x71) format = FORMAT_RGBA16F;
else if (header->format.fourCC == 0x72) format = FORMAT_R32F;
else if (header->format.fourCC == 0x73) format = FORMAT_RG32F;
else if (header->format.fourCC == 0x74) format = FORMAT_RGBA32F;
else lovrThrow("DDS file uses an unsupported FourCC format (%d)", header->format.fourCC);
} else {
lovrThrow("DDS file uses an unsupported format"); // TODO could handle more uncompressed formats
}
lovrCheck(~header->flags & DDSD_DEPTH, "Loading 3D DDS images is not supported");
uint32_t levels = MAX(1, header->mipmapCount);
Image* image = lovrCalloc(offsetof(Image, mipmaps) + levels * sizeof(Mipmap));
image->ref = 1;
image->flags = flags;
image->width = header->width;
image->height = header->height;
image->format = format;
image->layers = layers;
image->levels = levels;
image->blob = blob;
lovrRetain(blob);
size_t stride = 0;
for (uint32_t i = 0, width = image->width, height = image->height; i < levels; i++) {
size_t size = measure(width, height, format);
lovrAssert(length >= size, "DDS file overflow");
image->mipmaps[i] = (Mipmap) { data, size, 0 };
width = MAX(width >> 1, 1);
height = MAX(height >> 1, 1);
stride += size;
length -= size;
data += size;
}
for (uint32_t i = 0; i < levels; i++) {
image->mipmaps[i].stride = stride;
}
return image;
}
static Image* loadASTC(Blob* blob) {
struct {
uint32_t magic;
uint8_t blockX;
uint8_t blockY;
uint8_t blockZ;
uint8_t width[3];
uint8_t height[3];
uint8_t depth[3];
} header;
if (blob->size <= sizeof(header)) {
return NULL;
}
memcpy(&header, blob->data, sizeof(header));
if (header.magic != 0x5ca1ab13) {
return NULL;
}
TextureFormat format;
uint32_t bx = header.blockX, by = header.blockY, bz = header.blockZ;
if (bx == 4 && by == 4 && bz == 1) { format = FORMAT_ASTC_4x4; }
else if (bx == 5 && by == 4 && bz == 1) { format = FORMAT_ASTC_5x4; }
else if (bx == 5 && by == 5 && bz == 1) { format = FORMAT_ASTC_5x5; }
else if (bx == 6 && by == 5 && bz == 1) { format = FORMAT_ASTC_6x5; }
else if (bx == 6 && by == 6 && bz == 1) { format = FORMAT_ASTC_6x6; }
else if (bx == 8 && by == 5 && bz == 1) { format = FORMAT_ASTC_8x5; }
else if (bx == 8 && by == 6 && bz == 1) { format = FORMAT_ASTC_8x6; }
else if (bx == 8 && by == 8 && bz == 1) { format = FORMAT_ASTC_8x8; }
else if (bx == 10 && by == 5 && bz == 1) { format = FORMAT_ASTC_10x5; }
else if (bx == 10 && by == 6 && bz == 1) { format = FORMAT_ASTC_10x6; }
else if (bx == 10 && by == 8 && bz == 1) { format = FORMAT_ASTC_10x8; }
else if (bx == 10 && by == 10 && bz == 1) { format = FORMAT_ASTC_10x10; }
else if (bx == 12 && by == 10 && bz == 1) { format = FORMAT_ASTC_12x10; }
else if (bx == 12 && by == 12 && bz == 1) { format = FORMAT_ASTC_12x12; }
else { lovrThrow("Unsupported ASTC format %dx%dx%d", bx, by, bz); }
uint32_t width = header.width[0] + (header.width[1] << 8) + (header.width[2] << 16);
uint32_t height = header.height[0] + (header.height[1] << 8) + (header.height[2] << 16);
size_t imageSize = ((width + bx - 1) / bx) * ((height + by - 1) / by) * (128 / 8);
if (imageSize > blob->size - sizeof(header)) {
return NULL;
}
Image* image = lovrCalloc(sizeof(Image));
image->ref = 1;
image->width = width;
image->height = height;
image->format = format;
image->flags = IMAGE_SRGB;
image->layers = 1;
image->levels = 1;
image->blob = blob;
lovrRetain(blob);
image->mipmaps[0] = (Mipmap) { (char*) blob->data + sizeof(header), imageSize, 0 };
return image;
}
static Image* loadKTX1(Blob* blob) {
struct {
uint8_t magic[12];
uint32_t endianness;
uint32_t glType;
uint32_t glTypeSize;
uint32_t glFormat;
uint32_t glInternalFormat;
uint32_t glBaseInternalFormat;
uint32_t pixelWidth;
uint32_t pixelHeight;
uint32_t pixelDepth;
uint32_t numberOfArrayElements;
uint32_t numberOfFaces;
uint32_t numberOfMipmapLevels;
uint32_t bytesOfKeyValueData;
} header;
if (blob->size <= sizeof(header)) {
return NULL;
}
memcpy(&header, blob->data, sizeof(header));
uint8_t magic[] = { 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A };
if (memcmp(header.magic, magic, sizeof(magic)) || header.endianness != 0x04030201) {
return NULL;
}
char* data = blob->data;
size_t length = blob->size;
data += sizeof(header);
length -= sizeof(header);
if (length < header.bytesOfKeyValueData) {
return NULL;
}
data += header.bytesOfKeyValueData;
length -= header.bytesOfKeyValueData;
lovrCheck(header.pixelWidth > 0, "KTX image dimensions must be positive");
lovrCheck(header.pixelHeight > 0, "Unable to load 1D KTX images");
lovrCheck(header.pixelDepth == 0, "Unable to load 3D KTX images");
uint32_t layers = MAX(header.numberOfArrayElements, 1);
uint32_t levels = MAX(header.numberOfMipmapLevels, 1);
Image* image = lovrCalloc(offsetof(Image, mipmaps) + levels * sizeof(Mipmap));
image->ref = 1;
image->width = header.pixelWidth;
image->height = header.pixelHeight;
image->layers = layers;
image->levels = levels;
image->blob = blob;
lovrRetain(blob);
if (header.numberOfFaces > 1) {
lovrCheck(header.numberOfFaces == 6, "KTX files must have 1 or 6 faces");
lovrCheck(header.numberOfArrayElements == 0, "KTX files with cubemap arrays are not supported");
image->flags |= IMAGE_CUBEMAP;
image->layers = 6;
}
// Format
struct { uint32_t type, format, internalFormat, srgbInternalFormat; } lookup[] = {
[FORMAT_R8] = { 0x1401, 0x1903, 0x8229, 0 },
[FORMAT_RG8] = { 0x1401, 0x8227, 0x822B, 0 },
[FORMAT_RGBA8] = { 0x1401, 0x1908, 0x8058, 0x8C43 },
[FORMAT_R16] = { 0x1403, 0x1903, 0x822A, 0 },
[FORMAT_RG16] = { 0x1403, 0x8227, 0x822C, 0 },
[FORMAT_RGBA16] = { 0x1403, 0x1908, 0x805B, 0 },
[FORMAT_R16F] = { 0x140B, 0x1903, 0x822D, 0 },
[FORMAT_RG16F] = { 0x140B, 0x8227, 0x822F, 0 },
[FORMAT_RGBA16F] = { 0x140B, 0x1908, 0x881A, 0 },
[FORMAT_R32F] = { 0x1406, 0x1903, 0x822E, 0 },
[FORMAT_RG32F] = { 0x1406, 0x8227, 0x8230, 0 },
[FORMAT_RGBA32F] = { 0x1406, 0x1908, 0x8814, 0 },
[FORMAT_RGB565] = { 0x8363, 0x1907, 0x8D62, 0 },
[FORMAT_RGB5A1] = { 0x8034, 0x1908, 0x8057, 0 },
[FORMAT_RGB10A2] = { 0x8368, 0x1908, 0x8059, 0 },
[FORMAT_RG11B10F] = { 0x8C3A, 0x1907, 0x8C3A, 0 },
[FORMAT_D16] = { 0x1403, 0x1902, 0x81A5, 0 },
[FORMAT_D24] = { 0x1405, 0x1902, 0x81A6, 0 },
[FORMAT_D32F] = { 0x1406, 0x1902, 0x8CAC, 0 },
[FORMAT_D24S8] = { 0x84FA, 0x84F9, 0x88F0, 0 },
[FORMAT_D32FS8] = { 0x8DAD, 0x84F9, 0x8CAD, 0 },
[FORMAT_BC1] = { 0x0000, 0x0000, 0x83F1, 0x8C4D },
[FORMAT_BC2] = { 0x0000, 0x0000, 0x83F2, 0x8C4E },
[FORMAT_BC3] = { 0x0000, 0x0000, 0x83F3, 0x8C4F },
[FORMAT_BC4U] = { 0x0000, 0x0000, 0x8DBB, 0 },
[FORMAT_BC4S] = { 0x0000, 0x0000, 0x8DBC, 0 },
[FORMAT_BC5U] = { 0x0000, 0x0000, 0x8DBD, 0 },
[FORMAT_BC5S] = { 0x0000, 0x0000, 0x8DBE, 0 },
[FORMAT_BC6UF] = { 0x0000, 0x0000, 0x8E8F, 0 },
[FORMAT_BC6SF] = { 0x0000, 0x0000, 0x8E8E, 0 },
[FORMAT_BC7] = { 0x0000, 0x0000, 0x8E8C, 0x8E8D },
[FORMAT_ASTC_4x4] = { 0x0000, 0x0000, 0x93B0, 0x93D0 },
[FORMAT_ASTC_5x4] = { 0x0000, 0x0000, 0x93B1, 0x93D1 },
[FORMAT_ASTC_5x5] = { 0x0000, 0x0000, 0x93B2, 0x93D2 },
[FORMAT_ASTC_6x5] = { 0x0000, 0x0000, 0x93B3, 0x93D3 },
[FORMAT_ASTC_6x6] = { 0x0000, 0x0000, 0x93B4, 0x93D4 },
[FORMAT_ASTC_8x5] = { 0x0000, 0x0000, 0x93B5, 0x93D5 },
[FORMAT_ASTC_8x6] = { 0x0000, 0x0000, 0x93B6, 0x93D6 },
[FORMAT_ASTC_8x8] = { 0x0000, 0x0000, 0x93B7, 0x93D7 },
[FORMAT_ASTC_10x5] = { 0x0000, 0x0000, 0x93B8, 0x93D8 },
[FORMAT_ASTC_10x6] = { 0x0000, 0x0000, 0x93B9, 0x93D9 },
[FORMAT_ASTC_10x8] = { 0x0000, 0x0000, 0x93BA, 0x93DA },
[FORMAT_ASTC_10x10] = { 0x0000, 0x0000, 0x93BB, 0x93DB },
[FORMAT_ASTC_12x10] = { 0x0000, 0x0000, 0x93BC, 0x93DC },
[FORMAT_ASTC_12x12] = { 0x0000, 0x0000, 0x93BD, 0x93DD }
};
image->format = ~0u;
for (uint32_t i = 0; i < COUNTOF(lookup); i++) {
if (header.glType == lookup[i].type && header.glFormat == lookup[i].format) {
if (header.glInternalFormat == lookup[i].internalFormat) {
image->format = i;
break;
} else if (lookup[i].srgbInternalFormat && header.glInternalFormat == lookup[i].srgbInternalFormat) {
image->format = i;
image->flags |= IMAGE_SRGB;
break;
}
}
}
lovrCheck(image->format != ~0u, "KTX1 file uses an unsupported image format (glType = %d, glFormat = %d, glInternalFormat = %d)", header.glType, header.glFormat, header.glInternalFormat);
// Mipmaps
uint32_t width = image->width;
uint32_t height = image->height;
size_t divisor = (image->flags & IMAGE_CUBEMAP) ? 1 : image->layers;
for (uint32_t i = 0; i < image->levels; i++) {
uint32_t levelSize;
memcpy(&levelSize, data, 4);
size_t size = measure(width, height, image->format);
lovrAssert(levelSize / divisor == size, "KTX size mismatch");
length -= 4;
data += 4;
size_t totalSize = size * image->layers;
lovrAssert(length >= totalSize, "KTX file overflow");
image->mipmaps[i] = (Mipmap) { data, size, size };
width = MAX(width >> 1, 1);
height = MAX(height >> 1, 1);
length -= totalSize;
data += totalSize;
}
return image;
}
static Image* loadKTX2(Blob* blob) {
typedef struct {
uint8_t magic[12];
uint32_t vkFormat;
uint32_t typeSize;
uint32_t pixelWidth;
uint32_t pixelHeight;
uint32_t pixelDepth;
uint32_t layerCount;
uint32_t faceCount;
uint32_t levelCount;
uint32_t compression;
uint32_t dfdByteOffset;
uint32_t dfdByteLength;
uint32_t kvdByteOffset;
uint32_t kvdByteLength;
uint64_t sgdByteOffset;
uint64_t sgdByteLength;
struct {
uint64_t byteOffset;
uint64_t byteLength;
uint64_t uncompressedLength;
} levels[1];
} KTX2Header;
char* data = blob->data;
size_t length = blob->size;
KTX2Header* header = (KTX2Header*) data;
uint8_t magic[] = { 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x32, 0x30, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A };
if (length < sizeof(KTX2Header) || memcmp(header->magic, magic, sizeof(magic))) {
return NULL;
}
lovrCheck(header->pixelWidth > 0, "KTX image dimensions must be positive");
lovrCheck(header->pixelHeight > 0, "Unable to load 1D KTX images");
lovrCheck(header->pixelDepth == 0, "Unable to load 3D KTX images");
lovrCheck(header->faceCount == 1 || header->faceCount == 6, "Invalid KTX file (faceCount must be 1 or 6)");
lovrCheck(header->layerCount == 0 || header->faceCount == 1, "Unable to load cubemap array KTX images");
lovrCheck(!header->compression, "Supercompressed KTX files are not currently supported");
uint32_t layers = MAX(header->layerCount, 1);
uint32_t levels = MAX(header->levelCount, 1);
Image* image = lovrCalloc(offsetof(Image, mipmaps) + levels * sizeof(Mipmap));
image->ref = 1;
image->width = header->pixelWidth;
image->height = header->pixelHeight;
image->layers = layers;
image->levels = levels;
image->blob = blob;
lovrRetain(blob);
if (header->faceCount == 6) {
image->flags |= IMAGE_CUBEMAP;
image->layers = 6;
}
// Format
switch (header->vkFormat) {
case 9: image->format = FORMAT_R8; break;
case 16: image->format = FORMAT_RG8; break;
case 37: image->format = FORMAT_RGBA8; break;
case 43: image->format = FORMAT_RGBA8, image->flags |= IMAGE_SRGB; break;
case 70: image->format = FORMAT_R16; break;
case 77: image->format = FORMAT_RG16; break;
case 91: image->format = FORMAT_RGBA16; break;
case 76: image->format = FORMAT_R16F; break;
case 83: image->format = FORMAT_RG16F; break;
case 97: image->format = FORMAT_RGBA16F; break;
case 100: image->format = FORMAT_R32F; break;
case 103: image->format = FORMAT_RG32F; break;
case 4: image->format = FORMAT_RGB565; break;
case 6: image->format = FORMAT_RGB5A1; break;
case 64: image->format = FORMAT_RGB10A2; break;
case 122: image->format = FORMAT_RG11B10F; break;
case 124: image->format = FORMAT_D16; break;
case 125: image->format = FORMAT_D24; break;
case 126: image->format = FORMAT_D32F; break;
case 129: image->format = FORMAT_D24S8; break;
case 130: image->format = FORMAT_D32FS8; break;
case 132: image->flags |= IMAGE_SRGB; /* fallthrough */ case 131: image->format = FORMAT_BC1; break;
case 136: image->flags |= IMAGE_SRGB; /* fallthrough */ case 135: image->format = FORMAT_BC2; break;
case 138: image->flags |= IMAGE_SRGB; /* fallthrough */ case 137: image->format = FORMAT_BC3; break;
case 139: image->format = FORMAT_BC4U; break;
case 140: image->format = FORMAT_BC4S; break;
case 141: image->format = FORMAT_BC5U; break;
case 142: image->format = FORMAT_BC5S; break;
case 143: image->format = FORMAT_BC6UF; break;
case 144: image->format = FORMAT_BC6SF; break;
case 146: image->flags |= IMAGE_SRGB; /* fallthrough */ case 145: image->format = FORMAT_BC7; break;
case 158: image->flags |= IMAGE_SRGB; /* fallthrough */ case 157: image->format = FORMAT_ASTC_4x4; break;
case 160: image->flags |= IMAGE_SRGB; /* fallthrough */ case 159: image->format = FORMAT_ASTC_5x4; break;
case 162: image->flags |= IMAGE_SRGB; /* fallthrough */ case 161: image->format = FORMAT_ASTC_5x5; break;
case 164: image->flags |= IMAGE_SRGB; /* fallthrough */ case 163: image->format = FORMAT_ASTC_6x5; break;
case 166: image->flags |= IMAGE_SRGB; /* fallthrough */ case 165: image->format = FORMAT_ASTC_6x6; break;
case 168: image->flags |= IMAGE_SRGB; /* fallthrough */ case 167: image->format = FORMAT_ASTC_8x5; break;
case 170: image->flags |= IMAGE_SRGB; /* fallthrough */ case 169: image->format = FORMAT_ASTC_8x6; break;
case 172: image->flags |= IMAGE_SRGB; /* fallthrough */ case 171: image->format = FORMAT_ASTC_8x8; break;
case 174: image->flags |= IMAGE_SRGB; /* fallthrough */ case 173: image->format = FORMAT_ASTC_10x5; break;
case 176: image->flags |= IMAGE_SRGB; /* fallthrough */ case 175: image->format = FORMAT_ASTC_10x6; break;
case 178: image->flags |= IMAGE_SRGB; /* fallthrough */ case 177: image->format = FORMAT_ASTC_10x8; break;
case 180: image->flags |= IMAGE_SRGB; /* fallthrough */ case 179: image->format = FORMAT_ASTC_10x10; break;
case 182: image->flags |= IMAGE_SRGB; /* fallthrough */ case 181: image->format = FORMAT_ASTC_12x10; break;
case 184: image->flags |= IMAGE_SRGB; /* fallthrough */ case 183: image->format = FORMAT_ASTC_12x12; break;
default: lovrThrow("KTX file uses an unsupported image format");
}
// Mipmaps
uint32_t width = image->width;
uint32_t height = image->height;
for (uint32_t i = 0; i < image->levels; i++) {
uint64_t offset = header->levels[i].byteOffset;
uint64_t size = header->levels[i].byteLength;
size_t stride = size / image->layers;
lovrAssert(offset + size <= blob->size, "KTX file overflow");
lovrAssert(measure(width, height, image->format) == size, "KTX size mismatch");
image->mipmaps[i] = (Mipmap) { data + offset, stride, stride };
width = MAX(width >> 1, 1);
height = MAX(height >> 1, 1);
}
return image;
}
static Image* loadSTB(Blob* blob) {
void* data;
uint32_t flags = 0;
TextureFormat format;
int width, height, channels;
if (stbi_is_16_bit_from_memory(blob->data, (int) blob->size)) {
data = stbi_load_16_from_memory(blob->data, (int) blob->size, &width, &height, &channels, 0);
switch (channels) {
case 1: format = FORMAT_R16; break;
case 2: format = FORMAT_RG16; break;
case 4: format = FORMAT_RGBA16; break;
default: lovrThrow("Unsupported channel count for 16 bit image: %d", channels);
}
flags = IMAGE_SRGB;
} else if (stbi_is_hdr_from_memory(blob->data, (int) blob->size)) {
data = stbi_loadf_from_memory(blob->data, (int) blob->size, &width, &height, NULL, 4);
format = FORMAT_RGBA32F;
} else {
data = stbi_load_from_memory(blob->data, (int) blob->size, &width, &height, NULL, 4);
format = FORMAT_RGBA8;
flags = IMAGE_SRGB;
}
if (!data) {
return NULL;
}
size_t size = measure(width, height, format);
Image* image = lovrCalloc(sizeof(Image));
image->ref = 1;
image->flags = flags;
image->width = width;
image->height = height;
image->format = format;
image->layers = 1;
image->levels = 1;
image->blob = lovrBlobCreate(data, size, blob->name);
image->mipmaps[0] = (Mipmap) { data, size, 0 };
return image;
}