Files
ppsspp/GPU/Vulkan/GPU_Vulkan.cpp
T
Henrik RydgårdandClaude Opus 5.5 739efb7e19 Shader caches: End the Vulkan and GL caches in a checksum
Loading trusted whatever was in the file, apart from a few sanity
checks, and a damaged entry stayed in the cache for good, since loading
it put it back in the pipeline map and every save wrote it out again.
A Syphon Filter: Logan's Shadow cache had pipeline keys with garbage in
useHWTransform, which MoltenVK refused to create ("Vertex attribute
texcoord(1) is missing from the vertex descriptor").

Now the files end in an XXH3 checksum of everything before it, checked
before anything is read, and a cache that fails it is deleted and
rebuilt.

Bumps both cache versions, since the files have a new layout. Old files
fail the checksum anyway.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 13:25:18 +02:00

462 lines
16 KiB
C++

// Copyright (c) 2015- 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 <thread>
#include "Common/Profiler/Profiler.h"
#include "Common/Log.h"
#include "Common/TimeUtil.h"
#include "Common/File/FileUtil.h"
#include "Common/GPU/GraphicsContext.h"
#include "Common/StringUtils.h"
#include "Common/Data/Text/StringWriter.h"
#include "Core/Config.h"
#include "Core/Reporting.h"
#include "Core/System.h"
#include "Core/ELF/ParamSFO.h"
#include "GPU/GPUState.h"
#include "GPU/Common/FramebufferManagerCommon.h"
#include "GPU/Vulkan/ShaderManagerVulkan.h"
#include "GPU/Vulkan/GPU_Vulkan.h"
#include "GPU/Vulkan/FramebufferManagerVulkan.h"
#include "GPU/Vulkan/DrawEngineVulkan.h"
#include "GPU/Vulkan/TextureCacheVulkan.h"
#include "Common/GPU/Vulkan/VulkanRenderManager.h"
#include "Common/GPU/Vulkan/VulkanQueueRunner.h"
GPU_Vulkan::GPU_Vulkan(GraphicsContext *gfxCtx, Draw::DrawContext *draw)
: GPUCommonHW(gfxCtx, draw), drawEngine_(draw) {
_assert_(draw);
gstate_c.SetUseFlags(CheckGPUFeatures());
VulkanContext *vulkan = (VulkanContext *)gfxCtx->GetAPIContext();
vulkan->SetProfilerEnabledPtr(&g_Config.bGpuLogProfiler);
shaderManagerVulkan_ = new ShaderManagerVulkan(draw);
pipelineManager_ = new PipelineManagerVulkan(vulkan);
framebufferManagerVulkan_ = new FramebufferManagerVulkan(draw);
framebufferManager_ = framebufferManagerVulkan_;
drawEngineCommon_ = &drawEngine_;
textureCacheVulkan_ = new TextureCacheVulkan(draw, framebufferManager_->GetDraw2D(), vulkan);
textureCache_ = textureCacheVulkan_;
shaderManager_ = shaderManagerVulkan_;
drawEngine_.SetGPUCommon(this);
drawEngine_.SetTextureCache(textureCacheVulkan_);
drawEngine_.SetFramebufferManager(framebufferManagerVulkan_);
drawEngine_.SetShaderManager(shaderManagerVulkan_);
drawEngine_.SetPipelineManager(pipelineManager_);
drawEngine_.Init();
framebufferManagerVulkan_->SetTextureCache(textureCacheVulkan_);
framebufferManagerVulkan_->SetDrawEngine(&drawEngine_);
framebufferManagerVulkan_->SetShaderManager(shaderManagerVulkan_);
textureCacheVulkan_->SetFramebufferManager(framebufferManagerVulkan_);
textureCacheVulkan_->SetShaderManager(shaderManagerVulkan_);
textureCacheVulkan_->SetDrawEngine(&drawEngine_);
// Sanity check gstate
if ((int *)&gstate.transferstart - (int *)&gstate != 0xEA) {
ERROR_LOG(Log::G3D, "gstate has drifted out of sync!");
}
BuildReportingInfo();
textureCache_->NotifyConfigChanged();
drawEngine_.InitDeviceObjects(); // Creates important things like the pipeline layout. Required for loading the disk cache.
// Load shader cache.
std::string discID = g_paramSFO.GetDiscID();
if (discID.size()) {
File::CreateFullPath(GetSysDirectory(DIRECTORY_APP_CACHE));
shaderCachePath_ = GetSysDirectory(DIRECTORY_APP_CACHE) / (discID + ".vkshadercache");
LoadCache(shaderCachePath_, true);
}
InitDeviceObjects();
}
void GPU_Vulkan::FinishInitOnMainThread() {
// This can end up stopping/starting the vulkan render manager.
framebufferManagerVulkan_->Init(msaaLevel_);
}
void GPU_Vulkan::LoadCache(const Path &filename, bool waitForPipelines) {
_dbg_assert_(draw_);
if (!g_Config.bShaderCache) {
WARN_LOG(Log::G3D, "Shader cache disabled. Not loading.");
return;
}
// Actually precompiled by IsReady() since we're single-threaded.
FILE *f = File::OpenCFile(filename, "rb");
if (!f)
return;
if (!VerifyShaderCacheChecksum(filename)) {
WARN_LOG(Log::G3D, "Old or damaged Vulkan shader cache (no matching checksum) - rebuilding.");
fclose(f);
File::Delete(filename);
return;
}
// First compile shaders to SPIR-V, then load the pipeline cache and recreate the pipelines.
// It's when recreating the pipelines that the pipeline cache is useful - in the ideal case,
// it can just memcpy the finished shader binaries out of the pipeline cache file.
bool result = ShaderManagerVulkan::LoadCacheFlags(f, &drawEngine_);
if (!result) {
WARN_LOG(Log::G3D, "ShaderManagerVulkan failed to load cache header.");
}
if (result) {
// Reload use flags in case LoadCacheFlags() changed them.
gstate_c.SetUseFlags(CheckGPUFeatures());
result = shaderManagerVulkan_->LoadCache(f);
if (!result) {
WARN_LOG(Log::G3D, "ShaderManagerVulkan failed to load cache.");
}
}
if (result) {
// WARNING: See comment in LoadPipelineCache if you are tempted to flip the second parameter to true.
result = pipelineManager_->LoadPipelineCache(f, false, shaderManagerVulkan_, draw_, drawEngine_.GetPipelineLayout(), msaaLevel_);
}
fclose(f);
if (waitForPipelines) {
// Now, since we're on the loader thread, we can just block here until all pipelines are actually created.
// This makes it so that the on-screen spinner keeps spinning until we are done.
double start = time_now_d();
VulkanRenderManager *rm = (VulkanRenderManager *)draw_->GetNativeObject(Draw::NativeObject::RENDER_MANAGER);
int maxTasksSeen = rm->WaitForPipelines();
double seconds = time_now_d() - start;
INFO_LOG(Log::G3D, "Waited %0.1fms for at least %d pipeline tasks to finish compiling.", seconds * 1000.0, maxTasksSeen);
}
if (!result) {
WARN_LOG(Log::G3D, "Incompatible Vulkan pipeline cache - rebuilding.");
// Bad cache file for this GPU/Driver/etc. Delete it.
File::Delete(filename);
} else {
INFO_LOG(Log::G3D, "Loaded Vulkan pipeline cache.");
}
}
void GPU_Vulkan::SaveCache(const Path &filename) {
if (!g_Config.bShaderCache) {
INFO_LOG(Log::G3D, "Shader cache disabled. Not saving.");
return;
}
if (!draw_) {
// Already got the lost message, we're in shutdown.
WARN_LOG(Log::G3D, "Not saving shaders - shutting down from in-game.");
return;
}
FILE *f = File::OpenCFile(filename, "wb");
if (!f)
return;
shaderManagerVulkan_->SaveCache(f, &drawEngine_);
// WARNING: See comment in LoadCache if you are tempted to flip the second parameter to true.
pipelineManager_->SavePipelineCache(f, false, shaderManagerVulkan_, draw_);
fclose(f);
if (AppendShaderCacheChecksum(filename)) {
INFO_LOG(Log::G3D, "Saved Vulkan pipeline cache");
} else {
ERROR_LOG(Log::G3D, "Failed to write the Vulkan pipeline cache");
File::Delete(filename);
}
// And the fixed shaders the GPU compiled along the way.
g_spirvCache.SaveIfDirty();
}
GPU_Vulkan::~GPU_Vulkan() {
if (draw_) {
VulkanRenderManager *rm = (VulkanRenderManager *)draw_->GetNativeObject(Draw::NativeObject::RENDER_MANAGER);
// This now also does a hard sync with the render thread, so that we can safely delete our pipeline layout below.
rm->StopThreads();
rm->CheckNothingPending();
}
SaveCache(shaderCachePath_);
// StopThreads should have ensured that no pipelines are queued to compile at this point. So we can tear it down.
delete pipelineManager_;
pipelineManager_ = nullptr;
// Note: We save the cache in DeviceLost
DestroyDeviceObjects();
drawEngine_.DeviceLost();
shaderManager_->ClearShaders();
// other managers are deleted in ~GPUCommonHW.
if (draw_) {
VulkanRenderManager *rm = (VulkanRenderManager *)draw_->GetNativeObject(Draw::NativeObject::RENDER_MANAGER);
rm->StartThreads();
}
}
u32 GPU_Vulkan::CheckGPUFeatures() const {
uint32_t features = GPUCommonHW::CheckGPUFeatures();
VulkanContext *vulkan = (VulkanContext *)draw_->GetNativeObject(Draw::NativeObject::CONTEXT);
if (vulkan->SupportsPreRotation() && g_Config.bSkipBufferEffects) {
features |= GPU_USE_PRE_ROTATION;
}
if (!draw_->GetBugs().Has(Draw::Bugs::PVR_BAD_16BIT_TEXFORMATS)) {
// These are VULKAN_4444_FORMAT and friends.
// Note that we are now using the correct set of formats - the only cases where some may be missing
// are non-conformant implementations like MoltenVK.
uint32_t fmt4444 = draw_->GetDataFormatSupport(Draw::DataFormat::B4G4R4A4_UNORM_PACK16);
uint32_t fmt1555 = draw_->GetDataFormatSupport(Draw::DataFormat::A1R5G5B5_UNORM_PACK16);
uint32_t fmt565 = draw_->GetDataFormatSupport(Draw::DataFormat::R5G6B5_UNORM_PACK16);
if ((fmt4444 & Draw::FMT_TEXTURE) && (fmt565 & Draw::FMT_TEXTURE) && (fmt1555 & Draw::FMT_TEXTURE)) {
features |= GPU_USE_16BIT_FORMATS;
} else {
INFO_LOG(Log::G3D, "Deficient texture format support: 4444: %d 1555: %d 565: %d", fmt4444, fmt1555, fmt565);
}
}
if (g_Config.bStereoRendering && draw_->GetDeviceCaps().multiViewSupported) {
features |= GPU_USE_SINGLE_PASS_STEREO;
features |= GPU_USE_SIMPLE_STEREO_PERSPECTIVE;
}
// Attempt to workaround #17386
if (draw_->GetBugs().Has(Draw::Bugs::UNIFORM_INDEXING_BROKEN)) {
features &= ~GPU_USE_LIGHT_UBERSHADER;
}
features |= GPU_USE_FRAMEBUFFER_ARRAYS;
return CheckGPUFeaturesLate(features);
}
void GPU_Vulkan::BeginHostFrame(const DisplayLayoutConfig &config) {
GPUCommonHW::BeginHostFrame(config);
drawEngine_.BeginFrame();
textureCache_->StartFrame();
VulkanContext *vulkan = (VulkanContext *)draw_->GetNativeObject(Draw::NativeObject::CONTEXT);
int curFrame = vulkan->GetCurFrame();
framebufferManager_->BeginFrame(config);
gstate_c.Dirty(DIRTY_ALL);
if (gstate_c.useFlagsChanged) {
// TODO: It'd be better to recompile them in the background, probably?
// This most likely means that saw equal depth changed.
WARN_LOG(Log::G3D, "Shader use flags changed, clearing all shaders and depth buffers");
// TODO: Not all shaders need to be recompiled. In fact, quite few? Of course, depends on
// the use flag change.. This is a major frame rate hitch in the start of a race in Outrun.
// Pipelines first: their deletion callbacks block on in-flight compiles, which use the shader modules
// that the shaders' own (later) deletion callbacks free.
pipelineManager_->Clear();
shaderManager_->ClearShaders();
framebufferManager_->ClearAllDepthBuffers();
gstate_c.useFlagsChanged = false;
}
if (dumpNextFrame_) {
NOTICE_LOG(Log::G3D, "DUMPING THIS FRAME");
dumpThisFrame_ = true;
dumpNextFrame_ = false;
} else if (dumpThisFrame_) {
dumpThisFrame_ = false;
}
}
void GPU_Vulkan::EndHostFrame() {
VulkanContext *vulkan = (VulkanContext *)draw_->GetNativeObject(Draw::NativeObject::CONTEXT);
drawEngine_.EndFrame();
GPUCommonHW::EndHostFrame();
}
// Needs to be called on GPU thread, not reporting thread.
void GPU_Vulkan::BuildReportingInfo() {
VulkanContext *vulkan = (VulkanContext *)draw_->GetNativeObject(Draw::NativeObject::CONTEXT);
const auto &props = vulkan->GetPhysicalDeviceProperties().properties;
const auto &available = vulkan->GetDeviceFeatures().available;
#define CHECK_BOOL_FEATURE(n) do { if (available.standard.n) { featureNames += ", " #n; } } while (false)
#define CHECK_BOOL_FEATURE_MULTIVIEW(n) do { if (available.multiview.n) { featureNames += ", " #n; } } while (false)
std::string featureNames = "";
CHECK_BOOL_FEATURE(fullDrawIndexUint32);
CHECK_BOOL_FEATURE(geometryShader);
CHECK_BOOL_FEATURE(sampleRateShading);
CHECK_BOOL_FEATURE(dualSrcBlend);
CHECK_BOOL_FEATURE(logicOp);
CHECK_BOOL_FEATURE(multiDrawIndirect);
CHECK_BOOL_FEATURE(drawIndirectFirstInstance);
CHECK_BOOL_FEATURE(depthClamp);
CHECK_BOOL_FEATURE(depthBiasClamp);
CHECK_BOOL_FEATURE(depthBounds);
CHECK_BOOL_FEATURE(samplerAnisotropy);
CHECK_BOOL_FEATURE(textureCompressionETC2);
CHECK_BOOL_FEATURE(textureCompressionASTC_LDR);
CHECK_BOOL_FEATURE(textureCompressionBC);
CHECK_BOOL_FEATURE(occlusionQueryPrecise);
CHECK_BOOL_FEATURE(pipelineStatisticsQuery);
CHECK_BOOL_FEATURE(fragmentStoresAndAtomics);
CHECK_BOOL_FEATURE(shaderTessellationAndGeometryPointSize);
CHECK_BOOL_FEATURE(shaderStorageImageMultisample);
CHECK_BOOL_FEATURE(shaderSampledImageArrayDynamicIndexing);
CHECK_BOOL_FEATURE(shaderClipDistance);
CHECK_BOOL_FEATURE(shaderCullDistance);
CHECK_BOOL_FEATURE(shaderInt64);
CHECK_BOOL_FEATURE(shaderInt16);
CHECK_BOOL_FEATURE_MULTIVIEW(multiview);
CHECK_BOOL_FEATURE_MULTIVIEW(multiviewGeometryShader);
#undef CHECK_BOOL_FEATURE
if (!featureNames.empty()) {
featureNames = featureNames.substr(2);
}
reportingPrimaryInfo_ = props.deviceName;
reportingFullInfo_ = StringFromFormat("v%08x driver v%08x (%s), vendorID=%d, deviceID=%d (features: %s)", props.apiVersion, props.driverVersion, props.deviceName, props.vendorID, props.deviceID, featureNames.c_str());
Reporting::UpdateConfig();
}
void GPU_Vulkan::FinishDeferred() {
drawEngine_.FinishDeferred();
}
void GPU_Vulkan::InitDeviceObjects() {
INFO_LOG(Log::G3D, "GPU_Vulkan::InitDeviceObjects");
uint32_t hacks = 0;
if (PSP_CoreParameter().compat.flags().MGS2AcidHack)
hacks |= QUEUE_HACK_MGS2_ACID;
if (PSP_CoreParameter().compat.flags().SonicRivalsHack)
hacks |= QUEUE_HACK_SONIC;
if (PSP_CoreParameter().compat.flags().TiledDepalHack)
hacks |= QUEUE_HACK_TILED_DEPAL;
// Always on.
hacks |= QUEUE_HACK_RENDERPASS_MERGE;
if (hacks) {
VulkanRenderManager *rm = (VulkanRenderManager *)draw_->GetNativeObject(Draw::NativeObject::RENDER_MANAGER);
rm->GetQueueRunner()->EnableHacks(hacks);
}
}
void GPU_Vulkan::DestroyDeviceObjects() {
INFO_LOG(Log::G3D, "GPU_Vulkan::DestroyDeviceObjects");
// Need to turn off hacks when shutting down the GPU. Don't want them running in the menu.
if (draw_) {
VulkanRenderManager *rm = (VulkanRenderManager *)draw_->GetNativeObject(Draw::NativeObject::RENDER_MANAGER);
if (rm)
rm->GetQueueRunner()->EnableHacks(0);
}
}
void GPU_Vulkan::CheckRenderResized(const DisplayLayoutConfig &config) {
if (renderResized_) {
GPUCommonHW::CheckRenderResized(config);
pipelineManager_->InvalidateMSAAPipelines();
framebufferManager_->ReleasePipelines();
}
}
void GPU_Vulkan::DeviceLost() {
// draw_ is normally actually still valid here in Vulkan. But we null it out in GPUCommonHW::DeviceLost so we don't try to use it again.
// So, we have to save it here to be able to call ReleaseCompileQueue().
Draw::DrawContext *draw = draw_;
if (draw) {
VulkanRenderManager *rm = (VulkanRenderManager *)draw->GetNativeObject(Draw::NativeObject::RENDER_MANAGER);
rm->StopThreads();
}
if (shaderCachePath_.Valid()) {
SaveCache(shaderCachePath_);
}
DestroyDeviceObjects();
pipelineManager_->DeviceLost();
GPUCommonHW::DeviceLost();
if (draw) {
VulkanRenderManager *rm = (VulkanRenderManager *)draw->GetNativeObject(Draw::NativeObject::RENDER_MANAGER);
rm->StartThreads();
}
}
void GPU_Vulkan::DeviceRestore(Draw::DrawContext *draw) {
GPUCommonHW::DeviceRestore(draw); // this updates draw_.
VulkanContext *vulkan = (VulkanContext *)draw_->GetNativeObject(Draw::NativeObject::CONTEXT);
pipelineManager_->DeviceRestore(vulkan);
InitDeviceObjects();
// DeviceLost saved the cache and then threw everything away. Load it again, or the next save would
// overwrite the file with only what gets drawn from now on.
if (shaderCachePath_.Valid()) {
LoadCache(shaderCachePath_, false);
}
}
void GPU_Vulkan::GetStats(StringWriter &w) {
const DrawEngineVulkanStats &drawStats = drawEngine_.GetStats();
w.F("Vertex, Fragment, Pipelines loaded: %i, %i, %i\n"
"Pushbuffer space used: Vtx %d, Idx %d\n",
shaderManagerVulkan_->GetNumVertexShaders(),
shaderManagerVulkan_->GetNumFragmentShaders(),
pipelineManager_->GetNumPipelines(),
drawStats.pushVertexSpaceUsed,
drawStats.pushIndexSpaceUsed);
textureCacheVulkan_->GetStats(w);
FormatGPUStatsCommon(w);
}
std::vector<std::string> GPU_Vulkan::DebugGetShaderIDs(DebugShaderType type) {
switch (type) {
case SHADER_TYPE_PIPELINE:
return pipelineManager_->DebugGetObjectIDs(type);
case SHADER_TYPE_SAMPLER:
return textureCacheVulkan_->DebugGetSamplerIDs();
default:
return GPUCommonHW::DebugGetShaderIDs(type);
}
}
std::string GPU_Vulkan::DebugGetShaderString(std::string id, DebugShaderType type, DebugShaderStringType stringType) {
switch (type) {
case SHADER_TYPE_PIPELINE:
return pipelineManager_->DebugGetObjectString(id, type, stringType);
case SHADER_TYPE_SAMPLER:
return textureCacheVulkan_->DebugGetSamplerString(id, stringType);
default:
return GPUCommonHW::DebugGetShaderString(id, type, stringType);
}
}