From bef1567a61a0ac4467ad88e4e13c41fec54a07cb Mon Sep 17 00:00:00 2001 From: Macdu Date: Mon, 25 Dec 2023 23:56:48 +0100 Subject: [PATCH] renderer/vulkan: Implement async pipeline compilation (#3145) --- .github/workflows/c-cpp.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .gitmodules | 3 + external/CMakeLists.txt | 3 + external/concurrentqueue | 1 + vita3k/app/CMakeLists.txt | 2 +- vita3k/app/src/app_init.cpp | 4 +- vita3k/config/include/config/config.h | 1 + vita3k/config/include/config/state.h | 1 + vita3k/gui/CMakeLists.txt | 2 +- vita3k/gui/src/settings_dialog.cpp | 15 + vita3k/modules/SceGxm/SceGxm.cpp | 17 +- vita3k/renderer/CMakeLists.txt | 2 +- vita3k/renderer/include/renderer/functions.h | 2 +- vita3k/renderer/include/renderer/gl/state.h | 2 +- vita3k/renderer/include/renderer/gxm_types.h | 8 +- vita3k/renderer/include/renderer/state.h | 3 +- vita3k/renderer/include/renderer/types.h | 6 +- .../include/renderer/vulkan/pipeline_cache.h | 63 ++- .../renderer/include/renderer/vulkan/state.h | 5 +- .../include/renderer/vulkan/surface_cache.h | 1 + vita3k/renderer/src/creation.cpp | 11 +- vita3k/renderer/src/gl/renderer.cpp | 2 +- vita3k/renderer/src/gl/sync_state.cpp | 12 - vita3k/renderer/src/shaders.cpp | 2 +- vita3k/renderer/src/state_set.cpp | 6 +- vita3k/renderer/src/vulkan/pipeline_cache.cpp | 373 +++++++++++++----- vita3k/renderer/src/vulkan/renderer.cpp | 8 +- vita3k/renderer/src/vulkan/scene.cpp | 29 +- vita3k/renderer/src/vulkan/surface_cache.cpp | 16 +- vita3k/shader/include/shader/usse_disasm.h | 15 +- vita3k/shader/src/spirv_recompiler.cpp | 4 +- vita3k/shader/src/usse_disasm.cpp | 2 +- vita3k/util/include/util/containers.h | 8 +- 34 files changed, 447 insertions(+), 186 deletions(-) create mode 160000 external/concurrentqueue diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml index 2ded220cd..ca3e6ef88 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/c-cpp.yml @@ -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 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 96800404b..f2fa5e7ac 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -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: diff --git a/.gitmodules b/.gitmodules index bf59a3a2f..1bd6207a3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 36717a533..835215100 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -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. diff --git a/external/concurrentqueue b/external/concurrentqueue new file mode 160000 index 000000000..6dd38b8a1 --- /dev/null +++ b/external/concurrentqueue @@ -0,0 +1 @@ +Subproject commit 6dd38b8a1dbaa7863aa907045f32308a56a6ff5d diff --git a/vita3k/app/CMakeLists.txt b/vita3k/app/CMakeLists.txt index d8f15e7e7..5c78d2048 100644 --- a/vita3k/app/CMakeLists.txt +++ b/vita3k/app/CMakeLists.txt @@ -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) diff --git a/vita3k/app/src/app_init.cpp b/vita3k/app/src/app_init.cpp index d30b72c25..a94163784 100644 --- a/vita3k/app/src/app_init.cpp +++ b/vita3k/app/src/app_init.cpp @@ -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!"); diff --git a/vita3k/config/include/config/config.h b/vita3k/config/include/config/config.h index af6ab70d8..24040973a 100644 --- a/vita3k/config/include/config/config.h +++ b/vita3k/config/include/config/config.h @@ -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) \ diff --git a/vita3k/config/include/config/state.h b/vita3k/config/include/config/state.h index a9c83c982..77dac1500 100644 --- a/vita3k/config/include/config/state.h +++ b/vita3k/config/include/config/state.h @@ -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; diff --git a/vita3k/gui/CMakeLists.txt b/vita3k/gui/CMakeLists.txt index 392c4f915..03c9ebe62 100644 --- a/vita3k/gui/CMakeLists.txt +++ b/vita3k/gui/CMakeLists.txt @@ -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) diff --git a/vita3k/gui/src/settings_dialog.cpp b/vita3k/gui/src/settings_dialog.cpp index a43ca27f7..c5889a553 100644 --- a/vita3k/gui/src/settings_dialog.cpp +++ b/vita3k/gui/src/settings_dialog.cpp @@ -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 diff --git a/vita3k/modules/SceGxm/SceGxm.cpp b/vita3k/modules/SceGxm/SceGxm.cpp index 58ad4c8ff..3d101c974 100644 --- a/vita3k/modules/SceGxm/SceGxm.cpp +++ b/vita3k/modules/SceGxm/SceGxm.cpp @@ -3162,7 +3162,6 @@ EXPORT(int, sceGxmProgramCheck, const SceGxmProgram *program) { EXPORT(Ptr, sceGxmProgramFindParameterByName, Ptr program_ptr, const char *name) { TRACY_FUNC(sceGxmProgramFindParameterByName, program_ptr, name); - assert(program); if (!program_ptr || !name) return Ptr(); @@ -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); diff --git a/vita3k/renderer/CMakeLists.txt b/vita3k/renderer/CMakeLists.txt index 977ddc90a..23670b9a4 100644 --- a/vita3k/renderer/CMakeLists.txt +++ b/vita3k/renderer/CMakeLists.txt @@ -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) diff --git a/vita3k/renderer/include/renderer/functions.h b/vita3k/renderer/include/renderer/functions.h index 3d4327b9b..ecc5eb48d 100644 --- a/vita3k/renderer/include/renderer/functions.h +++ b/vita3k/renderer/include/renderer/functions.h @@ -32,7 +32,7 @@ struct State; struct VertexProgram; bool create(std::unique_ptr &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 &vp, State &state, const SceGxmProgram &program, GXPPtrMap &gxp_ptr_map, const char *cache_path, const char *title_id); +bool create(std::unique_ptr &vp, State &state, const SceGxmProgram &program, GXPPtrMap &gxp_ptr_map, const std::vector &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); diff --git a/vita3k/renderer/include/renderer/gl/state.h b/vita3k/renderer/include/renderer/gl/state.h index d63128221..ffb6c3a62 100644 --- a/vita3k/renderer/include/renderer/gl/state.h +++ b/vita3k/renderer/include/renderer/gl/state.h @@ -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; diff --git a/vita3k/renderer/include/renderer/gxm_types.h b/vita3k/renderer/include/renderer/gxm_types.h index 8ba71ba88..09b8791a6 100644 --- a/vita3k/renderer/include/renderer/gxm_types.h +++ b/vita3k/renderer/include/renderer/gxm_types.h @@ -274,8 +274,10 @@ struct GxmContextState { }; struct SceGxmFragmentProgram { - size_t reference_count = 1; + std::atomic reference_count = 1; Ptr program; + // only necessary with async compilation + std::atomic compile_threads_on = 0; bool is_maskupdate; std::unique_ptr renderer_data; }; @@ -320,12 +322,14 @@ struct SceGxmShaderPatcherParams { }; struct SceGxmVertexProgram { - size_t reference_count = 1; + std::atomic reference_count = 1; Ptr program; std::vector streams; std::vector attributes; std::unique_ptr renderer_data; uint64_t key_hash; + // only necessary with async compilation + std::atomic compile_threads_on = 0; }; struct SceGxmPrecomputedDraw { diff --git a/vita3k/renderer/include/renderer/state.h b/vita3k/renderer/include/renderer/state.h index 658085a81..9bceea125 100644 --- a/vita3k/renderer/include/renderer/state.h +++ b/vita3k/renderer/include/renderer/state.h @@ -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; } diff --git a/vita3k/renderer/include/renderer/types.h b/vita3k/renderer/include/renderer/types.h index 699cec1e3..24d83d460 100644 --- a/vita3k/renderer/include/renderer/types.h +++ b/vita3k/renderer/include/renderer/types.h @@ -141,8 +141,8 @@ struct GxmRecordState { std::array vertex_streams; // Programs. - Ptr fragment_program; - Ptr vertex_program; + Ptr fragment_program; + Ptr 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 { diff --git a/vita3k/renderer/include/renderer/vulkan/pipeline_cache.h b/vita3k/renderer/include/renderer/vulkan/pipeline_cache.h index b03c63e5e..882e0969d 100644 --- a/vita3k/renderer/include/renderer/vulkan/pipeline_cache.h +++ b/vita3k/renderer/include/renderer/vulkan/pipeline_cache.h @@ -22,24 +22,42 @@ #include #include +#include #include #include struct SceGxmProgram; +struct SceGxmFragmentProgram; +struct SceGxmVertexProgram; enum SceGxmPrimitiveType : uint32_t; struct SceGxmVertexAttribute; struct MemState; using Sha256Hash = std::array; -namespace renderer::vulkan { +namespace shader { +struct Hints; +} + +namespace renderer { + +struct GxmRecordState; + +namespace vulkan { struct VKState; struct VKContext; +struct CompileRequest; + +using PipelineCompileQueue = moodycamel::BlockingConcurrentQueue; 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 render_passes[2][2]; // render passes used along shader interlock std::map shader_interlock_pass; - std::map shaders; - unordered_map_fast pipelines; - // temp vars used to store the result computed by auxialiary functions before createPipeline is called - std::vector binding_descr; - std::vector 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 shaders; + unordered_map_stable pipelines; - vk::PipelineShaderStageCreateInfo retrieve_shader(const SceGxmProgram *program, const Sha256Hash &hash, bool is_vertex, bool maskupdate, MemState &mem, const std::vector *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::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 \ No newline at end of file +} // namespace vulkan +} // namespace renderer diff --git a/vita3k/renderer/include/renderer/vulkan/state.h b/vita3k/renderer/include/renderer/vulkan/state.h index 1f3e7b972..5d54fb14e 100644 --- a/vita3k/renderer/include/renderer/vulkan/state.h +++ b/vita3k/renderer/include/renderer/vulkan/state.h @@ -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 &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 address, uint32_t size) override; void unmap_memory(MemState &mem, Ptr address) override; diff --git a/vita3k/renderer/include/renderer/vulkan/surface_cache.h b/vita3k/renderer/include/renderer/vulkan/surface_cache.h index 384ea48bf..fd233f6a7 100644 --- a/vita3k/renderer/include/renderer/vulkan/surface_cache.h +++ b/vita3k/renderer/include/renderer/vulkan/surface_cache.h @@ -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; diff --git a/vita3k/renderer/src/creation.cpp b/vita3k/renderer/src/creation.cpp index 1572a0803..d9d449b96 100644 --- a/vita3k/renderer/src/creation.cpp +++ b/vita3k/renderer/src/creation.cpp @@ -205,7 +205,7 @@ bool create(std::unique_ptr &fp, State &state, const SceGxmProg return true; } -bool create(std::unique_ptr &vp, State &state, const SceGxmProgram &program, GXPPtrMap &gxp_ptr_map, const char *cache_path, const char *title_id) { +bool create(std::unique_ptr &vp, State &state, const SceGxmProgram &program, GXPPtrMap &gxp_ptr_map, const std::vector &attributes, const char *cache_path, const char *title_id) { switch (state.current_backend) { case Backend::OpenGL: gl::create(vp, dynamic_cast(state), program); @@ -231,9 +231,12 @@ bool create(std::unique_ptr &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(i), SCE_GXM_PARAMETER_TYPE_F32, false, false, false)); + } + } } return true; diff --git a/vita3k/renderer/src/gl/renderer.cpp b/vita3k/renderer/src/gl/renderer.cpp index 247c24ee6..1ad71f8a1 100644 --- a/vita3k/renderer/src/gl/renderer.cpp +++ b/vita3k/renderer/src/gl/renderer.cpp @@ -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); } diff --git a/vita3k/renderer/src/gl/sync_state.cpp b/vita3k/renderer/src/gl/sync_state.cpp index 748f68962..97faf6c91 100644 --- a/vita3k/renderer/src/gl/sync_state.cpp +++ b/vita3k/renderer/src/gl/sync_state.cpp @@ -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(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(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 offset_in_buffer; diff --git a/vita3k/renderer/src/shaders.cpp b/vita3k/renderer/src/shaders.cpp index 627e08383..41f88b30e 100644 --- a/vita3k/renderer/src/shaders.cpp +++ b/vita3k/renderer/src/shaders.cpp @@ -91,7 +91,7 @@ bool get_shaders_cache_hashs(State &renderer) { void save_shaders_cache_hashs(State &renderer, std::vector &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); diff --git a/vita3k/renderer/src/state_set.cpp b/vita3k/renderer/src/state_set.cpp index 17479cd16..2f6ab17d8 100644 --- a/vita3k/renderer/src/state_set.cpp +++ b/vita3k/renderer/src/state_set.cpp @@ -77,11 +77,11 @@ COMMAND_SET_STATE(region_clip) { COMMAND_SET_STATE(program) { TRACY_FUNC_COMMANDS_SET_STATE(program); - const Ptr program = helper.pop>(); + const Ptr program = helper.pop>(); const bool is_fragment = helper.pop(); if (is_fragment) { - render_context->record.fragment_program = program.cast(); + render_context->record.fragment_program = program.cast(); 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(); + render_context->record.vertex_program = program.cast(); const SceGxmVertexProgram *gxm_program = render_context->record.vertex_program.get(mem); render_context->record.vertex_program_hash = gxm_program->renderer_data->hash; } diff --git a/vita3k/renderer/src/vulkan/pipeline_cache.cpp b/vita3k/renderer/src/vulkan/pipeline_cache.cpp index 9ff2766be..d5345dc91 100644 --- a/vita3k/renderer/src/vulkan/pipeline_cache.cpp +++ b/vita3k/renderer/src/vulkan/pipeline_cache.cpp @@ -30,14 +30,42 @@ #include #include +#include + // don't use the dispatch version, because we always hash a small amount // with a known size #define XXH_INLINE_ALL #include 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(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 shader_cache_copy; + { + std::lock_guard guard(shaders_mutex); + shader_cache_copy = state.shaders_cache_hashs; + } + renderer::save_shaders_cache_hashs(state, state.shaders_cache_hashs); + const std::vector 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 *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(~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 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 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(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 binding_descr; + static thread_local std::vector 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::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( - 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(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( vertex_program_gxm.renderer_data.get()); const SceGxmProgram *gxm_fragment_shader = fragment_program_gxm.program.get(mem); + const VKFragmentProgram &fragment_program = *reinterpret_cast( + 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::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( + 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(type); + + // can't use constexpr because of apple clang... + const vk::Pipeline pipeline_compiling = std::bit_cast(~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::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 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 guard(shaders_mutex); + shaders[hash] = shader; + } - return true; + return shader; } -} // namespace renderer::vulkan \ No newline at end of file +} // namespace renderer::vulkan diff --git a/vita3k/renderer/src/vulkan/renderer.cpp b/vita3k/renderer/src/vulkan/renderer.cpp index 27b0ff962..b2ffb3b0e 100644 --- a/vita3k/renderer/src/vulkan/renderer.cpp +++ b/vita3k/renderer/src/vulkan/renderer.cpp @@ -627,7 +627,9 @@ bool VKState::create(SDL_Window *window, std::unique_ptr &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 VKState::get_gpu_list() { const std::vector gpus = instance.enumeratePhysicalDevices(); diff --git a/vita3k/renderer/src/vulkan/scene.cpp b/vita3k/renderer/src/vulkan/scene.cpp index 0caf1db20..e86e2d1c5 100644 --- a/vita3k/renderer/src/vulkan/scene.cpp +++ b/vita3k/renderer/src/vulkan/scene.cpp @@ -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(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); diff --git a/vita3k/renderer/src/vulkan/surface_cache.cpp b/vita3k/renderer/src/vulkan/surface_cache.cpp index 626112542..b7969cd9e 100644 --- a/vita3k/renderer/src/vulkan/surface_cache.cpp +++ b/vita3k/renderer/src/vulkan/surface_cache.cpp @@ -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(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(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; diff --git a/vita3k/shader/include/shader/usse_disasm.h b/vita3k/shader/include/shader/usse_disasm.h index 8513ba4b1..32415c2cc 100644 --- a/vita3k/shader/include/shader/usse_disasm.h +++ b/vita3k/shader/include/shader/usse_disasm.h @@ -18,6 +18,7 @@ #pragma once #include +#include #include #include @@ -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 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'; \ } diff --git a/vita3k/shader/src/spirv_recompiler.cpp b/vita3k/shader/src/spirv_recompiler.cpp index e9098f69d..1e94b6f4d 100644 --- a/vita3k/shader/src/spirv_recompiler.cpp +++ b/vita3k/shader/src/spirv_recompiler.cpp @@ -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); diff --git a/vita3k/shader/src/usse_disasm.cpp b/vita3k/shader/src/usse_disasm.cpp index abfd58547..efb960090 100644 --- a/vita3k/shader/src/usse_disasm.cpp +++ b/vita3k/shader/src/usse_disasm.cpp @@ -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 diff --git a/vita3k/util/include/util/containers.h b/vita3k/util/include/util/containers.h index 5ad68e8c8..2e91a7a8d 100644 --- a/vita3k/util/include/util/containers.h +++ b/vita3k/util/include/util/containers.h @@ -21,14 +21,18 @@ #include -#if BOOST_VERSION >= 108100 +#if BOOST_VERSION >= 108200 #include #include +#include // use a boost unordered_flat_map as performance really matters for fields using this type template using unordered_map_fast = boost::unordered::unordered_flat_map; template using unordered_set_fast = boost::unordered::unordered_flat_set; +// this is in case we need pointer stability under rehashing +template +using unordered_map_stable = boost::unordered::unordered_node_map; #else // fallback in case someone is using an old boost version #include @@ -37,6 +41,8 @@ template using unordered_map_fast = boost::unordered_map; template using unordered_set_fast = boost::unordered_set; +template +using unordered_map_stable = boost::unordered_map; #endif namespace lru {