diff --git a/Source/Core/VideoBackends/D3D/main.cpp b/Source/Core/VideoBackends/D3D/main.cpp index 48c7dd8391..5cc05f2af9 100644 --- a/Source/Core/VideoBackends/D3D/main.cpp +++ b/Source/Core/VideoBackends/D3D/main.cpp @@ -77,6 +77,7 @@ void VideoBackend::InitBackendInfo() g_Config.backend_info.bSupportsMultithreading = false; g_Config.backend_info.bSupportsInternalResolutionFrameDumps = false; g_Config.backend_info.bSupportsGPUTextureDecoding = false; + g_Config.backend_info.bSupportsST3CTextures = false; IDXGIFactory* factory; IDXGIAdapter* ad; diff --git a/Source/Core/VideoBackends/D3D12/main.cpp b/Source/Core/VideoBackends/D3D12/main.cpp index cb075fa071..d1ce50afb9 100644 --- a/Source/Core/VideoBackends/D3D12/main.cpp +++ b/Source/Core/VideoBackends/D3D12/main.cpp @@ -80,6 +80,7 @@ void VideoBackend::InitBackendInfo() g_Config.backend_info.bSupportsMultithreading = false; g_Config.backend_info.bSupportsInternalResolutionFrameDumps = false; g_Config.backend_info.bSupportsGPUTextureDecoding = false; + g_Config.backend_info.bSupportsST3CTextures = false; IDXGIFactory* factory; IDXGIAdapter* ad; diff --git a/Source/Core/VideoBackends/OGL/main.cpp b/Source/Core/VideoBackends/OGL/main.cpp index 430859932e..f9bf7c069e 100644 --- a/Source/Core/VideoBackends/OGL/main.cpp +++ b/Source/Core/VideoBackends/OGL/main.cpp @@ -103,6 +103,7 @@ void VideoBackend::InitBackendInfo() g_Config.backend_info.bSupportsPaletteConversion = true; g_Config.backend_info.bSupportsClipControl = true; g_Config.backend_info.bSupportsDepthClamp = true; + g_Config.backend_info.bSupportsST3CTextures = false; g_Config.backend_info.Adapters.clear(); diff --git a/Source/Core/VideoBackends/Software/SWmain.cpp b/Source/Core/VideoBackends/Software/SWmain.cpp index 0f67c0e6fd..1b26f1d40a 100644 --- a/Source/Core/VideoBackends/Software/SWmain.cpp +++ b/Source/Core/VideoBackends/Software/SWmain.cpp @@ -134,6 +134,7 @@ void VideoSoftware::InitBackendInfo() g_Config.backend_info.bSupportsComputeShaders = false; g_Config.backend_info.bSupportsInternalResolutionFrameDumps = false; g_Config.backend_info.bSupportsGPUTextureDecoding = false; + g_Config.backend_info.bSupportsST3CTextures = false; // aamodes g_Config.backend_info.AAModes = {1}; diff --git a/Source/Core/VideoBackends/Vulkan/VulkanContext.cpp b/Source/Core/VideoBackends/Vulkan/VulkanContext.cpp index 58b10dd92e..e51dad8091 100644 --- a/Source/Core/VideoBackends/Vulkan/VulkanContext.cpp +++ b/Source/Core/VideoBackends/Vulkan/VulkanContext.cpp @@ -245,6 +245,7 @@ void VulkanContext::PopulateBackendInfo(VideoConfig* config) config->backend_info.bSupportsFragmentStoresAndAtomics = false; // Dependent on features. config->backend_info.bSupportsSSAA = false; // Dependent on features. config->backend_info.bSupportsDepthClamp = false; // Dependent on features. + config->backend_info.bSupportsST3CTextures = false; // Dependent on features. config->backend_info.bSupportsReversedDepthRange = false; // No support yet due to driver bugs. } diff --git a/Source/Core/VideoCommon/CMakeLists.txt b/Source/Core/VideoCommon/CMakeLists.txt index 7651798e05..7eef4f715d 100644 --- a/Source/Core/VideoCommon/CMakeLists.txt +++ b/Source/Core/VideoCommon/CMakeLists.txt @@ -14,6 +14,7 @@ set(SRCS GeometryShaderGen.cpp GeometryShaderManager.cpp HiresTextures.cpp + HiresTextures_DDSLoader.cpp ImageWrite.cpp IndexGenerator.cpp LightingShaderGen.cpp diff --git a/Source/Core/VideoCommon/HiresTextures.cpp b/Source/Core/VideoCommon/HiresTextures.cpp index f4ad7448a5..d087ee8575 100644 --- a/Source/Core/VideoCommon/HiresTextures.cpp +++ b/Source/Core/VideoCommon/HiresTextures.cpp @@ -404,23 +404,18 @@ std::unique_ptr HiresTexture::Load(const std::string& base_filenam filename += StringFromFormat("_mip%u", level); } - if (s_textureMap.find(filename) != s_textureMap.end()) + auto filename_iter = s_textureMap.find(filename); + if (filename_iter != s_textureMap.end()) { Level l; File::IOFile file; - file.Open(s_textureMap[filename], "rb"); + file.Open(filename_iter->second, "rb"); std::vector buffer(file.GetSize()); file.ReadBytes(buffer.data(), file.GetSize()); - int channels; - l.data = - SOILPointer(SOIL_load_image_from_memory(buffer.data(), (int)buffer.size(), (int*)&l.width, - (int*)&l.height, &channels, SOIL_LOAD_RGBA), - SOIL_free_image_data); - l.data_size = (size_t)l.width * l.height * 4; - - if (l.data == nullptr) + // Try loading DDS textures first, that way we maintain compression of DXT formats. + if (!LoadDDSTexture(l, buffer) && !LoadTexture(l, buffer)) { ERROR_LOG(VIDEO, "Custom texture %s failed to load", filename.c_str()); break; @@ -467,9 +462,39 @@ std::unique_ptr HiresTexture::Load(const std::string& base_filenam } } + // All levels have to have the same format. + if (ret && std::any_of(ret->m_levels.begin(), ret->m_levels.end(), + [&ret](const Level& l) { return l.format != ret->m_levels[0].format; })) + { + ERROR_LOG(VIDEO, "Custom texture %s has inconsistent formats across mip levels.", + base_filename.c_str()); + ret.reset(); + } + return ret; } +bool HiresTexture::LoadTexture(Level& level, const std::vector& buffer) +{ + int channels; + int width; + int height; + + u8* data = SOIL_load_image_from_memory(buffer.data(), static_cast(buffer.size()), &width, + &height, &channels, SOIL_LOAD_RGBA); + if (!data) + return false; + + // Images loaded by SOIL are converted to RGBA. + level.width = static_cast(width); + level.height = static_cast(height); + level.format = HostTextureFormat::RGBA8; + level.data = ImageDataPointer(data, SOIL_free_image_data); + level.row_length = level.width; + level.data_size = static_cast(level.row_length) * 4 * level.height; + return true; +} + std::string HiresTexture::GetTextureDirectory(const std::string& game_id) { const std::string texture_directory = File::GetUserPath(D_HIRESTEXTURES_IDX) + game_id; @@ -484,3 +509,8 @@ std::string HiresTexture::GetTextureDirectory(const std::string& game_id) HiresTexture::~HiresTexture() { } + +HostTextureFormat HiresTexture::GetFormat() const +{ + return m_levels.at(0).format; +} diff --git a/Source/Core/VideoCommon/HiresTextures.h b/Source/Core/VideoCommon/HiresTextures.h index b9ddc3d793..c11b6f140b 100644 --- a/Source/Core/VideoCommon/HiresTextures.h +++ b/Source/Core/VideoCommon/HiresTextures.h @@ -9,11 +9,12 @@ #include #include "Common/CommonTypes.h" +#include "VideoCommon/VideoCommon.h" class HiresTexture { public: - using SOILPointer = std::unique_ptr; + using ImageDataPointer = std::unique_ptr; static void Init(); static void Update(); @@ -29,20 +30,25 @@ public: ~HiresTexture(); + HostTextureFormat GetFormat() const; struct Level { Level(); - SOILPointer data; - size_t data_size = 0; + ImageDataPointer data; + HostTextureFormat format = HostTextureFormat::RGBA8; u32 width = 0; u32 height = 0; + u32 row_length = 0; + size_t data_size = 0; }; std::vector m_levels; private: static std::unique_ptr Load(const std::string& base_filename, u32 width, u32 height); + static bool LoadDDSTexture(Level& level, const std::vector& buffer); + static bool LoadTexture(Level& level, const std::vector& buffer); static void Prefetch(); static std::string GetTextureDirectory(const std::string& game_id); diff --git a/Source/Core/VideoCommon/HiresTextures_DDSLoader.cpp b/Source/Core/VideoCommon/HiresTextures_DDSLoader.cpp new file mode 100644 index 0000000000..d7e32443a9 --- /dev/null +++ b/Source/Core/VideoCommon/HiresTextures_DDSLoader.cpp @@ -0,0 +1,257 @@ +// Copyright 2017 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include "VideoCommon/HiresTextures.h" +#include +#include +#include +#include +#include "Common/Align.h" +#include "VideoCommon/VideoConfig.h" + +namespace +{ +// From https://raw.githubusercontent.com/Microsoft/DirectXTex/master/DirectXTex/DDS.h +// +// This header defines constants and structures that are useful when parsing +// DDS files. DDS files were originally designed to use several structures +// and constants that are native to DirectDraw and are defined in ddraw.h, +// such as DDSURFACEDESC2 and DDSCAPS2. This file defines similar +// (compatible) constants and structures so that one can use DDS files +// without needing to include ddraw.h. +// +// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF +// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A +// PARTICULAR PURPOSE. +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// http://go.microsoft.com/fwlink/?LinkId=248926 + +#pragma pack(push, 1) + +const uint32_t DDS_MAGIC = 0x20534444; // "DDS " + +struct DDS_PIXELFORMAT +{ + uint32_t dwSize; + uint32_t dwFlags; + uint32_t dwFourCC; + uint32_t dwRGBBitCount; + uint32_t dwRBitMask; + uint32_t dwGBitMask; + uint32_t dwBBitMask; + uint32_t dwABitMask; +}; + +#define DDS_FOURCC 0x00000004 // DDPF_FOURCC +#define DDS_RGB 0x00000040 // DDPF_RGB +#define DDS_RGBA 0x00000041 // DDPF_RGB | DDPF_ALPHAPIXELS +#define DDS_LUMINANCE 0x00020000 // DDPF_LUMINANCE +#define DDS_LUMINANCEA 0x00020001 // DDPF_LUMINANCE | DDPF_ALPHAPIXELS +#define DDS_ALPHA 0x00000002 // DDPF_ALPHA +#define DDS_PAL8 0x00000020 // DDPF_PALETTEINDEXED8 +#define DDS_PAL8A 0x00000021 // DDPF_PALETTEINDEXED8 | DDPF_ALPHAPIXELS +#define DDS_BUMPDUDV 0x00080000 // DDPF_BUMPDUDV + +#ifndef MAKEFOURCC +#define MAKEFOURCC(ch0, ch1, ch2, ch3) \ + ((uint32_t)(uint8_t)(ch0) | ((uint32_t)(uint8_t)(ch1) << 8) | ((uint32_t)(uint8_t)(ch2) << 16) | \ + ((uint32_t)(uint8_t)(ch3) << 24)) +#endif /* defined(MAKEFOURCC) */ + +#define DDS_HEADER_FLAGS_TEXTURE \ + 0x00001007 // DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT +#define DDS_HEADER_FLAGS_MIPMAP 0x00020000 // DDSD_MIPMAPCOUNT +#define DDS_HEADER_FLAGS_VOLUME 0x00800000 // DDSD_DEPTH +#define DDS_HEADER_FLAGS_PITCH 0x00000008 // DDSD_PITCH +#define DDS_HEADER_FLAGS_LINEARSIZE 0x00080000 // DDSD_LINEARSIZE + +#define DDS_HEIGHT 0x00000002 // DDSD_HEIGHT +#define DDS_WIDTH 0x00000004 // DDSD_WIDTH + +#define DDS_SURFACE_FLAGS_TEXTURE 0x00001000 // DDSCAPS_TEXTURE +#define DDS_SURFACE_FLAGS_MIPMAP 0x00400008 // DDSCAPS_COMPLEX | DDSCAPS_MIPMAP +#define DDS_SURFACE_FLAGS_CUBEMAP 0x00000008 // DDSCAPS_COMPLEX + +#define DDS_CUBEMAP_POSITIVEX 0x00000600 // DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEX +#define DDS_CUBEMAP_NEGATIVEX 0x00000a00 // DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEX +#define DDS_CUBEMAP_POSITIVEY 0x00001200 // DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEY +#define DDS_CUBEMAP_NEGATIVEY 0x00002200 // DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEY +#define DDS_CUBEMAP_POSITIVEZ 0x00004200 // DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEZ +#define DDS_CUBEMAP_NEGATIVEZ 0x00008200 // DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEZ + +#define DDS_CUBEMAP_ALLFACES \ + (DDS_CUBEMAP_POSITIVEX | DDS_CUBEMAP_NEGATIVEX | DDS_CUBEMAP_POSITIVEY | DDS_CUBEMAP_NEGATIVEY | \ + DDS_CUBEMAP_POSITIVEZ | DDS_CUBEMAP_NEGATIVEZ) + +#define DDS_CUBEMAP 0x00000200 // DDSCAPS2_CUBEMAP + +#define DDS_FLAGS_VOLUME 0x00200000 // DDSCAPS2_VOLUME + +// Subset here matches D3D10_RESOURCE_DIMENSION and D3D11_RESOURCE_DIMENSION +enum DDS_RESOURCE_DIMENSION +{ + DDS_DIMENSION_TEXTURE1D = 2, + DDS_DIMENSION_TEXTURE2D = 3, + DDS_DIMENSION_TEXTURE3D = 4, +}; + +// Subset here matches D3D10_RESOURCE_MISC_FLAG and D3D11_RESOURCE_MISC_FLAG +enum DDS_RESOURCE_MISC_FLAG +{ + DDS_RESOURCE_MISC_TEXTURECUBE = 0x4L, +}; + +enum DDS_MISC_FLAGS2 +{ + DDS_MISC_FLAGS2_ALPHA_MODE_MASK = 0x7L, +}; + +enum DDS_ALPHA_MODE +{ + DDS_ALPHA_MODE_UNKNOWN = 0, + DDS_ALPHA_MODE_STRAIGHT = 1, + DDS_ALPHA_MODE_PREMULTIPLIED = 2, + DDS_ALPHA_MODE_OPAQUE = 3, + DDS_ALPHA_MODE_CUSTOM = 4, +}; + +struct DDS_HEADER +{ + uint32_t dwSize; + uint32_t dwFlags; + uint32_t dwHeight; + uint32_t dwWidth; + uint32_t dwPitchOrLinearSize; + uint32_t dwDepth; // only if DDS_HEADER_FLAGS_VOLUME is set in dwFlags + uint32_t dwMipMapCount; + uint32_t dwReserved1[11]; + DDS_PIXELFORMAT ddspf; + uint32_t dwCaps; + uint32_t dwCaps2; + uint32_t dwCaps3; + uint32_t dwCaps4; + uint32_t dwReserved2; +}; + +struct DDS_HEADER_DXT10 +{ + uint32_t dxgiFormat; + uint32_t resourceDimension; + uint32_t miscFlag; // see DDS_RESOURCE_MISC_FLAG + uint32_t arraySize; + uint32_t miscFlags2; // see DDS_MISC_FLAGS2 +}; + +#pragma pack(pop) + +static_assert(sizeof(DDS_HEADER) == 124, "DDS Header size mismatch"); +static_assert(sizeof(DDS_HEADER_DXT10) == 20, "DDS DX10 Extended Header size mismatch"); + +} // namespace + +bool HiresTexture::LoadDDSTexture(Level& level, const std::vector& buffer) +{ + u32 magic; + std::memcpy(&magic, buffer.data(), sizeof(magic)); + + // Exit as early as possible for non-DDS textures, since all extensions are currently + // passed through this function. + if (magic != DDS_MAGIC) + return false; + + DDS_HEADER header; + std::memcpy(&header, &buffer[sizeof(magic)], sizeof(header)); + if (header.dwSize < sizeof(header)) + return false; + + // Required fields. + if ((header.dwFlags & DDS_HEADER_FLAGS_TEXTURE) != DDS_HEADER_FLAGS_TEXTURE) + return false; + + // Image should be 2D. + if (header.dwFlags & DDS_HEADER_FLAGS_VOLUME) + return false; + + // Presence of width/height fields is already tested by DDS_HEADER_FLAGS_TEXTURE. + level.width = header.dwWidth; + level.height = header.dwHeight; + + // Currently, we only handle compressed textures here, and leave the rest to the SOIL loader. + // In the future, this could be extended, but these isn't much benefit in doing so currently. + // TODO: DX10 extension header handling. + u32 block_size = 1; + u32 bytes_per_block = 4; + bool needs_s3tc = false; + if (header.ddspf.dwFourCC == MAKEFOURCC('D', 'X', 'T', '1')) + { + level.format = HostTextureFormat::DXT1; + block_size = 4; + bytes_per_block = 8; + needs_s3tc = true; + } + else if (header.ddspf.dwFourCC == MAKEFOURCC('D', 'X', 'T', '3')) + { + level.format = HostTextureFormat::DXT3; + block_size = 4; + bytes_per_block = 16; + needs_s3tc = true; + } + else if (header.ddspf.dwFourCC == MAKEFOURCC('D', 'X', 'T', '5')) + { + level.format = HostTextureFormat::DXT5; + block_size = 4; + bytes_per_block = 16; + needs_s3tc = true; + } + else + { + // Leave all remaining formats to SOIL. + return false; + } + + // We also need to ensure the backend supports these formats natively before loading them, + // otherwise, fallback to SOIL, which will decompress them to RGBA. + if (needs_s3tc && !g_ActiveConfig.backend_info.bSupportsST3CTextures) + return false; + + // Mip levels smaller than the block size are padded to multiples of the block size. + u32 blocks_wide = std::max(level.width / block_size, 1u); + u32 blocks_high = std::max(level.height / block_size, 1u); + + // Pitch can be specified in the header, otherwise we can derive it from the dimensions. For + // compressed formats, both DDS_HEADER_FLAGS_LINEARSIZE and DDS_HEADER_FLAGS_PITCH should be + // set. See https://msdn.microsoft.com/en-us/library/windows/desktop/bb943982(v=vs.85).aspx + if (header.dwFlags & DDS_HEADER_FLAGS_PITCH && header.dwFlags & DDS_HEADER_FLAGS_LINEARSIZE) + { + // Convert pitch (in bytes) to texels/row length. + if (header.dwPitchOrLinearSize < bytes_per_block) + { + // Likely a corrupted or invalid file. + return false; + } + + level.row_length = std::max(header.dwPitchOrLinearSize / bytes_per_block, 1u) * block_size; + level.data_size = static_cast(level.row_length / block_size) * block_size * blocks_high; + } + else + { + // Assume no padding between rows of blocks. + level.row_length = blocks_wide * block_size; + level.data_size = blocks_wide * static_cast(bytes_per_block) * blocks_high; + } + + // Check for truncated or corrupted files. + size_t data_offset = sizeof(magic) + sizeof(DDS_HEADER); + if ((data_offset + level.data_size) > buffer.size()) + return false; + + // Copy to the final storage location. The deallocator here is simple, nothing extra is + // needed, compared to the SOIL-based loader. + level.data = ImageDataPointer(new u8[level.data_size], [](u8* data) { delete[] data; }); + std::memcpy(level.data.get(), &buffer[data_offset], level.data_size); + return true; +} diff --git a/Source/Core/VideoCommon/VideoCommon.h b/Source/Core/VideoCommon/VideoCommon.h index e25b2b66a5..cff2883318 100644 --- a/Source/Core/VideoCommon/VideoCommon.h +++ b/Source/Core/VideoCommon/VideoCommon.h @@ -77,6 +77,15 @@ enum class APIType Nothing }; +// Texture formats that videocommon can upload/use. +enum class HostTextureFormat : u32 +{ + RGBA8, + DXT1, + DXT3, + DXT5 +}; + inline u32 RGBA8ToRGBA6ToRGBA8(u32 src) { u32 color = src; diff --git a/Source/Core/VideoCommon/VideoCommon.vcxproj b/Source/Core/VideoCommon/VideoCommon.vcxproj index e742711edf..0a8f60781c 100644 --- a/Source/Core/VideoCommon/VideoCommon.vcxproj +++ b/Source/Core/VideoCommon/VideoCommon.vcxproj @@ -69,6 +69,7 @@ + @@ -185,4 +186,4 @@ - + \ No newline at end of file diff --git a/Source/Core/VideoCommon/VideoCommon.vcxproj.filters b/Source/Core/VideoCommon/VideoCommon.vcxproj.filters index e9a742a409..ec0150a272 100644 --- a/Source/Core/VideoCommon/VideoCommon.vcxproj.filters +++ b/Source/Core/VideoCommon/VideoCommon.vcxproj.filters @@ -164,6 +164,9 @@ Shader Generators + + Util + @@ -203,9 +206,6 @@ Decoding - - Decoding - Register Sections @@ -321,4 +321,4 @@ - + \ No newline at end of file diff --git a/Source/Core/VideoCommon/VideoConfig.cpp b/Source/Core/VideoCommon/VideoConfig.cpp index 604bdb4bdd..3865095f4b 100644 --- a/Source/Core/VideoCommon/VideoConfig.cpp +++ b/Source/Core/VideoCommon/VideoConfig.cpp @@ -41,6 +41,7 @@ VideoConfig::VideoConfig() backend_info.bSupportsExclusiveFullscreen = false; backend_info.bSupportsMultithreading = false; backend_info.bSupportsInternalResolutionFrameDumps = false; + backend_info.bSupportsST3CTextures = false; bEnableValidationLayer = false; bBackendMultithreading = true; diff --git a/Source/Core/VideoCommon/VideoConfig.h b/Source/Core/VideoCommon/VideoConfig.h index eacb30d8b1..44eac32dc9 100644 --- a/Source/Core/VideoCommon/VideoConfig.h +++ b/Source/Core/VideoCommon/VideoConfig.h @@ -197,6 +197,7 @@ struct VideoConfig final bool bSupportsMultithreading; bool bSupportsInternalResolutionFrameDumps; bool bSupportsGPUTextureDecoding; + bool bSupportsST3CTextures; } backend_info; // Utility