// MonoGame - Copyright (C) The MonoGame Team // This file is subject to the terms and conditions defined in // file 'LICENSE.txt', which is part of this source code package. using System; using Microsoft.Xna.Framework.Content.Pipeline.Graphics; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics.PackedVector; using System.IO; namespace Microsoft.Xna.Framework.Content.Pipeline { /// /// Loader class for DDS format image files. /// class DdsLoader { [Flags] enum Ddsd : uint { Caps = 0x1, // Required in every DDS file Height = 0x2, // Required in every DDS file Width = 0x4, // Required in every DDS file Pitch = 0x8, // Required when pitch is provided for an uncompressed texture PixelFormat = 0x1000, // Required in every DDS file MipMapCount = 0x2000, // Required in a mipmapped texture LinearSize = 0x80000, // Required when pitch is provided for a compressed texture Depth = 0x800000, // Required in a depth texture } [Flags] enum DdsCaps : uint { Complex = 0x8, // Optional; must be used on any file that contains more than one surface (a mipmap, a cubic environment map, or mipmapped volume texture) MipMap = 0x400000, // Optional; should be used for a mipmap Texture = 0x1000, // Required } [Flags] enum DdsCaps2 : uint { Cubemap = 0x200, CubemapPositiveX = 0x400, CubemapNegativeX = 0x800, CubemapPositiveY = 0x1000, CubemapNegativeY = 0x2000, CubemapPositiveZ = 0x4000, CubemapNegativeZ = 0x8000, Volume = 0x200000, CubemapAllFaces = Cubemap | CubemapPositiveX | CubemapNegativeX | CubemapPositiveY | CubemapNegativeY | CubemapPositiveZ | CubemapNegativeZ, } [Flags] enum Ddpf : uint { AlphaPixels = 0x1, Alpha = 0x2, FourCC = 0x4, Rgb = 0x40, Yuv = 0x200, Luminance = 0x20000, } static uint MakeFourCC(char c1, char c2, char c3, char c4) { return ((uint)c1 << 24) | ((uint)c2 << 16) | ((uint)c3 << 8) | (uint)c4; } static uint MakeFourCC(string cc) { return ((uint)cc[0] << 24) | ((uint)cc[1] << 16) | ((uint)cc[2] << 8) | (uint)cc[3]; } enum FourCC : uint { A32B32G32R32F = 116, Dxt1 = 0x31545844, Dxt2 = 0x32545844, Dxt3 = 0x33545844, Dxt4 = 0x34545844, Dxt5 = 0x35545844, Dx10 = 0x30315844, } struct DdsPixelFormat { public uint dwSize; public Ddpf dwFlags; public FourCC dwFourCC; public uint dwRgbBitCount; public uint dwRBitMask; public uint dwGBitMask; public uint dwBBitMask; public uint dwABitMask; } struct DdsHeader { public uint dwSize; public Ddsd dwFlags; public uint dwHeight; public uint dwWidth; public uint dwPitchOrLinearSize; public uint dwDepth; public uint dwMipMapCount; public DdsPixelFormat ddspf; public DdsCaps dwCaps; public DdsCaps2 dwCaps2; } static SurfaceFormat GetSurfaceFormat(ref DdsPixelFormat pixelFormat, out bool rbSwap) { rbSwap = false; if (pixelFormat.dwFlags.HasFlag(Ddpf.FourCC)) { switch (pixelFormat.dwFourCC) { case FourCC.A32B32G32R32F: return SurfaceFormat.Vector4; case FourCC.Dxt1: return SurfaceFormat.Dxt1; case FourCC.Dxt2: throw new ContentLoadException("Unsupported compression format DXT2"); case FourCC.Dxt3: return SurfaceFormat.Dxt3; case FourCC.Dxt4: throw new ContentLoadException("Unsupported compression format DXT4"); case FourCC.Dxt5: return SurfaceFormat.Dxt5; } } else if (pixelFormat.dwFlags.HasFlag(Ddpf.Rgb)) { // Uncompressed format if (pixelFormat.dwFlags.HasFlag(Ddpf.AlphaPixels)) { // Format contains RGB and A if (pixelFormat.dwRgbBitCount == 16) { if (pixelFormat.dwABitMask == 0xF) { rbSwap = pixelFormat.dwBBitMask == 0xF0; return SurfaceFormat.Bgra4444; } rbSwap = pixelFormat.dwBBitMask == 0x3E; return SurfaceFormat.Bgra5551; } else if (pixelFormat.dwRgbBitCount == 32) { rbSwap = pixelFormat.dwBBitMask == 0xFF; return SurfaceFormat.Color; } throw new ContentLoadException("Unsupported RGBA pixel format"); } else { // Format contains RGB only if (pixelFormat.dwRgbBitCount == 16) { rbSwap = pixelFormat.dwBBitMask == 0x1F; return SurfaceFormat.Bgr565; } else if (pixelFormat.dwRgbBitCount == 24) { rbSwap = pixelFormat.dwBBitMask == 0xFF; return SurfaceFormat.Color; } else if (pixelFormat.dwRgbBitCount == 32) { rbSwap = pixelFormat.dwBBitMask == 0xFF; return SurfaceFormat.Color; } throw new ContentLoadException("Unsupported RGB pixel format"); } } //else if (pixelFormat.dwFlags.HasFlag(Ddpf.Luminance)) //{ // return SurfaceFormat.Alpha8; //} throw new ContentLoadException("Unsupported pixel format"); } static BitmapContent CreateBitmapContent(SurfaceFormat format, int width, int height) { switch (format) { case SurfaceFormat.Color: return new PixelBitmapContent(width, height); case SurfaceFormat.Bgra4444: return new PixelBitmapContent(width, height); case SurfaceFormat.Bgra5551: return new PixelBitmapContent(width, height); case SurfaceFormat.Bgr565: return new PixelBitmapContent(width, height); case SurfaceFormat.Dxt1: return new Dxt1BitmapContent(width, height); case SurfaceFormat.Dxt3: return new Dxt3BitmapContent(width, height); case SurfaceFormat.Dxt5: return new Dxt5BitmapContent(width, height); case SurfaceFormat.Vector4: return new PixelBitmapContent(width, height); } throw new ContentLoadException("Unsupported SurfaceFormat " + format); } static int GetBitmapSize(SurfaceFormat format, int width, int height) { // It is recommended that the dwPitchOrLinearSize field is ignored and we calculate it ourselves // https://msdn.microsoft.com/en-us/library/bb943991.aspx int pitch = 0; int rows = 0; switch (format) { case SurfaceFormat.Color: case SurfaceFormat.Bgra4444: case SurfaceFormat.Bgra5551: case SurfaceFormat.Bgr565: case SurfaceFormat.Vector4: pitch = width * format.GetSize(); rows = height; break; case SurfaceFormat.Dxt1: case SurfaceFormat.Dxt3: case SurfaceFormat.Dxt5: pitch = ((width + 3) / 4) * format.GetSize(); rows = (height + 3) / 4; break; default: throw new ContentLoadException("Unsupported SurfaceFormat " + format); } return pitch * rows; } static internal TextureContent Import(string filename, ContentImporterContext context) { var identity = new ContentIdentity(filename); TextureContent output = null; using (var reader = new BinaryReader(new FileStream(filename, FileMode.Open, FileAccess.Read))) { // Read signature ("DDS ") var valid = reader.ReadByte() == 0x44; valid = valid && reader.ReadByte() == 0x44; valid = valid && reader.ReadByte() == 0x53; valid = valid && reader.ReadByte() == 0x20; if (!valid) throw new ContentLoadException("Invalid file signature"); var header = new DdsHeader(); // Read DDS_HEADER header.dwSize = reader.ReadUInt32(); if (header.dwSize != 124) throw new ContentLoadException("Invalid DDS_HEADER dwSize value"); header.dwFlags = (Ddsd)reader.ReadUInt32(); header.dwHeight = reader.ReadUInt32(); header.dwWidth = reader.ReadUInt32(); header.dwPitchOrLinearSize = reader.ReadUInt32(); header.dwDepth = reader.ReadUInt32(); header.dwMipMapCount = reader.ReadUInt32(); // The next 11 DWORDs are reserved and unused for (int i = 0; i < 11; ++i) reader.ReadUInt32(); // Read DDS_PIXELFORMAT header.ddspf.dwSize = reader.ReadUInt32(); if (header.ddspf.dwSize != 32) throw new ContentLoadException("Invalid DDS_PIXELFORMAT dwSize value"); header.ddspf.dwFlags = (Ddpf)reader.ReadUInt32(); header.ddspf.dwFourCC = (FourCC)reader.ReadUInt32(); header.ddspf.dwRgbBitCount = reader.ReadUInt32(); header.ddspf.dwRBitMask = reader.ReadUInt32(); header.ddspf.dwGBitMask = reader.ReadUInt32(); header.ddspf.dwBBitMask = reader.ReadUInt32(); header.ddspf.dwABitMask = reader.ReadUInt32(); // Continue reading DDS_HEADER header.dwCaps = (DdsCaps)reader.ReadUInt32(); header.dwCaps2 = (DdsCaps2)reader.ReadUInt32(); // dwCaps3 unused reader.ReadUInt32(); // dwCaps4 unused reader.ReadUInt32(); // dwReserved2 unused reader.ReadUInt32(); // Check for the existence of the DDS_HEADER_DXT10 struct next if (header.ddspf.dwFlags == Ddpf.FourCC && header.ddspf.dwFourCC == FourCC.Dx10) { throw new ContentLoadException("Unsupported DDS_HEADER_DXT10 struct found"); } int faceCount = 1; int mipMapCount = (int)(header.dwCaps.HasFlag(DdsCaps.MipMap) ? header.dwMipMapCount : 1); if (header.dwCaps2.HasFlag(DdsCaps2.Cubemap)) { if (!header.dwCaps2.HasFlag(DdsCaps2.CubemapAllFaces)) throw new ContentLoadException("Incomplete cubemap in DDS file"); faceCount = 6; output = new TextureCubeContent() { Identity = identity }; } else { output = new Texture2DContent() { Identity = identity }; } bool rbSwap; var format = GetSurfaceFormat(ref header.ddspf, out rbSwap); for (int f = 0; f < faceCount; ++f) { var w = (int)header.dwWidth; var h = (int)header.dwHeight; var mipMaps = new MipmapChain(); for (int m = 0; m < mipMapCount; ++m) { var content = CreateBitmapContent(format, w, h); var byteCount = GetBitmapSize(format, w, h); // A 24-bit format is slightly different if (header.ddspf.dwRgbBitCount == 24) byteCount = 3 * w * h; var bytes = reader.ReadBytes(byteCount); if (rbSwap) { switch (format) { case SurfaceFormat.Bgr565: ByteSwapBGR565(bytes); break; case SurfaceFormat.Bgra4444: ByteSwapBGRA4444(bytes); break; case SurfaceFormat.Bgra5551: ByteSwapBGRA5551(bytes); break; case SurfaceFormat.Color: if (header.ddspf.dwRgbBitCount == 32) ByteSwapRGBX(bytes); else if (header.ddspf.dwRgbBitCount == 24) ByteSwapRGB(bytes); break; } } if ((format == SurfaceFormat.Color) && header.ddspf.dwFlags.HasFlag(Ddpf.Rgb) && !header.ddspf.dwFlags.HasFlag(Ddpf.AlphaPixels)) { // Fill or add alpha with opaque if (header.ddspf.dwRgbBitCount == 32) ByteFillAlpha(bytes); else if (header.ddspf.dwRgbBitCount == 24) ByteExpandAlpha(ref bytes); } content.SetPixelData(bytes); mipMaps.Add(content); w = MathHelper.Max(1, w / 2); h = MathHelper.Max(1, h / 2); } output.Faces[f] = mipMaps; } } return output; } static void ByteFillAlpha(byte[] bytes) { for (int i = 0; i < bytes.Length; i += 4) { bytes[i + 3] = 255; } } static void ByteExpandAlpha(ref byte[] bytes) { var rgba = new byte[bytes.Length + (bytes.Length / 3)]; int j = 0; for (int i = 0; i < bytes.Length; i += 3) { rgba[j] = bytes[i]; rgba[j + 1] = bytes[i + 1]; rgba[j + 2] = bytes[i + 2]; rgba[j + 3] = 255; j += 4; } bytes = rgba; } static void ByteSwapRGB(byte[] bytes) { for (int i = 0; i < bytes.Length; i += 3) { byte r = bytes[i]; bytes[i] = bytes[i + 2]; bytes[i + 2] = r; } } static void ByteSwapRGBX(byte[] bytes) { for (int i = 0; i < bytes.Length; i += 4) { byte r = bytes[i]; bytes[i] = bytes[i + 2]; bytes[i + 2] = r; } } static void ByteSwapBGRA4444(byte[] bytes) { for (int i = 0; i < bytes.Length; i += 2) { var r = bytes[i] & 0xF0; var b = bytes[i + 1] & 0xF0; bytes[i] = (byte)((bytes[i] & 0x0F) | b); bytes[i + 1] = (byte)((bytes[i + 1] & 0x0F) | r); } } static void ByteSwapBGRA5551(byte[] bytes) { for (int i = 0; i < bytes.Length; i += 2) { var r = (bytes[i] & 0xF8) >> 3; var b = (bytes[i + 1] & 0x3E) >> 1; bytes[i] = (byte)((bytes[i] & 0x07) | (b << 3)); bytes[i + 1] = (byte)((bytes[i + 1] & 0xC1) | (r << 1)); } } static void ByteSwapBGR565(byte[] bytes) { for (int i = 0; i < bytes.Length; i += 2) { var r = (bytes[i] & 0xF8) >> 3; var b = bytes[i + 1] & 0x1F; bytes[i] = (byte)((bytes[i] & 0x07) | (b << 3)); bytes[i + 1] = (byte)((bytes[i + 1] & 0xE0) | r); } } internal static void WriteUncompressed(string filename, BitmapContent bitmapContent) { using (var writer = new BinaryWriter(new FileStream(filename, FileMode.Create, FileAccess.Write))) { // Write signature ("DDS ") writer.Write((byte)0x44); writer.Write((byte)0x44); writer.Write((byte)0x53); writer.Write((byte)0x20); var header = new DdsHeader(); header.dwSize = 124; header.dwFlags = Ddsd.Caps | Ddsd.Width | Ddsd.Height | Ddsd.Pitch | Ddsd.PixelFormat; header.dwWidth = (uint)bitmapContent.Width; header.dwHeight = (uint)bitmapContent.Height; header.dwPitchOrLinearSize = (uint)(bitmapContent.Width * 4); header.dwDepth = (uint)0; header.dwMipMapCount = (uint)0; writer.Write((uint)header.dwSize); writer.Write((uint)header.dwFlags); writer.Write((uint)header.dwHeight); writer.Write((uint)header.dwWidth); writer.Write((uint)header.dwPitchOrLinearSize); writer.Write((uint)header.dwDepth); writer.Write((uint)header.dwMipMapCount); // 11 unsed and reserved DWORDS. writer.Write((uint)0); writer.Write((uint)0); writer.Write((uint)0); writer.Write((uint)0); writer.Write((uint)0); writer.Write((uint)0); writer.Write((uint)0); writer.Write((uint)0); writer.Write((uint)0); writer.Write((uint)0); writer.Write((uint)0); SurfaceFormat format; if (!bitmapContent.TryGetFormat(out format) || format != SurfaceFormat.Color) throw new NotSupportedException("Unsupported bitmap content!"); header.ddspf.dwSize = 32; header.ddspf.dwFlags = Ddpf.AlphaPixels | Ddpf.Rgb; header.ddspf.dwFourCC = 0; header.ddspf.dwRgbBitCount = 32; header.ddspf.dwRBitMask = 0x000000ff; header.ddspf.dwGBitMask = 0x0000ff00; header.ddspf.dwBBitMask = 0x00ff0000; header.ddspf.dwABitMask = 0xff000000; // Write the DDS_PIXELFORMAT writer.Write((uint)header.ddspf.dwSize); writer.Write((uint)header.ddspf.dwFlags); writer.Write((uint)header.ddspf.dwFourCC); writer.Write((uint)header.ddspf.dwRgbBitCount); writer.Write((uint)header.ddspf.dwRBitMask); writer.Write((uint)header.ddspf.dwGBitMask); writer.Write((uint)header.ddspf.dwBBitMask); writer.Write((uint)header.ddspf.dwABitMask); header.dwCaps = DdsCaps.Texture; header.dwCaps2 = 0; // Continue reading DDS_HEADER writer.Write((uint)header.dwCaps); writer.Write((uint)header.dwCaps2); // More reserved unused DWORDs. writer.Write((uint)0); writer.Write((uint)0); writer.Write((uint)0); // Write out the face data. writer.Write(bitmapContent.GetPixelData()); } } } }