renderer/vulkan: Implement async pipeline compilation (#3145)

This commit is contained in:
Macdu
2023-12-25 23:56:48 +01:00
committed by GitHub
parent a49d8923d4
commit bef1567a61
34 changed files with 447 additions and 186 deletions
+1 -1
View File
@@ -74,7 +74,7 @@ jobs:
sudo add-apt-repository -y ppa:mhier/libboost-latest
sudo add-apt-repository universe
sudo apt update
sudo apt -y install ccache libboost-filesystem1.81-dev libboost-program-options1.81-dev libboost-system1.81-dev libgtk-3-dev libsdl2-dev ninja-build libfuse2
sudo apt -y install ccache libboost-filesystem1.83-dev libboost-program-options1.83-dev libboost-system1.83-dev libgtk-3-dev libsdl2-dev ninja-build libfuse2
if: matrix.os == 'ubuntu-latest'
- uses: ilammy/msvc-dev-cmd@v1
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
run: |
sudo add-apt-repository -y ppa:mhier/libboost-latest
sudo apt update
sudo apt -y install ccache libboost-filesystem1.81-dev libboost-program-options1.81-dev libboost-system1.81-dev libgtk-3-dev libsdl2-dev ninja-build
sudo apt -y install ccache libboost-filesystem1.83-dev libboost-program-options1.83-dev libboost-system1.83-dev libgtk-3-dev libsdl2-dev ninja-build
- uses: actions/cache@v3
with:
+3
View File
@@ -96,3 +96,6 @@
[submodule "external/cubeb"]
path = external/cubeb
url = https://github.com/mozilla/cubeb.git
[submodule "external/concurrentqueue"]
path = external/concurrentqueue
url = https://github.com/cameron314/concurrentqueue.git
+3
View File
@@ -78,6 +78,9 @@ target_include_directories(googletest PUBLIC googletest/googletest/include)
target_include_directories(googletest PRIVATE googletest/googletest)
target_compile_definitions(googletest PUBLIC GTEST_HAS_PTHREAD=0)
add_library(concurrentqueue INTERFACE)
target_include_directories(concurrentqueue INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/concurrentqueue")
add_subdirectory(libfat16)
# The imgui target is including both imgui and imgui_club.
Vendored Submodule
+1
+1 -1
View File
@@ -13,4 +13,4 @@ target_link_libraries(app PUBLIC emuenv mem)
if(USE_DISCORD_RICH_PRESENCE)
target_link_libraries(app PUBLIC discord-rpc)
endif()
target_link_libraries(app PRIVATE audio config display gdbstub gui io ngs renderer)
target_link_libraries(app PRIVATE audio config display gdbstub gui io ngs renderer concurrentqueue)
+3 -1
View File
@@ -329,7 +329,9 @@ bool init(EmuEnvState &state, Config &cfg, const Root &root_paths) {
}
bool late_init(EmuEnvState &state) {
state.renderer->late_init(state.cfg, state.app_path);
// note: mem is not initialized yet but that's not an issue
// the renderer is not using it yet, just storing it for later uses
state.renderer->late_init(state.cfg, state.app_path, state.mem);
if (!init(state.mem, state.renderer->need_page_table)) {
LOG_ERROR("Failed to initialize memory for emulator state!");
+1
View File
@@ -73,6 +73,7 @@ enum PerfomanceOverleyPosition {
code(bool, "v-sync", true, v_sync) \
code(int, "anisotropic-filtering", 1, anisotropic_filtering) \
code(bool, "texture-cache", true, texture_cache) \
code(bool, "async-pipeline-compilation", true, async_pipeline_compilation) \
code(bool, "show-compile-shaders", true, show_compile_shaders) \
code(bool, "hashless-texture-cache", false, hashless_texture_cache) \
code(bool, "import-textures", false, import_textures) \
+1
View File
@@ -126,6 +126,7 @@ public:
std::string screen_filter = "Bilinear";
bool v_sync = true;
int anisotropic_filtering = 1;
bool async_pipeline_compilation = true;
bool import_textures = false;
bool export_textures = false;
bool export_as_png = false;
+1 -1
View File
@@ -51,5 +51,5 @@ add_library(
target_include_directories(gui PUBLIC include ${CMAKE_SOURCE_DIR}/vita3k)
target_link_libraries(gui PUBLIC app compat config dialog emuenv ime imgui glutil lang regmgr np)
target_link_libraries(gui PRIVATE audio ctrl kernel miniz psvpfsparser pugixml::pugixml stb renderer packages sdl2 touch vkutil host::dialog)
target_link_libraries(gui PRIVATE audio ctrl kernel miniz psvpfsparser pugixml::pugixml stb renderer packages sdl2 touch vkutil host::dialog concurrentqueue)
target_link_libraries(gui PUBLIC tracy)
+15
View File
@@ -178,6 +178,7 @@ static bool get_custom_config(GuiState &gui, EmuEnvState &emuenv, const std::str
config.screen_filter = gpu_child.attribute("screen-filter").as_string();
config.v_sync = gpu_child.attribute("v-sync").as_bool();
config.anisotropic_filtering = gpu_child.attribute("anisotropic-filtering").as_int();
config.async_pipeline_compilation = gpu_child.attribute("async-pipeline-compilation").as_bool();
config.import_textures = gpu_child.attribute("import-textures").as_bool();
config.export_textures = gpu_child.attribute("export-textures").as_bool();
config.export_as_png = gpu_child.attribute("export-as-png").as_bool();
@@ -240,6 +241,7 @@ void init_config(GuiState &gui, EmuEnvState &emuenv, const std::string &app_path
config.screen_filter = emuenv.cfg.screen_filter;
config.v_sync = emuenv.cfg.v_sync;
config.anisotropic_filtering = emuenv.cfg.anisotropic_filtering;
config.async_pipeline_compilation = emuenv.cfg.async_pipeline_compilation;
config.import_textures = emuenv.cfg.import_textures;
config.export_textures = emuenv.cfg.export_textures;
config.export_as_png = emuenv.cfg.export_as_png;
@@ -321,6 +323,7 @@ static void save_config(GuiState &gui, EmuEnvState &emuenv) {
gpu_child.append_attribute("screen-filter") = config.screen_filter.c_str();
gpu_child.append_attribute("v-sync") = config.v_sync;
gpu_child.append_attribute("anisotropic-filtering") = config.anisotropic_filtering;
gpu_child.append_attribute("async-pipeline-compilation") = config.async_pipeline_compilation;
gpu_child.append_attribute("import-textures") = config.import_textures;
gpu_child.append_attribute("export-textures") = config.export_textures;
gpu_child.append_attribute("export-as-png") = config.export_as_png;
@@ -353,6 +356,7 @@ static void save_config(GuiState &gui, EmuEnvState &emuenv) {
emuenv.cfg.screen_filter = config.screen_filter;
emuenv.cfg.v_sync = config.v_sync;
emuenv.cfg.anisotropic_filtering = config.anisotropic_filtering;
emuenv.cfg.async_pipeline_compilation = config.async_pipeline_compilation;
emuenv.cfg.import_textures = config.import_textures;
emuenv.cfg.export_textures = config.export_textures;
emuenv.cfg.export_as_png = config.export_as_png;
@@ -415,6 +419,7 @@ void set_config(GuiState &gui, EmuEnvState &emuenv, const std::string &app_path)
emuenv.cfg.current_config.screen_filter = emuenv.cfg.screen_filter;
emuenv.cfg.current_config.v_sync = emuenv.cfg.v_sync;
emuenv.cfg.current_config.anisotropic_filtering = emuenv.cfg.anisotropic_filtering;
emuenv.cfg.current_config.async_pipeline_compilation = emuenv.cfg.async_pipeline_compilation;
emuenv.cfg.current_config.import_textures = emuenv.cfg.import_textures;
emuenv.cfg.current_config.export_textures = emuenv.cfg.export_textures;
emuenv.cfg.current_config.export_as_png = emuenv.cfg.export_as_png;
@@ -443,6 +448,7 @@ void set_config(GuiState &gui, EmuEnvState &emuenv, const std::string &app_path)
emuenv.renderer->set_anisotropic_filtering(emuenv.cfg.current_config.anisotropic_filtering);
emuenv.renderer->set_stretch_display(emuenv.cfg.stretch_the_display_area);
emuenv.renderer->get_texture_cache()->set_replacement_state(emuenv.cfg.current_config.import_textures, emuenv.cfg.current_config.export_textures, emuenv.cfg.current_config.export_as_png);
emuenv.renderer->set_async_compilation(emuenv.cfg.current_config.async_pipeline_compilation);
// No change it if app already running
if (emuenv.io.title_id.empty()) {
@@ -608,6 +614,15 @@ void draw_settings_dialog(GuiState &gui, EmuEnvState &emuenv) {
ImGui::Checkbox(lang.gpu["disable_surface_sync"].c_str(), &config.disable_surface_sync);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", lang.gpu["surface_sync_description"].c_str());
if (is_vulkan)
ImGui::SameLine();
}
if (is_vulkan) {
ImGui::Checkbox("Aynchronous pipeline compilation", &config.async_pipeline_compilation);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Allow pipelines to be compiler concurrently on multiple concurrent threads.\n This decreases pipeline compilation stutter at the cost of temporary graphical glitches");
}
// Screen Filter
+15 -2
View File
@@ -3162,7 +3162,6 @@ EXPORT(int, sceGxmProgramCheck, const SceGxmProgram *program) {
EXPORT(Ptr<SceGxmProgramParameter>, sceGxmProgramFindParameterByName, Ptr<const SceGxmProgram> program_ptr, const char *name) {
TRACY_FUNC(sceGxmProgramFindParameterByName, program_ptr, name);
assert(program);
if (!program_ptr || !name)
return Ptr<SceGxmProgramParameter>();
@@ -4441,7 +4440,7 @@ EXPORT(int, sceGxmShaderPatcherCreateVertexProgram, SceGxmShaderPatcher *shaderP
vp->attributes.insert(vp->attributes.end(), &attributes[0], &attributes[attributeCount]);
}
if (!renderer::create(vp->renderer_data, *emuenv.renderer, *programId->program.get(mem), emuenv.renderer->gxp_ptr_map, emuenv.cache_path.string().c_str(), emuenv.io.title_id.c_str())) {
if (!renderer::create(vp->renderer_data, *emuenv.renderer, *programId->program.get(mem), emuenv.renderer->gxp_ptr_map, vp->attributes, emuenv.cache_path.string().c_str(), emuenv.io.title_id.c_str())) {
return RET_ERROR(SCE_GXM_ERROR_DRIVER);
}
@@ -4472,6 +4471,10 @@ EXPORT(int, sceGxmShaderPatcherForceUnregisterProgram, SceGxmShaderPatcher *shad
if (rp->program.get(emuenv.mem)->is_vertex()) {
for (auto it = shaderPatcher->vertex_program_cache.begin(); it != shaderPatcher->vertex_program_cache.end();) {
if (it->first.vertex_program.program == rp->program) {
SceGxmVertexProgram *vertex_program = it->second.get(emuenv.mem);
while (vertex_program->compile_threads_on.load(std::memory_order_acquire) > 0)
std::this_thread::yield();
free_callbacked(emuenv, thread_id, shaderPatcher, it->second.address());
it = shaderPatcher->vertex_program_cache.erase(it);
} else {
@@ -4481,6 +4484,10 @@ EXPORT(int, sceGxmShaderPatcherForceUnregisterProgram, SceGxmShaderPatcher *shad
} else {
for (auto it = shaderPatcher->fragment_program_cache.begin(); it != shaderPatcher->fragment_program_cache.end();) {
if (it->first.fragment_program.program == rp->program) {
SceGxmFragmentProgram *frag_program = it->second.get(emuenv.mem);
while (frag_program->compile_threads_on.load(std::memory_order_acquire) > 0)
std::this_thread::yield();
free_callbacked(emuenv, thread_id, shaderPatcher, it->second.address());
it = shaderPatcher->fragment_program_cache.erase(it);
} else {
@@ -4577,6 +4584,9 @@ EXPORT(int, sceGxmShaderPatcherReleaseFragmentProgram, SceGxmShaderPatcher *shad
SceGxmFragmentProgram *const fp = fragmentProgram.get(emuenv.mem);
--fp->reference_count;
if (fp->reference_count == 0) {
while (fp->compile_threads_on.load(std::memory_order_acquire) > 0)
std::this_thread::yield();
for (FragmentProgramCache::const_iterator it = shaderPatcher->fragment_program_cache.begin(); it != shaderPatcher->fragment_program_cache.end(); ++it) {
if (it->second == fragmentProgram) {
shaderPatcher->fragment_program_cache.erase(it);
@@ -4597,6 +4607,9 @@ EXPORT(int, sceGxmShaderPatcherReleaseVertexProgram, SceGxmShaderPatcher *shader
SceGxmVertexProgram *const vp = vertexProgram.get(emuenv.mem);
--vp->reference_count;
if (vp->reference_count == 0) {
while (vp->compile_threads_on.load(std::memory_order_acquire) > 0)
std::this_thread::yield();
for (VertexProgramCache::const_iterator it = shaderPatcher->vertex_program_cache.begin(); it != shaderPatcher->vertex_program_cache.end(); ++it) {
if (it->second == vertexProgram) {
shaderPatcher->vertex_program_cache.erase(it);
+1 -1
View File
@@ -46,7 +46,7 @@ add_library(
target_include_directories(renderer PUBLIC include)
target_link_libraries(renderer PUBLIC crypto display dlmalloc mem stb shader glutil threads config util vkutil)
target_link_libraries(renderer PRIVATE ddspp sdl2 stb ffmpeg xxHash::xxhash)
target_link_libraries(renderer PRIVATE ddspp sdl2 stb ffmpeg xxHash::xxhash concurrentqueue)
# Marshmallow Tracy linking
if(TRACY_ENABLE_ON_CORE_COMPONENTS)
+1 -1
View File
@@ -32,7 +32,7 @@ struct State;
struct VertexProgram;
bool create(std::unique_ptr<FragmentProgram> &fp, State &state, const SceGxmProgram &program, const SceGxmBlendInfo *blend, GXPPtrMap &gxp_ptr_map, const char *cache_path, const char *title_id);
bool create(std::unique_ptr<VertexProgram> &vp, State &state, const SceGxmProgram &program, GXPPtrMap &gxp_ptr_map, const char *cache_path, const char *title_id);
bool create(std::unique_ptr<VertexProgram> &vp, State &state, const SceGxmProgram &program, GXPPtrMap &gxp_ptr_map, const std::vector<SceGxmVertexAttribute> &attributes, const char *cache_path, const char *title_id);
void create(SceGxmSyncObject *sync, State &state);
void destroy(SceGxmSyncObject *sync, State &state);
void finish(State &state, Context *context);
+1 -1
View File
@@ -45,7 +45,7 @@ struct GLState : public renderer::State {
ScreenRenderer screen_renderer;
bool init(const fs::path &static_assets, const bool hashless_texture_cache) override;
void late_init(const Config &cfg, const std::string_view game_id) override;
void late_init(const Config &cfg, const std::string_view game_id, MemState &mem) override;
TextureCache *get_texture_cache() override {
return &texture_cache;
+6 -2
View File
@@ -274,8 +274,10 @@ struct GxmContextState {
};
struct SceGxmFragmentProgram {
size_t reference_count = 1;
std::atomic<uint32_t> reference_count = 1;
Ptr<const SceGxmProgram> program;
// only necessary with async compilation
std::atomic<uint32_t> compile_threads_on = 0;
bool is_maskupdate;
std::unique_ptr<renderer::FragmentProgram> renderer_data;
};
@@ -320,12 +322,14 @@ struct SceGxmShaderPatcherParams {
};
struct SceGxmVertexProgram {
size_t reference_count = 1;
std::atomic<uint32_t> reference_count = 1;
Ptr<const SceGxmProgram> program;
std::vector<SceGxmVertexStream> streams;
std::vector<SceGxmVertexAttribute> attributes;
std::unique_ptr<renderer::VertexProgram> renderer_data;
uint64_t key_hash;
// only necessary with async compilation
std::atomic<uint32_t> compile_threads_on = 0;
};
struct SceGxmPrecomputedDraw {
+2 -1
View File
@@ -82,7 +82,7 @@ struct State {
bool need_page_table = false;
virtual bool init(const fs::path &static_assets, const bool hashless_texture_cache) = 0;
virtual void late_init(const Config &cfg, const std::string_view game_id) = 0;
virtual void late_init(const Config &cfg, const std::string_view game_id, MemState &mem) = 0;
virtual TextureCache *get_texture_cache() = 0;
@@ -99,6 +99,7 @@ struct State {
virtual void set_screen_filter(const std::string_view &filter) = 0;
virtual int get_max_anisotropic_filtering() = 0;
virtual void set_anisotropic_filtering(int anisotropic_filtering) = 0;
virtual void set_async_compilation(bool enable) {}
void set_surface_sync_state(bool disable) {
disable_surface_sync = disable;
}
+2 -4
View File
@@ -141,8 +141,8 @@ struct GxmRecordState {
std::array<GXMStreamInfo, SCE_GXM_MAX_VERTEX_STREAMS> vertex_streams;
// Programs.
Ptr<const SceGxmFragmentProgram> fragment_program;
Ptr<const SceGxmVertexProgram> vertex_program;
Ptr<SceGxmFragmentProgram> fragment_program;
Ptr<SceGxmVertexProgram> vertex_program;
SceGxmColorSurface color_surface;
SceGxmDepthStencilSurface depth_stencil_surface;
@@ -205,8 +205,6 @@ struct FragmentProgram : ShaderProgram {
struct VertexProgram : ShaderProgram {
shader::usse::AttributeInformationMap attribute_infos;
bool stripped_symbols_checked;
};
struct ShadersHash {
@@ -22,24 +22,42 @@
#include <map>
#include <set>
#include <blockingconcurrentqueue.h>
#include <util/containers.h>
#include <vkutil/objects.h>
struct SceGxmProgram;
struct SceGxmFragmentProgram;
struct SceGxmVertexProgram;
enum SceGxmPrimitiveType : uint32_t;
struct SceGxmVertexAttribute;
struct MemState;
using Sha256Hash = std::array<uint8_t, 32>;
namespace renderer::vulkan {
namespace shader {
struct Hints;
}
namespace renderer {
struct GxmRecordState;
namespace vulkan {
struct VKState;
struct VKContext;
struct CompileRequest;
using PipelineCompileQueue = moodycamel::BlockingConcurrentQueue<CompileRequest *>;
class PipelineCache {
private:
VKState &state;
VKContext *current_context;
// are we performing pipeline compilation on a parallel thread?
bool use_async_compilation = false;
// how many threads are used in case async compilation is enabled
int nb_worker_threads = 0;
// does the GPU support vertex attributes with 3 components (like R16G16B16_UNORM), some (like AMD GPUs) don't
// this is needed when creating the input state
@@ -47,7 +65,7 @@ private:
// how much time should we wait after the last shader compilation
// to update the disk-saved shader cache (in seconds)
static constexpr int pipeline_cache_save_delay = 30;
static constexpr int pipeline_cache_save_delay = 15;
vk::PipelineCache pipeline_cache;
@@ -56,21 +74,33 @@ private:
std::map<vk::Format, vk::RenderPass> render_passes[2][2];
// render passes used along shader interlock
std::map<vk::Format, vk::RenderPass> shader_interlock_pass;
std::map<Sha256Hash, vk::ShaderModule> shaders;
unordered_map_fast<uint64_t, vk::Pipeline> pipelines;
// temp vars used to store the result computed by auxialiary functions before createPipeline is called
std::vector<vk::VertexInputBindingDescription> binding_descr;
std::vector<vk::VertexInputAttributeDescription> attr_descr;
// only used when accessing the shaders map
std::mutex shaders_mutex;
// because of multithreading, we want the pointers to remain stable
unordered_map_stable<Sha256Hash, vk::ShaderModule> shaders;
unordered_map_stable<uint64_t, vk::Pipeline> pipelines;
vk::PipelineShaderStageCreateInfo retrieve_shader(const SceGxmProgram *program, const Sha256Hash &hash, bool is_vertex, bool maskupdate, MemState &mem, const std::vector<SceGxmVertexAttribute> *hint_attributes);
vk::PipelineLayout retrieve_pipeline_layout(const uint16_t vert_texture_count, const uint16_t frag_texture_count);
vk::PipelineVertexInputStateCreateInfo get_vertex_input_state(MemState &mem);
vk::PipelineShaderStageCreateInfo retrieve_shader(const SceGxmProgram *program, const Sha256Hash &hash, bool is_vertex, bool maskupdate, MemState &mem, const shader::Hints &hints);
vk::PipelineVertexInputStateCreateInfo get_vertex_input_state(const SceGxmVertexProgram &vertex_program, MemState &mem);
// queue containing request sent by the main thread to the compile threads
PipelineCompileQueue pipeline_compile_queue;
moodycamel::ProducerToken pipeline_compile_queue_token;
// each pipeline compiler thread uses this function as its entrypoint
void compiler_thread(MemState &mem);
vk::Pipeline compile_pipeline(SceGxmPrimitiveType type, vk::RenderPass render_pass, const SceGxmVertexProgram &vertex_program_gxm, const SceGxmFragmentProgram &fragment_program_gxm, const GxmRecordState &record, const shader::Hints &hints, MemState &mem);
public:
// if not 0, next time the pipeline cache should be saved (in seconds since epoch)
uint64_t next_pipeline_cache_save = std::numeric_limits<uint64_t>::max();
// modified by the surface cache, estimates if it is safe to use async pipeline compilation
// (i.e that it does not causes permanent graphical issues)
bool can_use_deferred_compilation;
vk::DescriptorSetLayout uniforms_layout;
// used for the mask, color attachment
vk::DescriptorSetLayout attachments_layout;
@@ -81,15 +111,18 @@ public:
// first index is vertex, second is fragment
vk::PipelineLayout pipeline_layouts[17][17] = {};
explicit PipelineCache(VKState &state);
PipelineCache(VKState &state);
void init();
void read_pipeline_cache();
void save_pipeline_cache();
vk::RenderPass retrieve_render_pass(vk::Format format, bool force_load, bool force_store, bool no_color = false);
vk::Pipeline retrieve_pipeline(VKContext &context, SceGxmPrimitiveType &type, MemState &mem);
vk::Pipeline retrieve_pipeline(VKContext &context, SceGxmPrimitiveType &type, bool consider_for_async, MemState &mem);
bool precompile_shader(const Sha256Hash &hash);
vk::ShaderModule precompile_shader(const Sha256Hash &hash, bool search_first = true);
void set_async_compilation(bool enable);
};
} // namespace renderer::vulkan
} // namespace vulkan
} // namespace renderer
@@ -40,6 +40,8 @@ struct Viewport {
};
struct VKState : public renderer::State {
MemState *mem;
// 0 = automatic, > 0 = order in instance.enumeratePhysicalDevices
int gpu_idx;
@@ -99,7 +101,7 @@ struct VKState : public renderer::State {
bool init(const fs::path &static_assets, const bool hashless_texture_cache) override;
bool create(SDL_Window *window, std::unique_ptr<renderer::State> &state, const Config &config);
void late_init(const Config &cfg, const std::string_view game_id) override;
void late_init(const Config &cfg, const std::string_view game_id, MemState &mem) override;
void cleanup();
TextureCache *get_texture_cache() override {
@@ -114,6 +116,7 @@ struct VKState : public renderer::State {
void set_screen_filter(const std::string_view &filter) override;
int get_max_anisotropic_filtering() override;
void set_anisotropic_filtering(int anisotropic_filtering) override;
void set_async_compilation(bool enable) override;
bool map_memory(MemState &mem, Ptr<void> address, uint32_t size) override;
void unmap_memory(MemState &mem, Ptr<void> address) override;
@@ -76,6 +76,7 @@ struct ColorSurfaceCacheInfo : public SurfaceCacheInfo {
uint16_t original_height;
uint16_t pixel_stride;
uint32_t total_bytes;
uint64_t last_frame_rendered;
SceGxmColorBaseFormat format;
vk::ComponentMapping swizzle;
+7 -4
View File
@@ -205,7 +205,7 @@ bool create(std::unique_ptr<FragmentProgram> &fp, State &state, const SceGxmProg
return true;
}
bool create(std::unique_ptr<VertexProgram> &vp, State &state, const SceGxmProgram &program, GXPPtrMap &gxp_ptr_map, const char *cache_path, const char *title_id) {
bool create(std::unique_ptr<VertexProgram> &vp, State &state, const SceGxmProgram &program, GXPPtrMap &gxp_ptr_map, const std::vector<SceGxmVertexAttribute> &attributes, const char *cache_path, const char *title_id) {
switch (state.current_backend) {
case Backend::OpenGL:
gl::create(vp, dynamic_cast<gl::GLState &>(state), program);
@@ -231,9 +231,12 @@ bool create(std::unique_ptr<VertexProgram> &vp, State &state, const SceGxmProgra
vp->texture_count = std::bit_width(vp->textures_used.to_ulong());
if (vp->attribute_infos.empty()) {
vp->stripped_symbols_checked = false;
} else {
vp->stripped_symbols_checked = true;
// Insert some symbols here
if (program.primary_reg_count != 0) {
for (size_t i = 0; i < attributes.size(); i++) {
vp->attribute_infos.emplace(attributes[i].regIndex, shader::usse::AttributeInformation(static_cast<uint16_t>(i), SCE_GXM_PARAMETER_TYPE_F32, false, false, false));
}
}
}
return true;
+1 -1
View File
@@ -271,7 +271,7 @@ bool GLState::init(const fs::path &static_assets, const bool hashless_texture_ca
return true;
}
void GLState::late_init(const Config &cfg, const std::string_view game_id) {
void GLState::late_init(const Config &cfg, const std::string_view game_id, MemState &mem) {
const fs::path texture_folder = fs::path(shared_path) / "textures";
texture_cache.init(cfg.hashless_texture_cache, texture_folder, game_id);
}
-12
View File
@@ -432,18 +432,6 @@ void sync_vertex_streams_and_attributes(GLContext &context, GxmRecordState &stat
const SceGxmVertexProgram &vertex_program = *state.vertex_program.get(mem);
GLVertexProgram *glvert = reinterpret_cast<GLVertexProgram *>(vertex_program.renderer_data.get());
if (!glvert->stripped_symbols_checked) {
// Insert some symbols here
const SceGxmProgram *vertex_program_body = vertex_program.program.get(mem);
if (vertex_program_body && (vertex_program_body->primary_reg_count != 0)) {
for (std::size_t i = 0; i < vertex_program.attributes.size(); i++) {
glvert->attribute_infos.emplace(vertex_program.attributes[i].regIndex, shader::usse::AttributeInformation(static_cast<std::uint16_t>(i), SCE_GXM_PARAMETER_TYPE_F32, false, false, false));
}
}
glvert->stripped_symbols_checked = true;
}
// Each draw will upload the stream data. Assuming that, we can just bind buffer, upload data
// The GXM submit side should already submit used buffer, but we just delete all just in case
std::array<std::size_t, SCE_GXM_MAX_VERTEX_STREAMS> offset_in_buffer;
+1 -1
View File
@@ -91,7 +91,7 @@ bool get_shaders_cache_hashs(State &renderer) {
void save_shaders_cache_hashs(State &renderer, std::vector<ShadersHash> &shaders_cache_hashs) {
const auto shaders_path{ fs::path(renderer.cache_path) / "shaders" / renderer.title_id / renderer.self_name };
if (!fs::exists(shaders_path))
fs::create_directory(shaders_path);
fs::create_directories(shaders_path);
std::string hash_file_name = fmt::format("hashs-{}.dat", (renderer.current_backend == Backend::OpenGL) ? "gl" : "vk");
fs::ofstream shaders_hashs(shaders_path / hash_file_name, std::ios::out | std::ios::binary);
+3 -3
View File
@@ -77,11 +77,11 @@ COMMAND_SET_STATE(region_clip) {
COMMAND_SET_STATE(program) {
TRACY_FUNC_COMMANDS_SET_STATE(program);
const Ptr<const void> program = helper.pop<Ptr<const void>>();
const Ptr<void> program = helper.pop<Ptr<void>>();
const bool is_fragment = helper.pop<bool>();
if (is_fragment) {
render_context->record.fragment_program = program.cast<const SceGxmFragmentProgram>();
render_context->record.fragment_program = program.cast<SceGxmFragmentProgram>();
const SceGxmFragmentProgram *gxm_program = render_context->record.fragment_program.get(mem);
render_context->record.fragment_program_hash = gxm_program->renderer_data->hash;
render_context->record.is_maskupdate = gxm_program->is_maskupdate;
@@ -99,7 +99,7 @@ COMMAND_SET_STATE(program) {
break;
}
} else {
render_context->record.vertex_program = program.cast<const SceGxmVertexProgram>();
render_context->record.vertex_program = program.cast<SceGxmVertexProgram>();
const SceGxmVertexProgram *gxm_program = render_context->record.vertex_program.get(mem);
render_context->record.vertex_program_hash = gxm_program->renderer_data->hash;
}
+271 -102
View File
@@ -30,14 +30,42 @@
#include <util/fs.h>
#include <util/log.h>
#include <SDL.h>
// don't use the dispatch version, because we always hash a small amount
// with a known size
#define XXH_INLINE_ALL
#include <xxhash.h>
namespace renderer::vulkan {
// Size of the record containing what is needed for the pipeline construction (what is after is dynamic state)
constexpr size_t record_pipeline_len = offsetof(GxmRecordState, vertex_streams);
// structure containing everything needed to compile a pipeline
struct CompileRequest {
// iterator to the pipeline location
vk::Pipeline *pipeline;
// this is everything we need to compile the shader on another thread (as the original data will change)
SceGxmPrimitiveType type;
vk::RenderPass render_pass;
SceGxmVertexProgram *vertex_program_gxm;
SceGxmFragmentProgram *fragment_program_gxm;
shader::Hints hints;
// the content of the record useful for the pipeline creation
alignas(8) uint8_t record_data[record_pipeline_len];
const GxmRecordState *get_record() {
// note: this object is only half defined, but we are only looking at the part that's defined
return reinterpret_cast<const GxmRecordState *>(record_data);
}
};
PipelineCache::PipelineCache(VKState &state)
: state(state) {
: state(state)
, pipeline_compile_queue_token(pipeline_compile_queue) {
}
void PipelineCache::init() {
@@ -151,6 +179,16 @@ void PipelineCache::init() {
}
}
// compute all possible pipeline layouts
for (uint32_t vert_texture_count = 0; vert_texture_count <= 16; vert_texture_count++) {
for (uint32_t frag_texture_count = 0; frag_texture_count <= 16; frag_texture_count++) {
vk::PipelineLayoutCreateInfo layout_info{};
vk::DescriptorSetLayout set_layouts[] = { uniforms_layout, attachments_layout, vertex_textures_layout[vert_texture_count], fragment_textures_layout[frag_texture_count] };
layout_info.setSetLayouts(set_layouts);
pipeline_layouts[vert_texture_count][frag_texture_count] = state.device.createPipelineLayout(layout_info);
}
}
{
// look for rgb vertex attribute support
// we need to look at each format because it is not the same for all usual 3-component formats (checked on AMD Radeon HD 7800)
@@ -164,6 +202,49 @@ void PipelineCache::init() {
}
state.features.support_rgb_attributes = unsupported_rgb_vertex_attribute_formats.empty();
}
const int nb_logical_threads = SDL_GetCPUCount();
// took this from RPCS3 (slightly modified)
if (nb_logical_threads > 12)
nb_worker_threads = 6;
else if (nb_logical_threads > 8)
nb_worker_threads = 4;
else if (nb_logical_threads >= 6)
nb_worker_threads = 2;
else
nb_worker_threads = 1;
if (use_async_compilation) {
// we could not initialize the worker threads previously
use_async_compilation = false;
set_async_compilation(true);
}
}
void PipelineCache::set_async_compilation(bool enable) {
if (enable == use_async_compilation)
return;
use_async_compilation = enable;
if (nb_worker_threads == 0)
// not ingame yet
return;
if (enable) {
LOG_INFO("Enabling asynchronous pipeline compilation with {} threads", nb_worker_threads);
// launch all the threads
for (int i = 0; i < nb_worker_threads; i++) {
std::thread thread(&PipelineCache::compiler_thread, this, std::ref(*state.mem));
thread.detach();
}
} else {
LOG_INFO("Asynchronous pipeline compilation is now disabled");
// we assume that by the time set_async_compilation is called again with enable=true, all previous worker threads have already exited
for (int i = 0; i < nb_worker_threads; i++)
// if a thread receives nullptr, it exits
pipeline_compile_queue.enqueue(nullptr);
}
}
// magic number put at the beginning of the pipeline cache file
@@ -226,6 +307,15 @@ void PipelineCache::read_pipeline_cache() {
}
void PipelineCache::save_pipeline_cache() {
// first save the shader hashes
// do a copy for thread safety
std::vector<ShadersHash> shader_cache_copy;
{
std::lock_guard<std::mutex> guard(shaders_mutex);
shader_cache_copy = state.shaders_cache_hashs;
}
renderer::save_shaders_cache_hashs(state, state.shaders_cache_hashs);
const std::vector<uint8_t> pipeline_data = state.device.getPipelineCacheData(pipeline_cache);
if (pipeline_data.empty())
// No pipeline was created
@@ -257,22 +347,40 @@ void PipelineCache::save_pipeline_cache() {
LOG_INFO("Pipeline cache saved");
}
vk::PipelineShaderStageCreateInfo PipelineCache::retrieve_shader(const SceGxmProgram *program, const Sha256Hash &hash, bool is_vertex, bool maskupdate, MemState &mem, const std::vector<SceGxmVertexAttribute> *hint_attributes) {
vk::PipelineShaderStageCreateInfo PipelineCache::retrieve_shader(const SceGxmProgram *program, const Sha256Hash &hash, bool is_vertex, bool maskupdate, MemState &mem, const shader::Hints &hints) {
if (maskupdate)
LOG_CRITICAL("Mask not implemented in the vulkan renderer!");
auto it = shaders.find(hash);
const vk::ShaderModule shader_compiling = std::bit_cast<vk::ShaderModule>(~0ULL);
if (it == shaders.end()) {
vk::ShaderModule *shader_module;
{
// look if it is in the cache
if (precompile_shader(hash))
it = shaders.find(hash);
std::unique_lock<std::mutex> lock(shaders_mutex);
shader_module = &shaders.insert({ hash, nullptr }).first->second;
if (*shader_module == shader_compiling) {
// another thread is compiling the same exact shader at the same time
// it's no use re-compiling it, so just wait for the other thread being done
lock.unlock();
// we shouldn't need atomics and the compiler shouldn't be able to optimize this
while (*shader_module == shader_compiling)
std::this_thread::yield();
}
if (*shader_module == nullptr)
// now mark the shader as compiling so that other threads accessing it won't try to compile it a second time
*shader_module = shader_compiling;
}
if (it != shaders.end()) {
if (*shader_module == shader_compiling) {
precompile_shader(hash, false);
}
if (*shader_module != shader_compiling) {
vk::PipelineShaderStageCreateInfo shader_stage_info{
.stage = is_vertex ? vk::ShaderStageFlagBits::eVertex : vk::ShaderStageFlagBits::eFragment,
.module = it->second,
.module = *shader_module,
.pName = is_vertex ? "main_vs" : "main_fs"
};
return shader_stage_info;
@@ -286,51 +394,35 @@ vk::PipelineShaderStageCreateInfo PipelineCache::retrieve_shader(const SceGxmPro
LOG_INFO("Generating vulkan spv shader {}", hash_text.data());
const std::string shader_version = fmt::format("vk{}", shader::CURRENT_VERSION);
// update shader hints
current_context->shader_hints.color_format = current_context->record.color_surface.colorFormat;
current_context->shader_hints.attributes = hint_attributes;
shader::usse::SpirvCode source = load_spirv_shader(*program, state.features, true, current_context->shader_hints, maskupdate, state.cache_path.c_str(), title_id, self_name, shader_version, true);
shader::usse::SpirvCode source = load_spirv_shader(*program, state.features, true, hints, maskupdate, state.cache_path.c_str(), title_id, self_name, shader_version, true);
vk::ShaderModuleCreateInfo shader_info{
.codeSize = sizeof(uint32_t) * source.size(),
.pCode = source.data()
};
vk::ShaderModule shader = current_context->state.device.createShaderModule(shader_info);
shaders[hash] = shader;
// Save shader cache haches
// vertex and fragment shaders are not linked together so no need to associate them
Sha256Hash empty_hash{};
if (is_vertex) {
state.shaders_cache_hashs.push_back({ hash, empty_hash });
} else {
state.shaders_cache_hashs.push_back({ empty_hash, hash });
*shader_module = state.device.createShaderModule(shader_info);
{
std::lock_guard<std::mutex> guard(shaders_mutex);
// Save shader cache haches
// vertex and fragment shaders are not linked together so no need to associate them
Sha256Hash empty_hash{};
if (is_vertex) {
state.shaders_cache_hashs.push_back({ hash, empty_hash });
} else {
state.shaders_cache_hashs.push_back({ empty_hash, hash });
}
}
renderer::save_shaders_cache_hashs(state, state.shaders_cache_hashs);
vk::PipelineShaderStageCreateInfo shader_stage_info{
.stage = is_vertex ? vk::ShaderStageFlagBits::eVertex : vk::ShaderStageFlagBits::eFragment,
.module = shader,
.module = *shader_module,
.pName = is_vertex ? "main_vs" : "main_fs"
};
return shader_stage_info;
}
vk::PipelineLayout PipelineCache::retrieve_pipeline_layout(const uint16_t vert_texture_count, const uint16_t frag_texture_count) {
if (!pipeline_layouts[vert_texture_count][frag_texture_count]) {
// create matching pipeline layout
vk::PipelineLayoutCreateInfo layout_info{};
vk::DescriptorSetLayout set_layouts[] = { uniforms_layout, attachments_layout, vertex_textures_layout[vert_texture_count], fragment_textures_layout[frag_texture_count] };
layout_info.setSetLayouts(set_layouts);
pipeline_layouts[vert_texture_count][frag_texture_count] = state.device.createPipelineLayout(layout_info);
}
return pipeline_layouts[vert_texture_count][frag_texture_count];
}
vk::RenderPass PipelineCache::retrieve_render_pass(vk::Format format, bool force_load, bool force_store, bool no_color) {
auto &render_passes_map = no_color ? shader_interlock_pass : render_passes[force_load][force_store];
@@ -455,27 +547,17 @@ vk::RenderPass PipelineCache::retrieve_render_pass(vk::Format format, bool force
return render_passes_map[format];
}
vk::PipelineVertexInputStateCreateInfo PipelineCache::get_vertex_input_state(MemState &mem) {
// Vertex attributes.
const GxmRecordState &state = current_context->record;
const SceGxmVertexProgram &vertex_program = *state.vertex_program.get(mem);
VertexProgram *vkvert = vertex_program.renderer_data.get();
if (!vkvert->stripped_symbols_checked) {
// Insert some symbols here
const SceGxmProgram *vertex_program_body = vertex_program.program.get(mem);
if (vertex_program_body && (vertex_program_body->primary_reg_count != 0)) {
for (std::size_t i = 0; i < vertex_program.attributes.size(); i++) {
vkvert->attribute_infos.emplace(vertex_program.attributes[i].regIndex, shader::usse::AttributeInformation(static_cast<std::uint16_t>(i), SCE_GXM_PARAMETER_TYPE_F32, false, false, false));
}
}
vkvert->stripped_symbols_checked = true;
}
vk::PipelineVertexInputStateCreateInfo PipelineCache::get_vertex_input_state(const SceGxmVertexProgram &vertex_program, MemState &mem) {
// pointer to these objects are returned (so it needs to be static)
// and each thread needs one (hence the thread_local)
static thread_local std::vector<vk::VertexInputBindingDescription> binding_descr;
static thread_local std::vector<vk::VertexInputAttributeDescription> attr_descr;
binding_descr.clear();
attr_descr.clear();
// Vertex attributes.
VertexProgram *vkvert = vertex_program.renderer_data.get();
uint32_t used_streams = 0;
for (const SceGxmVertexAttribute &attribute : vertex_program.attributes) {
@@ -556,6 +638,33 @@ vk::PipelineVertexInputStateCreateInfo PipelineCache::get_vertex_input_state(Mem
return vertex_input;
}
void PipelineCache::compiler_thread(MemState &mem) {
moodycamel::ConsumerToken consumer_token(pipeline_compile_queue);
// just a single loop, waiting for a pipeline compile request and compiling it
CompileRequest *request;
while (true) {
pipeline_compile_queue.wait_dequeue(consumer_token, request);
if (request == nullptr)
// use this as an instruction to stop the thread
break;
vk::Pipeline pipeline = compile_pipeline(request->type, request->render_pass, *request->vertex_program_gxm, *request->fragment_program_gxm, *request->get_record(), request->hints, mem);
*request->pipeline = pipeline;
request->vertex_program_gxm->compile_threads_on.fetch_sub(1, std::memory_order_release);
request->fragment_program_gxm->compile_threads_on.fetch_sub(1, std::memory_order_release);
const auto time_s = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
next_pipeline_cache_save = time_s + pipeline_cache_save_delay;
state.shaders_count_compiled++;
delete request;
}
}
static vk::StencilOpState convert_op_state(const GxmStencilStateOp &state) {
return vk::StencilOpState{
.failOp = translate_stencil_op(state.stencil_fail),
@@ -565,43 +674,18 @@ static vk::StencilOpState convert_op_state(const GxmStencilStateOp &state) {
};
}
vk::Pipeline PipelineCache::retrieve_pipeline(VKContext &context, SceGxmPrimitiveType &type, MemState &mem) {
current_context = &context;
const GxmRecordState &record = context.record;
// get the hash of the current context
constexpr size_t record_pipeline_len = offsetof(GxmRecordState, vertex_streams);
uint64_t key = XXH3_64bits(&record, record_pipeline_len);
// add the hash of the blending
const SceGxmFragmentProgram &fragment_program_gxm = *record.fragment_program.get(mem);
const VKFragmentProgram &fragment_program = *reinterpret_cast<VKFragmentProgram *>(
fragment_program_gxm.renderer_data.get());
key ^= fragment_program.blending_hash;
// add the hash of the attribute and stream layout
const SceGxmVertexProgram &vertex_program_gxm = *record.vertex_program.get(mem);
key ^= vertex_program_gxm.key_hash;
// and also add the primitive type
key ^= static_cast<uint64_t>(type);
auto it = pipelines.find(key);
if (it != pipelines.end()) {
if (it->second != nullptr)
return it->second;
} else {
// the pipeline hash was not in the cache
state.shaders_count_compiled++;
}
vk::Pipeline PipelineCache::compile_pipeline(SceGxmPrimitiveType type, vk::RenderPass render_pass, const SceGxmVertexProgram &vertex_program_gxm, const SceGxmFragmentProgram &fragment_program_gxm, const GxmRecordState &record, const shader::Hints &hints, MemState &mem) {
const VertexProgram &vertex_program = *reinterpret_cast<VertexProgram *>(
vertex_program_gxm.renderer_data.get());
const SceGxmProgram *gxm_fragment_shader = fragment_program_gxm.program.get(mem);
const VKFragmentProgram &fragment_program = *reinterpret_cast<VKFragmentProgram *>(
fragment_program_gxm.renderer_data.get());
// the vertex input state must be computed before shader are retrieved in case symbols are stripped
const vk::PipelineVertexInputStateCreateInfo vertex_input = get_vertex_input_state(mem);
const vk::PipelineVertexInputStateCreateInfo vertex_input = get_vertex_input_state(vertex_program_gxm, mem);
const vk::PipelineShaderStageCreateInfo vertex_shader = retrieve_shader(vertex_program_gxm.program.get(mem), vertex_program.hash, true, fragment_program_gxm.is_maskupdate, mem, &vertex_program_gxm.attributes);
const vk::PipelineShaderStageCreateInfo fragment_shader = retrieve_shader(gxm_fragment_shader, fragment_program.hash, false, fragment_program_gxm.is_maskupdate, mem, nullptr);
const vk::PipelineShaderStageCreateInfo vertex_shader = retrieve_shader(vertex_program_gxm.program.get(mem), vertex_program.hash, true, fragment_program_gxm.is_maskupdate, mem, hints);
const vk::PipelineShaderStageCreateInfo fragment_shader = retrieve_shader(gxm_fragment_shader, fragment_program.hash, false, fragment_program_gxm.is_maskupdate, mem, hints);
const vk::PipelineShaderStageCreateInfo shader_stages[] = { vertex_shader, fragment_shader };
// disable the fragment shader if gxm asks us to
const bool is_fragment_disabled = record.front_side_fragment_program_mode == SCE_GXM_FRAGMENT_PROGRAM_DISABLED || gxm_fragment_shader->has_no_effect();
@@ -652,7 +736,7 @@ vk::Pipeline PipelineCache::retrieve_pipeline(VKContext &context, SceGxmPrimitiv
color_blending.setAttachments(blending);
}
vk::PipelineLayout pipeline_layout = retrieve_pipeline_layout(vertex_program.texture_count, fragment_program.texture_count);
vk::PipelineLayout pipeline_layout = pipeline_layouts[vertex_program.texture_count][fragment_program.texture_count];
// all of these can be changed at any time using the vita graphics api (like opengl)
// Because each one can take a lot of different values, it's better to set them as dynamic
@@ -674,7 +758,6 @@ vk::Pipeline PipelineCache::retrieve_pipeline(VKContext &context, SceGxmPrimitiv
.scissorCount = 1
};
const vk::RenderPass render_pass = use_shader_interlock ? context.current_shader_interlock_pass : context.current_render_pass;
vk::GraphicsPipelineCreateInfo pipeline_info{
.stageCount = shader_stage_count,
.pStages = shader_stages,
@@ -697,22 +780,105 @@ vk::Pipeline PipelineCache::retrieve_pipeline(VKContext &context, SceGxmPrimitiv
return nullptr;
}
const auto time_s = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
next_pipeline_cache_save = time_s + pipeline_cache_save_delay;
pipelines[key] = result.value;
return result.value;
}
bool PipelineCache::precompile_shader(const Sha256Hash &hash) {
vk::Pipeline PipelineCache::retrieve_pipeline(VKContext &context, SceGxmPrimitiveType &type, bool consider_for_async, MemState &mem) {
const GxmRecordState &record = context.record;
// get the hash of the current context
uint64_t key = XXH3_64bits(&record, record_pipeline_len);
// add the hash of the blending
SceGxmFragmentProgram &fragment_program_gxm = *record.fragment_program.get(mem);
const VKFragmentProgram &fragment_program = *reinterpret_cast<VKFragmentProgram *>(
fragment_program_gxm.renderer_data.get());
key ^= fragment_program.blending_hash;
// add the hash of the attribute and stream layout
SceGxmVertexProgram &vertex_program_gxm = *record.vertex_program.get(mem);
key ^= vertex_program_gxm.key_hash;
// and also add the primitive type
key ^= static_cast<uint64_t>(type);
// can't use constexpr because of apple clang...
const vk::Pipeline pipeline_compiling = std::bit_cast<vk::Pipeline, uint64_t>(~0ULL);
// if the pipeline is in the pipeline cache, we can expect its creation time to be almost instantaneous
bool already_in_cache = false;
auto it = pipelines.find(key);
if (it != pipelines.end()) {
if (it->second != nullptr) {
if (it->second == pipeline_compiling)
// pipeline is still compiling
return nullptr;
else
return it->second;
}
already_in_cache = true;
} else {
// the pipeline hash was not in the cache;
it = pipelines.insert({ key, pipeline_compiling }).first;
}
// get the correct renderpass here
const SceGxmProgram *gxm_fragment_shader = fragment_program_gxm.program.get(mem);
const bool use_shader_interlock = state.features.support_shader_interlock && gxm_fragment_shader->is_frag_color_used();
const vk::RenderPass render_pass = use_shader_interlock ? context.current_shader_interlock_pass : context.current_render_pass;
// update the shader hints
context.shader_hints.color_format = record.color_surface.colorFormat;
context.shader_hints.attributes = &vertex_program_gxm.attributes;
const bool compile_pipeline_async = !already_in_cache && consider_for_async && use_async_compilation && can_use_deferred_compilation;
if (compile_pipeline_async) {
// create the pipeline compile request
CompileRequest *request = new CompileRequest;
*request = {
.pipeline = &it->second,
.type = type,
.render_pass = render_pass,
.vertex_program_gxm = &vertex_program_gxm,
.fragment_program_gxm = &fragment_program_gxm,
.hints = context.shader_hints
};
memcpy(request->record_data, &record, record_pipeline_len);
it->second = pipeline_compiling;
// we must not delete these programs until the worker is done
vertex_program_gxm.compile_threads_on.fetch_add(1, std::memory_order_relaxed);
fragment_program_gxm.compile_threads_on.fetch_add(1, std::memory_order_relaxed);
pipeline_compile_queue.enqueue(pipeline_compile_queue_token, request);
return nullptr;
} else {
// can't wait, compile it right now
vk::Pipeline result = compile_pipeline(type, render_pass, vertex_program_gxm, fragment_program_gxm, record, context.shader_hints, mem);
const auto time_s = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
next_pipeline_cache_save = time_s + pipeline_cache_save_delay;
state.shaders_count_compiled++;
it->second = result;
return result;
}
}
vk::ShaderModule PipelineCache::precompile_shader(const Sha256Hash &hash, bool search_first) {
if (search_first) {
// happens while loading the thread, no parallel access so no need for a mutex
auto it = shaders.find(hash);
if (it != shaders.end())
return it->second;
}
const auto shader_path{ fs::path(state.cache_path) / "shaders" / state.title_id / state.self_name };
if (shaders.contains(hash))
return true;
if (!fs::exists(shader_path) || fs::is_empty(shader_path))
return false;
return nullptr;
Sha256Hash shader_hash;
memcpy(shader_hash.data(), hash.data(), sizeof(Sha256Hash));
@@ -721,7 +887,7 @@ bool PipelineCache::precompile_shader(const Sha256Hash &hash) {
const std::vector<uint32_t> source = renderer::pre_load_shader_spirv(hash_ver.c_str(), "spv", state.cache_path.c_str(), state.title_id, state.self_name);
if (source.empty())
return false;
return nullptr;
vk::ShaderModuleCreateInfo shader_info{
.codeSize = sizeof(uint32_t) * source.size(),
@@ -729,8 +895,11 @@ bool PipelineCache::precompile_shader(const Sha256Hash &hash) {
};
vk::ShaderModule shader = state.device.createShaderModule(shader_info);
shaders[hash] = shader;
{
std::lock_guard<std::mutex> guard(shaders_mutex);
shaders[hash] = shader;
}
return true;
return shader;
}
} // namespace renderer::vulkan
} // namespace renderer::vulkan
+7 -1
View File
@@ -627,7 +627,9 @@ bool VKState::create(SDL_Window *window, std::unique_ptr<renderer::State> &state
return true;
}
void VKState::late_init(const Config &cfg, const std::string_view game_id) {
void VKState::late_init(const Config &cfg, const std::string_view game_id, MemState &mem) {
this->mem = &mem;
bool use_high_accuracy = cfg.current_config.high_accuracy;
// shader interlock is more accurate but slower
@@ -926,6 +928,10 @@ void VKState::set_anisotropic_filtering(int anisotropic_filtering) {
texture_cache.anisotropic_filtering = anisotropic_filtering;
}
void VKState::set_async_compilation(bool enable) {
pipeline_cache.set_async_compilation(enable);
}
std::vector<std::string> VKState::get_gpu_list() {
const std::vector<vk::PhysicalDevice> gpus = instance.enumeratePhysicalDevices();
+12 -17
View File
@@ -255,21 +255,6 @@ static void bind_vertex_streams(VKContext &context, MemState &mem) {
const SceGxmVertexProgram &vertex_program = *state.vertex_program.get(mem);
VertexProgram *vkvert = vertex_program.renderer_data.get();
// we need to do another check here (the same is done in pipeline_cache)
// because if a game (like Secret of Mana) uses two programs with the same shaders and the same vertex input stripped
// the pipeline cache won't add stripped symbols for the second program
if (!vkvert->stripped_symbols_checked) {
// Insert some symbols here
const SceGxmProgram *vertex_program_body = vertex_program.program.get(mem);
if (vertex_program_body && (vertex_program_body->primary_reg_count != 0)) {
for (std::size_t i = 0; i < vertex_program.attributes.size(); i++) {
vkvert->attribute_infos.emplace(vertex_program.attributes[i].regIndex, shader::usse::AttributeInformation(static_cast<std::uint16_t>(i), SCE_GXM_PARAMETER_TYPE_F32, false, false, false));
}
}
vkvert->stripped_symbols_checked = true;
}
int max_stream_idx = -1;
for (const SceGxmVertexAttribute &attribute : vertex_program.attributes) {
@@ -385,14 +370,24 @@ void draw(VKContext &context, SceGxmPrimitiveType type, SceGxmIndexFormat format
if (context.refresh_pipeline || type != context.last_primitive) {
context.refresh_pipeline = false;
context.last_primitive = type;
vk::Pipeline new_pipeline = context.state.pipeline_cache.retrieve_pipeline(context, type, mem);
// We don't want to defer cases where we draw a whole quad over the screen as these draws could be necessary
// to be able to see anything
bool can_be_whole_quad = instance_count == 1 && (count == 4 || count == 6);
vk::Pipeline new_pipeline = context.state.pipeline_cache.retrieve_pipeline(context, type, !can_be_whole_quad, mem);
if (new_pipeline != context.current_pipeline) {
context.current_pipeline = new_pipeline;
context.render_cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, context.current_pipeline);
if (new_pipeline != nullptr)
context.render_cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, context.current_pipeline);
}
}
// can happen with asynchronous pipeline compilation
if (context.current_pipeline == nullptr)
return;
if (config.log_active_shaders) {
const std::string hash_text_f = hex_string(context.record.fragment_program.get(mem)->renderer_data->hash);
const std::string hash_text_v = hex_string(context.record.vertex_program.get(mem)->renderer_data->hash);
+15 -1
View File
@@ -152,6 +152,8 @@ SurfaceRetrieveResult VKSurfaceCache::retrieve_color_surface_for_framebuffer(Mem
uint32_t bytes_per_stride = color->strideInPixels * gxm::bits_per_pixel(base_format) / 8;
uint32_t total_surface_size = bytes_per_stride * original_height;
VKContext *context = reinterpret_cast<VKContext *>(state.context);
if (overlap) {
ColorSurfaceCacheInfo &info = *ite->second;
@@ -179,6 +181,11 @@ SurfaceRetrieveResult VKSurfaceCache::retrieve_color_surface_for_framebuffer(Mem
color_surface_queue.set_as_mru(&info);
last_written_surface = &info;
// if this surface has not been rendered to for the last 60 frames, consider it is not safe not to render all shaders to it
constexpr uint64_t big_delay_between_frames = 60;
state.pipeline_cache.can_use_deferred_compilation = context->frame_timestamp - info.last_frame_rendered < big_delay_between_frames;
info.last_frame_rendered = context->frame_timestamp;
if (vk_format == info.texture.format) {
return { info.texture.view, &info.texture };
} else {
@@ -199,7 +206,6 @@ SurfaceRetrieveResult VKSurfaceCache::retrieve_color_surface_for_framebuffer(Mem
}
}
VKContext *context = reinterpret_cast<VKContext *>(state.context);
// get the least recently used (probably unused) color surface
ColorSurfaceCacheInfo &info_added = *color_surface_queue.get_lru();
if (info_added.texture.image)
@@ -209,6 +215,8 @@ SurfaceRetrieveResult VKSurfaceCache::retrieve_color_surface_for_framebuffer(Mem
color_address_lookup.erase(info_added.data.address());
color_surface_queue.set_as_mru(&info_added);
info_added.last_frame_rendered = context->frame_timestamp;
color_address_lookup[address] = &info_added;
info_added.width = width;
@@ -280,6 +288,9 @@ SurfaceRetrieveResult VKSurfaceCache::retrieve_color_surface_for_framebuffer(Mem
});
}
// it's not impossible that this surface will be rendered once and only used after, so do not skip any shader on it
state.pipeline_cache.can_use_deferred_compilation = false;
return { info_added.texture.view, &info_added.texture };
}
@@ -872,6 +883,9 @@ Framebuffer &VKSurfaceCache::retrieve_framebuffer_handle(MemState &mem, SceGxmCo
LOG_ERROR_ONCE("Depth stencil and color surface are both null!");
}
// might get modified by retrieve_color_surface_for_framebuffer
state.pipeline_cache.can_use_deferred_compilation = true;
// First retrieve separately the color surface and ds surface
SurfaceRetrieveResult color_result;
SurfaceRetrieveResult ds_result;
+8 -7
View File
@@ -18,6 +18,7 @@
#pragma once
#include <shader/usse_types.h>
#include <util/log.h>
#include <fstream>
#include <memory>
@@ -27,7 +28,7 @@ namespace shader {
namespace usse {
namespace disasm {
extern std::string *disasm_storage;
extern thread_local std::stringstream *disasm_storage;
//
// Disasm helpers
@@ -47,10 +48,10 @@ std::string swizzle_to_str(Swizzle<s> swizz, const Imm4 write_mask);
} // namespace shader
// TODO: make LOG_RAW
#define LOG_DISASM(fmt_str, ...) \
{ \
auto fmt_disasm = fmt::format(fmt_str, ##__VA_ARGS__); \
std::cout << fmt_disasm << std::endl; \
if (shader::usse::disasm::disasm_storage) \
*shader::usse::disasm::disasm_storage += fmt_disasm + '\n'; \
#define LOG_DISASM(fmt_str, ...) \
{ \
auto fmt_disasm = fmt::format(fmt_str, ##__VA_ARGS__); \
LOG_TRACE("\t{}", fmt_disasm); \
if (shader::usse::disasm::disasm_storage) \
*shader::usse::disasm::disasm_storage << fmt_disasm << '\n'; \
}
+2 -2
View File
@@ -1755,7 +1755,7 @@ static SpirvCode convert_gxp_to_spirv_impl(const SceGxmProgram &program, const s
break;
}
std::string disasm_dump;
std::stringstream disasm_dump;
// Put disasm storage
disasm::disasm_storage = &disasm_dump;
@@ -1838,7 +1838,7 @@ static SpirvCode convert_gxp_to_spirv_impl(const SceGxmProgram &program, const s
LOG_ERROR("SPIR-V Error:\n{}", spirv_log);
if (dumper) {
dumper("dsm", disasm_dump);
dumper("dsm", disasm_dump.str());
}
b.dump(spirv);
+1 -1
View File
@@ -26,7 +26,7 @@ using namespace shader::usse;
namespace shader::usse::disasm {
std::string *disasm_storage = nullptr;
thread_local std::stringstream *disasm_storage = nullptr;
//
// Disasm helpers
+7 -1
View File
@@ -21,14 +21,18 @@
#include <vector>
#if BOOST_VERSION >= 108100
#if BOOST_VERSION >= 108200
#include <boost/unordered/unordered_flat_map.hpp>
#include <boost/unordered/unordered_flat_set.hpp>
#include <boost/unordered/unordered_node_map.hpp>
// use a boost unordered_flat_map as performance really matters for fields using this type
template <typename T, typename S>
using unordered_map_fast = boost::unordered::unordered_flat_map<T, S>;
template <typename T>
using unordered_set_fast = boost::unordered::unordered_flat_set<T>;
// this is in case we need pointer stability under rehashing
template <typename T, typename S>
using unordered_map_stable = boost::unordered::unordered_node_map<T, S>;
#else
// fallback in case someone is using an old boost version
#include <boost/unordered_map.hpp>
@@ -37,6 +41,8 @@ template <typename T, typename S>
using unordered_map_fast = boost::unordered_map<T, S>;
template <typename T>
using unordered_set_fast = boost::unordered_set<T>;
template <typename T, typename S>
using unordered_map_stable = boost::unordered_map<T, S>;
#endif
namespace lru {