MetalDevice: Implement pipeline cache

This commit is contained in:
Stenzek 2024-09-13 19:26:44 +10:00
parent 191957547a
commit bd99688b9d
No known key found for this signature in database
11 changed files with 321 additions and 215 deletions

View file

@ -291,51 +291,45 @@ void D3D12Device::GetPipelineCacheHeader(PIPELINE_CACHE_HEADER* hdr)
hdr->unused = 0; hdr->unused = 0;
} }
bool D3D12Device::ReadPipelineCache(std::optional<DynamicHeapArray<u8>> data) bool D3D12Device::ReadPipelineCache(DynamicHeapArray<u8> data, Error* error)
{ {
PIPELINE_CACHE_HEADER expected_header; PIPELINE_CACHE_HEADER expected_header;
GetPipelineCacheHeader(&expected_header); GetPipelineCacheHeader(&expected_header);
if (data.has_value() && (data->size() < sizeof(PIPELINE_CACHE_HEADER) || if ((data.size() < sizeof(PIPELINE_CACHE_HEADER) ||
std::memcmp(data->data(), &expected_header, sizeof(PIPELINE_CACHE_HEADER)) != 0)) std::memcmp(data.data(), &expected_header, sizeof(PIPELINE_CACHE_HEADER)) != 0))
{ {
WARNING_LOG("Pipeline cache header does not match current device, ignoring."); Error::SetStringView(error, "Pipeline cache header does not match current device.");
data.reset(); return false;
} }
HRESULT hr = const HRESULT hr =
m_device->CreatePipelineLibrary(data.has_value() ? (data->data() + sizeof(PIPELINE_CACHE_HEADER)) : nullptr, m_device->CreatePipelineLibrary(&data[sizeof(PIPELINE_CACHE_HEADER)], data.size() - sizeof(PIPELINE_CACHE_HEADER),
data.has_value() ? (data->size() - sizeof(PIPELINE_CACHE_HEADER)) : 0,
IID_PPV_ARGS(m_pipeline_library.ReleaseAndGetAddressOf())); IID_PPV_ARGS(m_pipeline_library.ReleaseAndGetAddressOf()));
if (SUCCEEDED(hr))
{
if (data.has_value())
s_pipeline_cache_data = std::move(data.value());
return true;
}
// Try without the cache data.
if (data.has_value())
{
WARNING_LOG("CreatePipelineLibrary() failed, trying without cache data. Error: {}",
Error::CreateHResult(hr).GetDescription());
hr = m_device->CreatePipelineLibrary(nullptr, 0, IID_PPV_ARGS(m_pipeline_library.ReleaseAndGetAddressOf()));
if (SUCCEEDED(hr))
return true;
}
if (FAILED(hr)) if (FAILED(hr))
{ {
WARNING_LOG("CreatePipelineLibrary() failed, pipeline caching will not be available. Error: {}", Error::SetHResult(error, "CreatePipelineLibrary() failed: ", hr);
Error::CreateHResult(hr).GetDescription()); return false;
}
// Have to keep the buffer around, DX doesn't take a copy.
s_pipeline_cache_data = std::move(data);
return true;
}
bool D3D12Device::CreatePipelineCache(const std::string& path, Error* error)
{
const HRESULT hr =
m_device->CreatePipelineLibrary(nullptr, 0, IID_PPV_ARGS(m_pipeline_library.ReleaseAndGetAddressOf()));
if (FAILED(hr))
{
Error::SetHResult(error, "CreatePipelineLibrary() failed: ", hr);
return false; return false;
} }
return true; return true;
} }
bool D3D12Device::GetPipelineCacheData(DynamicHeapArray<u8>* data) bool D3D12Device::GetPipelineCacheData(DynamicHeapArray<u8>* data, Error* error)
{ {
if (!m_pipeline_library) if (!m_pipeline_library)
return false; return false;
@ -356,7 +350,7 @@ bool D3D12Device::GetPipelineCacheData(DynamicHeapArray<u8>* data)
const HRESULT hr = m_pipeline_library->Serialize(data->data() + sizeof(PIPELINE_CACHE_HEADER), size); const HRESULT hr = m_pipeline_library->Serialize(data->data() + sizeof(PIPELINE_CACHE_HEADER), size);
if (FAILED(hr)) if (FAILED(hr))
{ {
ERROR_LOG("Serialize() failed with HRESULT {:08X}", static_cast<unsigned>(hr)); Error::SetHResult(error, "Serialize() failed: ", hr);
data->deallocate(); data->deallocate();
return false; return false;
} }

View file

@ -187,8 +187,9 @@ protected:
FeatureMask disabled_features, Error* error) override; FeatureMask disabled_features, Error* error) override;
void DestroyDevice() override; void DestroyDevice() override;
bool ReadPipelineCache(std::optional<DynamicHeapArray<u8>> data) override; bool ReadPipelineCache(DynamicHeapArray<u8> data, Error* error) override;
bool GetPipelineCacheData(DynamicHeapArray<u8>* data) override; bool CreatePipelineCache(const std::string& path, Error* error) override;
bool GetPipelineCacheData(DynamicHeapArray<u8>* data, Error* error) override;
private: private:
enum DIRTY_FLAG : u32 enum DIRTY_FLAG : u32

View file

