mirror of
https://github.com/BinomialLLC/basis_universal.git
synced 2026-06-08 00:23:52 +00:00
333 lines
11 KiB
Python
333 lines
11 KiB
Python
# 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
|