From 55109b39fbf880b60df74f2ddfb2132571f9b97e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sun, 18 Oct 2026 01:06:44 +0200 Subject: [PATCH] TextureCacheVulkan: Move the texture scaling upload shader and passes to common So the Metal backend can use them too. No functional change. Co-Authored-By: Claude Opus 5.5 (1M context) --- GPU/Common/TextureShaderCommon.cpp | 86 ++++++++++++++++++++++++++++++ GPU/Common/TextureShaderCommon.h | 25 +++++++++ GPU/Vulkan/TextureCacheVulkan.cpp | 82 +--------------------------- GPU/Vulkan/TextureCacheVulkan.h | 18 +------ 4 files changed, 115 insertions(+), 96 deletions(-) diff --git a/GPU/Common/TextureShaderCommon.cpp b/GPU/Common/TextureShaderCommon.cpp index 94aa626c05..a95a4d164b 100644 --- a/GPU/Common/TextureShaderCommon.cpp +++ b/GPU/Common/TextureShaderCommon.cpp @@ -275,3 +275,89 @@ void ClutTextureCache::Decimate() { } gpuStats.perFrame.numClutTextures = (int)texCache_.size(); } + +const char *const textureScaleUploadShader = R"( +#version 450 +#extension GL_ARB_separate_shader_objects : enable + +// 8x8 is the most common compute shader workgroup size, and works great on all major +// hardware vendors. TODO: However, we should probably change to 16x16, as Qualcomm now has +// support for bigger groups... +layout (local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +uniform layout(set = 0, binding = 0, rgba8) writeonly image2D img; + +layout(std430, set = 0, binding = 1) buffer Buf { + uint data[]; +} buf; + +layout(push_constant) uniform Params { + int width; + int height; +} params; + +// The cbuffer, if present, is self-declared for layout flexibility +#define CBUFFER_SET 0 +#define CBUFFER_BINDING 4 + +uint readColoru(uvec2 p) { + return buf.data[p.y * params.width + p.x]; +} + +vec4 readColorf(uvec2 p) { + // Unpack the color (we could look it up in a CLUT here if we wanted...) + // The imageStore repack is free. + return unpackUnorm4x8(readColoru(p)); +} + +void writeColorf(ivec2 p, vec4 c) { + imageStore(img, p, c); +} + +%s + +// Note that main runs once per INPUT pixel, unlike the old model. +void main() { + uvec2 xy = gl_GlobalInvocationID.xy; + // Kill off any out-of-image threads to avoid stray writes. + // Should only happen on the tiniest mipmaps as PSP textures are power-of-2, + // and we use a 8x8 workgroup size. Probably not really necessary. + if (xy.x >= params.width || xy.y >= params.height) + return; + // applyScaling will write the upscaled pixels, using writeColorf above. + // It's expected to write a square of scale*scale pixels, at the location xy*scale. + applyScaling(xy); +} + +)"; + +bool GetTextureScaleMultipassLayout(int scaleFactor, std::vector *scratchDescs, std::vector *stageDescs) { + if (scaleFactor == 2) { + *scratchDescs = { + { "nnedi3_vertical", 1, 2 }, + { "nnedi3_horizontal", 2, 2 }, + }; + *stageDescs = { + { 0, -1, 0, 1, 1, 1, 2, false }, + { 1, 0, 1, 1, 2, 2, 2, false }, + { 2, 1, -1, 2, 2, 2, 2, false }, + }; + } else if (scaleFactor == 4) { + *scratchDescs = { + { "nnedi3_vertical", 1, 2 }, + { "nnedi3_horizontal", 2, 2 }, + { "nnedi3_vertical2", 2, 4 }, + { "nnedi3_horizontal2", 4, 4 }, + }; + *stageDescs = { + { 0, -1, 0, 1, 1, 1, 2, false }, + { 2, 0, 1, 1, 2, 2, 2, false }, + { 1, 1, 2, 2, 2, 2, 4, false }, + { 2, 2, 3, 2, 4, 4, 4, false }, + { 3, 3, -1, 4, 4, 4, 4, true }, + }; + } else { + return false; + } + return true; +} diff --git a/GPU/Common/TextureShaderCommon.h b/GPU/Common/TextureShaderCommon.h index 9b48a6b0ab..a6d3b38e57 100644 --- a/GPU/Common/TextureShaderCommon.h +++ b/GPU/Common/TextureShaderCommon.h @@ -77,3 +77,28 @@ private: Draw2D *draw2D_; std::unordered_map pipelineCache_; }; + +// Hardware texture upscaling, with compute shaders. + +// The single pass upload shader, with a %s for the scaling shader (its applyScaling()). +extern const char *const textureScaleUploadShader; + +struct TextureScaleScratchDesc { + const char *tag; + uint8_t widthScale; + uint8_t heightScale; +}; + +struct TextureScaleStageDesc { + uint8_t shaderIndex; + int8_t inputScratch; // -1 means the original upload buffer. + int8_t outputScratch; // -1 means the final destination image. + uint8_t srcWidthScale; + uint8_t srcHeightScale; + uint8_t dstWidthScale; + uint8_t dstHeightScale; + bool useFinalOutputSize; +}; + +// The scratch images and passes of a multipass scaling shader (NNEDI3). False for an unsupported scale factor. +bool GetTextureScaleMultipassLayout(int scaleFactor, std::vector *scratchDescs, std::vector *stageDescs); diff --git a/GPU/Vulkan/TextureCacheVulkan.cpp b/GPU/Vulkan/TextureCacheVulkan.cpp index 4b9937b375..5e6b776a44 100644 --- a/GPU/Vulkan/TextureCacheVulkan.cpp +++ b/GPU/Vulkan/TextureCacheVulkan.cpp @@ -55,60 +55,6 @@ using namespace PPSSPP_VK; #define TEXCACHE_MAX_SLAB_SIZE (32 * 1024 * 1024) #define TEXCACHE_SLAB_PRESSURE 4 -const char *uploadShader = R"( -#version 450 -#extension GL_ARB_separate_shader_objects : enable - -// 8x8 is the most common compute shader workgroup size, and works great on all major -// hardware vendors. TODO: However, we should probably change to 16x16, as Qualcomm now has -// support for bigger groups... -layout (local_size_x = 8, local_size_y = 8, local_size_z = 1) in; - -uniform layout(set = 0, binding = 0, rgba8) writeonly image2D img; - -layout(std430, set = 0, binding = 1) buffer Buf { - uint data[]; -} buf; - -layout(push_constant) uniform Params { - int width; - int height; -} params; - -// The cbuffer, if present, is self-declared for layout flexibility -#define CBUFFER_SET 0 -#define CBUFFER_BINDING 4 - -uint readColoru(uvec2 p) { - return buf.data[p.y * params.width + p.x]; -} - -vec4 readColorf(uvec2 p) { - // Unpack the color (we could look it up in a CLUT here if we wanted...) - // The imageStore repack is free. - return unpackUnorm4x8(readColoru(p)); -} - -void writeColorf(ivec2 p, vec4 c) { - imageStore(img, p, c); -} - -%s - -// Note that main runs once per INPUT pixel, unlike the old model. -void main() { - uvec2 xy = gl_GlobalInvocationID.xy; - // Kill off any out-of-image threads to avoid stray writes. - // Should only happen on the tiniest mipmaps as PSP textures are power-of-2, - // and we use a 8x8 workgroup size. Probably not really necessary. - if (xy.x >= params.width || xy.y >= params.height) - return; - // applyScaling will write the upscaled pixels, using writeColorf above. - // It's expected to write a square of scale*scale pixels, at the location xy*scale. - applyScaling(xy); -} - -)"; static int VkFormatBytesPerPixel(VkFormat format) { switch (format) { @@ -301,31 +247,7 @@ bool TextureCacheVulkan::CompileMultipassShader(VulkanContext *vulkan, const Tex if (!shaderInfo.computeShaderFiles.empty()) { shaderFiles = shaderInfo.computeShaderFiles; } - if (shaderInfo.scaleFactor == 2) { - multipassScratchDescs_ = { - { "nnedi3_vertical", 1, 2 }, - { "nnedi3_horizontal", 2, 2 }, - }; - multipassStageDescs_ = { - { 0, -1, 0, 1, 1, 1, 2, false }, - { 1, 0, 1, 1, 2, 2, 2, false }, - { 2, 1, -1, 2, 2, 2, 2, false }, - }; - } else if (shaderInfo.scaleFactor == 4) { - multipassScratchDescs_ = { - { "nnedi3_vertical", 1, 2 }, - { "nnedi3_horizontal", 2, 2 }, - { "nnedi3_vertical2", 2, 4 }, - { "nnedi3_horizontal2", 4, 4 }, - }; - multipassStageDescs_ = { - { 0, -1, 0, 1, 1, 1, 2, false }, - { 2, 0, 1, 1, 2, 2, 2, false }, - { 1, 1, 2, 2, 2, 2, 4, false }, - { 2, 2, 3, 2, 4, 4, 4, false }, - { 3, 3, -1, 4, 4, 4, 4, true }, - }; - } else { + if (!GetTextureScaleMultipassLayout(shaderInfo.scaleFactor, &multipassScratchDescs_, &multipassStageDescs_)) { ERROR_LOG(Log::G3D, "Unsupported multipass scale factor %d in section '%s'", shaderInfo.scaleFactor, shaderInfo.section.c_str()); return false; } @@ -395,7 +317,7 @@ void TextureCacheVulkan::CompileScalingShader() { } } else { std::string shaderSource = ReadShaderSrc(shaderInfo->computeShaderFiles[0]); - std::string fullUploadShader = StringFromFormat(uploadShader, shaderSource.c_str()); + std::string fullUploadShader = StringFromFormat(textureScaleUploadShader, shaderSource.c_str()); singlePassCS_ = CompileShaderModule(vulkan, VK_SHADER_STAGE_COMPUTE_BIT, fullUploadShader.c_str(), &error); _dbg_assert_msg_(singlePassCS_ != VK_NULL_HANDLE, "failed to compile upload shader"); if (singlePassCS_ == VK_NULL_HANDLE) diff --git a/GPU/Vulkan/TextureCacheVulkan.h b/GPU/Vulkan/TextureCacheVulkan.h index 0a95a74d53..c14c05d276 100644 --- a/GPU/Vulkan/TextureCacheVulkan.h +++ b/GPU/Vulkan/TextureCacheVulkan.h @@ -101,22 +101,8 @@ private: MULTIPASS, }; - struct MultipassScratchDesc { - const char *tag; - uint8_t widthScale; - uint8_t heightScale; - }; - - struct MultipassStageDesc { - uint8_t shaderIndex; - int8_t inputScratch; // -1 means the original upload buffer. - int8_t outputScratch; // -1 means the final destination image. - uint8_t srcWidthScale; - uint8_t srcHeightScale; - uint8_t dstWidthScale; - uint8_t dstHeightScale; - bool useFinalOutputSize; - }; + typedef TextureScaleScratchDesc MultipassScratchDesc; + typedef TextureScaleStageDesc MultipassStageDesc; void LoadVulkanTextureLevel(TexCacheEntry &entry, uint8_t *writePtr, int rowPitch, int level, int scaleFactor, VkFormat dstFmt); static VkFormat GetDestFormat(GETextureFormat format, GEPaletteFormat clutFormat) ;