@ -428,14 +428,29 @@ void GPUDevice::OpenShaderCache(std::string_view base_path, u32 version)
} }
s_pipeline_cache_path = {}; s_pipeline_cache_path = {};
s_pipeline_cache_size = 0;
s_pipeline_cache_hash = {};
if (m_features.pipeline_cache && !base_path.empty()) if (m_features.pipeline_cache && !base_path.empty())
{ {
const std::string basename = GetShaderCacheBaseName("pipelines"); Error error;
std::string filename = Path::Combine(base_path, TinyString::from_format("{}.bin", basename)); s_pipeline_cache_path =
if (OpenPipelineCache(filename)) Path::Combine(base_path, TinyString::from_format("{}.bin", GetShaderCacheBaseName("pipelines")));
s_pipeline_cache_path = std::move(filename); if (FileSystem::FileExists(s_pipeline_cache_path.c_str()))
else {
WARNING_LOG("Failed to read pipeline cache."); if (OpenPipelineCache(s_pipeline_cache_path, &error))
return;
WARNING_LOG("Failed to read pipeline cache '{}': {}", Path::GetFileName(s_pipeline_cache_path),
error.GetDescription());
}
if (!CreatePipelineCache(s_pipeline_cache_path, &error))
{
WARNING_LOG("Failed to create pipeline cache '{}': {}", Path::GetFileName(s_pipeline_cache_path),
error.GetDescription());
s_pipeline_cache_path = {};
}
} }
} }
@ -445,25 +460,11 @@ void GPUDevice::CloseShaderCache()
if (!s_pipeline_cache_path.empty()) if (!s_pipeline_cache_path.empty())
{ {
DynamicHeapArray<u8> data; Error error;
if (GetPipelineCacheData(&data)) if (!ClosePipelineCache(s_pipeline_cache_path, &error))
{ {
// Save disk writes if it hasn't changed, think of the poor SSDs. WARNING_LOG("Failed to close pipeline cache '{}': {}", Path::GetFileName(s_pipeline_cache_path),
if (s_pipeline_cache_size != data.size() || s_pipeline_cache_hash != SHA1Digest::GetDigest(data.cspan())) error.GetDescription());
{
Error error;
INFO_LOG("Compressing and writing {} bytes to '{}'", data.size(), Path::GetFileName(s_pipeline_cache_path));
if (!CompressHelpers::CompressToFile(CompressHelpers::CompressType::Zstandard, s_pipeline_cache_path.c_str(),
data.cspan(), -1, true, &error))
{
ERROR_LOG("Failed to write pipeline cache to '{}': {}", Path::GetFileName(s_pipeline_cache_path),
error.GetDescription());
}
}
else
{
INFO_LOG("Skipping updating pipeline cache '{}' due to no changes.", Path::GetFileName(s_pipeline_cache_path));
}
} }
s_pipeline_cache_path = {}; s_pipeline_cache_path = {};
@ -480,49 +481,56 @@ std::string GPUDevice::GetShaderCacheBaseName(std::string_view type) const
return fmt::format("{}_{}{}", lower_api_name, type, debug_suffix); return fmt::format("{}_{}{}", lower_api_name, type, debug_suffix);
} }
bool GPUDevice::OpenPipelineCache(const std::string& filename) bool GPUDevice::OpenPipelineCache(const std::string& path, Error* error)
{ {
s_pipeline_cache_size = 0; CompressHelpers::OptionalByteBuffer data =
s_pipeline_cache_hash = {}; CompressHelpers::DecompressFile(CompressHelpers::CompressType::Zstandard, path.c_str(), std::nullopt, error);
if (!data.has_value())
return false;
Error error; const size_t cache_size = data->size();
CompressHelpers::OptionalByteBuffer data; const std::array<u8, SHA1Digest::DIGEST_SIZE> cache_hash = SHA1Digest::GetDigest(data->cspan());
if (FileSystem::FileExists(filename.c_str()))
{
data =
CompressHelpers::DecompressFile(CompressHelpers::CompressType::Zstandard, filename.c_str(), std::nullopt, &error);
if (data.has_value())
{
s_pipeline_cache_size = data->size();
s_pipeline_cache_hash = SHA1Digest::GetDigest(data->cspan());
}
else
{
ERROR_LOG("Failed to load pipeline cache from '{}': {}", Path::GetFileName(filename), error.GetDescription());
}
}
INFO_LOG("Loading {} byte pipeline cache with hash {}", s_pipeline_cache_size, INFO_LOG("Loading {} byte pipeline cache with hash {}", s_pipeline_cache_size,
SHA1Digest::DigestToString(s_pipeline_cache_hash)); SHA1Digest::DigestToString(s_pipeline_cache_hash));
if (ReadPipelineCache(std::move(data))) if (!ReadPipelineCache(std::move(data.value()), error))
{
return true;
}
else
{
s_pipeline_cache_size = 0;
s_pipeline_cache_hash = {};
return false; return false;
}
s_pipeline_cache_size = cache_size;
s_pipeline_cache_hash = cache_hash;
return true;
} }
bool GPUDevice::ReadPipelineCache(std::optional<DynamicHeapArray<u8>> data) bool GPUDevice::CreatePipelineCache(const std::string& path, Error* error)
{ {
return false; return false;
} }
bool GPUDevice::GetPipelineCacheData(DynamicHeapArray<u8>* data) bool GPUDevice::ClosePipelineCache(const std::string& path, Error* error)
{
DynamicHeapArray<u8> data;
if (!GetPipelineCacheData(&data, error))
return false;
// Save disk writes if it hasn't changed, think of the poor SSDs.
if (s_pipeline_cache_size == data.size() && s_pipeline_cache_hash == SHA1Digest::GetDigest(data.cspan()))
{
INFO_LOG("Skipping updating pipeline cache '{}' due to no changes.", Path::GetFileName(path));
return true;
}
INFO_LOG("Compressing and writing {} bytes to '{}'", data.size(), Path::GetFileName(path));
return CompressHelpers::CompressToFile(CompressHelpers::CompressType::Zstandard, path.c_str(), data.cspan(), -1, true,
error);
}
bool GPUDevice::ReadPipelineCache(DynamicHeapArray<u8> data, Error* error)
{
return false;
}
bool GPUDevice::GetPipelineCacheData(DynamicHeapArray<u8>* data, Error* error)
{ {
return false; return false;
} }
@ -1705,7 +1713,7 @@ bool GPUDevice::TranslateVulkanSpvToLanguage(const std::span<const u8> spirv, GP
if (m_features.framebuffer_fetch && if (m_features.framebuffer_fetch &&
((sres = dyn_libs::spvc_compiler_options_set_uint(soptions, SPVC_COMPILER_OPTION_MSL_VERSION, ((sres = dyn_libs::spvc_compiler_options_set_uint(soptions, SPVC_COMPILER_OPTION_MSL_VERSION,
SPVC_MAKE_MSL_VERSION(2, 3, 0))) != SPVC_SUCCESS)) target_version)) != SPVC_SUCCESS))
{ {
Error::SetStringFmt(error, "spvc_compiler_options_set_uint(SPVC_COMPILER_OPTION_MSL_VERSION) failed: {}", Error::SetStringFmt(error, "spvc_compiler_options_set_uint(SPVC_COMPILER_OPTION_MSL_VERSION) failed: {}",
static_cast<int>(sres)); static_cast<int>(sres));

View file

@ -750,9 +750,11 @@ protected:
virtual void DestroyDevice() = 0; virtual void DestroyDevice() = 0;
std::string GetShaderCacheBaseName(std::string_view type) const; std::string GetShaderCacheBaseName(std::string_view type) const;
virtual bool OpenPipelineCache(const std::string& filename); virtual bool OpenPipelineCache(const std::string& path, Error* error);
virtual bool ReadPipelineCache(std::optional<DynamicHeapArray<u8>> data); virtual bool CreatePipelineCache(const std::string& path, Error* error);
virtual bool GetPipelineCacheData(DynamicHeapArray<u8>* data); virtual bool ReadPipelineCache(DynamicHeapArray<u8> data, Error* error);
virtual bool GetPipelineCacheData(DynamicHeapArray<u8>* data, Error* error);
virtual bool ClosePipelineCache(const std::string& path, Error* error);
virtual std::unique_ptr<GPUShader> CreateShaderFromBinary(GPUShaderStage stage, std::span<const u8> data, virtual std::unique_ptr<GPUShader> CreateShaderFromBinary(GPUShaderStage stage, std::span<const u8> data,
Error* error) = 0; Error* error) = 0;

View file

@ -288,6 +288,9 @@ protected:
bool CreateDevice(std::string_view adapter, std::optional<bool> exclusive_fullscreen_control, bool CreateDevice(std::string_view adapter, std::optional<bool> exclusive_fullscreen_control,
FeatureMask disabled_features, Error* error) override; FeatureMask disabled_features, Error* error) override;
void DestroyDevice() override; void DestroyDevice() override;
bool OpenPipelineCache(const std::string& path, Error* error) override;
bool CreatePipelineCache(const std::string& path, Error* error) override;
bool ClosePipelineCache(const std::string& path, Error* error) override;
private: private:
static constexpr u32 VERTEX_BUFFER_SIZE = 8 * 1024 * 1024; static constexpr u32 VERTEX_BUFFER_SIZE = 8 * 1024 * 1024;
@ -372,6 +375,7 @@ private:
MetalStreamBuffer m_texture_upload_buffer; MetalStreamBuffer m_texture_upload_buffer;
id<MTLLibrary> m_shaders = nil; id<MTLLibrary> m_shaders = nil;
id<MTLBinaryArchive> m_pipeline_archive = nil;
std::vector<std::pair<std::pair<GPUTexture::Format, GPUTexture::Format>, id<MTLComputePipelineState>>> std::vector<std::pair<std::pair<GPUTexture::Format, GPUTexture::Format>, id<MTLComputePipelineState>>>
m_resolve_pipelines; m_resolve_pipelines;
std::vector<std::pair<ClearPipelineConfig, id<MTLRenderPipelineState>>> m_clear_pipelines; std::vector<std::pair<ClearPipelineConfig, id<MTLRenderPipelineState>>> m_clear_pipelines;
@ -400,6 +404,7 @@ private:
GSVector4i m_current_scissor = {}; GSVector4i m_current_scissor = {};
bool m_vsync_enabled = false; bool m_vsync_enabled = false;
bool m_pipeline_cache_modified = false;
double m_accumulated_gpu_time = 0; double m_accumulated_gpu_time = 0;
double m_last_gpu_time_end = 0; double m_last_gpu_time_end = 0;

View file

@ -155,8 +155,7 @@ MetalDevice::MetalDevice() : m_current_viewport(0, 0, 1, 1), m_current_scissor(0
MetalDevice::~MetalDevice() MetalDevice::~MetalDevice()
{ {
Assert(m_layer == nil); Assert(m_pipeline_archive == nil && m_layer == nil && m_device == nil);
Assert(m_device == nil);
} }
bool MetalDevice::HasSurface() const bool MetalDevice::HasSurface() const
@ -251,8 +250,9 @@ bool MetalDevice::CreateDevice(std::string_view adapter, std::optional<bool> exc
void MetalDevice::SetFeatures(FeatureMask disabled_features) void MetalDevice::SetFeatures(FeatureMask disabled_features)
{ {
// Set version to Metal 2.3, that's all we're using. Use SPIRV-Cross version encoding.
m_render_api = RenderAPI::Metal; m_render_api = RenderAPI::Metal;
m_render_api_version = 100; // TODO: Make this more meaningful. m_render_api_version = 20300;
m_max_texture_size = GetMetalMaxTextureSize(m_device); m_max_texture_size = GetMetalMaxTextureSize(m_device);
m_max_multisamples = GetMetalMaxMultisamples(m_device); m_max_multisamples = GetMetalMaxMultisamples(m_device);
@ -277,8 +277,15 @@ void MetalDevice::SetFeatures(FeatureMask disabled_features)
m_features.explicit_present = false; m_features.explicit_present = false;
m_features.timed_present = true; m_features.timed_present = true;
m_features.shader_cache = true; m_features.shader_cache = true;
m_features.pipeline_cache = false; m_features.pipeline_cache = true;
m_features.prefer_unused_textures = true; m_features.prefer_unused_textures = true;
// Disable pipeline cache on Intel, apparently it's buggy.
if ([[m_device name] containsString:@"Intel"])
{
WARNING_LOG("Disabling Metal pipeline cache on Intel GPU.");
m_features.pipeline_cache = false;
}
} }
bool MetalDevice::LoadShaders() bool MetalDevice::LoadShaders()
@ -313,6 +320,76 @@ bool MetalDevice::LoadShaders()
} }
} }
bool MetalDevice::OpenPipelineCache(const std::string& path, Error* error)
{
@autoreleasepool
{
MTLBinaryArchiveDescriptor* archiveDescriptor = [[[MTLBinaryArchiveDescriptor alloc] init] autorelease];
archiveDescriptor.url = [NSURL fileURLWithPath:StringViewToNSString(path)];
NSError* nserror = nil;
m_pipeline_archive = [m_device newBinaryArchiveWithDescriptor:archiveDescriptor error:&nserror];
if (m_pipeline_archive == nil)
{
NSErrorToErrorObject(error, "newBinaryArchiveWithDescriptor failed: ", nserror);
return false;
}
m_pipeline_cache_modified = false;
return true;
}
}
bool MetalDevice::CreatePipelineCache(const std::string& path, Error* error)
{
@autoreleasepool
{
MTLBinaryArchiveDescriptor* archiveDescriptor = [[[MTLBinaryArchiveDescriptor alloc] init] autorelease];
archiveDescriptor.url = nil;
NSError* nserror = nil;
m_pipeline_archive = [m_device newBinaryArchiveWithDescriptor:archiveDescriptor error:&nserror];
if (m_pipeline_archive == nil)
{
NSErrorToErrorObject(error, "newBinaryArchiveWithDescriptor failed: ", nserror);
return false;
}
m_pipeline_cache_modified = false;
return true;
}
}
bool MetalDevice::ClosePipelineCache(const std::string& path, Error* error)
{
if (!m_pipeline_archive)
return false;
const ScopedGuard closer = [this]() {
[m_pipeline_archive release];
m_pipeline_archive = nil;
};
if (!m_pipeline_cache_modified)
{
INFO_LOG("Not saving pipeline cache, it has not been modified.");
return true;
}
@autoreleasepool
{
NSURL* url = [NSURL fileURLWithPath:StringViewToNSString(path)];
NSError* nserror = nil;
if (![m_pipeline_archive serializeToURL:url error:&nserror])
{
NSErrorToErrorObject(error, "serializeToURL failed: ", nserror);
return false;
}
return true;
}
}
id<MTLFunction> MetalDevice::GetFunctionFromLibrary(id<MTLLibrary> library, NSString* name) id<MTLFunction> MetalDevice::GetFunctionFromLibrary(id<MTLLibrary> library, NSString* name)
{ {
id<MTLFunction> function = [library newFunctionWithName:name]; id<MTLFunction> function = [library newFunctionWithName:name];
@ -609,30 +686,13 @@ void MetalShader::SetDebugName(std::string_view name)
} }
} }
// TODO: Clean this up, somehow..
namespace EmuFolders {
extern std::string DataRoot;
}
static void DumpShader(u32 n, std::string_view suffix, std::string_view data)
{
if (data.empty())
return;
auto fp = FileSystem::OpenManagedCFile(
Path::Combine(EmuFolders::DataRoot, fmt::format("shader{}_{}.txt", suffix, n)).c_str(), "wb");
if (!fp)
return;
std::fwrite(data.data(), data.length(), 1, fp.get());
}
std::unique_ptr<GPUShader> MetalDevice::CreateShaderFromMSL(GPUShaderStage stage, std::string_view source, std::unique_ptr<GPUShader> MetalDevice::CreateShaderFromMSL(GPUShaderStage stage, std::string_view source,
std::string_view entry_point, Error* error) std::string_view entry_point, Error* error)
{ {
@autoreleasepool @autoreleasepool
{ {
NSString* const ns_source = StringViewToNSString(source); NSString* const ns_source = StringViewToNSString(source);
NSError* nserror = nullptr; NSError* nserror = nil;
id<MTLLibrary> library = [m_device newLibraryWithSource:ns_source options:nil error:&nserror]; id<MTLLibrary> library = [m_device newLibraryWithSource:ns_source options:nil error:&nserror];
if (!library) if (!library)
{ {
@ -897,13 +957,41 @@ std::unique_ptr<GPUPipeline> MetalDevice::CreatePipeline(const GPUPipeline::Grap
if (config.layout == GPUPipeline::Layout::SingleTextureBufferAndPushConstants) if (config.layout == GPUPipeline::Layout::SingleTextureBufferAndPushConstants)
desc.fragmentBuffers[1].mutability = MTLMutabilityImmutable; desc.fragmentBuffers[1].mutability = MTLMutabilityImmutable;
NSError* nserror = nullptr; NSError* nserror = nil;
id<MTLRenderPipelineState> pipeline = [m_device newRenderPipelineStateWithDescriptor:desc error:&nserror];
// Try cached first.
id<MTLRenderPipelineState> pipeline = nil;
if (m_pipeline_archive != nil)
{
desc.binaryArchives = [NSArray arrayWithObjects:m_pipeline_archive, nil];
pipeline = [m_device newRenderPipelineStateWithDescriptor:desc
options:MTLPipelineOptionFailOnBinaryArchiveMiss
reflection:nil
error:&nserror];
if (pipeline == nil)
{
// Add it to the cache.
if (![m_pipeline_archive addRenderPipelineFunctionsWithDescriptor:desc error:&nserror])
{
LogNSError(nserror, "Failed to add render pipeline to binary archive");
desc.binaryArchives = nil;
}
else
{
m_pipeline_cache_modified = true;
}
}
}
if (pipeline == nil) if (pipeline == nil)
{ {
LogNSError(nserror, "Failed to create render pipeline state"); pipeline = [m_device newRenderPipelineStateWithDescriptor:desc error:&nserror];
NSErrorToErrorObject(error, "newRenderPipelineStateWithDescriptor failed: ", nserror); if (pipeline == nil)
return {}; {
LogNSError(nserror, "Failed to create render pipeline state");
NSErrorToErrorObject(error, "newRenderPipelineStateWithDescriptor failed: ", nserror);
return {};
}
} }
return std::unique_ptr<GPUPipeline>(new MetalPipeline(pipeline, depth, cull_mode, primitive)); return std::unique_ptr<GPUPipeline>(new MetalPipeline(pipeline, depth, cull_mode, primitive));

View file

@ -529,7 +529,6 @@ void OpenGLDevice::DestroyDevice()
if (!m_gl_context) if (!m_gl_context)
return; return;
ClosePipelineCache();
DestroyBuffers(); DestroyBuffers();
m_gl_context->DoneCurrent(); m_gl_context->DoneCurrent();

View file

@ -136,8 +136,9 @@ protected:
FeatureMask disabled_features, Error* error) override; FeatureMask disabled_features, Error* error) override;
void DestroyDevice() override; void DestroyDevice() override;
bool OpenPipelineCache(const std::string& filename) override; bool OpenPipelineCache(const std::string& path, Error* error) override;
bool GetPipelineCacheData(DynamicHeapArray<u8>* data) override; bool CreatePipelineCache(const std::string& path, Error* error) override;
bool ClosePipelineCache(const std::string& path, Error* error) override;
private: private:
static constexpr u8 NUM_TIMESTAMP_QUERIES = 3; static constexpr u8 NUM_TIMESTAMP_QUERIES = 3;
@ -172,7 +173,6 @@ private:
const GPUPipeline::GraphicsConfig& plconfig); const GPUPipeline::GraphicsConfig& plconfig);
void AddToPipelineCache(OpenGLPipeline::ProgramCacheItem* it); void AddToPipelineCache(OpenGLPipeline::ProgramCacheItem* it);
bool DiscardPipelineCache(); bool DiscardPipelineCache();
void ClosePipelineCache();
void ApplyRasterizationState(GPUPipeline::RasterizationState rs); void ApplyRasterizationState(GPUPipeline::RasterizationState rs);
void ApplyDepthState(GPUPipeline::DepthState ds); void ApplyDepthState(GPUPipeline::DepthState ds);
@ -224,7 +224,6 @@ private:
bool m_timestamp_query_started = false; bool m_timestamp_query_started = false;
std::FILE* m_pipeline_disk_cache_file = nullptr; std::FILE* m_pipeline_disk_cache_file = nullptr;
std::string m_pipeline_disk_cache_filename;
u32 m_pipeline_disk_cache_data_end = 0; u32 m_pipeline_disk_cache_data_end = 0;
bool m_pipeline_disk_cache_changed = false; bool m_pipeline_disk_cache_changed = false;

View file

@ -753,49 +753,29 @@ void OpenGLDevice::SetPipeline(GPUPipeline* pipeline)
} }
} }
bool OpenGLDevice::OpenPipelineCache(const std::string& filename) bool OpenGLDevice::OpenPipelineCache(const std::string& path, Error* error)
{ {
DebugAssert(!m_pipeline_disk_cache_file); DebugAssert(!m_pipeline_disk_cache_file);
Error error; m_pipeline_disk_cache_file = FileSystem::OpenCFile(path.c_str(), "r+b", error);
m_pipeline_disk_cache_file = FileSystem::OpenCFile(filename.c_str(), "r+b", &error);
m_pipeline_disk_cache_filename = filename;
if (!m_pipeline_disk_cache_file) if (!m_pipeline_disk_cache_file)
{ return false;
// Multiple instances running? Ignore.
if (errno == EACCES)
{
m_pipeline_disk_cache_filename = {};
return true;
}
// If it doesn't exist, we're going to create it.
if (errno != ENOENT)
{
WARNING_LOG("Failed to open shader cache: {}", error.GetDescription());
m_pipeline_disk_cache_filename = {};
return false;
}
WARNING_LOG("Disk cache does not exist, creating.");
return DiscardPipelineCache();
}
// Read footer. // Read footer.
const s64 size = FileSystem::FSize64(m_pipeline_disk_cache_file); const s64 size = FileSystem::FSize64(m_pipeline_disk_cache_file);
if (size < static_cast<s64>(sizeof(PipelineDiskCacheFooter)) || if (size < static_cast<s64>(sizeof(PipelineDiskCacheFooter)) ||
size >= static_cast<s64>(std::numeric_limits<u32>::max())) size >= static_cast<s64>(std::numeric_limits<u32>::max()))
{ {
return DiscardPipelineCache(); Error::SetStringView(error, "Invalid cache file size.");
return false;
} }
PipelineDiskCacheFooter file_footer; PipelineDiskCacheFooter file_footer;
if (FileSystem::FSeek64(m_pipeline_disk_cache_file, size - sizeof(PipelineDiskCacheFooter), SEEK_SET) != 0 || if (FileSystem::FSeek64(m_pipeline_disk_cache_file, size - sizeof(PipelineDiskCacheFooter), SEEK_SET) != 0 ||
std::fread(&file_footer, sizeof(file_footer), 1, m_pipeline_disk_cache_file) != 1) std::fread(&file_footer, sizeof(file_footer), 1, m_pipeline_disk_cache_file) != 1)
{ {
ERROR_LOG("Failed to read disk cache footer."); Error::SetStringView(error, "Invalid cache file footer.");
return DiscardPipelineCache(); return false;
} }
PipelineDiskCacheFooter expected_footer; PipelineDiskCacheFooter expected_footer;
@ -809,8 +789,8 @@ bool OpenGLDevice::OpenPipelineCache(const std::string& filename)
std::strncmp(file_footer.driver_version, expected_footer.driver_version, std::size(file_footer.driver_version)) != std::strncmp(file_footer.driver_version, expected_footer.driver_version, std::size(file_footer.driver_version)) !=
0) 0)
{ {
ERROR_LOG("Disk cache does not match expected driver/version."); Error::SetStringView(error, "Cache does not match expected driver/version.");
return DiscardPipelineCache(); return false;
} }
m_pipeline_disk_cache_data_end = static_cast<u32>(size) - sizeof(PipelineDiskCacheFooter) - m_pipeline_disk_cache_data_end = static_cast<u32>(size) - sizeof(PipelineDiskCacheFooter) -
@ -818,8 +798,8 @@ bool OpenGLDevice::OpenPipelineCache(const std::string& filename)
if (m_pipeline_disk_cache_data_end < 0 || if (m_pipeline_disk_cache_data_end < 0 ||
FileSystem::FSeek64(m_pipeline_disk_cache_file, m_pipeline_disk_cache_data_end, SEEK_SET) != 0) FileSystem::FSeek64(m_pipeline_disk_cache_file, m_pipeline_disk_cache_data_end, SEEK_SET) != 0)
{ {
ERROR_LOG("Failed to seek to start of index entries."); Error::SetStringView(error, "Failed to seek to start of index entries.");
return DiscardPipelineCache(); return false;
} }
// Read entries. // Read entries.
@ -829,14 +809,16 @@ bool OpenGLDevice::OpenPipelineCache(const std::string& filename)
if (std::fread(&entry, sizeof(entry), 1, m_pipeline_disk_cache_file) != 1 || if (std::fread(&entry, sizeof(entry), 1, m_pipeline_disk_cache_file) != 1 ||
(static_cast<s64>(entry.offset) + static_cast<s64>(entry.compressed_size)) >= size) (static_cast<s64>(entry.offset) + static_cast<s64>(entry.compressed_size)) >= size)
{ {
ERROR_LOG("Failed to read disk cache entry."); Error::SetStringView(error, "Failed to read disk cache entry.");
return DiscardPipelineCache(); m_program_cache.clear();
return false;
} }
if (m_program_cache.find(entry.key) != m_program_cache.end()) if (m_program_cache.find(entry.key) != m_program_cache.end())
{ {
ERROR_LOG("Duplicate program in disk cache."); Error::SetStringView(error, "Duplicate program in disk cache.");
return DiscardPipelineCache(); m_program_cache.clear();
return false;
} }
OpenGLPipeline::ProgramCacheItem pitem; OpenGLPipeline::ProgramCacheItem pitem;
@ -853,10 +835,15 @@ bool OpenGLDevice::OpenPipelineCache(const std::string& filename)
return true; return true;
} }
bool OpenGLDevice::GetPipelineCacheData(DynamicHeapArray<u8>* data) bool OpenGLDevice::CreatePipelineCache(const std::string& path, Error* error)
{ {
// Self-managed. m_pipeline_disk_cache_file = FileSystem::OpenCFile(path.c_str(), "w+b", error);
return false; if (!m_pipeline_disk_cache_file)
return false;
m_pipeline_disk_cache_data_end = 0;
m_pipeline_disk_cache_changed = true;
return true;
} }
GLuint OpenGLDevice::CreateProgramFromPipelineCache(const OpenGLPipeline::ProgramCacheItem& it, GLuint OpenGLDevice::CreateProgramFromPipelineCache(const OpenGLPipeline::ProgramCacheItem& it,
@ -976,42 +963,42 @@ bool OpenGLDevice::DiscardPipelineCache()
it = m_program_cache.erase(it); it = m_program_cache.erase(it);
} }
if (m_pipeline_disk_cache_file) if (!m_pipeline_disk_cache_file)
std::fclose(m_pipeline_disk_cache_file);
Error error;
m_pipeline_disk_cache_data_end = 0;
m_pipeline_disk_cache_file = FileSystem::OpenCFile(m_pipeline_disk_cache_filename.c_str(), "w+b", &error);
if (!m_pipeline_disk_cache_file) [[unlikely]]
{ {
ERROR_LOG("Failed to reopen pipeline cache: {}", error.GetDescription()); // Probably shouldn't get here...
m_pipeline_disk_cache_filename = {};
return false; return false;
} }
Error error;
if (!FileSystem::FTruncate64(m_pipeline_disk_cache_file, 0, &error))
{
ERROR_LOG("Failed to truncate pipeline cache: {}", error.GetDescription());
std::fclose(m_pipeline_disk_cache_file);
m_pipeline_disk_cache_file = nullptr;
return false;
}
m_pipeline_disk_cache_data_end = 0;
m_pipeline_disk_cache_changed = true;
return true; return true;
} }
void OpenGLDevice::ClosePipelineCache() bool OpenGLDevice::ClosePipelineCache(const std::string& filename, Error* error)
{ {
const ScopedGuard file_closer = [this]() {
if (m_pipeline_disk_cache_file)
{
std::fclose(m_pipeline_disk_cache_file);
m_pipeline_disk_cache_file = nullptr;
}
};
if (!m_pipeline_disk_cache_changed) if (!m_pipeline_disk_cache_changed)
{ {
VERBOSE_LOG("Not updating pipeline cache because it has not changed."); VERBOSE_LOG("Not updating pipeline cache because it has not changed.");
return; std::fclose(m_pipeline_disk_cache_file);
m_pipeline_disk_cache_file = nullptr;
return true;
} }
if (FileSystem::FSeek64(m_pipeline_disk_cache_file, m_pipeline_disk_cache_data_end, SEEK_SET) != 0) [[unlikely]] // Rewrite footer/index entries.
if (!FileSystem::FSeek64(m_pipeline_disk_cache_file, m_pipeline_disk_cache_data_end, SEEK_SET, error) != 0)
{ {
ERROR_LOG("Failed to seek to data end."); std::fclose(m_pipeline_disk_cache_file);
return; m_pipeline_disk_cache_file = nullptr;
return false;
} }
u32 count = 0; u32 count = 0;
@ -1030,8 +1017,10 @@ void OpenGLDevice::ClosePipelineCache()
if (std::fwrite(&entry, sizeof(entry), 1, m_pipeline_disk_cache_file) != 1) [[unlikely]] if (std::fwrite(&entry, sizeof(entry), 1, m_pipeline_disk_cache_file) != 1) [[unlikely]]
{ {
ERROR_LOG("Failed to write index entry."); Error::SetErrno(error, "fwrite() for entry failed: ", errno);
return; std::fclose(m_pipeline_disk_cache_file);
m_pipeline_disk_cache_file = nullptr;
return false;
} }
count++; count++;
@ -1042,5 +1031,14 @@ void OpenGLDevice::ClosePipelineCache()
footer.num_programs = count; footer.num_programs = count;
if (std::fwrite(&footer, sizeof(footer), 1, m_pipeline_disk_cache_file) != 1) [[unlikely]] if (std::fwrite(&footer, sizeof(footer), 1, m_pipeline_disk_cache_file) != 1) [[unlikely]]
ERROR_LOG("Failed to write footer."); {
Error::SetErrno(error, "fwrite() for footer failed: ", errno);
std::fclose(m_pipeline_disk_cache_file);
m_pipeline_disk_cache_file = nullptr;
}
if (std::fclose(m_pipeline_disk_cache_file) != 0)
Error::SetErrno(error, "fclose() failed: ", errno);
m_pipeline_disk_cache_file = nullptr;
return true;
} }

