Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DDS: various improvements #3573

Merged
merged 1 commit into from
Oct 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 16 additions & 13 deletions src/dds.imageio/dds_pvt.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ namespace DDS_pvt {
#define DDS_4CC_ATI1 DDS_MAKE4CC('A', 'T', 'I', '1')
#define DDS_4CC_ATI2 DDS_MAKE4CC('A', 'T', 'I', '2')
#define DDS_4CC_DX10 DDS_MAKE4CC('D', 'X', '1', '0')
#define DDS_4CC_RXGB DDS_MAKE4CC('R', 'X', 'G', 'B')
#define DDS_4CC_BC4U DDS_MAKE4CC('B', 'C', '4', 'U')
#define DDS_4CC_BC5U DDS_MAKE4CC('B', 'C', '5', 'U')

#define DDS_FORMAT_BC4_UNORM 80
#define DDS_FORMAT_BC5_UNORM 83
#define DDS_FORMAT_BC6H_UF16 95
Expand All @@ -43,24 +47,23 @@ enum class Compression {
/// images.
///
enum {
DDS_PF_ALPHA = 0x00000001, ///< image has alpha channel
DDS_PF_FOURCC = 0x00000004, ///< image is compressed
DDS_PF_LUMINANCE = 0x00020000, ///< image has luminance data
DDS_PF_RGB = 0x00000040, ///< image has RGB data
DDS_PF_YUV = 0x00000200 ///< image has YUV data
DDS_PF_ALPHA = 0x00000001, ///< image has alpha channel
DDS_PF_ALPHAONLY = 0x00000002, ///< image has only the alpha channel
DDS_PF_FOURCC = 0x00000004, ///< image is compressed
DDS_PF_LUMINANCE = 0x00020000, ///< image has luminance data
DDS_PF_RGB = 0x00000040, ///< image has RGB data
DDS_PF_YUV = 0x00000200, ///< image has YUV data
DDS_PF_NORMAL = 0x80000000u, ///< image is a tangent space normal map
};

/// DDS pixel format structure.
///
typedef struct {
uint32_t size; ///< structure size, must be 32
uint32_t flags; ///< flags to indicate valid fields
uint32_t fourCC; ///< compression four-character code
uint32_t bpp; ///< bits per pixel
uint32_t rmask; ///< bitmask for the red channel
uint32_t gmask; ///< bitmask for the green channel
uint32_t bmask; ///< bitmask for the blue channel
uint32_t amask; ///< bitmask for the alpha channel
uint32_t size; ///< structure size, must be 32
uint32_t flags; ///< flags to indicate valid fields
uint32_t fourCC; ///< compression four-character code
uint32_t bpp; ///< bits per pixel
uint32_t masks[4]; ///< bitmasks for the r,g,b,a channels
} dds_pixformat;

/// DDS caps flags, field 1.
Expand Down
158 changes: 117 additions & 41 deletions src/dds.imageio/ddsinput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,11 @@ class DDSInput final : public ImageInput {
std::vector<unsigned char> m_buf; ///< Buffer the image pixels
int m_subimage;
int m_miplevel;
int m_nchans; ///< Number of colour channels in image
int m_nfaces; ///< Number of cube map sides in image
int m_Bpp; ///< Number of bytes per pixel
int m_redL, m_redR; ///< Bit shifts to extract red channel
int m_greenL, m_greenR; ///< Bit shifts to extract green channel
int m_blueL, m_blueR; ///< Bit shifts to extract blue channel
int m_alphaL, m_alphaR; ///< Bit shifts to extract alpha channel
int m_nchans; ///< Number of colour channels in image
int m_nfaces; ///< Number of cube map sides in image
int m_Bpp; ///< Number of bytes per pixel
uint32_t m_BitCounts[4]; ///< Bit counts in r,g,b,a channels
uint32_t m_RightShifts[4]; ///< Shifts to extract r,g,b,a channels
Compression m_compression = Compression::None;
dds_header m_dds; ///< DDS header
dds_header_dx10 m_dx10;
Expand All @@ -91,7 +89,8 @@ class DDSInput final : public ImageInput {

/// Helper function: calculate bit shifts to properly extract channel data
///
inline void calc_shifts(int mask, int& left, int& right);
inline static void calc_shifts(uint32_t mask, uint32_t& count,
uint32_t& right);

/// Helper function: performs the actual file seeking.
///
Expand All @@ -111,8 +110,10 @@ GetBaseType(Compression cmp)
}

static int
GetChannelCount(Compression cmp)
GetChannelCount(Compression cmp, bool isNormal)
{
if (isNormal)
return 3;
if (cmp == Compression::BC4)
return 1;
if (cmp == Compression::BC5)
Expand All @@ -136,15 +137,61 @@ GetStorageRequirements(size_t width, size_t height, Compression cmp)
return blockCount * GetBlockSize(cmp);
}

static uint8_t
ComputeNormalZ(uint8_t x, uint8_t y)
{
float nx = 2 * (x / 255.0f) - 1;
float ny = 2 * (y / 255.0f) - 1;
float nz = 0.0f;
float nz2 = 1 - nx * nx - ny * ny;
if (nz2 > 0) {
nz = sqrtf(nz2);
}
int z = int(255.0f * (nz + 1) / 2.0f);
if (z < 0)
z = 0;
if (z > 255)
z = 255;
return z;
}

static void
ComputeNormalRG(uint8_t rgba[kBlockSize * kBlockSize * 4])
{
// expand from RG into RGB, computing B from RG
for (int i = kBlockSize * kBlockSize - 1; i >= 0; --i) {
uint8_t x = rgba[i * 2 + 0];
uint8_t y = rgba[i * 2 + 1];
rgba[i * 3 + 0] = x;
rgba[i * 3 + 1] = y;
rgba[i * 3 + 2] = ComputeNormalZ(x, y);
}
}

static void
ComputeNormalAG(uint8_t rgba[kBlockSize * kBlockSize * 4])
{
// contract from RGBA (R & B unused) to RGB, computing B from GA
for (int i = 0; i < kBlockSize * kBlockSize; ++i) {
uint8_t x = rgba[i * 4 + 3];
uint8_t y = rgba[i * 4 + 1];
rgba[i * 3 + 0] = x;
rgba[i * 3 + 1] = y;
rgba[i * 3 + 2] = ComputeNormalZ(x, y);
}
}


static void
DecompressImage(uint8_t* rgba, int width, int height, void const* blocks,
Compression cmp)
Compression cmp, const dds_pixformat& pixelFormat)
{
uint8_t rgbai[kBlockSize * kBlockSize * 4];
uint16_t rgbh[kBlockSize * kBlockSize * 3];
const uint8_t* sourceBlock = reinterpret_cast<const uint8_t*>(blocks);
const size_t blockSize = GetBlockSize(cmp);
const int channelCount = GetChannelCount(cmp);
const int channelCount = GetChannelCount(cmp,
pixelFormat.flags & DDS_PF_NORMAL);
for (int y = 0; y < height; y += kBlockSize) {
for (int x = 0; x < width; x += kBlockSize) {
// decompress the BCn block
Expand Down Expand Up @@ -178,6 +225,25 @@ DecompressImage(uint8_t* rgba, int width, int height, void const* blocks,
}
sourceBlock += blockSize;

/* Swap R & A for RXGB format case. */
if (cmp == Compression::DXT5
&& pixelFormat.fourCC == DDS_4CC_RXGB) {
for (int i = 0; i < 16; ++i) {
uint8_t r = rgbai[i * 4 + 0];
uint8_t a = rgbai[i * 4 + 3];
rgbai[i * 4 + 0] = a;
rgbai[i * 4 + 3] = r;
}
}
/* Convert into full normal map if needed. */
else if (pixelFormat.flags & DDS_PF_NORMAL) {
if (cmp == Compression::BC5) {
ComputeNormalRG(rgbai);
} else if (cmp == Compression::DXT5) {
ComputeNormalAG(rgbai);
}
}

// write the pixels into the destination image location
if (cmp == Compression::BC6HU || cmp == Compression::BC6HS) {
// HDR formats: half
Expand Down Expand Up @@ -326,9 +392,9 @@ DDSInput::open(const std::string& name, ImageSpec& newspec)
|| !(m_dds.flags & DDS_HEIGHT) || !m_dds.height
|| ((m_dds.flags & DDS_DEPTH) && !m_dds.depth)
|| (!(m_dds.fmt.flags & DDS_PF_FOURCC)
&& !((m_dds.fmt.flags & DDS_PF_RGB)
| (m_dds.fmt.flags & DDS_PF_LUMINANCE)
| (m_dds.fmt.flags & DDS_PF_ALPHA)))) {
&& !(m_dds.fmt.flags
& (DDS_PF_RGB | DDS_PF_LUMINANCE | DDS_PF_ALPHA
| DDS_PF_ALPHAONLY)))) {
errorf("Image with no data");
return false;
}
Expand All @@ -348,8 +414,14 @@ DDSInput::open(const std::string& name, ImageSpec& newspec)
case DDS_4CC_DXT3: m_compression = Compression::DXT3; break;
case DDS_4CC_DXT4: m_compression = Compression::DXT4; break;
case DDS_4CC_DXT5: m_compression = Compression::DXT5; break;
case DDS_4CC_RXGB:
m_compression = Compression::DXT5;
m_dds.fmt.flags &= ~DDS_PF_NORMAL;
break;
case DDS_4CC_ATI1: m_compression = Compression::BC4; break;
case DDS_4CC_ATI2: m_compression = Compression::BC5; break;
case DDS_4CC_BC4U: m_compression = Compression::BC4; break;
case DDS_4CC_BC5U: m_compression = Compression::BC5; break;
case DDS_4CC_DX10: {
switch (m_dx10.dxgiFormat) {
case DDS_FORMAT_BC4_UNORM: m_compression = Compression::BC4; break;
Expand All @@ -370,19 +442,27 @@ DDSInput::open(const std::string& name, ImageSpec& newspec)
}
}

// treat BC5 as normal maps if global attribute is set
if ((m_compression == Compression::BC5)
&& OIIO::get_int_attribute("dds:bc5normal")) {
m_dds.fmt.flags |= DDS_PF_NORMAL;
}

// determine the number of channels we have
if (m_compression != Compression::None) {
m_nchans = GetChannelCount(m_compression);
m_nchans = GetChannelCount(m_compression,
m_dds.fmt.flags & DDS_PF_NORMAL);
} else {
m_nchans = ((m_dds.fmt.flags & DDS_PF_LUMINANCE) ? 1 : 3)
m_nchans = ((m_dds.fmt.flags & (DDS_PF_LUMINANCE | DDS_PF_ALPHAONLY))
? 1
: 3)
+ ((m_dds.fmt.flags & DDS_PF_ALPHA) ? 1 : 0);
// also calculate bytes per pixel and the bit shifts
m_Bpp = (m_dds.fmt.bpp + 7) >> 3;
if (!(m_dds.fmt.flags & DDS_PF_LUMINANCE)) {
calc_shifts(m_dds.fmt.rmask, m_redL, m_redR);
calc_shifts(m_dds.fmt.gmask, m_greenL, m_greenR);
calc_shifts(m_dds.fmt.bmask, m_blueL, m_blueR);
calc_shifts(m_dds.fmt.amask, m_alphaL, m_alphaR);
for (int i = 0; i < 4; ++i)
calc_shifts(m_dds.fmt.masks[i], m_BitCounts[i],
m_RightShifts[i]);
}
}

Expand Down Expand Up @@ -412,25 +492,25 @@ DDSInput::open(const std::string& name, ImageSpec& newspec)


inline void
DDSInput::calc_shifts(int mask, int& left, int& right)
DDSInput::calc_shifts(uint32_t mask, uint32_t& count, uint32_t& right)
{
if (mask == 0) {
left = right = 0;
count = right = 0;
return;
}

int i, tmp = mask;
for (i = 0; i < 32; i++, tmp >>= 1) {
if (tmp & 1)
int i;
for (i = 0; i < 32; i++, mask >>= 1) {
if (mask & 1)
break;
}
right = i;

for (i = 0; i < 8; i++, tmp >>= 1) {
if (!(tmp & 1))
for (i = 0; i < 32; i++, mask >>= 1) {
if (!(mask & 1))
break;
}
left = 8 - i;
count = i;
}


Expand Down Expand Up @@ -577,7 +657,8 @@ DDSInput::seek_subimage(int subimage, int miplevel)
}

if (m_dds.fmt.bpp
&& (m_dds.fmt.flags & (DDS_PF_RGB | DDS_PF_LUMINANCE | DDS_PF_YUV)))
&& (m_dds.fmt.flags
& (DDS_PF_RGB | DDS_PF_LUMINANCE | DDS_PF_YUV | DDS_PF_ALPHAONLY)))
m_spec.attribute("oiio:BitsPerSample", m_dds.fmt.bpp);

// linear color space for HDR-ish images
Expand Down Expand Up @@ -636,8 +717,6 @@ DDSInput::seek_subimage(int subimage, int miplevel)
return true;
}



bool
DDSInput::internal_readimg(unsigned char* dst, int w, int h, int d)
{
Expand All @@ -649,7 +728,7 @@ DDSInput::internal_readimg(unsigned char* dst, int w, int h, int d)
if (!ioread(&tmp[0], tmp.size(), 1))
return false;
// decompress image
DecompressImage(dst, w, h, &tmp[0], m_compression);
DecompressImage(dst, w, h, &tmp[0], m_compression, m_dds.fmt);
tmp.clear();
// correct pre-multiplied alpha, if necessary
if (m_compression == Compression::DXT2
Expand Down Expand Up @@ -685,15 +764,12 @@ DDSInput::internal_readimg(unsigned char* dst, int w, int h, int d)
uint32_t pixel = 0;
OIIO_DASSERT(tmp.size() >= size_t(x * m_Bpp + m_Bpp));
memcpy(&pixel, tmp.data() + x * m_Bpp, m_Bpp);
dst[k + 0] = ((pixel & m_dds.fmt.rmask) >> m_redR)
<< m_redL;
dst[k + 1] = ((pixel & m_dds.fmt.gmask) >> m_greenR)
<< m_greenL;
dst[k + 2] = ((pixel & m_dds.fmt.bmask) >> m_blueR)
<< m_blueL;
if (m_dds.fmt.flags & DDS_PF_ALPHA)
dst[k + 3] = ((pixel & m_dds.fmt.amask) >> m_alphaR)
<< m_alphaL;
for (int ch = 0; ch < m_spec.nchannels; ++ch) {
dst[k + ch]
= bit_range_convert((pixel & m_dds.fmt.masks[ch])
>> m_RightShifts[ch],
m_BitCounts[ch], 8);
}
}
}
}
Expand Down
26 changes: 21 additions & 5 deletions src/doc/builtinplugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,22 @@ DDS
DDS (Direct Draw Surface) is an image file format designed by Microsoft
for use in Direct3D graphics. DDS files use the extension :file:`.dds`.

DDS is an awful format, with several compression modes that are all so
lossy as to be completely useless for high-end graphics. Nevertheless,
they are widely used in games and graphics hardware directly supports
these compression modes. Alas.
DDS is primarily meant for images that are directly usable by the GPU.
It supports 2D, cube and volume images with or without MIPmaps; using
either uncompressed pixel formats or one of the lossy compression
schemes supported by the graphics hardware (BC1-BC7).

OpenImageIO currently only supports reading DDS files, not writing them.

DDS files containing a "normal map" (`0x80000000`) pixel format flag
will be interpreted as a tangent space normal map. When reading such files,
the resulting image will be a 3-channel image with red & green channels
coming from file data, and the blue channel computed as if it were the
Z component of a normal map. This applies to images using DXT5 compression
(normal X & Y components are assumed to be in alpha & green channels)
and images using BC5/ATI2 compression (normal X & Y components are in
red & green channels).

.. list-table::
:widths: 30 10 65
:header-rows: 1
Expand Down Expand Up @@ -187,7 +196,14 @@ attributes are supported:
* - ``oiio:ioproxy``
- ptr
- Pointer to a ``Filesystem::IOProxy`` that will handle the I/O, for
example by reading from memory rather than the file system.
example by reading from memory rather than the file system.

Additionally, an integer ``dds:bc5normal`` global attribute is supported
to control behaviour of images compressed in BC5/ATI2 compression format.
When the attribute value is set to non-zero (default is zero), any input
image using BC5/ATI2 compression format is assumed to be a normal map,
even if pixel format "normal map" flag is not set.


**Configuration settings for DDS output**

Expand Down
5 changes: 5 additions & 0 deletions src/include/OpenImageIO/imageio.h
Original file line number Diff line number Diff line change
Expand Up @@ -2750,6 +2750,11 @@ OIIO_API std::string geterror(bool clear = true);
/// may not read these correctly, but OIIO will. That's why the default
/// is not to support it.
///
/// - `int dds:bc5normal`
///
/// When nonzero, treats BC5/ATI2 format files as normal maps (loads as
/// 3 channels, computes blue from red and green). Default is 0.
///
/// - `int openexr:core`
///
/// When nonzero, use the new "OpenEXR core C library" when available,
Expand Down
Loading