diff --git a/Source/Core/Common/Common.vcxproj b/Source/Core/Common/Common.vcxproj index 15418f6b29..8a40dc7c58 100644 --- a/Source/Core/Common/Common.vcxproj +++ b/Source/Core/Common/Common.vcxproj @@ -93,6 +93,7 @@ + @@ -225,4 +226,4 @@ - + \ No newline at end of file diff --git a/Source/Core/Common/Common.vcxproj.filters b/Source/Core/Common/Common.vcxproj.filters index 4cd0ac4af8..ee4f949251 100644 --- a/Source/Core/Common/Common.vcxproj.filters +++ b/Source/Core/Common/Common.vcxproj.filters @@ -248,6 +248,9 @@ GL\GLExtensions + + GL\GLExtensions + @@ -321,4 +324,4 @@ - + \ No newline at end of file diff --git a/Source/Core/Common/GL/GLExtensions/EXT_texture_compression_s3tc.h b/Source/Core/Common/GL/GLExtensions/EXT_texture_compression_s3tc.h new file mode 100644 index 0000000000..5eb17791c6 --- /dev/null +++ b/Source/Core/Common/GL/GLExtensions/EXT_texture_compression_s3tc.h @@ -0,0 +1,29 @@ +/* +** Copyright (c) 2013-2015 The Khronos Group Inc. +** +** Permission is hereby granted, free of charge, to any person obtaining a +** copy of this software and/or associated documentation files (the +** "Materials"), to deal in the Materials without restriction, including +** without limitation the rights to use, copy, modify, merge, publish, +** distribute, sublicense, and/or sell copies of the Materials, and to +** permit persons to whom the Materials are 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 Materials. +** +** THE MATERIALS ARE 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 +** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. +*/ + +#include "Common/GL/GLExtensions/gl_common.h" + +#define GL_COMPRESSED_RGB_S3TC_DXT1_EXT 0x83F0 +#define GL_COMPRESSED_RGBA_S3TC_DXT1_EXT 0x83F1 +#define GL_COMPRESSED_RGBA_S3TC_DXT3_EXT 0x83F2 +#define GL_COMPRESSED_RGBA_S3TC_DXT5_EXT 0x83F3 diff --git a/Source/Core/Common/GL/GLExtensions/GLExtensions.h b/Source/Core/Common/GL/GLExtensions/GLExtensions.h index 49e9fc282f..7745d78e60 100644 --- a/Source/Core/Common/GL/GLExtensions/GLExtensions.h +++ b/Source/Core/Common/GL/GLExtensions/GLExtensions.h @@ -31,6 +31,7 @@ #include "Common/GL/GLExtensions/ARB_uniform_buffer_object.h" #include "Common/GL/GLExtensions/ARB_vertex_array_object.h" #include "Common/GL/GLExtensions/ARB_viewport_array.h" +#include "Common/GL/GLExtensions/EXT_texture_compression_s3tc.h" #include "Common/GL/GLExtensions/EXT_texture_filter_anisotropic.h" #include "Common/GL/GLExtensions/HP_occlusion_test.h" #include "Common/GL/GLExtensions/KHR_debug.h" diff --git a/Source/Core/VideoBackends/D3D/D3DBase.cpp b/Source/Core/VideoBackends/D3D/D3DBase.cpp index 5b4328cb7f..06ee82b450 100644 --- a/Source/Core/VideoBackends/D3D/D3DBase.cpp +++ b/Source/Core/VideoBackends/D3D/D3DBase.cpp @@ -239,6 +239,19 @@ D3D_FEATURE_LEVEL GetFeatureLevel(IDXGIAdapter* adapter) return feat_level; } +static bool SupportsS3TCTextures(ID3D11Device* device) +{ + UINT bc1_support, bc2_support, bc3_support; + if (FAILED(device->CheckFormatSupport(DXGI_FORMAT_BC1_UNORM, &bc1_support)) || + FAILED(device->CheckFormatSupport(DXGI_FORMAT_BC2_UNORM, &bc2_support)) || + FAILED(device->CheckFormatSupport(DXGI_FORMAT_BC3_UNORM, &bc3_support))) + { + return false; + } + + return ((bc1_support & bc2_support & bc3_support) & D3D11_FORMAT_SUPPORT_TEXTURE2D) != 0; +} + HRESULT Create(HWND wnd) { hWnd = wnd; @@ -427,6 +440,7 @@ HRESULT Create(HWND wnd) UINT format_support; device->CheckFormatSupport(DXGI_FORMAT_B8G8R8A8_UNORM, &format_support); bgra_textures_supported = (format_support & D3D11_FORMAT_SUPPORT_TEXTURE2D) != 0; + g_Config.backend_info.bSupportsST3CTextures = SupportsS3TCTextures(device); stateman = new StateManager; return S_OK; diff --git a/Source/Core/VideoBackends/D3D/TextureCache.cpp b/Source/Core/VideoBackends/D3D/TextureCache.cpp index d6369f290f..cd737e7201 100644 --- a/Source/Core/VideoBackends/D3D/TextureCache.cpp +++ b/Source/Core/VideoBackends/D3D/TextureCache.cpp @@ -7,6 +7,7 @@ #include #include +#include "Common/Assert.h" #include "Common/CommonTypes.h" #include "Common/Logging/Log.h" @@ -29,6 +30,25 @@ static std::unique_ptr g_encoder; const size_t MAX_COPY_BUFFERS = 32; ID3D11Buffer* efbcopycbuf[MAX_COPY_BUFFERS] = {0}; +static DXGI_FORMAT GetDXGIFormatForHostFormat(HostTextureFormat format) +{ + switch (format) + { + case HostTextureFormat::DXT1: + return DXGI_FORMAT_BC1_UNORM; + + case HostTextureFormat::DXT3: + return DXGI_FORMAT_BC2_UNORM; + + case HostTextureFormat::DXT5: + return DXGI_FORMAT_BC3_UNORM; + + case HostTextureFormat::RGBA8: + default: + return DXGI_FORMAT_R8G8B8A8_UNORM; + } +} + TextureCache::TCacheEntry::~TCacheEntry() { texture->Release(); @@ -41,6 +61,11 @@ void TextureCache::TCacheEntry::Bind(unsigned int stage) bool TextureCache::TCacheEntry::Save(const std::string& filename, unsigned int level) { + // We can't dump compressed textures currently (it would mean drawing them to a RGBA8 + // framebuffer, and saving that). TextureCache does not call Save for custom textures + // anyway, so this is fine for now. + _assert_(config.format == HostTextureFormat::RGBA8); + // Create a staging/readback texture with the dimensions of the specified mip level. u32 mip_width = std::max(config.width >> level, 1u); u32 mip_height = std::max(config.height >> level, 1u); @@ -129,28 +154,30 @@ void TextureCache::TCacheEntry::CopyRectangleFromTexture(const TCacheEntryBase* g_renderer->RestoreAPIState(); } -void TextureCache::TCacheEntry::Load(const u8* buffer, u32 width, u32 height, u32 expanded_width, - u32 level) +void TextureCache::TCacheEntry::Load(u32 level, u32 width, u32 height, u32 row_length, + const u8* buffer, size_t buffer_size) { - unsigned int src_pitch = 4 * expanded_width; - D3D::context->UpdateSubresource(texture->GetTex(), level, nullptr, buffer, src_pitch, 0); + size_t src_pitch = CalculateHostTextureLevelPitch(config.format, row_length); + D3D::context->UpdateSubresource(texture->GetTex(), level, nullptr, buffer, + static_cast(src_pitch), 0); } TextureCacheBase::TCacheEntryBase* TextureCache::CreateTexture(const TCacheEntryConfig& config) { + DXGI_FORMAT dxgi_format = GetDXGIFormatForHostFormat(config.format); if (config.rendertarget) { return new TCacheEntry( - config, D3DTexture2D::Create( - config.width, config.height, (D3D11_BIND_FLAG)((int)D3D11_BIND_RENDER_TARGET | - (int)D3D11_BIND_SHADER_RESOURCE), - D3D11_USAGE_DEFAULT, DXGI_FORMAT_R8G8B8A8_UNORM, 1, config.layers)); + config, D3DTexture2D::Create(config.width, config.height, + (D3D11_BIND_FLAG)((int)D3D11_BIND_RENDER_TARGET | + (int)D3D11_BIND_SHADER_RESOURCE), + D3D11_USAGE_DEFAULT, dxgi_format, 1, config.layers)); } else { const D3D11_TEXTURE2D_DESC texdesc = - CD3D11_TEXTURE2D_DESC(DXGI_FORMAT_R8G8B8A8_UNORM, config.width, config.height, 1, - config.levels, D3D11_BIND_SHADER_RESOURCE, D3D11_USAGE_DEFAULT, 0); + CD3D11_TEXTURE2D_DESC(dxgi_format, config.width, config.height, 1, config.levels, + D3D11_BIND_SHADER_RESOURCE, D3D11_USAGE_DEFAULT, 0); ID3D11Texture2D* pTexture; const HRESULT hr = D3D::device->CreateTexture2D(&texdesc, nullptr, &pTexture); diff --git a/Source/Core/VideoBackends/D3D/TextureCache.h b/Source/Core/VideoBackends/D3D/TextureCache.h index 01b229cf72..78450eefdd 100644 --- a/Source/Core/VideoBackends/D3D/TextureCache.h +++ b/Source/Core/VideoBackends/D3D/TextureCache.h @@ -30,7 +30,8 @@ private: const MathUtil::Rectangle& srcrect, const MathUtil::Rectangle& dstrect) override; - void Load(const u8* buffer, u32 width, u32 height, u32 expanded_width, u32 levels) override; + void Load(u32 level, u32 width, u32 height, u32 row_length, const u8* buffer, + size_t buffer_size) override; void FromRenderTarget(bool is_depth_copy, const EFBRectangle& srcRect, bool scaleByHalf, unsigned int cbufid, const float* colmat) override; 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/D3DBase.cpp b/Source/Core/VideoBackends/D3D12/D3DBase.cpp index 9f1da4042e..56c5bc6534 100644 --- a/Source/Core/VideoBackends/D3D12/D3DBase.cpp +++ b/Source/Core/VideoBackends/D3D12/D3DBase.cpp @@ -262,6 +262,21 @@ std::vector EnumAAModes(ID3D12Device* device) return aa_modes; } +static bool SupportsS3TCTextures(ID3D12Device* device) +{ + auto CheckForFormat = [](ID3D12Device* device, DXGI_FORMAT format) { + D3D12_FEATURE_DATA_FORMAT_SUPPORT data = {format}; + if (FAILED(device->CheckFeatureSupport(D3D12_FEATURE_FORMAT_SUPPORT, &data, sizeof(data)))) + return false; + + return (data.Support1 & D3D12_FORMAT_SUPPORT1_TEXTURE2D) != 0; + }; + + return CheckForFormat(device, DXGI_FORMAT_BC1_UNORM) && + CheckForFormat(device, DXGI_FORMAT_BC2_UNORM) && + CheckForFormat(device, DXGI_FORMAT_BC3_UNORM); +} + HRESULT Create(HWND wnd) { hWnd = wnd; @@ -478,6 +493,8 @@ HRESULT Create(HWND wnd) SAFE_RELEASE(factory); SAFE_RELEASE(adapter); + g_Config.backend_info.bSupportsST3CTextures = SupportsS3TCTextures(device12); + return S_OK; } diff --git a/Source/Core/VideoBackends/D3D12/TextureCache.cpp b/Source/Core/VideoBackends/D3D12/TextureCache.cpp index d626cb7732..4b0a47b158 100644 --- a/Source/Core/VideoBackends/D3D12/TextureCache.cpp +++ b/Source/Core/VideoBackends/D3D12/TextureCache.cpp @@ -33,6 +33,25 @@ static u32 s_efb_copy_last_cbuf_id = UINT_MAX; static ID3D12Resource* s_texture_cache_entry_readback_buffer = nullptr; static size_t s_texture_cache_entry_readback_buffer_size = 0; +static DXGI_FORMAT GetDXGIFormatForHostFormat(HostTextureFormat format) +{ + switch (format) + { + case HostTextureFormat::DXT1: + return DXGI_FORMAT_BC1_UNORM; + + case HostTextureFormat::DXT3: + return DXGI_FORMAT_BC2_UNORM; + + case HostTextureFormat::DXT5: + return DXGI_FORMAT_BC3_UNORM; + + case HostTextureFormat::RGBA8: + default: + return DXGI_FORMAT_R8G8B8A8_UNORM; + } +} + TextureCache::TCacheEntry::~TCacheEntry() { m_texture->Release(); @@ -51,6 +70,11 @@ bool TextureCache::TCacheEntry::Save(const std::string& filename, unsigned int l Common::AlignUp(level_width * sizeof(u32), D3D12_TEXTURE_DATA_PITCH_ALIGNMENT); size_t required_readback_buffer_size = level_pitch * level_height; + // We can't dump compressed textures currently (it would mean drawing them to a RGBA8 + // framebuffer, and saving that). TextureCache does not call Save for custom textures + // anyway, so this is fine for now. + _assert_(config.format == HostTextureFormat::RGBA8); + // Check if the current readback buffer is large enough if (required_readback_buffer_size > s_texture_cache_entry_readback_buffer_size) { @@ -175,22 +199,24 @@ void TextureCache::TCacheEntry::CopyRectangleFromTexture(const TCacheEntryBase* g_renderer->RestoreAPIState(); } -void TextureCache::TCacheEntry::Load(const u8* buffer, u32 width, u32 height, u32 expanded_width, - u32 level) +void TextureCache::TCacheEntry::Load(u32 level, u32 width, u32 height, u32 row_length, + const u8* buffer, size_t buffer_size) { - unsigned int src_pitch = 4 * expanded_width; - D3D::ReplaceRGBATexture2D(m_texture->GetTex12(), buffer, width, height, src_pitch, level, + size_t src_pitch = CalculateHostTextureLevelPitch(config.format, row_length); + D3D::ReplaceRGBATexture2D(m_texture->GetTex12(), buffer, width, height, + static_cast(src_pitch), level, m_texture->GetResourceUsageState()); } TextureCacheBase::TCacheEntryBase* TextureCache::CreateTexture(const TCacheEntryConfig& config) { + DXGI_FORMAT dxgi_format = GetDXGIFormatForHostFormat(config.format); if (config.rendertarget) { D3DTexture2D* texture = D3DTexture2D::Create(config.width, config.height, TEXTURE_BIND_FLAG_SHADER_RESOURCE | TEXTURE_BIND_FLAG_RENDER_TARGET, - DXGI_FORMAT_R8G8B8A8_UNORM, 1, config.layers); + dxgi_format, 1, config.layers); TCacheEntry* entry = new TCacheEntry(config, texture); @@ -204,8 +230,8 @@ TextureCacheBase::TCacheEntryBase* TextureCache::CreateTexture(const TCacheEntry { ID3D12Resource* texture_resource = nullptr; - D3D12_RESOURCE_DESC texture_resource_desc = CD3DX12_RESOURCE_DESC::Tex2D( - DXGI_FORMAT_R8G8B8A8_UNORM, config.width, config.height, 1, config.levels); + D3D12_RESOURCE_DESC texture_resource_desc = + CD3DX12_RESOURCE_DESC::Tex2D(dxgi_format, config.width, config.height, 1, config.levels); CheckHR(D3D::device12->CreateCommittedResource( &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT), D3D12_HEAP_FLAG_NONE, diff --git a/Source/Core/VideoBackends/D3D12/TextureCache.h b/Source/Core/VideoBackends/D3D12/TextureCache.h index f6921afe5b..fb8cdf3385 100644 --- a/Source/Core/VideoBackends/D3D12/TextureCache.h +++ b/Source/Core/VideoBackends/D3D12/TextureCache.h @@ -39,7 +39,8 @@ private: const MathUtil::Rectangle& src_rect, const MathUtil::Rectangle& dst_rect) override; - void Load(const u8* buffer, u32 width, u32 height, u32 expanded_width, u32 levels) override; + void Load(u32 level, u32 width, u32 height, u32 row_length, const u8* buffer, + size_t buffer_size) override; void FromRenderTarget(bool is_depth_copy, const EFBRectangle& src_rect, bool scale_by_half, unsigned int cbuf_id, const float* colmat) override; 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/Null/NullBackend.cpp b/Source/Core/VideoBackends/Null/NullBackend.cpp index 7892b86927..bcbbd1aaab 100644 --- a/Source/Core/VideoBackends/Null/NullBackend.cpp +++ b/Source/Core/VideoBackends/Null/NullBackend.cpp @@ -45,6 +45,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; // aamodes: We only support 1 sample, so no MSAA g_Config.backend_info.Adapters.clear(); diff --git a/Source/Core/VideoBackends/Null/TextureCache.h b/Source/Core/VideoBackends/Null/TextureCache.h index 48d111dce6..399af04764 100644 --- a/Source/Core/VideoBackends/Null/TextureCache.h +++ b/Source/Core/VideoBackends/Null/TextureCache.h @@ -31,7 +31,10 @@ private: { TCacheEntry(const TCacheEntryConfig& _config) : TCacheEntryBase(_config) {} ~TCacheEntry() {} - void Load(const u8* buffer, u32 width, u32 height, u32 expanded_width, u32 level) override {} + void Load(u32 level, u32 width, u32 height, u32 row_length, const u8* buffer, + size_t buffer_size) override + { + } void FromRenderTarget(bool is_depth_copy, const EFBRectangle& src_rect, bool scale_by_half, unsigned int cbufid, const float* colmat) override { diff --git a/Source/Core/VideoBackends/OGL/Render.cpp b/Source/Core/VideoBackends/OGL/Render.cpp index ce58be68cc..360bc76a70 100644 --- a/Source/Core/VideoBackends/OGL/Render.cpp +++ b/Source/Core/VideoBackends/OGL/Render.cpp @@ -462,6 +462,8 @@ Renderer::Renderer() g_ogl_config.bSupportsConservativeDepth = GLExtensions::Supports("GL_ARB_conservative_depth"); g_ogl_config.bSupportsAniso = GLExtensions::Supports("GL_EXT_texture_filter_anisotropic"); g_Config.backend_info.bSupportsComputeShaders = GLExtensions::Supports("GL_ARB_compute_shader"); + g_Config.backend_info.bSupportsST3CTextures = + GLExtensions::Supports("GL_EXT_texture_compression_s3tc"); if (GLInterface->GetMode() == GLInterfaceMode::MODE_OPENGLES3) { diff --git a/Source/Core/VideoBackends/OGL/TextureCache.cpp b/Source/Core/VideoBackends/OGL/TextureCache.cpp index 3bf1342809..2031d7e220 100644 --- a/Source/Core/VideoBackends/OGL/TextureCache.cpp +++ b/Source/Core/VideoBackends/OGL/TextureCache.cpp @@ -11,6 +11,7 @@ #include #include +#include "Common/Assert.h" #include "Common/GL/GLInterfaceBase.h" #include "Common/MsgHandler.h" #include "Common/StringUtil.h" @@ -87,6 +88,48 @@ bool SaveTexture(const std::string& filename, u32 textarget, u32 tex, int virtua return TextureToPng(data.data(), width * 4, filename, width, height, true); } +static GLenum GetGLInternalFormatForTextureFormat(HostTextureFormat format, bool storage) +{ + switch (format) + { + case HostTextureFormat::DXT1: + return GL_COMPRESSED_RGBA_S3TC_DXT1_EXT; + case HostTextureFormat::DXT3: + return GL_COMPRESSED_RGBA_S3TC_DXT3_EXT; + case HostTextureFormat::DXT5: + return GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; + case HostTextureFormat::RGBA8: + default: + return storage ? GL_RGBA8 : GL_RGBA; + } +} + +static GLenum GetGLFormatForTextureFormat(HostTextureFormat format) +{ + switch (format) + { + case HostTextureFormat::RGBA8: + return GL_RGBA; + + // Compressed texture formats don't use this parameter. + default: + return GL_UNSIGNED_BYTE; + } +} + +static GLenum GetGLTypeForTextureFormat(HostTextureFormat format) +{ + switch (format) + { + case HostTextureFormat::RGBA8: + return GL_UNSIGNED_BYTE; + + // Compressed texture formats don't use this parameter. + default: + return GL_UNSIGNED_BYTE; + } +} + TextureCache::TCacheEntry::~TCacheEntry() { if (texture) @@ -129,6 +172,11 @@ void TextureCache::TCacheEntry::Bind(unsigned int stage) bool TextureCache::TCacheEntry::Save(const std::string& filename, unsigned int level) { + // We can't dump compressed textures currently (it would mean drawing them to a RGBA8 + // framebuffer, and saving that). TextureCache does not call Save for custom textures + // anyway, so this is fine for now. + _assert_(config.format == HostTextureFormat::RGBA8); + return SaveTexture(filename, GL_TEXTURE_2D_ARRAY, texture, config.width, config.height, level); } @@ -143,12 +191,15 @@ TextureCache::TCacheEntryBase* TextureCache::CreateTexture(const TCacheEntryConf if (g_ogl_config.bSupportsTextureStorage) { - glTexStorage3D(GL_TEXTURE_2D_ARRAY, config.levels, GL_RGBA8, config.width, config.height, - config.layers); + GLenum gl_internal_format = GetGLInternalFormatForTextureFormat(config.format, true); + glTexStorage3D(GL_TEXTURE_2D_ARRAY, config.levels, gl_internal_format, config.width, + config.height, config.layers); } if (config.rendertarget) { + // We can't render to compressed formats. + _assert_(!IsCompressedHostTextureFormat(config.format)); if (!g_ogl_config.bSupportsTextureStorage) { for (u32 level = 0; level < config.levels; level++) @@ -202,8 +253,8 @@ void TextureCache::TCacheEntry::CopyRectangleFromTexture(const TCacheEntryBase* g_renderer->RestoreAPIState(); } -void TextureCache::TCacheEntry::Load(const u8* buffer, u32 width, u32 height, u32 expanded_width, - u32 level) +void TextureCache::TCacheEntry::Load(u32 level, u32 width, u32 height, u32 row_length, + const u8* buffer, size_t buffer_size) { if (level >= config.levels) PanicAlert("Texture only has %d levels, can't update level %d", config.levels, level); @@ -216,21 +267,40 @@ void TextureCache::TCacheEntry::Load(const u8* buffer, u32 width, u32 height, u3 glActiveTexture(GL_TEXTURE9); glBindTexture(GL_TEXTURE_2D_ARRAY, texture); - if (expanded_width != width) - glPixelStorei(GL_UNPACK_ROW_LENGTH, expanded_width); + if (row_length != width) + glPixelStorei(GL_UNPACK_ROW_LENGTH, row_length); - if (g_ogl_config.bSupportsTextureStorage) + GLenum gl_internal_format = GetGLInternalFormatForTextureFormat(config.format, false); + if (IsCompressedHostTextureFormat(config.format)) { - glTexSubImage3D(GL_TEXTURE_2D_ARRAY, level, 0, 0, 0, width, height, 1, GL_RGBA, - GL_UNSIGNED_BYTE, buffer); + if (g_ogl_config.bSupportsTextureStorage) + { + glCompressedTexSubImage3D(GL_TEXTURE_2D_ARRAY, level, 0, 0, 0, width, height, 1, + gl_internal_format, static_cast(buffer_size), buffer); + } + else + { + glCompressedTexImage3D(GL_TEXTURE_2D_ARRAY, level, gl_internal_format, width, height, 1, 0, + static_cast(buffer_size), buffer); + } } else { - glTexImage3D(GL_TEXTURE_2D_ARRAY, level, GL_RGBA, width, height, 1, 0, GL_RGBA, - GL_UNSIGNED_BYTE, buffer); + GLenum gl_format = GetGLFormatForTextureFormat(config.format); + GLenum gl_type = GetGLTypeForTextureFormat(config.format); + if (g_ogl_config.bSupportsTextureStorage) + { + glTexSubImage3D(GL_TEXTURE_2D_ARRAY, level, 0, 0, 0, width, height, 1, gl_format, gl_type, + buffer); + } + else + { + glTexImage3D(GL_TEXTURE_2D_ARRAY, level, gl_internal_format, width, height, 1, 0, gl_format, + gl_type, buffer); + } } - if (expanded_width != width) + if (row_length != width) glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); TextureCache::SetStage(); diff --git a/Source/Core/VideoBackends/OGL/TextureCache.h b/Source/Core/VideoBackends/OGL/TextureCache.h index aece35cd6c..de0de485f5 100644 --- a/Source/Core/VideoBackends/OGL/TextureCache.h +++ b/Source/Core/VideoBackends/OGL/TextureCache.h @@ -45,7 +45,8 @@ private: const MathUtil::Rectangle& srcrect, const MathUtil::Rectangle& dstrect) override; - void Load(const u8* buffer, u32 width, u32 height, u32 expanded_width, u32 level) override; + void Load(u32 level, u32 width, u32 height, u32 row_length, const u8* buffer, + size_t buffer_size) override; void FromRenderTarget(bool is_depth_copy, const EFBRectangle& srcRect, bool scaleByHalf, unsigned int cbufid, const float* colmat) override; 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..e004a0cac3 100644 --- a/Source/Core/VideoBackends/Software/SWmain.cpp +++ b/Source/Core/VideoBackends/Software/SWmain.cpp @@ -65,7 +65,10 @@ private: { TCacheEntry(const TCacheEntryConfig& _config) : TCacheEntryBase(_config) {} ~TCacheEntry() {} - void Load(const u8* buffer, u32 width, u32 height, u32 expanded_width, u32 level) override {} + void Load(u32 level, u32 width, u32 height, u32 row_length, const u8* buffer, + size_t buffer_size) override + { + } void FromRenderTarget(bool is_depth_copy, const EFBRectangle& srcRect, bool scaleByHalf, unsigned int cbufid, const float* colmat) override { @@ -134,6 +137,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/Constants.h b/Source/Core/VideoBackends/Vulkan/Constants.h index 7b6b6d3583..9d1ab3fecd 100644 --- a/Source/Core/VideoBackends/Vulkan/Constants.h +++ b/Source/Core/VideoBackends/Vulkan/Constants.h @@ -109,7 +109,7 @@ constexpr size_t MAXIMUM_TEXTURE_UPLOAD_BUFFER_SIZE = 64 * 1024 * 1024; // streaming buffer and be blocking frequently. Games are unlikely to have textures this // large anyway, so it's only really an issue for HD texture packs, and memory is not // a limiting factor in these scenarios anyway. -constexpr size_t STAGING_TEXTURE_UPLOAD_THRESHOLD = 1024 * 1024 * 4; +constexpr size_t STAGING_TEXTURE_UPLOAD_THRESHOLD = 1024 * 1024 * 8; // Streaming uniform buffer size constexpr size_t INITIAL_UNIFORM_STREAM_BUFFER_SIZE = 16 * 1024 * 1024; diff --git a/Source/Core/VideoBackends/Vulkan/StagingBuffer.cpp b/Source/Core/VideoBackends/Vulkan/StagingBuffer.cpp index 6bd8170fc9..6924f41eca 100644 --- a/Source/Core/VideoBackends/Vulkan/StagingBuffer.cpp +++ b/Source/Core/VideoBackends/Vulkan/StagingBuffer.cpp @@ -127,7 +127,7 @@ void StagingBuffer::InvalidateCPUCache(VkDeviceSize offset, VkDeviceSize size) void StagingBuffer::Read(VkDeviceSize offset, void* data, size_t size, bool invalidate_caches) { _assert_((offset + size) <= m_size); - _assert_(offset >= m_map_offset && size < (m_map_size + (offset - m_map_offset))); + _assert_(offset >= m_map_offset && size <= (m_map_size + (offset - m_map_offset))); if (invalidate_caches) InvalidateCPUCache(offset, size); @@ -138,7 +138,7 @@ void StagingBuffer::Write(VkDeviceSize offset, const void* data, size_t size, bool invalidate_caches) { _assert_((offset + size) <= m_size); - _assert_(offset >= m_map_offset && size < (m_map_size + (offset - m_map_offset))); + _assert_(offset >= m_map_offset && size <= (m_map_size + (offset - m_map_offset))); memcpy(m_map_pointer + (offset - m_map_offset), data, size); if (invalidate_caches) diff --git a/Source/Core/VideoBackends/Vulkan/TextureCache.cpp b/Source/Core/VideoBackends/Vulkan/TextureCache.cpp index 08c370b48f..67faf980ea 100644 --- a/Source/Core/VideoBackends/Vulkan/TextureCache.cpp +++ b/Source/Core/VideoBackends/Vulkan/TextureCache.cpp @@ -9,6 +9,7 @@ #include #include +#include "Common/Align.h" #include "Common/Assert.h" #include "Common/CommonFuncs.h" #include "Common/Logging/Log.h" @@ -238,9 +239,10 @@ TextureCacheBase::TCacheEntryBase* TextureCache::CreateTexture(const TCacheEntry usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; // Allocate texture object + VkFormat vk_format = Util::GetVkFormatForHostTextureFormat(config.format); std::unique_ptr texture = Texture2D::Create( - config.width, config.height, config.levels, config.layers, TEXTURECACHE_TEXTURE_FORMAT, - VK_SAMPLE_COUNT_1_BIT, VK_IMAGE_VIEW_TYPE_2D_ARRAY, VK_IMAGE_TILING_OPTIMAL, usage); + config.width, config.height, config.levels, config.layers, vk_format, VK_SAMPLE_COUNT_1_BIT, + VK_IMAGE_VIEW_TYPE_2D_ARRAY, VK_IMAGE_TILING_OPTIMAL, usage); if (!texture) return nullptr; @@ -341,8 +343,8 @@ TextureCache::TCacheEntry::~TCacheEntry() g_command_buffer_mgr->DeferFramebufferDestruction(m_framebuffer); } -void TextureCache::TCacheEntry::Load(const u8* buffer, unsigned int width, unsigned int height, - unsigned int expanded_width, unsigned int level) +void TextureCache::TCacheEntry::Load(u32 level, u32 width, u32 height, u32 row_length, + const u8* buffer, size_t buffer_size) { // Can't copy data larger than the texture extents. width = std::max(1u, std::min(width, m_texture->GetWidth() >> level)); @@ -366,87 +368,68 @@ void TextureCache::TCacheEntry::Load(const u8* buffer, unsigned int width, unsig m_texture->TransitionToLayout(g_command_buffer_mgr->GetCurrentInitCommandBuffer(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); - // Does this texture data fit within the streaming buffer? - u32 upload_width = width; - u32 upload_pitch = upload_width * sizeof(u32); - u32 upload_size = upload_pitch * height; + // For unaligned textures, we can save some memory in the transfer buffer by skipping the rows + // that lie outside of the texture's dimensions. u32 upload_alignment = static_cast(g_vulkan_context->GetBufferImageGranularity()); - u32 source_pitch = expanded_width * 4; - if ((upload_size + upload_alignment) <= STAGING_TEXTURE_UPLOAD_THRESHOLD && - (upload_size + upload_alignment) <= MAXIMUM_TEXTURE_UPLOAD_BUFFER_SIZE) - { - // Assume tightly packed rows, with no padding as the buffer source. - StreamBuffer* upload_buffer = TextureCache::GetInstance()->m_texture_upload_buffer.get(); + u32 block_size = Util::GetBlockSize(m_texture->GetFormat()); + u32 num_rows = Common::AlignUp(height, block_size) / block_size; + size_t source_pitch = CalculateHostTextureLevelPitch(config.format, row_length); + size_t upload_size = source_pitch * num_rows; + std::unique_ptr temp_buffer; + VkBuffer upload_buffer; + VkDeviceSize upload_buffer_offset; - // Allocate memory from the streaming buffer for the texture data. - if (!upload_buffer->ReserveMemory(upload_size, g_vulkan_context->GetBufferImageGranularity())) + // Does this texture data fit within the streaming buffer? + if (upload_size <= STAGING_TEXTURE_UPLOAD_THRESHOLD && + upload_size <= MAXIMUM_TEXTURE_UPLOAD_BUFFER_SIZE) + { + StreamBuffer* stream_buffer = TextureCache::GetInstance()->m_texture_upload_buffer.get(); + if (!stream_buffer->ReserveMemory(upload_size, upload_alignment)) { // Execute the command buffer first. WARN_LOG(VIDEO, "Executing command list while waiting for space in texture upload buffer"); Util::ExecuteCurrentCommandsAndRestoreState(false); // Try allocating again. This may cause a fence wait. - if (!upload_buffer->ReserveMemory(upload_size, g_vulkan_context->GetBufferImageGranularity())) + if (!stream_buffer->ReserveMemory(upload_size, upload_alignment)) PanicAlert("Failed to allocate space in texture upload buffer"); } - // Grab buffer pointers - VkBuffer image_upload_buffer = upload_buffer->GetBuffer(); - VkDeviceSize image_upload_buffer_offset = upload_buffer->GetCurrentOffset(); - u8* image_upload_buffer_pointer = upload_buffer->GetCurrentHostPointer(); - - // Copy to the buffer using the stride from the subresource layout - const u8* source_ptr = buffer; - if (upload_pitch != source_pitch) - { - VkDeviceSize copy_pitch = std::min(source_pitch, upload_pitch); - for (unsigned int row = 0; row < height; row++) - { - memcpy(image_upload_buffer_pointer + row * upload_pitch, source_ptr + row * source_pitch, - copy_pitch); - } - } - else - { - // Can copy the whole thing in one block, the pitch matches - memcpy(image_upload_buffer_pointer, source_ptr, upload_size); - } - - // Flush buffer memory if necessary - upload_buffer->CommitMemory(upload_size); - - // Copy from the streaming buffer to the actual image. - VkBufferImageCopy image_copy = { - image_upload_buffer_offset, // VkDeviceSize bufferOffset - 0, // uint32_t bufferRowLength - 0, // uint32_t bufferImageHeight - {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, 1}, // VkImageSubresourceLayers imageSubresource - {0, 0, 0}, // VkOffset3D imageOffset - {width, height, 1} // VkExtent3D imageExtent - }; - vkCmdCopyBufferToImage(g_command_buffer_mgr->GetCurrentInitCommandBuffer(), image_upload_buffer, - m_texture->GetImage(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, - &image_copy); + // Copy to the streaming buffer. + upload_buffer = stream_buffer->GetBuffer(); + upload_buffer_offset = stream_buffer->GetCurrentOffset(); + std::memcpy(stream_buffer->GetCurrentHostPointer(), buffer, upload_size); + stream_buffer->CommitMemory(upload_size); } else { - // Slow path. The data for the image is too large to fit in the streaming buffer, so we need - // to allocate a temporary texture to store the data in, then copy to the real texture. - std::unique_ptr staging_texture = StagingTexture2D::Create( - STAGING_BUFFER_TYPE_UPLOAD, width, height, TEXTURECACHE_TEXTURE_FORMAT); - - if (!staging_texture || !staging_texture->Map()) + // Create a temporary staging buffer that is destroyed after the image is copied. + temp_buffer = StagingBuffer::Create(STAGING_BUFFER_TYPE_UPLOAD, upload_size, + VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + if (!temp_buffer || !temp_buffer->Map()) { PanicAlert("Failed to allocate staging texture for large texture upload."); return; } - // Copy data to staging texture first, then to the "real" texture. - staging_texture->WriteTexels(0, 0, width, height, buffer, source_pitch); - staging_texture->CopyToImage(g_command_buffer_mgr->GetCurrentInitCommandBuffer(), - m_texture->GetImage(), VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, width, - height, level, 0); + upload_buffer = temp_buffer->GetBuffer(); + upload_buffer_offset = 0; + temp_buffer->Write(0, buffer, upload_size, true); + temp_buffer->Unmap(); } + + // Copy from the streaming buffer to the actual image. + VkBufferImageCopy image_copy = { + upload_buffer_offset, // VkDeviceSize bufferOffset + row_length, // uint32_t bufferRowLength + 0, // uint32_t bufferImageHeight + {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, 1}, // VkImageSubresourceLayers imageSubresource + {0, 0, 0}, // VkOffset3D imageOffset + {width, height, 1} // VkExtent3D imageExtent + }; + vkCmdCopyBufferToImage(g_command_buffer_mgr->GetCurrentInitCommandBuffer(), upload_buffer, + m_texture->GetImage(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, + &image_copy); } void TextureCache::TCacheEntry::FromRenderTarget(bool is_depth_copy, const EFBRectangle& src_rect, @@ -544,6 +527,11 @@ bool TextureCache::TCacheEntry::Save(const std::string& filename, unsigned int l { _assert_(level < config.levels); + // We can't dump compressed textures currently (it would mean drawing them to a RGBA8 + // framebuffer, and saving that). TextureCache does not call Save for custom textures + // anyway, so this is fine for now. + _assert_(config.format == HostTextureFormat::RGBA8); + // Determine dimensions of image we want to save. u32 level_width = std::max(1u, config.width >> level); u32 level_height = std::max(1u, config.height >> level); @@ -582,7 +570,8 @@ bool TextureCache::TCacheEntry::Save(const std::string& filename, unsigned int l // It's okay to throw this texture away immediately, since we're done with it, and // we blocked until the copy completed on the GPU anyway. bool result = TextureToPng(reinterpret_cast(staging_texture->GetMapPointer()), - staging_texture->GetRowStride(), filename, level_width, level_height); + static_cast(staging_texture->GetRowStride()), filename, + level_width, level_height); staging_texture->Unmap(); return result; diff --git a/Source/Core/VideoBackends/Vulkan/TextureCache.h b/Source/Core/VideoBackends/Vulkan/TextureCache.h index 5fd25c23d8..884d82ddf4 100644 --- a/Source/Core/VideoBackends/Vulkan/TextureCache.h +++ b/Source/Core/VideoBackends/Vulkan/TextureCache.h @@ -27,8 +27,8 @@ public: Texture2D* GetTexture() const { return m_texture.get(); } VkFramebuffer GetFramebuffer() const { return m_framebuffer; } - void Load(const u8* buffer, unsigned int width, unsigned int height, - unsigned int expanded_width, unsigned int level) override; + void Load(u32 level, u32 width, u32 height, u32 row_length, const u8* buffer, + size_t buffer_size) override; void FromRenderTarget(bool is_depth_copy, const EFBRectangle& src_rect, bool scale_by_half, unsigned int cbufid, const float* colmat) override; void CopyRectangleFromTexture(const TCacheEntryBase* source, diff --git a/Source/Core/VideoBackends/Vulkan/Util.cpp b/Source/Core/VideoBackends/Vulkan/Util.cpp index 5de96e23cb..7ce433a79e 100644 --- a/Source/Core/VideoBackends/Vulkan/Util.cpp +++ b/Source/Core/VideoBackends/Vulkan/Util.cpp @@ -53,6 +53,20 @@ bool IsDepthFormat(VkFormat format) } } +bool IsCompressedFormat(VkFormat format) +{ + switch (format) + { + case VK_FORMAT_BC1_RGBA_UNORM_BLOCK: + case VK_FORMAT_BC2_UNORM_BLOCK: + case VK_FORMAT_BC3_UNORM_BLOCK: + return true; + + default: + return false; + } +} + VkFormat GetLinearFormat(VkFormat format) { switch (format) @@ -74,6 +88,25 @@ VkFormat GetLinearFormat(VkFormat format) } } +VkFormat GetVkFormatForHostTextureFormat(HostTextureFormat format) +{ + switch (format) + { + case HostTextureFormat::DXT1: + return VK_FORMAT_BC1_RGBA_UNORM_BLOCK; + + case HostTextureFormat::DXT3: + return VK_FORMAT_BC2_UNORM_BLOCK; + + case HostTextureFormat::DXT5: + return VK_FORMAT_BC3_UNORM_BLOCK; + + case HostTextureFormat::RGBA8: + default: + return VK_FORMAT_R8G8B8A8_UNORM; + } +} + u32 GetTexelSize(VkFormat format) { // Only contains pixel formats we use. @@ -91,12 +124,33 @@ u32 GetTexelSize(VkFormat format) case VK_FORMAT_B8G8R8A8_UNORM: return 4; + case VK_FORMAT_BC1_RGBA_UNORM_BLOCK: + return 8; + + case VK_FORMAT_BC2_UNORM_BLOCK: + case VK_FORMAT_BC3_UNORM_BLOCK: + return 16; + default: PanicAlert("Unhandled pixel format"); return 1; } } +u32 GetBlockSize(VkFormat format) +{ + switch (format) + { + case VK_FORMAT_BC1_RGBA_UNORM_BLOCK: + case VK_FORMAT_BC2_UNORM_BLOCK: + case VK_FORMAT_BC3_UNORM_BLOCK: + return 4; + + default: + return 1; + } +} + VkRect2D ClampRect2D(const VkRect2D& rect, u32 width, u32 height) { VkRect2D out; diff --git a/Source/Core/VideoBackends/Vulkan/Util.h b/Source/Core/VideoBackends/Vulkan/Util.h index d61c699bfe..85d447cf75 100644 --- a/Source/Core/VideoBackends/Vulkan/Util.h +++ b/Source/Core/VideoBackends/Vulkan/Util.h @@ -25,8 +25,11 @@ size_t AlignBufferOffset(size_t offset, size_t alignment); u32 MakeRGBA8Color(float r, float g, float b, float a); bool IsDepthFormat(VkFormat format); +bool IsCompressedFormat(VkFormat format); VkFormat GetLinearFormat(VkFormat format); +VkFormat GetVkFormatForHostTextureFormat(HostTextureFormat format); u32 GetTexelSize(VkFormat format); +u32 GetBlockSize(VkFormat format); // Clamps a VkRect2D to the specified dimensions. VkRect2D ClampRect2D(const VkRect2D& rect, u32 width, u32 height); diff --git a/Source/Core/VideoBackends/Vulkan/VulkanContext.cpp b/Source/Core/VideoBackends/Vulkan/VulkanContext.cpp index 58b10dd92e..8978cd2df6 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. } @@ -283,6 +284,9 @@ void VulkanContext::PopulateBackendInfoFeatures(VideoConfig* config, VkPhysicalD config->backend_info.bSupportsDepthClamp = (features.depthClamp == VK_TRUE && features.shaderClipDistance == VK_TRUE); + // textureCompressionBC implies BC1 through BC7, which is a superset of DXT1/3/5, which we need. + config->backend_info.bSupportsST3CTextures = features.textureCompressionBC == VK_TRUE; + // Our usage of primitive restart appears to be broken on AMD's binary drivers. // Seems to be fine on GCN Gen 1-2, unconfirmed on GCN Gen 3, causes driver resets on GCN Gen 4. if (DriverDetails::HasBug(DriverDetails::BUG_PRIMITIVE_RESTART)) @@ -459,6 +463,7 @@ bool VulkanContext::SelectDeviceFeatures() m_device_features.occlusionQueryPrecise = available_features.occlusionQueryPrecise; m_device_features.shaderClipDistance = available_features.shaderClipDistance; m_device_features.depthClamp = available_features.depthClamp; + m_device_features.textureCompressionBC = available_features.textureCompressionBC; return true; } 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..d601370fb5 100644 --- a/Source/Core/VideoCommon/HiresTextures.cpp +++ b/Source/Core/VideoCommon/HiresTextures.cpp @@ -366,6 +366,21 @@ std::string HiresTexture::GenBaseName(const u8* texture, size_t texture_size, co return name; } +u32 HiresTexture::CalculateMipCount(u32 width, u32 height) +{ + u32 mip_width = width; + u32 mip_height = height; + u32 mip_count = 1; + while (mip_width > 1 || mip_height > 1) + { + mip_width = std::max(mip_width / 2, 1u); + mip_height = std::max(mip_height / 2, 1u); + mip_count++; + } + + return mip_count; +} + std::shared_ptr HiresTexture::Search(const u8* texture, size_t texture_size, const u8* tlut, size_t tlut_size, u32 width, u32 height, int format, bool has_mipmaps) @@ -395,81 +410,134 @@ std::shared_ptr HiresTexture::Search(const u8* texture, size_t tex std::unique_ptr HiresTexture::Load(const std::string& base_filename, u32 width, u32 height) { - std::unique_ptr ret; - for (int level = 0;; level++) + // We need to have a level 0 custom texture to even consider loading. + auto filename_iter = s_textureMap.find(base_filename); + if (filename_iter == s_textureMap.end()) + return nullptr; + + // Try to load level 0 (and any mipmaps) from a DDS file. + // If this fails, it's fine, we'll just load level0 again using SOIL. + // Can't use make_unique due to private constructor. + std::unique_ptr ret = std::unique_ptr(new HiresTexture()); + const std::string& first_mip_filename = filename_iter->second; + LoadDDSTexture(ret.get(), first_mip_filename); + + // Load remaining mip levels, or from the start if it's not a DDS texture. + for (u32 mip_level = static_cast(ret->m_levels.size());; mip_level++) { std::string filename = base_filename; - if (level) - { - filename += StringFromFormat("_mip%u", level); - } + if (mip_level != 0) + filename += StringFromFormat("_mip%u", mip_level); - if (s_textureMap.find(filename) != s_textureMap.end()) - { - Level l; + filename_iter = s_textureMap.find(filename); + if (filename_iter == s_textureMap.end()) + break; + // Try loading DDS textures first, that way we maintain compression of DXT formats. + // TODO: Reduce the number of open() calls here. We could use one fd. + Level level; + if (!LoadDDSTexture(level, filename_iter->second)) + { 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) + if (!LoadTexture(level, buffer)) { ERROR_LOG(VIDEO, "Custom texture %s failed to load", filename.c_str()); break; } + } - if (!level) - { - if (l.width * height != l.height * width) - ERROR_LOG(VIDEO, "Invalid custom texture size %dx%d for texture %s. The aspect differs " - "from the native size %dx%d.", - l.width, l.height, filename.c_str(), width, height); - if (width && height && (l.width % width || l.height % height)) - WARN_LOG(VIDEO, "Invalid custom texture size %dx%d for texture %s. Please use an integer " - "upscaling factor based on the native size %dx%d.", - l.width, l.height, filename.c_str(), width, height); - width = l.width; - height = l.height; - } - else if (width != l.width || height != l.height) - { - ERROR_LOG( - VIDEO, - "Invalid custom texture size %dx%d for texture %s. This mipmap layer _must_ be %dx%d.", - l.width, l.height, filename.c_str(), width, height); - l.data.reset(); - break; - } + ret->m_levels.push_back(std::move(level)); + } - if (!ret) - ret = std::unique_ptr(new HiresTexture); - ret->m_levels.push_back(std::move(l)); + // If we failed to load any mip levels, we can't use this texture at all. + if (ret->m_levels.empty()) + return nullptr; - // no more mipmaps available - if (width == 1 && height == 1) - break; + // Verify that the aspect ratio of the texture hasn't changed, as this could have side-effects. + const Level& first_mip = ret->m_levels[0]; + if (first_mip.width * height != first_mip.height * width) + { + ERROR_LOG(VIDEO, "Invalid custom texture size %ux%u for texture %s. The aspect differs " + "from the native size %ux%u.", + first_mip.width, first_mip.height, first_mip_filename.c_str(), width, height); + } - // calculate the size of the next mipmap - width = std::max(1u, width >> 1); - height = std::max(1u, height >> 1); + // Same deal if the custom texture isn't a multiple of the native size. + if (width != 0 && height != 0 && (first_mip.width % width || first_mip.height % height)) + { + ERROR_LOG(VIDEO, "Invalid custom texture size %ux%u for texture %s. Please use an integer " + "upscaling factor based on the native size %ux%u.", + first_mip.width, first_mip.height, first_mip_filename.c_str(), width, height); + } + + // Verify that each mip level is the correct size (divide by 2 each time). + u32 current_mip_width = first_mip.width; + u32 current_mip_height = first_mip.height; + for (u32 mip_level = 1; mip_level < static_cast(ret->m_levels.size()); mip_level++) + { + if (current_mip_width != 1 || current_mip_height != 1) + { + current_mip_width = std::max(current_mip_width / 2, 1u); + current_mip_height = std::max(current_mip_height / 2, 1u); + + const Level& level = ret->m_levels[mip_level]; + if (current_mip_width == level.width && current_mip_height == level.height) + continue; + + ERROR_LOG(VIDEO, + "Invalid custom texture size %dx%d for texture %s. Mipmap level %u must be %dx%d.", + level.width, level.height, first_mip_filename.c_str(), mip_level, current_mip_width, + current_mip_height); } else { - break; + // It is invalid to have more than a single 1x1 mipmap. + ERROR_LOG(VIDEO, "Custom texture %s has too many 1x1 mipmaps. Skipping extra levels.", + first_mip_filename.c_str()); } + + // Drop this mip level and any others after it. + while (ret->m_levels.size() > mip_level) + ret->m_levels.pop_back(); + } + + // All levels have to have the same format. + if (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.", + first_mip_filename.c_str()); + + return nullptr; } 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 +552,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..30bb96d40c 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(); @@ -27,22 +28,30 @@ public: size_t tlut_size, u32 width, u32 height, int format, bool has_mipmaps, bool dump = false); + static u32 CalculateMipCount(u32 width, u32 height); + ~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(HiresTexture* tex, const std::string& filename); + static bool LoadDDSTexture(Level& level, const std::string& filename); + 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..d58d699a56 --- /dev/null +++ b/Source/Core/VideoCommon/HiresTextures_DDSLoader.cpp @@ -0,0 +1,486 @@ +// Copyright 2017 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include "VideoCommon/HiresTextures.h" +#include +#include +#include +#include +#include +#include "Common/Align.h" +#include "Common/FileUtil.h" +#include "Common/Swap.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 + +// 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, +}; + +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"); + +constexpr DDS_PIXELFORMAT DDSPF_A8R8G8B8 = { + sizeof(DDS_PIXELFORMAT), DDS_RGBA, 0, 32, 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000}; +constexpr DDS_PIXELFORMAT DDSPF_X8R8G8B8 = { + sizeof(DDS_PIXELFORMAT), DDS_RGB, 0, 32, 0x00ff0000, 0x0000ff00, 0x000000ff, 0x00000000}; +constexpr DDS_PIXELFORMAT DDSPF_A8B8G8R8 = { + sizeof(DDS_PIXELFORMAT), DDS_RGBA, 0, 32, 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000}; +constexpr DDS_PIXELFORMAT DDSPF_X8B8G8R8 = { + sizeof(DDS_PIXELFORMAT), DDS_RGB, 0, 32, 0x000000ff, 0x0000ff00, 0x00ff0000, 0x00000000}; +constexpr DDS_PIXELFORMAT DDSPF_R8G8B8 = { + sizeof(DDS_PIXELFORMAT), DDS_RGB, 0, 24, 0x00ff0000, 0x0000ff00, 0x000000ff, 0x00000000}; + +// End of Microsoft code from DDS.h. + +bool DDSPixelFormatMatches(const DDS_PIXELFORMAT& pf1, const DDS_PIXELFORMAT& pf2) +{ + return std::tie(pf1.dwSize, pf1.dwFlags, pf1.dwFourCC, pf1.dwRGBBitCount, pf1.dwRBitMask, + pf1.dwGBitMask, pf1.dwGBitMask, pf1.dwBBitMask, pf1.dwABitMask) == + std::tie(pf2.dwSize, pf2.dwFlags, pf2.dwFourCC, pf2.dwRGBBitCount, pf2.dwRBitMask, + pf2.dwGBitMask, pf2.dwGBitMask, pf2.dwBBitMask, pf2.dwABitMask); +} + +struct DDSLoadInfo +{ + u32 block_size = 1; + u32 bytes_per_block = 4; + u32 width = 0; + u32 height = 0; + u32 mip_count = 0; + HostTextureFormat format = HostTextureFormat::RGBA8; + size_t first_mip_offset = 0; + size_t first_mip_size = 0; + u32 first_mip_row_length = 0; + + std::function conversion_function; +}; + +u32 GetBlockCount(u32 extent, u32 block_size) +{ + return std::max(Common::AlignUp(extent, block_size) / block_size, 1u); +} + +HiresTexture::ImageDataPointer AllocateLevelData(size_t size) +{ + return HiresTexture::ImageDataPointer(new u8[size], [](u8* data) { delete[] data; }); +} + +void ConvertTexture_X8B8G8R8(HiresTexture::Level* level) +{ + u8* data_ptr = level->data.get(); + for (u32 row = 0; row < level->height; row++) + { + for (u32 x = 0; x < level->row_length; x++) + { + // Set alpha channel to full intensity. + data_ptr[3] = 0xFF; + data_ptr += sizeof(u32); + } + } +} + +void ConvertTexture_A8R8G8B8(HiresTexture::Level* level) +{ + u8* data_ptr = level->data.get(); + for (u32 row = 0; row < level->height; row++) + { + for (u32 x = 0; x < level->row_length; x++) + { + // Byte swap ABGR -> RGBA + u32 val; + std::memcpy(&val, data_ptr, sizeof(val)); + val = ((val & 0xFF00FF00) | ((val >> 16) & 0xFF) | ((val << 16) & 0xFF0000)); + std::memcpy(data_ptr, &val, sizeof(u32)); + data_ptr += sizeof(u32); + } + } +} + +void ConvertTexture_X8R8G8B8(HiresTexture::Level* level) +{ + u8* data_ptr = level->data.get(); + for (u32 row = 0; row < level->height; row++) + { + for (u32 x = 0; x < level->row_length; x++) + { + // Byte swap XBGR -> RGBX, and set alpha to full intensity. + u32 val; + std::memcpy(&val, data_ptr, sizeof(val)); + val = ((val & 0x0000FF00) | ((val >> 16) & 0xFF) | ((val << 16) & 0xFF0000)) | 0xFF000000; + std::memcpy(data_ptr, &val, sizeof(u32)); + data_ptr += sizeof(u32); + } + } +} + +void ConvertTexture_R8G8B8(HiresTexture::Level* level) +{ + // Have to reallocate the buffer for this one, since the data in the file + // does not have an alpha byte. + level->data_size = level->row_length * level->height * sizeof(u32); + HiresTexture::ImageDataPointer rgb_data = AllocateLevelData(level->data_size); + std::swap(level->data, rgb_data); + + const u8* rgb_data_ptr = rgb_data.get(); + u8* data_ptr = level->data.get(); + + for (u32 row = 0; row < level->height; row++) + { + for (u32 x = 0; x < level->row_length; x++) + { + // This is BGR in memory. + u32 val; + std::memcpy(&val, rgb_data_ptr, sizeof(val)); + val = ((val & 0x0000FF00) | ((val >> 16) & 0xFF) | ((val << 16) & 0xFF0000)) | 0xFF000000; + std::memcpy(data_ptr, &val, sizeof(u32)); + data_ptr += sizeof(u32); + rgb_data_ptr += 3; + } + } +} + +bool ParseDDSHeader(File::IOFile& file, DDSLoadInfo* info) +{ + // Exit as early as possible for non-DDS textures, since all extensions are currently + // passed through this function. + u32 magic; + if (!file.ReadBytes(&magic, sizeof(magic)) || magic != DDS_MAGIC) + return false; + + DDS_HEADER header; + size_t header_size = sizeof(header); + if (!file.ReadBytes(&header, header_size) || header.dwSize < header_size) + 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. + info->width = header.dwWidth; + info->height = header.dwHeight; + if (info->width == 0 || info->height == 0) + return false; + + // Check for mip levels. + if (header.dwFlags & DDS_HEADER_FLAGS_MIPMAP) + { + info->mip_count = header.dwMipMapCount; + if (header.dwMipMapCount != 0) + info->mip_count = header.dwMipMapCount; + else + info->mip_count = HiresTexture::CalculateMipCount(info->width, info->height); + } + else + { + info->mip_count = 1; + } + + // Handle fourcc formats vs uncompressed formats. + bool has_fourcc = (header.ddspf.dwFlags & DDS_FOURCC) != 0; + bool needs_s3tc = false; + if (has_fourcc) + { + // Handle DX10 extension header. + u32 dxt10_format = 0; + if (header.ddspf.dwFourCC == MAKEFOURCC('D', 'X', '1', '0')) + { + DDS_HEADER_DXT10 dxt10_header; + if (!file.ReadBytes(&dxt10_header, sizeof(dxt10_header))) + return false; + + // Can't handle array textures here. Doesn't make sense to use them, anyway. + if (dxt10_header.resourceDimension != DDS_DIMENSION_TEXTURE2D || dxt10_header.arraySize != 1) + return false; + + header_size += sizeof(dxt10_header); + dxt10_format = dxt10_header.dxgiFormat; + } + + // 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. + if (header.ddspf.dwFourCC == MAKEFOURCC('D', 'X', 'T', '1') || dxt10_format == 71) + { + info->format = HostTextureFormat::DXT1; + info->block_size = 4; + info->bytes_per_block = 8; + needs_s3tc = true; + } + else if (header.ddspf.dwFourCC == MAKEFOURCC('D', 'X', 'T', '3') || dxt10_format == 74) + { + info->format = HostTextureFormat::DXT3; + info->block_size = 4; + info->bytes_per_block = 16; + needs_s3tc = true; + } + else if (header.ddspf.dwFourCC == MAKEFOURCC('D', 'X', 'T', '5') || dxt10_format == 77) + { + info->format = HostTextureFormat::DXT5; + info->block_size = 4; + info->bytes_per_block = 16; + needs_s3tc = true; + } + else + { + // Leave all remaining formats to SOIL. + return false; + } + } + else + { + if (DDSPixelFormatMatches(header.ddspf, DDSPF_A8R8G8B8)) + { + info->conversion_function = ConvertTexture_A8R8G8B8; + } + else if (DDSPixelFormatMatches(header.ddspf, DDSPF_X8R8G8B8)) + { + info->conversion_function = ConvertTexture_X8R8G8B8; + } + else if (DDSPixelFormatMatches(header.ddspf, DDSPF_X8B8G8R8)) + { + info->conversion_function = ConvertTexture_X8B8G8R8; + } + else if (DDSPixelFormatMatches(header.ddspf, DDSPF_R8G8B8)) + { + info->conversion_function = ConvertTexture_R8G8B8; + } + else if (DDSPixelFormatMatches(header.ddspf, DDSPF_A8B8G8R8)) + { + // This format is already in RGBA order, so no conversion necessary. + } + else + { + return false; + } + + // All these formats are RGBA, just with byte swapping. + info->format = HostTextureFormat::RGBA8; + info->block_size = 1; + info->bytes_per_block = header.ddspf.dwRGBBitCount / 8; + } + + // 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 = GetBlockCount(info->width, info->block_size); + u32 blocks_high = GetBlockCount(info->height, info->block_size); + + // 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 < info->bytes_per_block) + { + // Likely a corrupted or invalid file. + return false; + } + + info->first_mip_row_length = + std::max(header.dwPitchOrLinearSize / info->bytes_per_block, 1u) * info->block_size; + info->first_mip_size = static_cast(info->first_mip_row_length / info->block_size) * + info->block_size * blocks_high; + } + else + { + // Assume no padding between rows of blocks. + info->first_mip_row_length = blocks_wide * info->block_size; + info->first_mip_size = blocks_wide * static_cast(info->bytes_per_block) * blocks_high; + } + + // Check for truncated or corrupted files. + info->first_mip_offset = sizeof(magic) + header_size; + if (info->first_mip_offset >= file.GetSize()) + return false; + + return true; +} + +bool ReadMipLevel(HiresTexture::Level* level, File::IOFile& file, const DDSLoadInfo& info, + u32 width, u32 height, u32 row_length, size_t size) +{ + // Copy to the final storage location. The deallocator here is simple, nothing extra is + // needed, compared to the SOIL-based loader. + level->width = width; + level->height = height; + level->format = info.format; + level->row_length = row_length; + level->data_size = size; + level->data = AllocateLevelData(level->data_size); + if (!file.ReadBytes(level->data.get(), level->data_size)) + { + level->data.reset(); + return false; + } + + // Apply conversion function for uncompressed textures. + if (info.conversion_function) + info.conversion_function(level); + + return true; +} + +} // namespace + +bool HiresTexture::LoadDDSTexture(HiresTexture* tex, const std::string& filename) +{ + File::IOFile file; + file.Open(filename, "rb"); + if (!file.IsOpen()) + return false; + + DDSLoadInfo info; + if (!ParseDDSHeader(file, &info)) + return false; + + // Read first mip level, as it may have a custom pitch. + Level first_level; + if (!file.Seek(info.first_mip_offset, SEEK_SET) || + !ReadMipLevel(&first_level, file, info, info.width, info.height, info.first_mip_row_length, + info.first_mip_size)) + { + return false; + } + + tex->m_levels.push_back(std::move(first_level)); + + // Read in any remaining mip levels in the file. + // If the .dds file does not contain a full mip chain, we'll fall back to the old path. + u32 mip_width = info.width; + u32 mip_height = info.height; + for (u32 i = 1; i < info.mip_count; i++) + { + mip_width = std::max(mip_width / 2, 1u); + mip_height = std::max(mip_height / 2, 1u); + + // Pitch can't be specified with each mip level, so we have to calculate it ourselves. + u32 blocks_wide = GetBlockCount(mip_width, info.block_size); + u32 blocks_high = GetBlockCount(mip_height, info.block_size); + u32 mip_row_length = blocks_wide * info.block_size; + size_t mip_size = blocks_wide * static_cast(info.bytes_per_block) * blocks_high; + Level level; + if (!ReadMipLevel(&level, file, info, mip_width, mip_height, mip_row_length, mip_size)) + break; + + tex->m_levels.push_back(std::move(level)); + } + + return true; +} + +bool HiresTexture::LoadDDSTexture(Level& level, const std::string& filename) +{ + // Only loading a single mip level. + File::IOFile file; + file.Open(filename, "rb"); + if (!file.IsOpen()) + return false; + + DDSLoadInfo info; + if (!ParseDDSHeader(file, &info)) + return false; + + return ReadMipLevel(&level, file, info, info.width, info.height, info.first_mip_row_length, + info.first_mip_size); +} diff --git a/Source/Core/VideoCommon/TextureCacheBase.cpp b/Source/Core/VideoCommon/TextureCacheBase.cpp index 89d473cfec..613f2dfed9 100644 --- a/Source/Core/VideoCommon/TextureCacheBase.cpp +++ b/Source/Core/VideoCommon/TextureCacheBase.cpp @@ -47,6 +47,29 @@ TextureCacheBase::TCacheEntryBase::~TCacheEntryBase() { } +bool TextureCacheBase::IsCompressedHostTextureFormat(HostTextureFormat format) +{ + // This will need to be changed if we add any other uncompressed formats. + return format != HostTextureFormat::RGBA8; +} + +size_t TextureCacheBase::CalculateHostTextureLevelPitch(HostTextureFormat format, u32 row_length) +{ + switch (format) + { + case HostTextureFormat::DXT1: + return static_cast(std::max(1u, row_length / 4)) * 8; + + case HostTextureFormat::DXT3: + case HostTextureFormat::DXT5: + return static_cast(std::max(1u, row_length / 4)) * 16; + + case HostTextureFormat::RGBA8: + default: + return static_cast(row_length) * 4; + } +} + void TextureCacheBase::CheckTempSize(size_t required_size) { if (required_size <= temp_size) @@ -750,8 +773,6 @@ TextureCacheBase::TCacheEntryBase* TextureCacheBase::Load(const u32 stage) } expandedWidth = level.width; expandedHeight = level.height; - CheckTempSize(level.data_size); - memcpy(temp, level.data.get(), level.data_size); } } @@ -774,6 +795,7 @@ TextureCacheBase::TCacheEntryBase* TextureCacheBase::Load(const u32 stage) config.width = width; config.height = height; config.levels = texLevels; + config.format = hires_tex ? hires_tex->GetFormat() : HostTextureFormat::RGBA8; TCacheEntryBase* entry = AllocateTexture(config); GFX_DEBUGGER_PAUSE_AT(NEXT_NEW_TEXTURE, true); @@ -784,17 +806,20 @@ TextureCacheBase::TCacheEntryBase* TextureCacheBase::Load(const u32 stage) const u8* tlut = &texMem[tlutaddr]; if (hires_tex) { - entry->Load(temp, width, height, expandedWidth, 0); + const auto& level = hires_tex->m_levels[0]; + entry->Load(0, level.width, level.height, level.row_length, level.data.get(), level.data_size); } - else if (decode_on_gpu) + if (!hires_tex && decode_on_gpu) { u32 row_stride = bytes_per_block * (expandedWidth / bsw); g_texture_cache->DecodeTextureOnGPU( entry, 0, src_data, texture_size, static_cast(texformat), width, height, expandedWidth, expandedHeight, row_stride, tlut, static_cast(tlutfmt)); } - else + else if (!hires_tex) { + size_t decoded_texture_size = expandedWidth * sizeof(u32) * expandedHeight; + CheckTempSize(decoded_texture_size); if (!(texformat == GX_TF_RGBA8 && from_tmem)) { TexDecoder_Decode(temp, src_data, expandedWidth, expandedHeight, texformat, tlut, @@ -807,7 +832,7 @@ TextureCacheBase::TCacheEntryBase* TextureCacheBase::Load(const u32 stage) TexDecoder_DecodeRGBA8FromTmem(temp, src_data, src_data_gb, expandedWidth, expandedHeight); } - entry->Load(temp, width, height, expandedWidth, 0); + entry->Load(0, width, height, expandedWidth, temp, decoded_texture_size); } iter = textures_by_address.emplace(address, entry); @@ -837,9 +862,8 @@ TextureCacheBase::TCacheEntryBase* TextureCacheBase::Load(const u32 stage) for (u32 level_index = 1; level_index != texLevels; ++level_index) { const auto& level = hires_tex->m_levels[level_index]; - CheckTempSize(level.data_size); - memcpy(temp, level.data.get(), level.data_size); - entry->Load(temp, level.width, level.height, level.width, level_index); + entry->Load(level_index, level.width, level.height, level.row_length, level.data.get(), + level.data_size); } } else @@ -877,9 +901,11 @@ TextureCacheBase::TCacheEntryBase* TextureCacheBase::Load(const u32 stage) } else { + // No need to call CheckTempSize here, as mips will always be smaller than the base level. + size_t decoded_mip_size = expanded_mip_width * sizeof(u32) * expanded_mip_height; TexDecoder_Decode(temp, mip_src_data, expanded_mip_width, expanded_mip_height, texformat, tlut, (TlutFormat)tlutfmt); - entry->Load(temp, mip_width, mip_height, expanded_mip_width, level); + entry->Load(level, mip_width, mip_height, expanded_mip_width, temp, decoded_mip_size); } mip_src_data += mip_size; diff --git a/Source/Core/VideoCommon/TextureCacheBase.h b/Source/Core/VideoCommon/TextureCacheBase.h index 4c9582079b..7f2491a5c7 100644 --- a/Source/Core/VideoCommon/TextureCacheBase.h +++ b/Source/Core/VideoCommon/TextureCacheBase.h @@ -27,16 +27,16 @@ public: bool operator==(const TCacheEntryConfig& o) const { - return std::tie(width, height, levels, layers, rendertarget) == - std::tie(o.width, o.height, o.levels, o.layers, o.rendertarget); + return std::tie(width, height, levels, layers, format, rendertarget) == + std::tie(o.width, o.height, o.levels, o.layers, o.format, o.rendertarget); } struct Hasher : std::hash { size_t operator()(const TCacheEntryConfig& c) const { - u64 id = (u64)c.rendertarget << 63 | (u64)c.layers << 48 | (u64)c.levels << 32 | - (u64)c.height << 16 | (u64)c.width; + u64 id = (u64)c.rendertarget << 63 | (u64)c.format << 50 | (u64)c.layers << 48 | + (u64)c.levels << 32 | (u64)c.height << 16 | (u64)c.width; return std::hash::operator()(id); } }; @@ -45,6 +45,7 @@ public: u32 height = 0; u32 levels = 1; u32 layers = 1; + HostTextureFormat format = HostTextureFormat::RGBA8; bool rendertarget = false; }; @@ -129,7 +130,8 @@ public: const MathUtil::Rectangle& srcrect, const MathUtil::Rectangle& dstrect) = 0; - virtual void Load(const u8* buffer, u32 width, u32 height, u32 expanded_width, u32 level) = 0; + virtual void Load(u32 level, u32 width, u32 height, u32 row_length, const u8* buffer, + size_t buffer_size) = 0; virtual void FromRenderTarget(bool is_depth_copy, const EFBRectangle& srcRect, bool scaleByHalf, unsigned int cbufid, const float* colmat) = 0; @@ -144,6 +146,10 @@ public: virtual ~TextureCacheBase(); // needs virtual for DX11 dtor + // TODO: Move these to AbstractTexture once it is finished. + static bool IsCompressedHostTextureFormat(HostTextureFormat format); + static size_t CalculateHostTextureLevelPitch(HostTextureFormat format, u32 row_length); + void OnConfigChanged(VideoConfig& config); // Removes textures which aren't used for more than TEXTURE_KILL_THRESHOLD frames, 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