View file

@ -2098,37 +2098,37 @@ void VulkanDevice::DestroyDevice()
Vulkan::UnloadVulkanLibrary(); Vulkan::UnloadVulkanLibrary();
} }
bool VulkanDevice::ValidatePipelineCacheHeader(const VK_PIPELINE_CACHE_HEADER& header) bool VulkanDevice::ValidatePipelineCacheHeader(const VK_PIPELINE_CACHE_HEADER& header, Error* error)
{ {
if (header.header_length < sizeof(VK_PIPELINE_CACHE_HEADER)) if (header.header_length < sizeof(VK_PIPELINE_CACHE_HEADER))
{ {
ERROR_LOG("Pipeline cache failed validation: Invalid header length"); Error::SetStringView(error, "Invalid header length");
return false; return false;
} }
if (header.header_version != VK_PIPELINE_CACHE_HEADER_VERSION_ONE) if (header.header_version != VK_PIPELINE_CACHE_HEADER_VERSION_ONE)
{ {
ERROR_LOG("Pipeline cache failed validation: Invalid header version"); Error::SetStringView(error, "Invalid header version");
return false; return false;
} }
if (header.vendor_id != m_device_properties.vendorID) if (header.vendor_id != m_device_properties.vendorID)
{ {
ERROR_LOG("Pipeline cache failed validation: Incorrect vendor ID (file: 0x{:X}, device: 0x{:X})", header.vendor_id, Error::SetStringFmt(error, "Incorrect vendor ID (file: 0x{:X}, device: 0x{:X})", header.vendor_id,
m_device_properties.vendorID); m_device_properties.vendorID);
return false; return false;
} }
if (header.device_id != m_device_properties.deviceID) if (header.device_id != m_device_properties.deviceID)
{ {
ERROR_LOG("Pipeline cache failed validation: Incorrect device ID (file: 0x{:X}, device: 0x{:X})", header.device_id, Error::SetStringFmt(error, "Incorrect device ID (file: 0x{:X}, device: 0x{:X})", header.device_id,
m_device_properties.deviceID); m_device_properties.deviceID);
return false; return false;
} }
if (std::memcmp(header.uuid, m_device_properties.pipelineCacheUUID, VK_UUID_SIZE) != 0) if (std::memcmp(header.uuid, m_device_properties.pipelineCacheUUID, VK_UUID_SIZE) != 0)
{ {
ERROR_LOG("Pipeline cache failed validation: Incorrect UUID"); Error::SetStringView(error, "Incorrect UUID");
return false; return false;
} }
@ -2144,35 +2144,46 @@ void VulkanDevice::FillPipelineCacheHeader(VK_PIPELINE_CACHE_HEADER* header)
std::memcpy(header->uuid, m_device_properties.pipelineCacheUUID, VK_UUID_SIZE); std::memcpy(header->uuid, m_device_properties.pipelineCacheUUID, VK_UUID_SIZE);
} }
bool VulkanDevice::ReadPipelineCache(std::optional<DynamicHeapArray<u8>> data) bool VulkanDevice::ReadPipelineCache(DynamicHeapArray<u8> data, Error* error)
{ {
if (data.has_value()) if (data.size() < sizeof(VK_PIPELINE_CACHE_HEADER))
{ {
if (data->size() < sizeof(VK_PIPELINE_CACHE_HEADER)) Error::SetStringView(error, "Pipeline cache is too small.");
{ return false;
ERROR_LOG("Pipeline cache is too small, ignoring.");
data.reset();
}
VK_PIPELINE_CACHE_HEADER header;
std::memcpy(&header, data->data(), sizeof(header));
if (!ValidatePipelineCacheHeader(header))
data.reset();
} }
const VkPipelineCacheCreateInfo ci{VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO, nullptr, 0, // alignment reasons...
data.has_value() ? data->size() : 0, data.has_value() ? data->data() : nullptr}; VK_PIPELINE_CACHE_HEADER header;
std::memcpy(&header, data.data(), sizeof(header));
if (!ValidatePipelineCacheHeader(header, error))
return false;
const VkPipelineCacheCreateInfo ci{VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO, nullptr, 0, data.size(),
data.data()};
VkResult res = vkCreatePipelineCache(m_device, &ci, nullptr, &m_pipeline_cache); VkResult res = vkCreatePipelineCache(m_device, &ci, nullptr, &m_pipeline_cache);
if (res != VK_SUCCESS) if (res != VK_SUCCESS)
{ {
LOG_VULKAN_ERROR(res, "vkCreatePipelineCache() failed: "); Vulkan::SetErrorObject(error, "vkCreatePipelineCache() failed: ", res);
return false; return false;
} }
return true; return true;
} }
bool VulkanDevice::GetPipelineCacheData(DynamicHeapArray<u8>* data) bool VulkanDevice::CreatePipelineCache(const std::string& path, Error* error)
{
const VkPipelineCacheCreateInfo ci{VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO, nullptr, 0, 0, nullptr};
VkResult res = vkCreatePipelineCache(m_device, &ci, nullptr, &m_pipeline_cache);
if (res != VK_SUCCESS)
{
Vulkan::SetErrorObject(error, "vkCreatePipelineCache() failed: ", res);
return false;
}
return true;
}
bool VulkanDevice::GetPipelineCacheData(DynamicHeapArray<u8>* data, Error* error)
{ {
if (m_pipeline_cache == VK_NULL_HANDLE) if (m_pipeline_cache == VK_NULL_HANDLE)
return false; return false;
@ -2181,7 +2192,7 @@ bool VulkanDevice::GetPipelineCacheData(DynamicHeapArray<u8>* data)
VkResult res = vkGetPipelineCacheData(m_device, m_pipeline_cache, &data_size, nullptr); VkResult res = vkGetPipelineCacheData(m_device, m_pipeline_cache, &data_size, nullptr);
if (res != VK_SUCCESS) if (res != VK_SUCCESS)
{ {
LOG_VULKAN_ERROR(res, "vkGetPipelineCacheData() failed: "); Vulkan::SetErrorObject(error, "vkGetPipelineCacheData() failed: ", res);
return false; return false;
} }
@ -2189,7 +2200,7 @@ bool VulkanDevice::GetPipelineCacheData(DynamicHeapArray<u8>* data)
res = vkGetPipelineCacheData(m_device, m_pipeline_cache, &data_size, data->data()); res = vkGetPipelineCacheData(m_device, m_pipeline_cache, &data_size, data->data());
if (res != VK_SUCCESS) if (res != VK_SUCCESS)
{ {
LOG_VULKAN_ERROR(res, "vkGetPipelineCacheData() (2) failed: "); Vulkan::SetErrorObject(error, "vkGetPipelineCacheData() (2) failed: ", res);
return false; return false;
} }

View file

@ -234,8 +234,9 @@ protected:
FeatureMask disabled_features, Error* error) override; FeatureMask disabled_features, Error* error) override;
void DestroyDevice() override; void DestroyDevice() override;
bool ReadPipelineCache(std::optional<DynamicHeapArray<u8>> data) override; bool ReadPipelineCache(DynamicHeapArray<u8> data, Error* error) override;
bool GetPipelineCacheData(DynamicHeapArray<u8>* data) override; bool CreatePipelineCache(const std::string& path, Error* error) override;
bool GetPipelineCacheData(DynamicHeapArray<u8>* data, Error* error) override;
private: private:
enum DIRTY_FLAG : u32 enum DIRTY_FLAG : u32
@ -305,7 +306,7 @@ private:
static VkInstance CreateVulkanInstance(const WindowInfo& wi, OptionalExtensions* oe, bool enable_debug_utils, static VkInstance CreateVulkanInstance(const WindowInfo& wi, OptionalExtensions* oe, bool enable_debug_utils,
bool enable_validation_layer); bool enable_validation_layer);
bool ValidatePipelineCacheHeader(const VK_PIPELINE_CACHE_HEADER& header); bool ValidatePipelineCacheHeader(const VK_PIPELINE_CACHE_HEADER& header, Error* error);
void FillPipelineCacheHeader(VK_PIPELINE_CACHE_HEADER* header); void FillPipelineCacheHeader(VK_PIPELINE_CACHE_HEADER* header);
// Enable/disable debug message runtime. // Enable/disable debug message runtime.