From 34ab1c8a36e61b386c210fb908b9ebfa86513149 Mon Sep 17 00:00:00 2001 From: Marcin Nowak Date: Thu, 24 Nov 2022 01:31:04 +0200 Subject: [PATCH] Implement loading DDS textures at run-time --- core/io/image.cpp | 10 + core/io/image.h | 2 + doc/classes/Image.xml | 7 + modules/dds/image_loader_dds.cpp | 422 +++++++++++++++++++++++++++++ modules/dds/image_loader_dds.h | 43 +++ modules/dds/register_types.cpp | 8 + modules/dds/texture_loader_dds.cpp | 362 +------------------------ tests/core/io/test_image.h | 10 + tests/data/images/icon.dds | Bin 0 -> 87536 bytes 9 files changed, 507 insertions(+), 357 deletions(-) create mode 100644 modules/dds/image_loader_dds.cpp create mode 100644 modules/dds/image_loader_dds.h create mode 100644 tests/data/images/icon.dds diff --git a/core/io/image.cpp b/core/io/image.cpp index 7326563f18..a5fea09113 100644 --- a/core/io/image.cpp +++ b/core/io/image.cpp @@ -3017,6 +3017,7 @@ ImageMemLoadFunc Image::_webp_mem_loader_func = nullptr; ImageMemLoadFunc Image::_tga_mem_loader_func = nullptr; ImageMemLoadFunc Image::_bmp_mem_loader_func = nullptr; ScalableImageMemLoadFunc Image::_svg_scalable_mem_loader_func = nullptr; +ImageMemLoadFunc Image::_dds_mem_loader_func = nullptr; void (*Image::_image_compress_bc_func)(Image *, Image::UsedChannels) = nullptr; void (*Image::_image_compress_bptc_func)(Image *, Image::UsedChannels) = nullptr; @@ -3488,6 +3489,7 @@ void Image::_bind_methods() { ClassDB::bind_method(D_METHOD("load_webp_from_buffer", "buffer"), &Image::load_webp_from_buffer); ClassDB::bind_method(D_METHOD("load_tga_from_buffer", "buffer"), &Image::load_tga_from_buffer); ClassDB::bind_method(D_METHOD("load_bmp_from_buffer", "buffer"), &Image::load_bmp_from_buffer); + ClassDB::bind_method(D_METHOD("load_dds_from_buffer", "buffer"), &Image::load_dds_from_buffer); ClassDB::bind_method(D_METHOD("load_svg_from_buffer", "buffer", "scale"), &Image::load_svg_from_buffer, DEFVAL(1.0)); ClassDB::bind_method(D_METHOD("load_svg_from_string", "svg_str", "scale"), &Image::load_svg_from_string, DEFVAL(1.0)); @@ -3863,6 +3865,14 @@ Error Image::load_svg_from_string(const String &p_svg_str, float scale) { return load_svg_from_buffer(p_svg_str.to_utf8_buffer(), scale); } +Error Image::load_dds_from_buffer(const Vector &p_array) { + ERR_FAIL_NULL_V_MSG( + _dds_mem_loader_func, + ERR_UNAVAILABLE, + "The DDS module isn't enabled. Recompile the Godot editor or export template binary with the `module_dds_enabled=yes` SCons option."); + return _load_from_buffer(p_array, _dds_mem_loader_func); +} + void Image::convert_rg_to_ra_rgba8() { ERR_FAIL_COND(format != FORMAT_RGBA8); ERR_FAIL_COND(!data.size()); diff --git a/core/io/image.h b/core/io/image.h index f877b00ee6..f68543ba24 100644 --- a/core/io/image.h +++ b/core/io/image.h @@ -150,6 +150,7 @@ public: static ImageMemLoadFunc _tga_mem_loader_func; static ImageMemLoadFunc _bmp_mem_loader_func; static ScalableImageMemLoadFunc _svg_scalable_mem_loader_func; + static ImageMemLoadFunc _dds_mem_loader_func; static void (*_image_compress_bc_func)(Image *, UsedChannels p_channels); static void (*_image_compress_bptc_func)(Image *, UsedChannels p_channels); @@ -402,6 +403,7 @@ public: Error load_webp_from_buffer(const Vector &p_array); Error load_tga_from_buffer(const Vector &p_array); Error load_bmp_from_buffer(const Vector &p_array); + Error load_dds_from_buffer(const Vector &p_array); Error load_svg_from_buffer(const Vector &p_array, float scale = 1.0); Error load_svg_from_string(const String &p_svg_str, float scale = 1.0); diff --git a/doc/classes/Image.xml b/doc/classes/Image.xml index 81954dd7de..1486990995 100644 --- a/doc/classes/Image.xml +++ b/doc/classes/Image.xml @@ -312,6 +312,13 @@ [b]Note:[/b] Godot's BMP module doesn't support 16-bit per pixel images. Only 1-bit, 4-bit, 8-bit, 24-bit, and 32-bit per pixel images are supported. + + + + + Loads an image from the binary contents of a DDS file. + + diff --git a/modules/dds/image_loader_dds.cpp b/modules/dds/image_loader_dds.cpp new file mode 100644 index 0000000000..42c8120595 --- /dev/null +++ b/modules/dds/image_loader_dds.cpp @@ -0,0 +1,422 @@ +/**************************************************************************/ +/* image_loader_dds.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "image_loader_dds.h" + +#include "core/os/os.h" + +#include "core/io/file_access.h" +#include "core/io/file_access_memory.h" + +#include + +#define PF_FOURCC(s) ((uint32_t)(((s)[3] << 24U) | ((s)[2] << 16U) | ((s)[1] << 8U) | ((s)[0]))) + +// Reference: https://docs.microsoft.com/en-us/windows/win32/direct3ddds/dds-header + +enum { + DDS_MAGIC = 0x20534444, + DDSD_PITCH = 0x00000008, + DDSD_LINEARSIZE = 0x00080000, + DDSD_MIPMAPCOUNT = 0x00020000, + DDPF_FOURCC = 0x00000004, + DDPF_ALPHAPIXELS = 0x00000001, + DDPF_INDEXED = 0x00000020, + DDPF_RGB = 0x00000040, +}; + +enum DDSFormat { + DDS_DXT1, + DDS_DXT3, + DDS_DXT5, + DDS_ATI1, + DDS_ATI2, + DDS_A2XY, + DDS_BGRA8, + DDS_BGR8, + DDS_RGBA8, //flipped in dds + DDS_RGB8, //flipped in dds + DDS_BGR5A1, + DDS_BGR565, + DDS_BGR10A2, + DDS_INDEXED, + DDS_LUMINANCE, + DDS_LUMINANCE_ALPHA, + DDS_MAX +}; + +struct DDSFormatInfo { + const char *name = nullptr; + bool compressed = false; + bool palette = false; + uint32_t divisor = 0; + uint32_t block_size = 0; + Image::Format format = Image::Format::FORMAT_BPTC_RGBA; +}; + +static const DDSFormatInfo dds_format_info[DDS_MAX] = { + { "DXT1/BC1", true, false, 4, 8, Image::FORMAT_DXT1 }, + { "DXT3/BC2", true, false, 4, 16, Image::FORMAT_DXT3 }, + { "DXT5/BC3", true, false, 4, 16, Image::FORMAT_DXT5 }, + { "ATI1/BC4", true, false, 4, 8, Image::FORMAT_RGTC_R }, + { "ATI2/3DC/BC5", true, false, 4, 16, Image::FORMAT_RGTC_RG }, + { "A2XY/DXN/BC5", true, false, 4, 16, Image::FORMAT_RGTC_RG }, + { "BGRA8", false, false, 1, 4, Image::FORMAT_RGBA8 }, + { "BGR8", false, false, 1, 3, Image::FORMAT_RGB8 }, + { "RGBA8", false, false, 1, 4, Image::FORMAT_RGBA8 }, + { "RGB8", false, false, 1, 3, Image::FORMAT_RGB8 }, + { "BGR5A1", false, false, 1, 2, Image::FORMAT_RGBA8 }, + { "BGR565", false, false, 1, 2, Image::FORMAT_RGB8 }, + { "BGR10A2", false, false, 1, 4, Image::FORMAT_RGBA8 }, + { "GRAYSCALE", false, false, 1, 1, Image::FORMAT_L8 }, + { "GRAYSCALE_ALPHA", false, false, 1, 2, Image::FORMAT_LA8 } +}; + +static Ref _dds_mem_loader_func(const uint8_t *p_buffer, int p_buffer_len) { + Ref memfile; + memfile.instantiate(); + Error open_memfile_error = memfile->open_custom(p_buffer, p_buffer_len); + ERR_FAIL_COND_V_MSG(open_memfile_error, Ref(), "Could not create memfile for DDS image buffer."); + + Ref img; + img.instantiate(); + Error load_error = ImageLoaderDDS().load_image(img, memfile, false, 1.0f); + ERR_FAIL_COND_V_MSG(load_error, Ref(), "Failed to load DDS image."); + return img; +} + +Error ImageLoaderDDS::load_image(Ref p_image, Ref f, BitField p_flags, float p_scale) { + uint32_t magic = f->get_32(); + uint32_t hsize = f->get_32(); + uint32_t flags = f->get_32(); + uint32_t height = f->get_32(); + uint32_t width = f->get_32(); + uint32_t pitch = f->get_32(); + /* uint32_t depth = */ f->get_32(); + uint32_t mipmaps = f->get_32(); + + //skip 11 + for (int i = 0; i < 11; i++) { + f->get_32(); + } + + //validate + + // We don't check DDSD_CAPS or DDSD_PIXELFORMAT, as they're mandatory when writing, + // but non-mandatory when reading (as some writers don't set them)... + if (magic != DDS_MAGIC || hsize != 124) { + ERR_FAIL_V_MSG(ERR_FILE_CORRUPT, "Invalid or unsupported DDS texture file '" + f->get_path() + "'."); + } + + /* uint32_t format_size = */ f->get_32(); + uint32_t format_flags = f->get_32(); + uint32_t format_fourcc = f->get_32(); + uint32_t format_rgb_bits = f->get_32(); + uint32_t format_red_mask = f->get_32(); + uint32_t format_green_mask = f->get_32(); + uint32_t format_blue_mask = f->get_32(); + uint32_t format_alpha_mask = f->get_32(); + + /* uint32_t caps_1 = */ f->get_32(); + /* uint32_t caps_2 = */ f->get_32(); + /* uint32_t caps_ddsx = */ f->get_32(); + + //reserved skip + f->get_32(); + f->get_32(); + + /* + print_line("DDS width: "+itos(width)); + print_line("DDS height: "+itos(height)); + print_line("DDS mipmaps: "+itos(mipmaps)); + + printf("fourcc: %x fflags: %x, rgbbits: %x, fsize: %x\n",format_fourcc,format_flags,format_rgb_bits,format_size); + printf("rmask: %x gmask: %x, bmask: %x, amask: %x\n",format_red_mask,format_green_mask,format_blue_mask,format_alpha_mask); + */ + + //must avoid this later + while (f->get_position() < 128) { + f->get_8(); + } + + DDSFormat dds_format; + + if (format_flags & DDPF_FOURCC && format_fourcc == PF_FOURCC("DXT1")) { + dds_format = DDS_DXT1; + } else if (format_flags & DDPF_FOURCC && format_fourcc == PF_FOURCC("DXT3")) { + dds_format = DDS_DXT3; + + } else if (format_flags & DDPF_FOURCC && format_fourcc == PF_FOURCC("DXT5")) { + dds_format = DDS_DXT5; + } else if (format_flags & DDPF_FOURCC && format_fourcc == PF_FOURCC("ATI1")) { + dds_format = DDS_ATI1; + } else if (format_flags & DDPF_FOURCC && format_fourcc == PF_FOURCC("ATI2")) { + dds_format = DDS_ATI2; + } else if (format_flags & DDPF_FOURCC && format_fourcc == PF_FOURCC("A2XY")) { + dds_format = DDS_A2XY; + + } else if (format_flags & DDPF_RGB && format_flags & DDPF_ALPHAPIXELS && format_rgb_bits == 32 && format_red_mask == 0xff0000 && format_green_mask == 0xff00 && format_blue_mask == 0xff && format_alpha_mask == 0xff000000) { + dds_format = DDS_BGRA8; + } else if (format_flags & DDPF_RGB && !(format_flags & DDPF_ALPHAPIXELS) && format_rgb_bits == 24 && format_red_mask == 0xff0000 && format_green_mask == 0xff00 && format_blue_mask == 0xff) { + dds_format = DDS_BGR8; + } else if (format_flags & DDPF_RGB && format_flags & DDPF_ALPHAPIXELS && format_rgb_bits == 32 && format_red_mask == 0xff && format_green_mask == 0xff00 && format_blue_mask == 0xff0000 && format_alpha_mask == 0xff000000) { + dds_format = DDS_RGBA8; + } else if (format_flags & DDPF_RGB && !(format_flags & DDPF_ALPHAPIXELS) && format_rgb_bits == 24 && format_red_mask == 0xff && format_green_mask == 0xff00 && format_blue_mask == 0xff0000) { + dds_format = DDS_RGB8; + + } else if (format_flags & DDPF_RGB && format_flags & DDPF_ALPHAPIXELS && format_rgb_bits == 16 && format_red_mask == 0x00007c00 && format_green_mask == 0x000003e0 && format_blue_mask == 0x0000001f && format_alpha_mask == 0x00008000) { + dds_format = DDS_BGR5A1; + } else if (format_flags & DDPF_RGB && format_flags & DDPF_ALPHAPIXELS && format_rgb_bits == 32 && format_red_mask == 0x3ff00000 && format_green_mask == 0xffc00 && format_blue_mask == 0x3ff && format_alpha_mask == 0xc0000000) { + dds_format = DDS_BGR10A2; + } else if (format_flags & DDPF_RGB && !(format_flags & DDPF_ALPHAPIXELS) && format_rgb_bits == 16 && format_red_mask == 0x0000f800 && format_green_mask == 0x000007e0 && format_blue_mask == 0x0000001f) { + dds_format = DDS_BGR565; + } else if (!(format_flags & DDPF_ALPHAPIXELS) && format_rgb_bits == 8 && format_red_mask == 0xff && format_green_mask == 0xff && format_blue_mask == 0xff) { + dds_format = DDS_LUMINANCE; + } else if ((format_flags & DDPF_ALPHAPIXELS) && format_rgb_bits == 16 && format_red_mask == 0xff && format_green_mask == 0xff && format_blue_mask == 0xff && format_alpha_mask == 0xff00) { + dds_format = DDS_LUMINANCE_ALPHA; + } else if (format_flags & DDPF_INDEXED && format_rgb_bits == 8) { + dds_format = DDS_BGR565; + } else { + //printf("unrecognized fourcc %x format_flags: %x - rgbbits %i - red_mask %x green mask %x blue mask %x alpha mask %x\n", format_fourcc, format_flags, format_rgb_bits, format_red_mask, format_green_mask, format_blue_mask, format_alpha_mask); + ERR_FAIL_V_MSG(ERR_FILE_CORRUPT, "Unrecognized or unsupported color layout in DDS '" + f->get_path() + "'."); + } + + if (!(flags & DDSD_MIPMAPCOUNT)) { + mipmaps = 1; + } + + Vector src_data; + + const DDSFormatInfo &info = dds_format_info[dds_format]; + uint32_t w = width; + uint32_t h = height; + + if (info.compressed) { + //compressed bc + + uint32_t size = MAX(info.divisor, w) / info.divisor * MAX(info.divisor, h) / info.divisor * info.block_size; + ERR_FAIL_COND_V(size != pitch, ERR_FILE_CORRUPT); + ERR_FAIL_COND_V(!(flags & DDSD_LINEARSIZE), ERR_FILE_CORRUPT); + + for (uint32_t i = 1; i < mipmaps; i++) { + w = MAX(1u, w >> 1); + h = MAX(1u, h >> 1); + uint32_t bsize = MAX(info.divisor, w) / info.divisor * MAX(info.divisor, h) / info.divisor * info.block_size; + //printf("%i x %i - block: %i\n",w,h,bsize); + size += bsize; + } + + src_data.resize(size); + uint8_t *wb = src_data.ptrw(); + f->get_buffer(wb, size); + + } else if (info.palette) { + //indexed + ERR_FAIL_COND_V(!(flags & DDSD_PITCH), ERR_FILE_CORRUPT); + ERR_FAIL_COND_V(format_rgb_bits != 8, ERR_FILE_CORRUPT); + + uint32_t size = pitch * height; + ERR_FAIL_COND_V(size != width * height * info.block_size, ERR_FILE_CORRUPT); + + uint8_t palette[256 * 4]; + f->get_buffer(palette, 256 * 4); + + int colsize = 3; + for (int i = 0; i < 256; i++) { + if (palette[i * 4 + 3] < 255) { + colsize = 4; + } + } + + int w2 = width; + int h2 = height; + + for (uint32_t i = 1; i < mipmaps; i++) { + w2 = (w2 + 1) >> 1; + h2 = (h2 + 1) >> 1; + size += w2 * h2 * info.block_size; + } + + src_data.resize(size + 256 * colsize); + uint8_t *wb = src_data.ptrw(); + f->get_buffer(wb, size); + + for (int i = 0; i < 256; i++) { + int dst_ofs = size + i * colsize; + int src_ofs = i * 4; + wb[dst_ofs + 0] = palette[src_ofs + 2]; + wb[dst_ofs + 1] = palette[src_ofs + 1]; + wb[dst_ofs + 2] = palette[src_ofs + 0]; + if (colsize == 4) { + wb[dst_ofs + 3] = palette[src_ofs + 3]; + } + } + } else { + //uncompressed generic... + + uint32_t size = width * height * info.block_size; + + for (uint32_t i = 1; i < mipmaps; i++) { + w = (w + 1) >> 1; + h = (h + 1) >> 1; + size += w * h * info.block_size; + } + + if (dds_format == DDS_BGR565) { + size = size * 3 / 2; + } else if (dds_format == DDS_BGR5A1) { + size = size * 2; + } + + src_data.resize(size); + uint8_t *wb = src_data.ptrw(); + f->get_buffer(wb, size); + + switch (dds_format) { + case DDS_BGR5A1: { + // TO RGBA + int colcount = size / 4; + + for (int i = colcount - 1; i >= 0; i--) { + int src_ofs = i * 2; + int dst_ofs = i * 4; + + uint8_t a = wb[src_ofs + 1] & 0x80; + uint8_t b = wb[src_ofs] & 0x1F; + uint8_t g = (wb[src_ofs] >> 5) | ((wb[src_ofs + 1] & 0x3) << 3); + uint8_t r = (wb[src_ofs + 1] >> 2) & 0x1F; + wb[dst_ofs + 0] = r << 3; + wb[dst_ofs + 1] = g << 3; + wb[dst_ofs + 2] = b << 3; + wb[dst_ofs + 3] = a ? 255 : 0; + } + } break; + case DDS_BGR565: { + int colcount = size / 3; + + for (int i = colcount - 1; i >= 0; i--) { + int src_ofs = i * 2; + int dst_ofs = i * 3; + + uint8_t b = wb[src_ofs] & 0x1F; + uint8_t g = (wb[src_ofs] >> 5) | ((wb[src_ofs + 1] & 0x7) << 3); + uint8_t r = wb[src_ofs + 1] >> 3; + wb[dst_ofs + 0] = r << 3; + wb[dst_ofs + 1] = g << 2; + wb[dst_ofs + 2] = b << 3; //b<<3; + } + + } break; + case DDS_BGR10A2: { + // TO RGBA + int colcount = size / 4; + + for (int i = colcount - 1; i >= 0; i--) { + int ofs = i * 4; + + uint32_t w32 = uint32_t(wb[ofs + 0]) | (uint32_t(wb[ofs + 1]) << 8) | (uint32_t(wb[ofs + 2]) << 16) | (uint32_t(wb[ofs + 3]) << 24); + + uint8_t a = (w32 & 0xc0000000) >> 24; + uint8_t r = (w32 & 0x3ff00000) >> 22; + uint8_t g = (w32 & 0xffc00) >> 12; + uint8_t b = (w32 & 0x3ff) >> 2; + + wb[ofs + 0] = r; + wb[ofs + 1] = g; + wb[ofs + 2] = b; + wb[ofs + 3] = a == 0xc0 ? 255 : a; //0xc0 should be opaque + } + } break; + case DDS_BGRA8: { + int colcount = size / 4; + + for (int i = 0; i < colcount; i++) { + SWAP(wb[i * 4 + 0], wb[i * 4 + 2]); + } + + } break; + case DDS_BGR8: { + int colcount = size / 3; + + for (int i = 0; i < colcount; i++) { + SWAP(wb[i * 3 + 0], wb[i * 3 + 2]); + } + } break; + case DDS_RGBA8: { + /* do nothing either + int colcount = size/4; + + for(int i=0;iset_data(width, height, mipmaps - 1, info.format, src_data); + return OK; +} + +ImageLoaderDDS::ImageLoaderDDS() { + Image::_dds_mem_loader_func = _dds_mem_loader_func; +} + +void ImageLoaderDDS::get_recognized_extensions(List *p_extensions) const { + p_extensions->push_back("dds"); +} diff --git a/modules/dds/image_loader_dds.h b/modules/dds/image_loader_dds.h new file mode 100644 index 0000000000..81cfd43551 --- /dev/null +++ b/modules/dds/image_loader_dds.h @@ -0,0 +1,43 @@ +/**************************************************************************/ +/* image_loader_dds.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef IMAGE_LOADER_DDS_H +#define IMAGE_LOADER_DDS_H + +#include "core/io/image_loader.h" + +class ImageLoaderDDS : public ImageFormatLoader { +public: + virtual Error load_image(Ref p_image, Ref f, BitField p_flags, float p_scale); + virtual void get_recognized_extensions(List *p_extensions) const; + ImageLoaderDDS(); +}; + +#endif // IMAGE_LOADER_DDS_H diff --git a/modules/dds/register_types.cpp b/modules/dds/register_types.cpp index d336269eb3..b4d406eda9 100644 --- a/modules/dds/register_types.cpp +++ b/modules/dds/register_types.cpp @@ -30,9 +30,11 @@ #include "register_types.h" +#include "image_loader_dds.h" #include "texture_loader_dds.h" static Ref resource_loader_dds; +static Ref image_loader_dds; void initialize_dds_module(ModuleInitializationLevel p_level) { if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { @@ -41,6 +43,9 @@ void initialize_dds_module(ModuleInitializationLevel p_level) { resource_loader_dds.instantiate(); ResourceLoader::add_resource_format_loader(resource_loader_dds); + + image_loader_dds.instantiate(); + ImageLoader::add_image_format_loader(image_loader_dds); } void uninitialize_dds_module(ModuleInitializationLevel p_level) { @@ -50,4 +55,7 @@ void uninitialize_dds_module(ModuleInitializationLevel p_level) { ResourceLoader::remove_resource_format_loader(resource_loader_dds); resource_loader_dds.unref(); + + ImageLoader::remove_image_format_loader(image_loader_dds); + image_loader_dds.unref(); } diff --git a/modules/dds/texture_loader_dds.cpp b/modules/dds/texture_loader_dds.cpp index 8a3a36e84b..861cf20cc2 100644 --- a/modules/dds/texture_loader_dds.cpp +++ b/modules/dds/texture_loader_dds.cpp @@ -29,72 +29,11 @@ /**************************************************************************/ #include "texture_loader_dds.h" +#include "image_loader_dds.h" #include "core/io/file_access.h" #include "scene/resources/image_texture.h" -#define PF_FOURCC(s) ((uint32_t)(((s)[3] << 24U) | ((s)[2] << 16U) | ((s)[1] << 8U) | ((s)[0]))) - -// Reference: https://docs.microsoft.com/en-us/windows/win32/direct3ddds/dds-header - -enum { - DDS_MAGIC = 0x20534444, - DDSD_PITCH = 0x00000008, - DDSD_LINEARSIZE = 0x00080000, - DDSD_MIPMAPCOUNT = 0x00020000, - DDPF_FOURCC = 0x00000004, - DDPF_ALPHAPIXELS = 0x00000001, - DDPF_INDEXED = 0x00000020, - DDPF_RGB = 0x00000040, -}; - -enum DDSFormat { - DDS_DXT1, - DDS_DXT3, - DDS_DXT5, - DDS_ATI1, - DDS_ATI2, - DDS_A2XY, - DDS_BGRA8, - DDS_BGR8, - DDS_RGBA8, //flipped in dds - DDS_RGB8, //flipped in dds - DDS_BGR5A1, - DDS_BGR565, - DDS_BGR10A2, - DDS_INDEXED, - DDS_LUMINANCE, - DDS_LUMINANCE_ALPHA, - DDS_MAX -}; - -struct DDSFormatInfo { - const char *name = nullptr; - bool compressed = false; - bool palette = false; - uint32_t divisor = 0; - uint32_t block_size = 0; - Image::Format format = Image::Format::FORMAT_BPTC_RGBA; -}; - -static const DDSFormatInfo dds_format_info[DDS_MAX] = { - { "DXT1/BC1", true, false, 4, 8, Image::FORMAT_DXT1 }, - { "DXT3/BC2", true, false, 4, 16, Image::FORMAT_DXT3 }, - { "DXT5/BC3", true, false, 4, 16, Image::FORMAT_DXT5 }, - { "ATI1/BC4", true, false, 4, 8, Image::FORMAT_RGTC_R }, - { "ATI2/3DC/BC5", true, false, 4, 16, Image::FORMAT_RGTC_RG }, - { "A2XY/DXN/BC5", true, false, 4, 16, Image::FORMAT_RGTC_RG }, - { "BGRA8", false, false, 1, 4, Image::FORMAT_RGBA8 }, - { "BGR8", false, false, 1, 3, Image::FORMAT_RGB8 }, - { "RGBA8", false, false, 1, 4, Image::FORMAT_RGBA8 }, - { "RGB8", false, false, 1, 3, Image::FORMAT_RGB8 }, - { "BGR5A1", false, false, 1, 2, Image::FORMAT_RGBA8 }, - { "BGR565", false, false, 1, 2, Image::FORMAT_RGB8 }, - { "BGR10A2", false, false, 1, 4, Image::FORMAT_RGBA8 }, - { "GRAYSCALE", false, false, 1, 1, Image::FORMAT_L8 }, - { "GRAYSCALE_ALPHA", false, false, 1, 2, Image::FORMAT_LA8 } -}; - Ref ResourceFormatDDS::load(const String &p_path, const String &p_original_path, Error *r_error, bool p_use_sub_threads, float *r_progress, CacheMode p_cache_mode) { if (r_error) { *r_error = ERR_CANT_OPEN; @@ -113,303 +52,12 @@ Ref ResourceFormatDDS::load(const String &p_path, const String &p_orig ERR_FAIL_COND_V_MSG(err != OK, Ref(), "Unable to open DDS texture file '" + p_path + "'."); - uint32_t magic = f->get_32(); - uint32_t hsize = f->get_32(); - uint32_t flags = f->get_32(); - uint32_t height = f->get_32(); - uint32_t width = f->get_32(); - uint32_t pitch = f->get_32(); - /* uint32_t depth = */ f->get_32(); - uint32_t mipmaps = f->get_32(); - - //skip 11 - for (int i = 0; i < 11; i++) { - f->get_32(); + Ref img = memnew(Image); + Error i_error = ImageLoaderDDS().load_image(img, f, false, 1.0); + if (r_error) { + *r_error = i_error; } - //validate - - // We don't check DDSD_CAPS or DDSD_PIXELFORMAT, as they're mandatory when writing, - // but non-mandatory when reading (as some writers don't set them)... - if (magic != DDS_MAGIC || hsize != 124) { - ERR_FAIL_V_MSG(Ref(), "Invalid or unsupported DDS texture file '" + p_path + "'."); - } - - /* uint32_t format_size = */ f->get_32(); - uint32_t format_flags = f->get_32(); - uint32_t format_fourcc = f->get_32(); - uint32_t format_rgb_bits = f->get_32(); - uint32_t format_red_mask = f->get_32(); - uint32_t format_green_mask = f->get_32(); - uint32_t format_blue_mask = f->get_32(); - uint32_t format_alpha_mask = f->get_32(); - - /* uint32_t caps_1 = */ f->get_32(); - /* uint32_t caps_2 = */ f->get_32(); - /* uint32_t caps_ddsx = */ f->get_32(); - - //reserved skip - f->get_32(); - f->get_32(); - - /* - print_line("DDS width: "+itos(width)); - print_line("DDS height: "+itos(height)); - print_line("DDS mipmaps: "+itos(mipmaps)); - - printf("fourcc: %x fflags: %x, rgbbits: %x, fsize: %x\n",format_fourcc,format_flags,format_rgb_bits,format_size); - printf("rmask: %x gmask: %x, bmask: %x, amask: %x\n",format_red_mask,format_green_mask,format_blue_mask,format_alpha_mask); - */ - - //must avoid this later - while (f->get_position() < 128) { - f->get_8(); - } - - DDSFormat dds_format; - - if (format_flags & DDPF_FOURCC && format_fourcc == PF_FOURCC("DXT1")) { - dds_format = DDS_DXT1; - } else if (format_flags & DDPF_FOURCC && format_fourcc == PF_FOURCC("DXT3")) { - dds_format = DDS_DXT3; - - } else if (format_flags & DDPF_FOURCC && format_fourcc == PF_FOURCC("DXT5")) { - dds_format = DDS_DXT5; - } else if (format_flags & DDPF_FOURCC && format_fourcc == PF_FOURCC("ATI1")) { - dds_format = DDS_ATI1; - } else if (format_flags & DDPF_FOURCC && format_fourcc == PF_FOURCC("ATI2")) { - dds_format = DDS_ATI2; - } else if (format_flags & DDPF_FOURCC && format_fourcc == PF_FOURCC("A2XY")) { - dds_format = DDS_A2XY; - - } else if (format_flags & DDPF_RGB && format_flags & DDPF_ALPHAPIXELS && format_rgb_bits == 32 && format_red_mask == 0xff0000 && format_green_mask == 0xff00 && format_blue_mask == 0xff && format_alpha_mask == 0xff000000) { - dds_format = DDS_BGRA8; - } else if (format_flags & DDPF_RGB && !(format_flags & DDPF_ALPHAPIXELS) && format_rgb_bits == 24 && format_red_mask == 0xff0000 && format_green_mask == 0xff00 && format_blue_mask == 0xff) { - dds_format = DDS_BGR8; - } else if (format_flags & DDPF_RGB && format_flags & DDPF_ALPHAPIXELS && format_rgb_bits == 32 && format_red_mask == 0xff && format_green_mask == 0xff00 && format_blue_mask == 0xff0000 && format_alpha_mask == 0xff000000) { - dds_format = DDS_RGBA8; - } else if (format_flags & DDPF_RGB && !(format_flags & DDPF_ALPHAPIXELS) && format_rgb_bits == 24 && format_red_mask == 0xff && format_green_mask == 0xff00 && format_blue_mask == 0xff0000) { - dds_format = DDS_RGB8; - - } else if (format_flags & DDPF_RGB && format_flags & DDPF_ALPHAPIXELS && format_rgb_bits == 16 && format_red_mask == 0x00007c00 && format_green_mask == 0x000003e0 && format_blue_mask == 0x0000001f && format_alpha_mask == 0x00008000) { - dds_format = DDS_BGR5A1; - } else if (format_flags & DDPF_RGB && format_flags & DDPF_ALPHAPIXELS && format_rgb_bits == 32 && format_red_mask == 0x3ff00000 && format_green_mask == 0xffc00 && format_blue_mask == 0x3ff && format_alpha_mask == 0xc0000000) { - dds_format = DDS_BGR10A2; - } else if (format_flags & DDPF_RGB && !(format_flags & DDPF_ALPHAPIXELS) && format_rgb_bits == 16 && format_red_mask == 0x0000f800 && format_green_mask == 0x000007e0 && format_blue_mask == 0x0000001f) { - dds_format = DDS_BGR565; - } else if (!(format_flags & DDPF_ALPHAPIXELS) && format_rgb_bits == 8 && format_red_mask == 0xff && format_green_mask == 0xff && format_blue_mask == 0xff) { - dds_format = DDS_LUMINANCE; - } else if ((format_flags & DDPF_ALPHAPIXELS) && format_rgb_bits == 16 && format_red_mask == 0xff && format_green_mask == 0xff && format_blue_mask == 0xff && format_alpha_mask == 0xff00) { - dds_format = DDS_LUMINANCE_ALPHA; - } else if (format_flags & DDPF_INDEXED && format_rgb_bits == 8) { - dds_format = DDS_BGR565; - } else { - //printf("unrecognized fourcc %x format_flags: %x - rgbbits %i - red_mask %x green mask %x blue mask %x alpha mask %x\n", format_fourcc, format_flags, format_rgb_bits, format_red_mask, format_green_mask, format_blue_mask, format_alpha_mask); - ERR_FAIL_V_MSG(Ref(), "Unrecognized or unsupported color layout in DDS '" + p_path + "'."); - } - - if (!(flags & DDSD_MIPMAPCOUNT)) { - mipmaps = 1; - } - - Vector src_data; - - const DDSFormatInfo &info = dds_format_info[dds_format]; - uint32_t w = width; - uint32_t h = height; - - if (info.compressed) { - //compressed bc - - uint32_t size = MAX(info.divisor, w) / info.divisor * MAX(info.divisor, h) / info.divisor * info.block_size; - ERR_FAIL_COND_V(size != pitch, Ref()); - ERR_FAIL_COND_V(!(flags & DDSD_LINEARSIZE), Ref()); - - for (uint32_t i = 1; i < mipmaps; i++) { - w = MAX(1u, w >> 1); - h = MAX(1u, h >> 1); - uint32_t bsize = MAX(info.divisor, w) / info.divisor * MAX(info.divisor, h) / info.divisor * info.block_size; - //printf("%i x %i - block: %i\n",w,h,bsize); - size += bsize; - } - - src_data.resize(size); - uint8_t *wb = src_data.ptrw(); - f->get_buffer(wb, size); - - } else if (info.palette) { - //indexed - ERR_FAIL_COND_V(!(flags & DDSD_PITCH), Ref()); - ERR_FAIL_COND_V(format_rgb_bits != 8, Ref()); - - uint32_t size = pitch * height; - ERR_FAIL_COND_V(size != width * height * info.block_size, Ref()); - - uint8_t palette[256 * 4]; - f->get_buffer(palette, 256 * 4); - - int colsize = 3; - for (int i = 0; i < 256; i++) { - if (palette[i * 4 + 3] < 255) { - colsize = 4; - } - } - - int w2 = width; - int h2 = height; - - for (uint32_t i = 1; i < mipmaps; i++) { - w2 = (w2 + 1) >> 1; - h2 = (h2 + 1) >> 1; - size += w2 * h2 * info.block_size; - } - - src_data.resize(size + 256 * colsize); - uint8_t *wb = src_data.ptrw(); - f->get_buffer(wb, size); - - for (int i = 0; i < 256; i++) { - int dst_ofs = size + i * colsize; - int src_ofs = i * 4; - wb[dst_ofs + 0] = palette[src_ofs + 2]; - wb[dst_ofs + 1] = palette[src_ofs + 1]; - wb[dst_ofs + 2] = palette[src_ofs + 0]; - if (colsize == 4) { - wb[dst_ofs + 3] = palette[src_ofs + 3]; - } - } - } else { - //uncompressed generic... - - uint32_t size = width * height * info.block_size; - - for (uint32_t i = 1; i < mipmaps; i++) { - w = (w + 1) >> 1; - h = (h + 1) >> 1; - size += w * h * info.block_size; - } - - if (dds_format == DDS_BGR565) { - size = size * 3 / 2; - } else if (dds_format == DDS_BGR5A1) { - size = size * 2; - } - - src_data.resize(size); - uint8_t *wb = src_data.ptrw(); - f->get_buffer(wb, size); - - switch (dds_format) { - case DDS_BGR5A1: { - // TO RGBA - int colcount = size / 4; - - for (int i = colcount - 1; i >= 0; i--) { - int src_ofs = i * 2; - int dst_ofs = i * 4; - - uint8_t a = wb[src_ofs + 1] & 0x80; - uint8_t b = wb[src_ofs] & 0x1F; - uint8_t g = (wb[src_ofs] >> 5) | ((wb[src_ofs + 1] & 0x3) << 3); - uint8_t r = (wb[src_ofs + 1] >> 2) & 0x1F; - wb[dst_ofs + 0] = r << 3; - wb[dst_ofs + 1] = g << 3; - wb[dst_ofs + 2] = b << 3; - wb[dst_ofs + 3] = a ? 255 : 0; - } - } break; - case DDS_BGR565: { - int colcount = size / 3; - - for (int i = colcount - 1; i >= 0; i--) { - int src_ofs = i * 2; - int dst_ofs = i * 3; - - uint8_t b = wb[src_ofs] & 0x1F; - uint8_t g = (wb[src_ofs] >> 5) | ((wb[src_ofs + 1] & 0x7) << 3); - uint8_t r = wb[src_ofs + 1] >> 3; - wb[dst_ofs + 0] = r << 3; - wb[dst_ofs + 1] = g << 2; - wb[dst_ofs + 2] = b << 3; //b<<3; - } - - } break; - case DDS_BGR10A2: { - // TO RGBA - int colcount = size / 4; - - for (int i = colcount - 1; i >= 0; i--) { - int ofs = i * 4; - - uint32_t w32 = uint32_t(wb[ofs + 0]) | (uint32_t(wb[ofs + 1]) << 8) | (uint32_t(wb[ofs + 2]) << 16) | (uint32_t(wb[ofs + 3]) << 24); - - uint8_t a = (w32 & 0xc0000000) >> 24; - uint8_t r = (w32 & 0x3ff00000) >> 22; - uint8_t g = (w32 & 0xffc00) >> 12; - uint8_t b = (w32 & 0x3ff) >> 2; - - wb[ofs + 0] = r; - wb[ofs + 1] = g; - wb[ofs + 2] = b; - wb[ofs + 3] = a == 0xc0 ? 255 : a; //0xc0 should be opaque - } - } break; - case DDS_BGRA8: { - int colcount = size / 4; - - for (int i = 0; i < colcount; i++) { - SWAP(wb[i * 4 + 0], wb[i * 4 + 2]); - } - - } break; - case DDS_BGR8: { - int colcount = size / 3; - - for (int i = 0; i < colcount; i++) { - SWAP(wb[i * 3 + 0], wb[i * 3 + 2]); - } - } break; - case DDS_RGBA8: { - /* do nothing either - int colcount = size/4; - - for(int i=0;i img = memnew(Image(width, height, mipmaps - 1, info.format, src_data)); Ref texture = ImageTexture::create_from_image(img); if (r_error) { diff --git a/tests/core/io/test_image.h b/tests/core/io/test_image.h index a117f518de..92ab166ae8 100644 --- a/tests/core/io/test_image.h +++ b/tests/core/io/test_image.h @@ -115,6 +115,16 @@ TEST_CASE("[Image] Saving and loading") { image_bmp->load_bmp_from_buffer(data_bmp) == OK, "The BMP image should load successfully."); + // Load DDS + Ref image_dds = memnew(Image()); + Ref f_dds = FileAccess::open(TestUtils::get_data_path("images/icon.dds"), FileAccess::READ, &err); + PackedByteArray data_dds; + data_dds.resize(f_dds->get_length() + 1); + f_dds->get_buffer(data_dds.ptrw(), f_dds->get_length()); + CHECK_MESSAGE( + image_dds->load_dds_from_buffer(data_dds) == OK, + "The DDS image should load successfully."); + // Load JPG Ref image_jpg = memnew(Image()); Ref f_jpg = FileAccess::open(TestUtils::get_data_path("images/icon.jpg"), FileAccess::READ, &err); diff --git a/tests/data/images/icon.dds b/tests/data/images/icon.dds new file mode 100644 index 0000000000000000000000000000000000000000..8a9de402cbf13d8d031586d450e0e0cb48df294d GIT binary patch literal 87536 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 literal 0 HcmV?d00001