Files
ppsspp/GPU/D3D11/TextureCacheD3D11.cpp
T
Claude b383377039 Texture replacement: Color indexed replacements on the GPU
An [indexed] entry set to gpu uploads the indices as an R8 image, and
the CLUT8_INDEXED depal shader applies the palette. A texture whose
palette changes a lot then costs no CPU time per palette, and keeps one
texture instead of one per palette.

Once such a replacement is found, the texture is looked up without the
CLUT hash, like with the TextureCLUTInShader compat flag. Only the first
mip level is used, and the replacer's filtering overrides are skipped
since filtering indices mixes up palette entries. Without shader bitwise
ops it falls back to coloring on the CPU.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 11:05:12 +02:00

540 lines
17 KiB
C++

// Copyright (c) 2012- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include <algorithm>
#include <cstring>
#include <cfloat>
#include "Common/CommonWindows.h"
#include <d3d11.h>
#include <wrl/client.h>
#include "GPU/ge_constants.h"
#include "GPU/GPUState.h"
#include "GPU/GPUDefinitions.h"
#include "GPU/Common/GPUStateUtils.h"
#include "GPU/Common/DrawEngineCommon.h"
#include "GPU/D3D11/TextureCacheD3D11.h"
#include "GPU/D3D11/FramebufferManagerD3D11.h"
#include "GPU/D3D11/D3D11Util.h"
#include "Core/Config.h"
#include "ext/xxhash.h"
using namespace Microsoft::WRL;
// For depth depal
struct DepthPushConstants {
float z_scale;
float z_offset;
float pad[2];
};
static const D3D11_INPUT_ELEMENT_DESC g_QuadVertexElements[] = {
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, },
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12,},
};
// NOTE: In the D3D backends, we flip R and B in the shaders, so while these look wrong, they're OK.
static Draw::DataFormat FromD3D11Format(u32 fmt) {
switch (fmt) {
case DXGI_FORMAT_B4G4R4A4_UNORM: return Draw::DataFormat::A4R4G4B4_UNORM_PACK16;
case DXGI_FORMAT_B5G5R5A1_UNORM: return Draw::DataFormat::A1R5G5B5_UNORM_PACK16;
case DXGI_FORMAT_B5G6R5_UNORM: return Draw::DataFormat::R5G6B5_UNORM_PACK16;
case DXGI_FORMAT_R8_UNORM: return Draw::DataFormat::R8_UNORM;
case DXGI_FORMAT_R8G8B8A8_UNORM: default: return Draw::DataFormat::R8G8B8A8_UNORM;
}
}
static DXGI_FORMAT ToDXGIFormat(Draw::DataFormat fmt) {
switch (fmt) {
case Draw::DataFormat::BC1_RGBA_UNORM_BLOCK: return DXGI_FORMAT_BC1_UNORM;
case Draw::DataFormat::BC2_UNORM_BLOCK: return DXGI_FORMAT_BC2_UNORM;
case Draw::DataFormat::BC3_UNORM_BLOCK: return DXGI_FORMAT_BC3_UNORM;
case Draw::DataFormat::BC4_UNORM_BLOCK: return DXGI_FORMAT_BC4_UNORM;
case Draw::DataFormat::BC5_UNORM_BLOCK: return DXGI_FORMAT_BC5_UNORM;
case Draw::DataFormat::BC7_UNORM_BLOCK: return DXGI_FORMAT_BC7_UNORM;
case Draw::DataFormat::R8G8B8A8_UNORM: return DXGI_FORMAT_R8G8B8A8_UNORM;
default: _dbg_assert_(false); return DXGI_FORMAT_UNKNOWN;
}
}
SamplerCacheD3D11::~SamplerCacheD3D11() {
}
HRESULT SamplerCacheD3D11::GetOrCreateSampler(ID3D11Device *device, const SamplerCacheKey &key, ID3D11SamplerState **ppSamplerState) {
auto iter = cache_.find(key);
if (iter != cache_.end()) {
iter->second->AddRef();
*ppSamplerState = iter->second.Get();
return S_OK;
}
D3D11_SAMPLER_DESC samp{};
samp.AddressU = key.sClamp ? D3D11_TEXTURE_ADDRESS_CLAMP : D3D11_TEXTURE_ADDRESS_WRAP;
samp.AddressV = key.tClamp ? D3D11_TEXTURE_ADDRESS_CLAMP : D3D11_TEXTURE_ADDRESS_WRAP;
samp.AddressW = samp.AddressU; // Mali benefits from all clamps being the same, and this one is irrelevant.
if (key.aniso) {
samp.MaxAnisotropy = (float)(1 << key.anisoLevel);
} else {
samp.MaxAnisotropy = 1.0f;
}
int filterKey = ((int)key.minFilt << 2) | ((int)key.magFilt << 1) | ((int)key.mipFilt);
static const D3D11_FILTER filters[8] = {
D3D11_FILTER_MIN_MAG_MIP_POINT,
D3D11_FILTER_MIN_MAG_POINT_MIP_LINEAR,
D3D11_FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT,
D3D11_FILTER_MIN_POINT_MAG_MIP_LINEAR,
D3D11_FILTER_MIN_LINEAR_MAG_MIP_POINT,
D3D11_FILTER_MIN_LINEAR_MAG_POINT_MIP_LINEAR,
D3D11_FILTER_MIN_MAG_LINEAR_MIP_POINT,
D3D11_FILTER_MIN_MAG_MIP_LINEAR,
};
// Only switch to aniso if linear min and mag are set.
if (key.aniso && key.magFilt != 0 && key.minFilt != 0)
samp.Filter = D3D11_FILTER_ANISOTROPIC;
else
samp.Filter = filters[filterKey];
// Can't set MaxLOD on Feature Level <= 9_3.
if (device->GetFeatureLevel() <= D3D_FEATURE_LEVEL_9_3) {
samp.MaxLOD = FLT_MAX;
samp.MinLOD = -FLT_MAX;
samp.MipLODBias = 0.0f;
} else {
samp.MaxLOD = key.maxLevel / 256.0f;
samp.MinLOD = key.minLevel / 256.0f;
samp.MipLODBias = key.lodBias / 256.0f;
}
samp.ComparisonFunc = D3D11_COMPARISON_NEVER;
for (int i = 0; i < 4; i++) {
samp.BorderColor[i] = 1.0f;
}
ASSERT_SUCCESS(device->CreateSamplerState(&samp, ppSamplerState));
cache_[key] = *ppSamplerState;
return S_OK;
}
TextureCacheD3D11::TextureCacheD3D11(Draw::DrawContext *draw, Draw2D *draw2D)
: TextureCacheCommon(draw, draw2D) {
device_ = (ID3D11Device *)draw->GetNativeObject(Draw::NativeObject::DEVICE);
context_ = (ID3D11DeviceContext *)draw->GetNativeObject(Draw::NativeObject::CONTEXT);
lastBoundTexture_ = D3D11_INVALID_TEX;
InitDeviceObjects();
}
TextureCacheD3D11::~TextureCacheD3D11() {
Clear(true);
DestroyDeviceObjects();
}
void TextureCacheD3D11::InitDeviceObjects() {
D3D11_BUFFER_DESC desc{ sizeof(DepthPushConstants), D3D11_USAGE_DYNAMIC, D3D11_BIND_CONSTANT_BUFFER, D3D11_CPU_ACCESS_WRITE };
HRESULT hr = device_->CreateBuffer(&desc, nullptr, &depalConstants_);
_dbg_assert_(SUCCEEDED(hr));
D3D11_SAMPLER_DESC sampler_desc{};
sampler_desc.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT;
sampler_desc.ComparisonFunc = D3D11_COMPARISON_NEVER;
for (int i = 0; i < 4; i++)
sampler_desc.BorderColor[i] = 1.0f;
sampler_desc.MinLOD = -FLT_MAX;
sampler_desc.MaxLOD = FLT_MAX;
sampler_desc.MipLODBias = 0.0f;
sampler_desc.MaxAnisotropy = 1;
sampler_desc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
sampler_desc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
sampler_desc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
ASSERT_SUCCESS(device_->CreateSamplerState(&sampler_desc, &samplerPoint2DClamp_));
sampler_desc.Filter = D3D11_FILTER_MIN_MAG_LINEAR_MIP_POINT;
ASSERT_SUCCESS(device_->CreateSamplerState(&sampler_desc, &samplerLinear2DClamp_));
}
void TextureCacheD3D11::DestroyDeviceObjects() {
depalConstants_.Reset();
samplerPoint2DClamp_.Reset();
samplerLinear2DClamp_.Reset();
lastBoundTexture_ = D3D11_INVALID_TEX;
samplerCache_.Destroy();
}
void TextureCacheD3D11::DeviceLost() {
TextureCacheCommon::DeviceLost();
DestroyDeviceObjects();
draw_ = nullptr;
device_ = nullptr;
context_ = nullptr;
}
void TextureCacheD3D11::DeviceRestore(Draw::DrawContext *draw) {
// The restored context can be a new device, so don't keep the old pointers.
device_ = (ID3D11Device *)draw->GetNativeObject(Draw::NativeObject::DEVICE);
context_ = (ID3D11DeviceContext *)draw->GetNativeObject(Draw::NativeObject::CONTEXT);
TextureCacheCommon::DeviceRestore(draw);
InitDeviceObjects();
}
void TextureCacheD3D11::SetFramebufferManager(FramebufferManagerD3D11 *fbManager) {
framebufferManager_ = fbManager;
}
void TextureCacheD3D11::ReleaseTexture(TexCacheEntry *entry, bool delete_them) {
ID3D11Texture2D *texture = (ID3D11Texture2D *)entry->texturePtr;
ID3D11ShaderResourceView *view = (ID3D11ShaderResourceView *)entry->textureView;
if (texture) {
texture->Release();
entry->texturePtr = nullptr;
}
if (view) {
view->Release();
entry->textureView = nullptr;
}
}
void TextureCacheD3D11::ForgetLastTexture() {
lastBoundTexture_ = D3D11_INVALID_TEX;
ID3D11ShaderResourceView *nullTex[4]{};
context_->PSSetShaderResources(0, 4, nullTex);
}
void TextureCacheD3D11::BindTexture(TexCacheEntry *entry) {
if (!entry) {
ID3D11ShaderResourceView *textureView = nullptr;
context_->PSSetShaderResources(0, 1, &textureView);
return;
}
ID3D11ShaderResourceView *textureView = DxView(entry);
if (textureView != lastBoundTexture_) {
context_->PSSetShaderResources(0, 1, &textureView);
lastBoundTexture_ = textureView;
}
}
void TextureCacheD3D11::ApplySamplerByKey(const SamplerCacheKey &key) {
ComPtr<ID3D11SamplerState> state;
samplerCache_.GetOrCreateSampler(device_, key, &state);
context_->PSSetSamplers(0, 1, state.GetAddressOf());
}
void TextureCacheD3D11::Unbind() {
ForgetLastTexture();
}
void TextureCacheD3D11::BindAsClutTexture(Draw::Texture *tex, bool smooth) {
ID3D11ShaderResourceView *clutTexture = (ID3D11ShaderResourceView *)draw_->GetNativeObject(Draw::NativeObject::TEXTURE_VIEW, tex);
context_->PSSetShaderResources(TEX_SLOT_CLUT, 1, &clutTexture);
context_->PSSetSamplers(3, 1, smooth ? samplerLinear2DClamp_.GetAddressOf() : samplerPoint2DClamp_.GetAddressOf());
}
void TextureCacheD3D11::BuildTexture(TexCacheEntry *const entry) {
BuildTexturePlan plan;
if (!PrepareBuildTexture(plan, entry)) {
// We're screwed?
return;
}
DXGI_FORMAT dstFmt = GetDestFormat(GETextureFormat(entry->format), gstate.getClutPaletteFormat());
if (plan.doReplace) {
// Indices for the GPU are uploaded as the R8 format of the CLUT8 textures.
dstFmt = plan.decodeToClut8 ? DXGI_FORMAT_R8_UNORM : ToDXGIFormat(plan.replaced->Format());
} else if (plan.scaleFactor > 1 || plan.saveTexture) {
dstFmt = DXGI_FORMAT_R8G8B8A8_UNORM;
} else if (plan.decodeToClut8) {
dstFmt = DXGI_FORMAT_R8_UNORM;
}
int levels;
ID3D11ShaderResourceView *view;
ID3D11Resource *texture = DxTex(entry);
_assert_(texture == nullptr);
// The PSP only supports 8 mip levels, but we support more in the texture replacer. 16 will never run out,
// D3D11 caps textures at 16384 pixels anyway.
D3D11_SUBRESOURCE_DATA subresData[16]{};
auto freeSubresData = [&subresData]() {
for (size_t i = 0; i < ARRAY_SIZE(subresData); i++) {
if (subresData[i].pSysMem) {
FreeAlignedMemory((void *)subresData[i].pSysMem);
subresData[i].pSysMem = nullptr;
}
}
};
if (plan.depth == 1) {
// We don't yet have mip generation, so clamp the number of levels to the ones we can load directly.
levels = std::min(plan.levelsToCreate, plan.levelsToLoad);
// Only the 2D path indexes subresData per level - the 3D path puts everything in slot 0.
_dbg_assert_(levels <= (int)ARRAY_SIZE(subresData));
} else {
levels = plan.depth;
}
Draw::DataFormat texFmt = FromD3D11Format(dstFmt);
for (int i = 0; i < levels; i++) {
int srcLevel = (i == 0) ? plan.baseLevelSrc : i;
int mipWidth;
int mipHeight;
plan.GetMipSize(i, &mipWidth, &mipHeight);
u8 *data = nullptr;
int stride = 0;
int dataSize;
if (plan.doReplace) {
int blockSize = 0;
if (Draw::DataFormatIsBlockCompressed(plan.replaced->Format(), &blockSize)) {
stride = ((mipWidth + 3) & ~3) * blockSize / 4; // Number of blocks * 4 * Size of a block / 4
dataSize = plan.replaced->GetLevelDataSizeAfterCopy(i);
} else {
int bpp = plan.decodeToClut8 ? 1 : (int)Draw::DataFormatSizeInBytes(plan.replaced->Format());
stride = std::max(mipWidth * bpp, 16);
dataSize = stride * mipHeight;
}
} else {
int bpp = 0;
if (plan.scaleFactor > 1) {
bpp = 4;
} else {
bpp = dstFmt == DXGI_FORMAT_R8G8B8A8_UNORM ? 4 : 2;
}
stride = std::max(mipWidth * bpp, 16);
dataSize = stride * mipHeight;
}
if (plan.depth == 1) {
data = (u8 *)AllocateAlignedMemory(dataSize, 16);
subresData[i].pSysMem = data;
subresData[i].SysMemPitch = stride;
subresData[i].SysMemSlicePitch = 0;
} else {
if (i == 0) {
subresData[0].pSysMem = AllocateAlignedMemory(stride * mipHeight * plan.depth, 16);
subresData[0].SysMemPitch = stride;
subresData[0].SysMemSlicePitch = stride * mipHeight;
}
data = (uint8_t *)subresData[0].pSysMem + stride * mipHeight * i;
}
if (!data) {
ERROR_LOG(Log::G3D, "Ran out of RAM trying to allocate a temporary texture upload buffer (%dx%d)", mipWidth, mipHeight);
freeSubresData();
return;
}
LoadTextureLevel(*entry, data, 0, stride, plan, srcLevel, texFmt, TexDecodeFlags{});
}
int tw;
int th;
plan.GetMipSize(0, &tw, &th);
if (tw > 16384)
tw = 16384;
if (th > 16384)
th = 16384;
// NOTE: For block-compressed textures, we'll force the size up to the closest 4x4. This is due to an
// unfortunate restriction in D3D11 (and early D3D12). We'll warn about it in the log to give texture pack
// authors notice to fix it.
if (plan.doReplace && Draw::DataFormatIsBlockCompressed(plan.replaced->Format(), nullptr)) {
tw = (tw + 3) & ~3;
th = (th + 3) & ~3;
}
if (plan.depth == 1) {
// We don't yet have mip generation, so clamp the number of levels to the ones we can load directly.
levels = std::min(plan.levelsToCreate, plan.levelsToLoad);
ID3D11Texture2D *tex = nullptr;
D3D11_TEXTURE2D_DESC desc{};
desc.CPUAccessFlags = 0;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.ArraySize = 1;
desc.SampleDesc.Count = 1;
desc.Width = tw;
desc.Height = th;
desc.Format = dstFmt;
desc.MipLevels = levels;
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
ASSERT_SUCCESS(device_->CreateTexture2D(&desc, subresData, &tex));
texture = tex;
} else {
ID3D11Texture3D *tex = nullptr;
D3D11_TEXTURE3D_DESC desc{};
desc.CPUAccessFlags = 0;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.Width = tw;
desc.Height = th;
desc.Depth = plan.depth;
desc.Format = dstFmt;
desc.MipLevels = 1;
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
ASSERT_SUCCESS(device_->CreateTexture3D(&desc, subresData, &tex));
texture = tex;
levels = plan.depth;
}
ASSERT_SUCCESS(device_->CreateShaderResourceView(texture, nullptr, &view));
entry->texturePtr = texture;
entry->textureView = view;
freeSubresData();
// Signal that we support depth textures so use it as one.
if (plan.depth > 1) {
entry->status |= TexStatus::IS_3D;
}
if (levels == 1) {
entry->status |= TexStatus::NO_MIPS;
} else {
entry->status &= ~TexStatus::NO_MIPS;
}
if (plan.doReplace) {
entry->SetAlphaStatus(plan.replaced->AlphaStatus());
}
}
DXGI_FORMAT GetClutDestFormatD3D11(GEPaletteFormat format) {
switch (format) {
case GE_CMODE_16BIT_ABGR4444:
return DXGI_FORMAT_B4G4R4A4_UNORM;
case GE_CMODE_16BIT_ABGR5551:
return DXGI_FORMAT_B5G5R5A1_UNORM;
case GE_CMODE_16BIT_BGR5650:
return DXGI_FORMAT_B5G6R5_UNORM;
case GE_CMODE_32BIT_ABGR8888:
return DXGI_FORMAT_R8G8B8A8_UNORM;
}
// Should never be here !
return DXGI_FORMAT_R8G8B8A8_UNORM;
}
DXGI_FORMAT TextureCacheD3D11::GetDestFormat(GETextureFormat format, GEPaletteFormat clutFormat) const {
if (!gstate_c.Use(GPU_USE_16BIT_FORMATS)) {
return DXGI_FORMAT_R8G8B8A8_UNORM;
}
switch (format) {
case GE_TFMT_CLUT4:
case GE_TFMT_CLUT8:
case GE_TFMT_CLUT16:
case GE_TFMT_CLUT32:
return GetClutDestFormatD3D11(clutFormat);
case GE_TFMT_4444:
return DXGI_FORMAT_B4G4R4A4_UNORM;
case GE_TFMT_5551:
return DXGI_FORMAT_B5G5R5A1_UNORM;
case GE_TFMT_5650:
return DXGI_FORMAT_B5G6R5_UNORM;
case GE_TFMT_8888:
case GE_TFMT_DXT1:
case GE_TFMT_DXT3:
case GE_TFMT_DXT5:
default:
return DXGI_FORMAT_R8G8B8A8_UNORM;
}
}
bool TextureCacheD3D11::GetCurrentTextureDebug(GPUDebugBuffer &buffer, int level, bool *isFramebuffer) {
// Apply texture may need to rebuild the texture if we're about to render, or bind a framebuffer.
TextureApplyResult textureResult = ApplyTexture(true);
if (textureResult.framebuffer) {
*isFramebuffer = true;
return GetFramebufferTextureDebug(textureResult.framebuffer, textureResult.framebufferTextureChannel, buffer);
}
const TexCacheEntry *entry = textureResult.texCacheEntry;
if (!entry) {
return false;
}
ID3D11Texture2D *texture = (ID3D11Texture2D *)entry->texturePtr;
if (!texture) {
// Hm.
return false;
}
D3D11_TEXTURE2D_DESC desc;
texture->GetDesc(&desc);
int width = desc.Width >> level;
int height = desc.Height >> level;
switch (desc.Format) {
case DXGI_FORMAT_R8G8B8A8_UNORM:
buffer.Allocate(width, height, GPU_DBG_FORMAT_8888);
break;
case DXGI_FORMAT_B5G6R5_UNORM:
buffer.Allocate(width, height, GPU_DBG_FORMAT_565);
break;
case DXGI_FORMAT_B4G4R4A4_UNORM:
buffer.Allocate(width, height, GPU_DBG_FORMAT_4444);
break;
case DXGI_FORMAT_B5G5R5A1_UNORM:
buffer.Allocate(width, height, GPU_DBG_FORMAT_5551);
break;
case DXGI_FORMAT_R8_UNORM:
buffer.Allocate(width, height, GPU_DBG_FORMAT_8BIT);
break;
default:
return false;
}
desc.BindFlags = 0;
desc.Usage = D3D11_USAGE_STAGING;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
ComPtr<ID3D11Texture2D> stagingCopy;
device_->CreateTexture2D(&desc, nullptr, &stagingCopy);
if (!stagingCopy)
return false;
context_->CopyResource(stagingCopy.Get(), texture);
D3D11_MAPPED_SUBRESOURCE map;
if (FAILED(context_->Map(stagingCopy.Get(), level, D3D11_MAP_READ, 0, &map))) {
return false;
}
int bufferRowSize = buffer.PixelSize() * width;
for (int y = 0; y < height; y++) {
memcpy(buffer.GetData() + bufferRowSize * y, (const uint8_t *)map.pData + map.RowPitch * y, bufferRowSize);
}
context_->Unmap(stagingCopy.Get(), level);
*isFramebuffer = false;
return true;
}
void *TextureCacheD3D11::GetNativeTextureView(const TexCacheEntry *entry, bool flat) const {
ID3D11ShaderResourceView *textureView = DxView(entry);
return (void *)textureView;
}