blob: f1c95433c06878300b45b945f4bcbefbc87e77e7 [file] [log] [blame] [edit]
# dds_writer.py
#
# Minimal DDS writer that mirrors the C/C++ save_dds() implementation you provided.
# It writes a DX9-style DDS header, and optionally a DX10 extension header,
# followed by the raw compressed blocks.
#
# No mipmaps, no cubes, no 3D volumes – exactly like the original C code.
import struct
import sys
from typing import Union
# ---------------------------------------------------------------------------
# FourCC helper (same as PIXEL_FMT_FOURCC macro)
# ---------------------------------------------------------------------------
def make_fourcc(a: str, b: str, c: str, d: str) -> int:
return (ord(a) |
(ord(b) << 8) |
(ord(c) << 16) |
(ord(d) << 24))
# ---------------------------------------------------------------------------
# DDS-related constants (only the ones we actually use)
# ---------------------------------------------------------------------------
# DDSD flags
DDSD_CAPS = 0x00000001
DDSD_HEIGHT = 0x00000002
DDSD_WIDTH = 0x00000004
DDSD_PIXELFORMAT= 0x00001000
DDSD_LINEARSIZE = 0x00080000
# DDPF flags
DDPF_FOURCC = 0x00000004
# DDSCAPS flags
DDSCAPS_TEXTURE = 0x00001000
# DXGI_FORMAT subset (values must match the C enum)
class DXGI_FORMAT:
UNKNOWN = 0
BC1_UNORM = 71
BC3_UNORM = 77
BC4_UNORM = 80
BC5_UNORM = 83
# You can add more as needed; for DX10 header we just write the integer value.
# DX10 resource dimension
class D3D10_RESOURCE_DIMENSION:
UNKNOWN = 0
BUFFER = 1
TEXTURE1D = 2
TEXTURE2D = 3
TEXTURE3D = 4
# ---------------------------------------------------------------------------
# DDS writer class
# ---------------------------------------------------------------------------
class DDSWriter:
"""
Python port of the C save_dds() function.
Usage:
writer = DDSWriter()
ok = writer.save_dds(
filename="out.dds",
width=width,
height=height,
blocks=bc_data, # bytes or bytearray
pixel_format_bpp=4, # e.g. 4 for BC1, 8 for BC3/4/5/etc.
dxgi_format=DXGI_FORMAT.BC1_UNORM,
srgb=False,
force_dx10_header=False,
)
"""
DDS_MAGIC = b"DDS " # same as fwrite("DDS ", 4, 1, pFile);
def save_dds(
self,
filename: str,
width: int,
height: int,
blocks: Union[bytes, bytearray, memoryview],
pixel_format_bpp: int,
dxgi_format: int,
srgb: bool = False,
force_dx10_header: bool = False,
) -> bool:
"""
Port of:
bool save_dds(const char* pFilename,
uint32_t width, uint32_t height,
const void* pBlocks,
uint32_t pixel_format_bpp,
DXGI_FORMAT dxgi_format,
bool srgb,
bool force_dx10_header);
The 'blocks' buffer is written as-is (up to computed linear size).
"""
# srgb is intentionally unused in the original C code (commented logic).
_ = srgb
# Open file like the C code
try:
f = open(filename, "wb")
except OSError:
print(f"Failed creating file {filename}!", file=sys.stderr)
return False
try:
# Write the "DDS " magic
f.write(self.DDS_MAGIC)
# -----------------------------------------------------------------
# Build DDSURFACEDESC2 equivalent
# -----------------------------------------------------------------
# We'll pack DDSURFACEDESC2 as 31 uint32's (124 bytes) in little-endian:
# struct DDSURFACEDESC2 {
# uint32 dwSize;
# uint32 dwFlags;
# uint32 dwHeight;
# uint32 dwWidth;
# uint32 lPitch_or_dwLinearSize;
# uint32 dwBackBufferCount;
# uint32 dwMipMapCount;
# uint32 dwAlphaBitDepth;
# uint32 dwUnused0;
# uint32 lpSurface;
# DDCOLORKEY unused0; (2 * uint32)
# DDCOLORKEY unused1; (2 * uint32)
# DDCOLORKEY unused2; (2 * uint32)
# DDCOLORKEY unused3; (2 * uint32)
# DDPIXELFORMAT ddpfPixelFormat; (8 * uint32)
# DDSCAPS2 ddsCaps; (4 * uint32)
# uint32 dwUnused1;
# };
dwSize = 124 # sizeof(DDSURFACEDESC2)
dwFlags = (
DDSD_WIDTH |
DDSD_HEIGHT |
DDSD_PIXELFORMAT |
DDSD_CAPS
)
dwWidth = int(width)
dwHeight = int(height)
# lPitch (actually LinearSize for compressed formats), same as:
# (((dwWidth + 3) & ~3) * ((dwHeight + 3) & ~3) * pixel_format_bpp) >> 3;
lPitch = (
((dwWidth + 3) & ~3)
* ((dwHeight + 3) & ~3)
* int(pixel_format_bpp)
) >> 3
dwFlags |= DDSD_LINEARSIZE
dwBackBufferCount = 0
dwMipMapCount = 0
dwAlphaBitDepth = 0
dwUnused0 = 0
lpSurface = 0
# DDCOLORKEY unused0..3, all zero
ddcolorkey_zero = [0, 0] * 4 # 4 DDCOLORKEY structs
# DDPIXELFORMAT
# struct DDPIXELFORMAT {
# uint32 dwSize;
# uint32 dwFlags;
# uint32 dwFourCC;
# uint32 dwRGBBitCount;
# uint32 dwRBitMask;
# uint32 dwGBitMask;
# uint32 dwBBitMask;
# uint32 dwRGBAlphaBitMask;
# };
ddpf_dwSize = 32
ddpf_dwFlags = DDPF_FOURCC
ddpf_dwFourCC = 0
ddpf_dwRGBBitCount = 0
ddpf_dwRBitMask = 0
ddpf_dwGBitMask = 0
ddpf_dwBBitMask = 0
ddpf_dwRGBAlphaBitMask = 0
# DDSCAPS2
# struct DDSCAPS2 {
# uint32 dwCaps;
# uint32 dwCaps2;
# uint32 dwCaps3;
# uint32 dwCaps4;
# };
ddsCaps_dwCaps = DDSCAPS_TEXTURE
ddsCaps_dwCaps2 = 0
ddsCaps_dwCaps3 = 0
ddsCaps_dwCaps4 = 0
dwUnused1 = 0
# Decide whether to use legacy FourCC (DXT1/DXT5/ATI1/ATI2) or DX10 header
use_legacy = (
not force_dx10_header and
dxgi_format in (
DXGI_FORMAT.BC1_UNORM,
DXGI_FORMAT.BC3_UNORM,
DXGI_FORMAT.BC4_UNORM,
DXGI_FORMAT.BC5_UNORM,
)
)
if use_legacy:
if dxgi_format == DXGI_FORMAT.BC1_UNORM:
ddpf_dwFourCC = make_fourcc('D', 'X', 'T', '1')
elif dxgi_format == DXGI_FORMAT.BC3_UNORM:
ddpf_dwFourCC = make_fourcc('D', 'X', 'T', '5')
elif dxgi_format == DXGI_FORMAT.BC4_UNORM:
ddpf_dwFourCC = make_fourcc('A', 'T', 'I', '1')
elif dxgi_format == DXGI_FORMAT.BC5_UNORM:
ddpf_dwFourCC = make_fourcc('A', 'T', 'I', '2')
else:
# Write DX10 header, FourCC = "DX10"
ddpf_dwFourCC = make_fourcc('D', 'X', '1', '0')
# Build the 31 uint32's for DDSURFACEDESC2
header_values = [
dwSize,
dwFlags,
dwHeight,
dwWidth,
lPitch,
dwBackBufferCount,
dwMipMapCount,
dwAlphaBitDepth,
dwUnused0,
lpSurface,
]
header_values.extend(ddcolorkey_zero) # 8 uint32's
ddpf_values = [
ddpf_dwSize,
ddpf_dwFlags,
ddpf_dwFourCC,
ddpf_dwRGBBitCount,
ddpf_dwRBitMask,
ddpf_dwGBitMask,
ddpf_dwBBitMask,
ddpf_dwRGBAlphaBitMask,
]
header_values.extend(ddpf_values) # 8 uint32's
ddsCaps_values = [
ddsCaps_dwCaps,
ddsCaps_dwCaps2,
ddsCaps_dwCaps3,
ddsCaps_dwCaps4,
]
header_values.extend(ddsCaps_values) # 4 uint32's
header_values.append(dwUnused1) # final uint32
if len(header_values) != 31:
raise RuntimeError("Internal error: DDSURFACEDESC2 must contain 31 uint32's")
# Pack and write DDSURFACEDESC2
dds_header = struct.pack("<31I", *header_values)
f.write(dds_header)
# If needed, write the DX10 header (DDS_HEADER_DXT10)
if not use_legacy:
# struct DDS_HEADER_DXT10 {
# DXGI_FORMAT dxgiFormat;
# D3D10_RESOURCE_DIMENSION resourceDimension;
# uint32 miscFlag;
# uint32 arraySize;
# uint32 miscFlags2;
# };
dxgiFormat = int(dxgi_format)
resourceDimension = D3D10_RESOURCE_DIMENSION.TEXTURE2D
miscFlag = 0
arraySize = 1
miscFlags2 = 0
dxt10_header = struct.pack(
"<5I",
dxgiFormat,
resourceDimension,
miscFlag,
arraySize,
miscFlags2,
)
f.write(dxt10_header)
# -----------------------------------------------------------------
# Write the actual texture data blocks (pBlocks)
# -----------------------------------------------------------------
# C code: fwrite(pBlocks, desc.lPitch, 1, pFile);
# i.e. write exactly lPitch bytes.
data = memoryview(blocks)
if len(data) < lPitch:
raise ValueError(
f"blocks buffer too small: need at least {lPitch} bytes, got {len(data)}"
)
f.write(data[:lPitch])
except Exception as e:
# Mimic the C-style error reporting as much as practical
print(f"Failed writing to DDS file {filename}: {e}", file=sys.stderr)
try:
f.close()
except Exception:
pass
return False
# Close file
try:
f.close()
except OSError:
print(f"Failed closing DDS file {filename}!", file=sys.stderr)
return False
return True