Skip to content

Conversation

Fire-Cube
Copy link
Contributor

This is currently still unfinished and there may be crashes and graphical errors. The caching system is currently kept as simple as possible but when everything works I will make sure that we only have one or two files for saving

@LNDF
Copy link
Member

LNDF commented Jun 26, 2025

From the discord:

just as an idea, shouldn't shader caches be separated by game (so they can be easily deleted or transferred?
Also, maybe it is a good idea to combine shader of a game into a single file for sharing proposes and dump individual cached shaders if shader dumping is enabled (idk if sharing caches would be ok. After all, it's just spirv with metadata and afaik sharing spirv is ok)

@georgemoralis
Copy link
Collaborator

Just a quote here : sharing cache shaders is also copyright material and it is not allowed

[[nodiscard]] inline u32 HashCombine(const u32 seed, const u32 hash) {
return seed ^ (hash + 0x9e3779b9 + (seed << 6) + (seed >> 2));
template <typename T, typename U>
T HashCombine(const T& seed, const U& value) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution. You can see how the 32 but and 64 bit versions of the hash are different (see the bit shifts).

I don't know if this affects the quality of the hash. You should check this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve now adjusted it so that it always shifts like the 64-bit version that shouldn’t really cause any issues.

Comment on lines 10 to 21
namespace ShaderCache {

u64 CalculateSpecializationHash(const Shader::StageSpecialization& spec);
void SerializeInfo(
std::ostream& info_serialized, Shader::Info info);
void DeserializeInfo(std::istream& info_serialized, Shader::Info& info);

bool CheckShaderCache(std::string shader_id);
void GetShader(std::string shader_id, Shader::Info& info, std::vector<u32>& spv);
void AddShader(std::string shader_id, std::vector<u32> spv, std::ostream& info_serialized);

} // namespace ShaderCache
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe instead of managing this in a namespace, use a class. Pass the id of the name in the constructor so it knows where to save the cache.

I would create an instance in PipelineCache (since is there where it's used.

const auto shader_cache_dir = Common::FS::GetUserPath(Common::FS::PathType::ShaderDir) / "cache";
std::unordered_map<std::string, std::vector<u32>> g_ud_storage;

u64 CalculateSpecializationHash(const Shader::StageSpecialization& spec) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe specialize std::hash instead of calculating here. Then you could store the specialization info itself as a key in the map (which would handle collisions)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite understand what you mean

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, this function takes an StageSpecialization and outputs the hash. I think it is better to specialize std::hash<Shader::StageSpecialization> insteed.

}
}

bool CheckShaderCache(std::string shader_id) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is better to store the entire shader cache into a single file.

I would use a map with a key consisting of the shader id and specialization, and value being the info and the spirv blob.

Then load the entire cache into memory when the game starts. This is easily achievable with a serialization library.

Maybe in the future, we could cache entire pipeline states, as well as use a separate cache for the vulkan compiled pipelines themselves.

@StevenMiller123
Copy link
Collaborator

Assassin's Creed® Unity crashes on a non-GPU memory assert when running with cached shaders.

[Debug] <Critical> page_manager.cpp:215 operator(): Assertion Failed!
Attempted to track non-GPU memory at address 0x103fa50000, size 0x7f000.

CUSA00663.log

BLEACH Rebirth of Souls crashes on an assert when running with cached shaders.

[Debug] <Critical> vk_graphics_pipeline.cpp:299 operator(): Assertion Failed!
Failed to create graphics pipeline: ErrorUnknown

CUSA27765.log

Bloodborne™ crashes on [Debug] <Critical> info.h:291 operator(): Assertion Failed! with no cached shaders when loading ingame.
CUSA00900.log

Call of Duty®: Advanced Warfare crashes on an assert when running with cached shaders.

[Debug] <Critical> vk_graphics_pipeline.cpp:299 operator(): Assertion Failed!
Failed to create graphics pipeline: ErrorUnknown

CUSA00803.log

For the sake of time, these are all I'll test for right now, seems like a lot of games are suffering from that graphics pipeline crash.

// Shader::Info::UserDataMask
template<class Archive>
void serialize(Archive& ar, Shader::Info::UserDataMask& mask) {
ar(mask.mask);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is opinionated, and I don't either know what the best design is.

But you can declare the member function serialize/save/load inside of the class directly and it will automaticly be serializable (I'm not 100% into it either, but just in case you didn't know).

https://uscilab.github.io/cereal/serialization_functions.html

@Fire-Cube Fire-Cube force-pushed the shader_cache branch 2 times, most recently from 8b0c8fb to ad1f59e Compare July 19, 2025 15:19
(Common::FS::GetUserPath(Common::FS::PathType::ShaderDir) / "cache" / "portable")

#define SHADER_CACHE_BLOB_PATH \
(SHADER_CACHE_DIR / (std::string{Common::ElfInfo::Instance().GameSerial()} + "_shaders.bin"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use macros like that. It seems confusing while reading the code and it looks like shaders are not game separated.

#include "shader_recompiler/specialization.h"
#include "video_core/renderer_vulkan/shader_cache_serialization.h"

namespace ShaderCache {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a lot better now. But I think there are some key issues. I will first describe them and explain a possible way to solve them later.

  • You are managing offsets to shader data manualy. That should be the job of the serialization library.
  • Shader cache being in a namespace doesn't follow the style of ofther components of this emulator, like the PipelineCache.

I can see that the shader cache is a dependency of PipelineCache. So I would do the following:

This will be all pseudocode.

struct ShaderCache {
    struct ShaderCacheKey {
        u32 pgm_hash;
        StageSpecialization spec; // You specialize std::hash<StageSpecialization>() for convinience (you already have this as CalculateSpecializationHash, you just convert it to the std::hash<> specialization.
    // You also specialize std::hash<ShaderCacheKey>() so you can use in std::unordered_map as the key. You just CombineHash() both.
    };

    struct ShaderCacheEntry {
        Info info; // Also, define serialization for this
        std::vector<u32> spv;

        // Define serialize for this (archive both).
    };

    std::unordered_map<ShaderCacheKey, ShaderCacheEntry> cache_entries;
    // Maybe it is interesting to save some more info about the game in order to invalidate cache?
    std::string game_id; // IDK if the type is correct (in general, IDK if types are correct in this example)

    std::filesystem::path cache_path; // You don't need to serialize this.

    // Define serialization/deserialization for this class. Archive all members besides shader path (dynamicly generated).

    ShaderCache(const std::string& game_id) {
        // Here, you calculate the cache path and deserialize itself. like ar(*this, ...);
        // After deserializing, validate whether the game id is equal. Also, cereal includes a versioning system for serialization
        // If you use the versioning here, you could invalidate the cache if the version is different.
    }

    // Define the AddShader/GetShader/IsShaderCached functions passing the shader key as argument.
    // It should be as simple as interacting with the std::unordered_map

    // You also want a strategy to know when to save the cache to disk (I/O is slow and serialzing also is slow). Maybe every N shaders? Or when emu closes?
};

As I mentioned before, this is a dependency of PipelineCache. So I would pass instantiate the ShaderCache as a member of the PipelineCache.

If you have any problem implementing something like this, reach out on Discord.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that the separate offsets aren’t ideal, but in my opinion, we have to rely on them.
Cereal doesn’t support partial loading, which wouldn’t be practical for on-demand mode.
I believe we’d run into a similar issue when saving.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gow big is a shader cache? I think the advantage of a shader cache is to pre load all of them before starting the game. That way you could even compile the pipelines so there are no stutters (yes, that would take longer to load but afaik other emus do that)

In the future we could even cache the compiled pipelines so the games could load quick even with shader cache.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dragon Quest uses around 17 MB just from a single boot. I’m not sure how much bigger it gets during active gameplay, since the game is affected by the UE rendering issue

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imo that is not a big size. But maybe it is better to have someone else's opinion on this. Maybe it is good idea to ask on discord.

return seed ^ (hash + 0x9e3779b9 + (seed << 12) + (seed >> 4));
}
template <typename T1, typename T2>
[[nodiscard]] constexpr u64 HashCombine(T1 seed, T2 hash) noexcept {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still not sure about this...
You can see how the output hash is also different with. Maybe it is a better way to do this (I susptect you are wanting to combine 32 and 64 bit types?)

}
}

void SerializeInfo(std::ostream& info_serialized, Shader::Info& info) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to serialize like this insteed of using the serializer?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants